sozsoft-platform/configs/ai/Chat.json

489 lines
53 KiB
JSON
Raw Normal View History

2026-03-21 16:59:22 +00:00
{
2026-06-24 20:46:32 +00:00
"name": "Chat",
2026-03-21 16:59:22 +00:00
"nodes": [
{
"parameters": {
"multipleMethods": true,
"httpMethod": [
"POST"
],
"path": "chat",
"responseMode": "lastNode",
"responseData": "allEntries",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-39376,
-3072
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "46b4fe58-58c7-4344-a6fb-c099caab6651",
2026-06-24 16:59:51 +00:00
"name": "Webhook",
"webhookId": "b0b9c7f7-9eab-4227-8673-85e604bab2d2"
2026-03-21 16:59:22 +00:00
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "const body = $('Webhook').first().json.body || {};\nconst question = String(body.question || body.message || '').trim();\nreturn [{ json: {\n question,\n ollamaBody: {\n model: body.intentModel || 'gemma3:4b',\n stream: false,\n options: { temperature: 0, top_p: 0.1, num_ctx: 1024, num_predict: 8 },\n messages: [\n { role: 'system', content: 'Sen bir niyet sınıflandırma motorusun. Sadece CHAT veya SQL döndür. Kullanıcı veritabanından veri istiyorsa SQL döndür. Listele, getir, göster, say, kaç, adet, toplam, özet, rapor, sorgula, ara, filtrele, grupla, gruplandır, bazında, kırılım, pivot, tanımlama, kayıt, tablo, kolon, müşteri, sipariş, stok, ayar gibi veri/tablo amaçları varsa SQL döndür. Selamlaşma, açıklama, yardım, teşekkür, genel sohbet ise CHAT döndür. Açıklama yazma.' },\n { role: 'user', content: question }\n ]\n }\n}}];\n"
2026-03-21 16:59:22 +00:00
},
2026-06-23 23:23:23 +00:00
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-39168,
-3072
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "f5ab8edd-fd83-4e5c-94cc-6c58ff80bad6",
2026-06-23 23:23:23 +00:00
"name": "Build Intent Request"
},
{
"parameters": {
"method": "POST",
"url": "http://ollama:11434/api/chat",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.ollamaBody }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2026-06-24 20:46:32 +00:00
-38944,
-3072
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "d9a8bd55-350e-40dd-b145-2f70aaf76f79",
2026-06-23 23:23:23 +00:00
"name": "Intent Ollama"
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "const res = $input.first().json || {};\nconst body = $('Webhook').first().json.body || {};\nconst content = String(res.message?.content || res.response || res.output || '').toUpperCase();\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\n\nfunction normalizeText(value) {\n return String(value || '')\n .toLocaleLowerCase('tr-TR')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/ı/g, 'i')\n .replace(/ğ/g, 'g')\n .replace(/ü/g, 'u')\n .replace(/ş/g, 's')\n .replace(/ö/g, 'o')\n .replace(/ç/g, 'c')\n .replace(/[^a-z0-9]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nconst q = normalizeText(question);\nconst tokens = new Set(q.split(' ').filter(Boolean));\n\n// LLM yanlışlıkla CHAT dese bile bu kelimeler veri sorgusu niyetidir.\nconst sqlActionWords = [\n 'listele','liste','listesi','getir','goster','ara','bul','sorgula','filtrele','raporla','rapor',\n 'say','sayisi','kac','adet','toplam','ozet','ozetle','sum','count',\n 'grupla','gruplandir','gruplama','bazinda','kirilim','pivot','group','grouping',\n 'select','sql'\n];\n\nconst dataObjectWords = [\n 'tanim','tanimlama','tanimlamalari','ayar','ayarlari','kayit','kayitlari','tablo','kolon',\n 'musteri','siparis','stok','urun','firma','depo','fatura','personel','kumas','iplik'\n];\n\nconst hasSqlAction = sqlActionWords.some(w => tokens.has(w) || q.includes(w));\nconst hasDataObject = dataObjectWords.some(w => tokens.has(w) || q.includes(w));\nconst forceSql = body.forceSql === true || body.intent === 'SQL' || body.type === 'sql';\n\nlet intent;\nif (forceSql || hasSqlAction || (hasDataObject && /\\b(ana|grup|anahtar|key|bazinda|toplam|ozet|adet|sayisi)\\b/.test(q))) {\n intent = 'SQL';\n} else {\n intent = content.includes('SQL') ? 'SQL' : 'CHAT';\n}\n\nreturn [{ json: { question, tenantName, intent, intentRaw: res, intentByRule: intent === 'SQL' && !content.includes('SQL') } }];\n"
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-38720,
-3072
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "27993516-1b97-422e-b026-162afb4fb99b",
2026-06-23 23:23:23 +00:00
"name": "Normalize Intent"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
2026-06-24 16:59:51 +00:00
"version": 1
2026-06-23 23:23:23 +00:00
},
"conditions": [
{
2026-06-24 16:59:51 +00:00
"id": "89f40b61-7147-4055-8ffb-5e24ad9f7879",
2026-06-23 23:23:23 +00:00
"leftValue": "={{ $json.intent }}",
"rightValue": "SQL",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-38496,
-3072
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "2b7ee598-6977-4f0a-921c-11411576b1a9",
2026-06-23 23:23:23 +00:00
"name": "Is SQL?"
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "\nconst body = $('Webhook').first().json.body || {};\nconst question = String(body.question || body.message || '').trim();\nreturn [{ json: { type:'chat', question, ollamaBody: {\n model: body.chatModel || 'gemma3:4b',\n stream: false,\n options: { temperature: 0.2, top_p: 0.8, num_ctx: 2048, num_predict: 384 },\n messages: [\n { role: 'system', content: 'Sen Türkçe konuşan yardımcı bir asistansın. Kullanıcı SQL/veri istemiyorsa kısa ve net cevap ver. Veritabanı sorgusu üretme.' },\n { role: 'user', content: question }\n ]\n}}}];\n"
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
2026-03-21 16:59:22 +00:00
"position": [
2026-06-24 20:46:32 +00:00
-38288,
-2832
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "f5fa30e8-2934-4bf7-a2ae-96dc98acb64a",
2026-06-23 23:23:23 +00:00
"name": "Build Chat Request"
2026-03-21 16:59:22 +00:00
},
{
"parameters": {
2026-06-23 23:23:23 +00:00
"method": "POST",
"url": "http://ollama:11434/api/chat",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.ollamaBody }}",
2026-03-21 16:59:22 +00:00
"options": {}
},
2026-06-23 23:23:23 +00:00
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2026-06-24 20:46:32 +00:00
-38064,
-2832
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "e68d00f6-fe8d-4f6a-8170-fce9cdd14f0a",
2026-06-23 23:23:23 +00:00
"name": "Chat Ollama"
},
{
"parameters": {
2026-06-24 16:59:51 +00:00
"jsCode": "const res=$input.first().json||{}; return [{json:{type:'chat', question:$('Webhook').first().json.body?.question || '', answer:res.message?.content || res.response || res.output || ''}}];"
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-37840,
-2832
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "7cde5a7e-e0f2-497c-a38a-62de4084241d",
2026-06-23 23:23:23 +00:00
"name": "Chat Response"
},
{
"parameters": {
2026-06-24 16:59:51 +00:00
"jsCode": "const body = $('Webhook').first().json.body || {};\n\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\nconst definitionCultureName = body.definitionCultureName || 'en';\nconst uiCultureName = body.uiCultureName || 'tr';\nconst listFormCode = body.listFormCode || null;\nconst onlyBaseDefinitions = body.onlyBaseDefinitions === false ? 0 : 1;\nconst resourceName = body.resourceName || null;\nconst maxForms = Number(body.maxForms || 500);\n\nfunction q(v) {\n if (v === null || v === undefined || v === '') return 'NULL';\n return \"N'\" + String(v).replace(/'/g, \"''\") + \"'\";\n}\n\nconst safeMaxForms = Number.isFinite(maxForms) && maxForms > 0 ? Math.min(maxForms, 2000) : 500;\n\n// v23: Sadece dbo.Sas_H_DbTableColumn tablosundan okur.\nconst dictionaryQuery = `\nSELECT TOP (1)\n JsonData AS ListFormDataDictionaryJson,\n CacheKey,\n RefreshedAt,\n FormCount,\n FieldCount,\n JsonLength\nFROM dbo.Sas_H_DbTableColumn WITH (NOLOCK)\nWHERE DictionaryType = N'ListFormJson'\n AND ISNULL(IsActive, 0) = 1\n AND DefinitionCultureName = ${q(definitionCultureName)}\n AND UiCultureName = ${q(uiCultureName)}\n AND ISNULL(ListFormCode, N'') = ISNULL(${q(listFormCode)}, N'')\n AND ISNULL(OnlyBaseDefinitions, 1) = ${onlyBaseDefinitions}\n AND ISNULL(ResourceName, N'') = ISNULL(${q(resourceName)}, N'')\n AND ISNULL(MaxForms, 500) = ${safeMaxForms}\nORDER BY RefreshedAt DESC;\n`;\n\nreturn [{\n json: {\n question,\n tenantName,\n definitionCultureName,\n uiCultureName,\n listFormCode,\n onlyBaseDefinitions,\n resourceName,\n maxForms: safeMaxForms,\n dictionaryQuery\n }\n}];"
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-38288,
-3248
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "326e9983-14e0-490e-bcb8-4852bbca1d49",
2026-06-24 16:59:51 +00:00
"name": "Build Dictionary Query"
2026-06-23 23:23:23 +00:00
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ $json.dictionaryQuery }}"
},
"type": "n8n-nodes-base.microsoftSql",
2026-03-21 16:59:22 +00:00
"typeVersion": 1.1,
"position": [
2026-06-24 20:46:32 +00:00
-38048,
-3248
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "986bc23a-44ac-48e6-bd68-96c70b48e7dc",
2026-06-24 16:59:51 +00:00
"name": "Get Dictionary JSON",
"credentials": {
"microsoftSql": {
"id": "FV4335uaFWUGPlIf",
"name": "Microsoft SQL account"
}
}
2026-03-21 16:59:22 +00:00
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "\nconst row = $input.first().json || {};\nconst body = $('Webhook').first().json.body || {};\n\nconst question = String(body.question || body.message || '').trim();\nconst tenantName = String(body.tenantName || 'Sozsoft').trim();\nconst dictText = String(row.ListFormDataDictionaryJson || '').trim();\n\nif (!dictText) {\n return [{\n json: {\n valid: false,\n type: 'dictionary_error',\n error: 'Sas_H_DbTableColumn tablosunda aktif JSON veri sözlüğü bulunamadı. Önce JSON üretip dbo.Sas_H_DbTableColumn tablosuna yazın.',\n question\n }\n }];\n}\n\nlet dict;\ntry {\n dict = JSON.parse(dictText);\n} catch (e) {\n return [{\n json: {\n valid: false,\n type: 'dictionary_error',\n error: 'JsonData parse edilemedi: ' + e.message,\n question\n }\n }];\n}\n\nconst listForms = Array.isArray(dict.listForms) ? dict.listForms : [];\n\nfunction escapeRegExp(s) {\n return String(s || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeText(value) {\n return String(value || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/"/gi, '')\n .replace(/[\\\"'`[\\]\\\\]/g, ' ')\n .toLocaleLowerCase('tr-TR')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/ı/g, 'i')\n .replace(/ğ/g, 'g')\n .replace(/ü/g, 'u')\n .replace(/ş/g, 's')\n .replace(/ö/g, 'o')\n .replace(/ç/g, 'c')\n .replace(/[^a-z0-9]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction decodeSqlEscapes(value) {\n return String(value || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/u0022/g, '')\n .replace(/\\\\u0027/g, \"'\")\n .replace(/\\u0027/g, \"'\")\n .replace(/u0027/g, \"'\")\n .replace(/"/gi, '')\n .replace(/'/gi, \"'\");\n}\n\nfunction cleanIdentifier(value) {\n let s = decodeSqlEscapes(value)\n .replace(/LookupQuery\\s*:/gi, '')\n .replace(/[\"'`\\[\\]\\\\]/g, '')\n .replace(/\\s+AS\\s+(KEY|NAME)$/i, '')\n .replace(/^dbo\\./i, '')\n .trim();\n\n const dbRe = new RegExp('^' + escapeRegExp(tenantName) + '\\\\.', 'i');\n s = s.replace(dbRe, '').replace(/^dbo\\./i, '').trim();\n\n const parts = s.split('.').map(x => x.trim()).filter(Boolean);\n s = parts.length ? parts[parts.length - 1] : s;\n\n // Only allow real identifier tokens. SQL expressions such as CONCAT(Name,' (',DisplayName,')') are not column names.\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) return '';\n return s;\n}\n\nfunction simpleIdentifierFromExpression(value) {\n let s = decodeSqlEscapes(value).trim();\n s = s.replace(/^\\s*SELECT\\s+/i, '').replace(/\\s+AS\\s+(KEY|NAME)\\s*$/i, '').trim();\n\n // remove outer brackets/quotes/backticks for a simple identifier only\n if (/^\\[[A-Za-z_][A-Za-z0-9_]*\\]$/.test(s)) s = s.slice(1, -1);\n if (/^\"[A-Za-z_][A-Za-z0-9_]*\"$/.test(s)) s = s.slice(1, -1);\n if (/^`[A-Za-z_][A-Za-z0-9_]*`$/.test(s)) s = s.slice(1, -1);\n\n // dbo.Column or Table.Column => use last part only if all parts are simple identifiers\n const parts = s.split('.').map(x => x.replace(/[\\[\\]\"`]/g, '').trim()).filter(Boolean);\n if (parts.length && parts.every(x => /^[A-Za-z_][A-Za-z0-9_]*$/.test(x))) return parts[parts.length - 1];\n\n return '';\n}\n\nfunction bracketIdentifier(value, fallback = 'Id') {\n const id = cleanIdentifier(value) || cleanIdentifier(fallback) || 'Id';\n return '[' + id.replace(/]/g, ']]') + ']';\n}\n\nfunction cleanAliasLabel(value, fallback) {\n let label = decodeSqlEscapes(value)\n .replace(/\\r|\\n|\\t/g, ' ')\n .replace(/[\\[\\]]/g, '')\n .replace(/[\\u0000-\\u001F]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n // Alias kullanıcıya görünen başlıktır; Türkçe karakter ve boşluk kalabilir.\n // Ancak SQL ifadesi veya boş değer alias olamaz.\n if (!label || /^(CONCAT|CASE|SELECT|NULLIF|ISNULL|COALESCE)\\s*\\(/i.test(label)) {\n label = decodeSqlEscapes(fallback).replace(/[\\[\\]]/g, '').trim();\n }\n if (!label) label = 'Column';\n
2026-03-21 16:59:22 +00:00
},
2026-06-23 23:23:23 +00:00
"type": "n8n-nodes-base.code",
"typeVersion": 2,
2026-03-21 16:59:22 +00:00
"position": [
2026-06-24 20:46:32 +00:00
-37808,
-3248
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "61007e7c-6f51-4572-ad61-13d9f0bffa4c",
2026-06-23 23:23:23 +00:00
"name": "Build SQL Request"
2026-03-21 16:59:22 +00:00
},
{
"parameters": {
2026-06-23 23:23:23 +00:00
"method": "POST",
"url": "http://ollama:11434/api/chat",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.ollamaBody }}",
2026-03-21 16:59:22 +00:00
"options": {}
},
2026-06-23 23:23:23 +00:00
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
2026-03-21 16:59:22 +00:00
"position": [
2026-06-24 20:46:32 +00:00
-37568,
-3248
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "2b6630c2-f520-472a-8733-06e362264849",
2026-06-23 23:23:23 +00:00
"name": "SQL Ollama"
2026-03-21 16:59:22 +00:00
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "const res = $input.first().json || {};\nconst build = $('Build SQL Request').first().json || {};\nconst question = build.question || $('Webhook').first().json.body?.question || '';\nconst tenantName = String(build.tenantName || $('Webhook').first().json.body?.tenantName || 'Sozsoft').trim();\n\nfunction escapeRegExp(s) {\n return String(s || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction cleanName(v) {\n let s = String(v || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/&quot;/gi, '')\n .replace(/LookupQuery\\s*:/gi, '')\n .replace(/[\"`\\[\\]\\\\]/g, '')\n .replace(/^dbo\\./i, '')\n .trim();\n\n const dbRe = new RegExp('^' + escapeRegExp(tenantName) + '\\\\.', 'i');\n s = s.replace(dbRe, '').replace(/^dbo\\./i, '').trim();\n\n const parts = s.split('.').map(x => x.trim()).filter(Boolean);\n return parts.length ? parts[parts.length - 1] : s;\n}\n\nconst allowedTables = new Set((build.allowedTables || []).map(cleanName).filter(Boolean));\n\nfunction getOllamaText(obj) {\n return String(\n obj.message?.content ||\n obj.response ||\n obj.output ||\n obj.text ||\n obj.content ||\n ''\n ).trim();\n}\n\nconst raw = getOllamaText(res);\n\nfunction cleanSql(text) {\n let t = String(text || '')\n .replace(/```sql/gi, '')\n .replace(/```/g, '')\n .replace(/^SQL\\s*:/i, '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/&quot;/gi, '')\n .replace(/\"/g, '')\n .trim();\n\n const idx = t.search(/\\bSELECT\\b/i);\n if (idx < 0) return '';\n\n t = t.substring(idx).trim();\n const semi = t.indexOf(';');\n if (semi >= 0) t = t.substring(0, semi).trim();\n return t.replace(/\\s+/g, ' ').trim();\n}\n\nfunction canonicalizeSql(sql) {\n let q = String(sql || '')\n .replace(/\\\\u0022/g, '')\n .replace(/\\u0022/g, '')\n .replace(/&quot;/gi, '')\n .replace(/\\[LookupQuery:\\s*([^\\]]+)\\]/gi, '[$1]');\n\n const db = escapeRegExp(tenantName);\n\n // [Sozsoft].[Sozsoft].[dbo].[X] -> [Sozsoft].[dbo].[X]\n q = q.replace(new RegExp('\\\\[' + db + '\\\\]\\\\.\\\\[' + db + '\\\\]\\\\.\\\\[dbo\\\\]\\\\.', 'gi'), '[' + tenantName + '].[dbo].');\n q = q.replace(new RegExp(db + '\\\\.' + db + '\\\\.dbo\\\\.', 'gi'), '[' + tenantName + '].[dbo].');\n\n // [dbo].[X] -> [Sozsoft].[dbo].[X], eğer önünde zaten [Sozsoft]. yoksa\n q = q.replace(/\\[dbo\\]\\.\\[([^\\]]+)\\]/gi, (m, t, off, str) => {\n const before = str.slice(Math.max(0, off - tenantName.length - 4), off);\n const alreadyDb = new RegExp('\\\\[' + db + '\\\\]\\\\.$', 'i').test(before);\n if (alreadyDb) return m;\n return '[' + tenantName + '].[dbo].[' + cleanName(t) + ']';\n });\n\n // DİKKAT: Köşeli parantez içindeki her şeyi temizleme.\n // Aliaslar [Adı], [Üst Adı], [Çalışma Saati] gibi Türkçe/boşluklu olabilir.\n // Global bracket temizliği aliasları [] haline getirir.\n return q.replace(/\\s+/g, ' ').trim();\n}\n\nfunction qualifyTables(sql) {\n let q = canonicalizeSql(sql);\n const tables = Array.from(allowedTables).sort((a, b) => b.length - a.length);\n\n for (const table of tables) {\n const t = escapeRegExp(table);\n\n // FROM dbo.Table / JOIN dbo.Table\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+dbo\\\\.\\\\[?' + t + '\\\\]?', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n\n // FROM [dbo].[Table] / JOIN [dbo].[Table]\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+\\\\[dbo\\\\]\\\\.\\\\[' + t + '\\\\]', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n\n // FROM Table / JOIN Table veya FROM [Table] / JOIN [Table]\n q = q.replace(new RegExp('\\\\b(FROM|JOIN)\\\\s+\\\\[?' + t + '\\\\]?(?=\\\\s|$)', 'gi'), '$1 [' + tenantName + '].[dbo].[' + table + ']');\n }\n\n return canonicalizeSql(q);\n}\n\nfunction extractTables(sql) {\n const tables = [];\n const re = /\\b(?:FROM|JOIN)\\s+((?:\\[[^\\]]+\\]\\.){0,3}\\[[^\\]]+\\]|(?:[A-Za-z0-9_]+\\.){0,3}[A-Za-z0-9_]+)/gi;\n let m;\n while ((m = re.exec(sql)) !== null) {\n const
2026-03-21 16:59:22 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-37328,
-3248
2026-03-21 16:59:22 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "2a9ba43b-f687-4c27-a00b-841d912cc9a1",
2026-06-23 23:23:23 +00:00
"name": "Validate SQL"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
2026-06-24 16:59:51 +00:00
"version": 1
2026-06-23 23:23:23 +00:00
},
"conditions": [
{
2026-06-24 16:59:51 +00:00
"id": "c6195621-ef9a-4298-9317-ae22e6c86b92",
2026-06-23 23:23:23 +00:00
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-37104,
-3248
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "4445ba0d-c494-4389-be21-1b2468fade63",
2026-06-23 23:23:23 +00:00
"name": "SQL Valid?"
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ $json.sql }}"
},
"type": "n8n-nodes-base.microsoftSql",
"typeVersion": 1.1,
"position": [
2026-06-24 20:46:32 +00:00
-36880,
-3344
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "ec109600-5013-4b38-aeee-be8f2b654864",
2026-06-24 16:59:51 +00:00
"name": "Execute Generated SQL",
"credentials": {
"microsoftSql": {
"id": "FV4335uaFWUGPlIf",
"name": "Microsoft SQL account"
}
}
2026-06-23 23:23:23 +00:00
},
{
"parameters": {
2026-06-24 20:46:32 +00:00
"jsCode": "\nconst rows = $input.all().map(item => item.json);\nconst v = $('Validate SQL').first().json || {};\nconst question = String(v.question || '').toLocaleLowerCase('tr-TR');\n\nfunction normalizeText(value) {\n return String(value || '')\n .toLocaleLowerCase('tr-TR')\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/ı/g, 'i')\n .replace(/ğ/g, 'g')\n .replace(/ü/g, 'u')\n .replace(/ş/g, 's')\n .replace(/ö/g, 'o')\n .replace(/ç/g, 'c')\n .replace(/[^a-z0-9]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction isNumberValue(value) {\n if (value === null || value === undefined || value === '') return false;\n if (typeof value === 'number') return Number.isFinite(value);\n return !isNaN(Number(String(value).replace(',', '.')));\n}\n\nfunction toNumber(value) {\n if (typeof value === 'number') return value;\n return Number(String(value).replace(',', '.'));\n}\n\nconst q = normalizeText(question);\nconst columns = rows.length > 0 ? Object.keys(rows[0]) : [];\n\nconst wantsPivot =\n q.includes('pivot') ||\n q.includes('bazinda') ||\n q.includes('kirilim') ||\n q.includes('gruplandir') ||\n q.includes('gruplama') ||\n q.includes('grouping') ||\n q.includes('group');\n\nconst wantsSummary =\n wantsPivot ||\n q.includes('ozet') ||\n q.includes('summarize') ||\n q.includes('summary') ||\n q.includes('toplam') ||\n q.includes('ortalama') ||\n q.includes('adet') ||\n q.includes('kac') ||\n q.includes('sayisi');\n\nif (!rows.length) {\n return [{\n json: {\n type: 'sql_result',\n mode: wantsPivot ? 'pivot' : wantsSummary ? 'summary' : 'list',\n question: v.question,\n sql: v.sql,\n rowCount: 0,\n columns,\n rows: [],\n debug: { sqlSource: v.sqlSource, usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n }\n }];\n}\n\nconst numericColumns = columns.filter(col => rows.some(r => isNumberValue(r[col])));\nconst dimensionColumns = columns.filter(col => !numericColumns.includes(col));\n\nif (wantsSummary && !wantsPivot) {\n const summary = {\n totalRows: rows.length\n };\n\n for (const col of numericColumns) {\n const values = rows.map(r => toNumber(r[col])).filter(n => Number.isFinite(n));\n summary[col] = {\n toplam: values.reduce((a, b) => a + b, 0),\n ortalama: values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0,\n min: values.length ? Math.min(...values) : null,\n max: values.length ? Math.max(...values) : null\n };\n }\n\n return [{\n json: {\n type: 'sql_result',\n mode: 'summary',\n question: v.question,\n sql: v.sql,\n rowCount: rows.length,\n columns,\n summary,\n rows,\n debug: { sqlSource: v.sqlSource, usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n }\n }];\n}\n\nif (wantsPivot) {\n // SQL zaten GROUP BY ile döndüyse doğrudan pivotlanabilir tablo gibi aktar.\n const hasAggregateColumn = columns.some(c => ['adet', 'count', 'total', 'toplam'].includes(normalizeText(c)));\n if (hasAggregateColumn) {\n return [{\n json: {\n type: 'sql_result',\n mode: 'pivot',\n question: v.question,\n sql: v.sql,\n rowCount: rows.length,\n columns,\n rows,\n debug: { sqlSource: v.sqlSource, usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n }\n }];\n }\n\n const groupColumn = dimensionColumns[0] || columns[0];\n const valueColumn = numericColumns[0] || null;\n const map = {};\n\n for (const row of rows) {\n const key = row[groupColumn] ?? 'Boş';\n if (!map[key]) {\n map[key] = { [groupColumn]: key, Adet: 0 };\n if (valueColumn) map[key][`${valueColumn}_Toplam`] = 0;\n }\n\n map[key].Adet++;\n\n if (valueColumn && isNumberValue(row[valueColumn])) {\n map[key][`${valueColumn}_Toplam`] += toNumber(row[valueColumn]);\n }
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-36640,
-3344
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "4797a41c-acb6-4a8b-b66c-e3cebc230dae",
"name": "Format Smart SQL Result"
2026-06-23 23:23:23 +00:00
},
{
"parameters": {
2026-06-24 16:59:51 +00:00
"jsCode": "\nconst e = $input.first().json || {};\nreturn [{ json: { type: e.type || 'sql_error', question: e.question || $('Webhook').first().json.body?.question || '', error: e.error || 'SQL üretilemedi.', generatedText: e.generatedText || null, cleanedText: e.cleanedText || null, unknownTables: e.unknownTables || null, allowedTables: e.allowedTables || null } }];\n"
2026-06-23 23:23:23 +00:00
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2026-06-24 20:46:32 +00:00
-36880,
-3104
2026-06-23 23:23:23 +00:00
],
2026-06-24 20:46:32 +00:00
"id": "ce66daea-a390-43ae-bd7d-0a00d80c86be",
2026-06-23 23:23:23 +00:00
"name": "SQL Error Response"
2026-03-21 16:59:22 +00:00
}
],
"pinData": {},
"connections": {
"Webhook": {
"main": [
[
{
2026-06-23 23:23:23 +00:00
"node": "Build Intent Request",
2026-03-21 16:59:22 +00:00
"type": "main",
"index": 0
}
2026-06-23 23:23:23 +00:00
]
]
},
"Build Intent Request": {
"main": [
[
{
"node": "Intent Ollama",
"type": "main",
"index": 0
}
]
]
},
"Intent Ollama": {
"main": [
[
{
"node": "Normalize Intent",
"type": "main",
"index": 0
}
]
]
},
"Normalize Intent": {
"main": [
[
{
"node": "Is SQL?",
"type": "main",
"index": 0
}
]
2026-03-21 16:59:22 +00:00
]
},
2026-06-23 23:23:23 +00:00
"Is SQL?": {
2026-03-21 16:59:22 +00:00
"main": [
[
{
2026-06-24 16:59:51 +00:00
"node": "Build Dictionary Query",
2026-03-21 16:59:22 +00:00
"type": "main",
"index": 0
}
],
[
{
2026-06-23 23:23:23 +00:00
"node": "Build Chat Request",
2026-03-21 16:59:22 +00:00
"type": "main",
"index": 0
}
]
]
},
2026-06-23 23:23:23 +00:00
"Build Chat Request": {
2026-03-21 16:59:22 +00:00
"main": [
[
{
2026-06-23 23:23:23 +00:00
"node": "Chat Ollama",
2026-03-21 16:59:22 +00:00
"type": "main",
"index": 0
}
]
]
},
2026-06-23 23:23:23 +00:00
"Chat Ollama": {
"main": [
2026-03-21 16:59:22 +00:00
[
{
2026-06-23 23:23:23 +00:00
"node": "Chat Response",
"type": "main",
2026-03-21 16:59:22 +00:00
"index": 0
2026-06-23 23:23:23 +00:00
}
]
]
},
2026-06-24 16:59:51 +00:00
"Build Dictionary Query": {
2026-06-23 23:23:23 +00:00
"main": [
[
2026-03-21 16:59:22 +00:00
{
2026-06-24 16:59:51 +00:00
"node": "Get Dictionary JSON",
2026-06-23 23:23:23 +00:00
"type": "main",
2026-03-21 16:59:22 +00:00
"index": 0
}
]
]
},
2026-06-24 16:59:51 +00:00
"Get Dictionary JSON": {
2026-06-23 23:23:23 +00:00
"main": [
2026-03-21 16:59:22 +00:00
[
{
2026-06-23 23:23:23 +00:00
"node": "Build SQL Request",
"type": "main",
"index": 0
}
]
]
},
"Build SQL Request": {
"main": [
[
{
"node": "SQL Ollama",
"type": "main",
"index": 0
}
]
]
},
"SQL Ollama": {
"main": [
[
{
"node": "Validate SQL",
"type": "main",
"index": 0
}
]
]
},
"Validate SQL": {
"main": [
[
{
"node": "SQL Valid?",
"type": "main",
"index": 0
}
]
]
},
"SQL Valid?": {
"main": [
[
{
"node": "Execute Generated SQL",
"type": "main",
"index": 0
}
],
[
{
"node": "SQL Error Response",
"type": "main",
"index": 0
}
]
]
},
"Execute Generated SQL": {
"main": [
[
{
2026-06-24 20:46:32 +00:00
"node": "Format Smart SQL Result",
2026-06-23 23:23:23 +00:00
"type": "main",
2026-03-21 16:59:22 +00:00
"index": 0
}
]
]
}
},
2026-06-24 16:59:51 +00:00
"active": true,
2026-03-21 16:59:22 +00:00
"settings": {
2026-06-24 16:59:51 +00:00
"executionOrder": "v1",
"availableInMCP": false,
"timeSavedMode": "fixed",
"callerPolicy": "workflowsFromSameOwner"
2026-03-21 16:59:22 +00:00
},
2026-06-24 20:46:32 +00:00
"versionId": "39108360-6275-4878-8546-69b1baa0c871",
2026-03-21 16:59:22 +00:00
"meta": {
2026-06-24 16:59:51 +00:00
"templateCredsSetupCompleted": true,
"instanceId": "1d288821beaaeeada5e8dce6f282c802098a0c83ef6ddb35a174b09a9d43850e"
2026-03-21 16:59:22 +00:00
},
2026-06-24 16:59:51 +00:00
"id": "MgXO29wLFA4urmNk",
2026-03-21 16:59:22 +00:00
"tags": []
}