...\n const m = s.match(/\\bselect\\s+(.+?)\\s+as\\s+key\\s*,\\s*(.+?)\\s+as\\s+name\\s+from\\s+([a-zA-Z0-9_.]+)/i);\n if (!m) return null;\n\n const valueColumn = simpleIdentifierFromExpression(m[1]);\n let displayColumn = simpleIdentifierFromExpression(m[2]);\n\n // If display expression is a SQL expression like CONCAT(...), do not treat it as a column.\n // Use the lookup key column as a safe display fallback instead of generating [Lx].[CONCAT(...)] or [L0].\n if (!displayColumn) displayColumn = valueColumn;\n\n return {\n tableName: cleanIdentifier(m[3]),\n valueColumn,\n displayColumn\n };\n}\n\nfunction compactField(field) {\n const parsedLookup = parseLookupSql(field.lookupJsonText || field.lookupJson || '');\n const sqlColumnName = cleanIdentifier(field.sqlColumnName || field.field);\n const displayName = String(field.displayName || field.captionValue || field.captionKey || sqlColumnName || '');\n\n const lookupTableName = cleanIdentifier(\n parsedLookup?.tableName ||\n field.lookupTableName ||\n field.parsedLookupTableName ||\n ''\n );\n\n const lookupValueColumn = cleanIdentifier(\n parsedLookup?.valueColumn ||\n field.parsedLookupValueColumn ||\n field.lookupValueColumn ||\n ''\n );\n\n let lookupDisplayColumn = cleanIdentifier(\n parsedLookup?.displayColumn ||\n field.parsedLookupDisplayColumn ||\n field.lookupDisplayColumn ||\n ''\n );\n\n if (!lookupDisplayColumn && lookupValueColumn) lookupDisplayColumn = lookupValueColumn;\n\n // A field is a usable lookup only when table/value/display are clean real identifiers.\n // This prevents SQL like [L0].[u0027)u0027)] and [L1].[CONCAT(...)] from being generated.\n const hasLookup = Boolean(lookupTableName && lookupValueColumn && lookupDisplayColumn);\n let lookupJoinMode = hasLookup ? (field.lookupJoinMode || '') : '';\n\n if (hasLookup && !lookupJoinMode) {\n const baseNorm = normalizeText(sqlColumnName);\n const valueNorm = normalizeText(lookupValueColumn);\n const idLike = baseNorm.endsWith('id') || valueNorm === 'id';\n lookupJoinMode = idLike ? 'convert-nvarchar-safe' : 'direct-string';\n }\n\n return {\n sqlColumnName,\n displayName,\n captionKey: String(field.captionKey || ''),\n sqlDataType: String(field.sqlDataType || ''),\n listOrderNo: Number(field.listOrderNo ?? 999999),\n isBooleanColumn: Boolean(field.isBooleanColumn),\n isVisible: field.isVisible !== false,\n isDefaultSelectColumn: Boolean(field.isDefaultSelectColumn),\n isActiveStatusColumn: Boolean(field.isActiveStatusColumn),\n isDeletedStatusColumn: Boolean(field.isDeletedStatusColumn),\n isSensitive: Boolean(field.isSensitive) || /password|token|secret|apikey|privatekey|connectionstring/i.test(sqlColumnName),\n hasLookup,\n lookup: hasLookup ? {\n tableName: lookupTableName,\n valueColumn: lookupValueColumn,\n displayColumn: lookupDisplayColumn || lookupValueColumn,\n joinMode: lookupJoinMode,\n fallbackToSourceColumn: Boolean(field.lookupFallbackToSourceColumn)\n } : null,\n searchText: normalizeText([\n field.sqlColumnName,\n field.field,\n field.displayName,\n field.captionValue,\n field.captionKey,\n field.lookupSearchText\n ].join(' '))\n };\n}\n\nfunction compactForm(form) {\n const fields = Array.isArray(form.fields) ? form.fields.map(compactField).filter(f => f.sqlColumnName && !f.isSensitive) : [];\n return {\n formCode: String(form.formCode || form.listFormCode || ''),\n displayName: String(form.displayName || form.nameValue || form.title || ''),\n tableName: cleanIdentifier(form.sqlTableName || form.tableName || form.sqlSourceName || ''),\n primaryKeyColumn: cleanIdentifier(form.primaryKeyColumn || form.keyFieldName || 'Id'),\n sourceKind: form.sourceKind || 'table',\n searchText: normalizeText([\n form.formCode,\n form.listFormCode,\n form.displayName,\n form.nameValue,\n form.title,\n form.description,\n form.tableName,\n form.sqlTableName,\n form.searchText\n ].join(' ')),\n fields\n };\n}\n\nconst compactAllForms = listForms.map(compactForm).filter(f => f.tableName && f.fields.length > 0);\n\n// Generic question semantics; these are filter/action words, not entity names.\n// They are intentionally not mapped to any specific table.\nconst actionOrFilterWords = new Set([\n 'liste', 'listesi', 'listele', 'goster', 'getir', 'ver', 'bul', 'ara',\n 'aktif', 'pasif', 'active', 'inactive', 'etkin', 'silinmis', 'silinen',\n 'toplam', 'say', 'adet', 'kac', 'kaç', 'rapor', 'ozet', 'özet', 'ozetle', 'özetle', 'pivot', 'bazinda', 'bazında', 'gruplandir', 'gruplandır', 'group', 'grouping', 'kirilim', 'kırılım'\n]);\n\nconst questionTokens = uniqueTokens(question);\nconst discriminatingTokens = questionTokens.filter(t => !actionOrFilterWords.has(t));\n\nfunction tokenHitScore(token, text) {\n if (!token || !text) return 0;\n if (text.split(' ').includes(token)) return 12;\n if (token.length >= 3 && text.includes(token)) return 6;\n return 0;\n}\n\nfunction scoreForm(form) {\n const formText = normalizeText([form.formCode, form.displayName, form.tableName, form.searchText].join(' '));\n const fieldText = normalizeText(form.fields.map(f => [f.sqlColumnName, f.displayName, f.captionKey].join(' ')).join(' '));\n\n let score = 0;\n\n for (const token of discriminatingTokens) {\n score += tokenHitScore(token, formText) * 4;\n score += tokenHitScore(token, fieldText);\n }\n\n // If no discriminating token exists, allow generic tokens to select by visible labels.\n if (discriminatingTokens.length === 0) {\n for (const token of questionTokens) {\n score += tokenHitScore(token, formText);\n score += tokenHitScore(token, fieldText) / 2;\n }\n }\n\n // Exact normalized phrase in form/table/display name gets a generic boost.\n const qNorm = normalizeText(question);\n if (qNorm && formText.includes(qNorm)) score += 30;\n\n // Penalize forms that only match action/filter words.\n if (score === 0 && questionTokens.some(t => actionOrFilterWords.has(t)) && form.fields.some(f => f.isActiveStatusColumn)) {\n score += 1;\n }\n\n return score;\n}\n\nconst maxCandidateForms = Math.max(1, Math.min(Number(body.maxCandidateForms || 5), 12));\nconst scored = compactAllForms\n .map(form => ({ form, score: scoreForm(form) }))\n .sort((a, b) => b.score - a.score || a.form.formCode.localeCompare(b.form.formCode));\n\nlet compactForms = scored.filter(x => x.score > 0).slice(0, maxCandidateForms).map(x => x.form);\nif (compactForms.length === 0) compactForms = compactAllForms.slice(0, maxCandidateForms);\n\nconst selectedForm = compactForms[0] || null;\n\nconst allowedTables = new Set();\nfor (const form of compactForms) {\n if (form.tableName) allowedTables.add(form.tableName);\n for (const f of form.fields || []) {\n if (f.lookup?.tableName) allowedTables.add(f.lookup.tableName);\n }\n}\n\nfunction aliasFor(index) {\n return '[L' + index + ']';\n}\n\nfunction sqlExpressionForField(baseAlias, lookupAlias, field) {\n const col = bracketIdentifier(field.sqlColumnName);\n const alias = bracketAlias(field.displayName, field.sqlColumnName);\n\n if (lookupAlias && lookupAlias !== '[L0]' && field.hasLookup && field.lookup?.tableName && field.lookup?.displayColumn) {\n const displayColumn = bracketIdentifier(field.lookup.displayColumn);\n if (field.lookup.fallbackToSourceColumn) {\n return `COALESCE(CONVERT(NVARCHAR(4000), ${lookupAlias}.${displayColumn}), CONVERT(NVARCHAR(4000), ${baseAlias}.${col})) AS ${alias}`;\n }\n return `${lookupAlias}.${displayColumn} AS ${alias}`;\n }\n\n if (field.isBooleanColumn) {\n return `CASE WHEN ${baseAlias}.${col} = 1 THEN 'Yes' WHEN ${baseAlias}.${col} = 0 THEN 'No' ELSE NULL END AS ${alias}`;\n }\n\n return `${baseAlias}.${col} AS ${alias}`;\n}\n\n\nfunction detectResultMode(questionText) {\n const q = normalizeText(questionText);\n const tokens = new Set(q.split(' ').filter(Boolean));\n\n const wantsPivot = tokens.has('pivot') || tokens.has('kirılım') || tokens.has('kirilim') || tokens.has('bazinda') || tokens.has('bazli') || tokens.has('gruplandir') || tokens.has('gruplama') || tokens.has('group') || tokens.has('grouping');\n const wantsSummary = tokens.has('ozet') || tokens.has('ozetle') || tokens.has('summarize') || tokens.has('summary') || tokens.has('toplam') || tokens.has('adet') || tokens.has('say') || tokens.has('sayisi') || tokens.has('kac') || tokens.has('kaç') || wantsPivot;\n\n if (wantsPivot) return 'pivot';\n if (wantsSummary) return 'summary';\n return 'list';\n}\n\nfunction isNumericSqlType(typeName) {\n return /^(bigint|int|smallint|tinyint|decimal|numeric|money|smallmoney|float|real)$/i.test(String(typeName || ''));\n}\n\nfunction scoreGroupField(field, qNorm) {\n const fieldNorm = normalizeText([field.sqlColumnName, field.displayName, field.captionKey].join(' '));\n if (!fieldNorm) return 0;\n\n let score = 0;\n if (qNorm.includes(fieldNorm)) score += 100;\n\n const parts = fieldNorm.split(' ').filter(Boolean);\n for (const p of parts) {\n if (p.length >= 2 && qNorm.split(' ').includes(p)) score += 10;\n }\n\n // \"Ana Grup Anahtarı bazında\" gibi isteklerde özellikle metin/kod alanlarını tercih et.\n if (/grup|group|kategori|category|tip|type|tur|tür|anahtar|key|kod|code|name|ad/i.test(fieldNorm)) score += 8;\n\n // ID alanlarını kullanıcı özellikle istemiyorsa geriye at.\n if (/id$/i.test(field.sqlColumnName)) score -= 15;\n\n return score;\n}\n\nfunction pickGroupFields(fields, questionText, maxCount = 2) {\n const qNorm = normalizeText(questionText);\n\n const candidates = fields\n .filter(f => f.sqlColumnName && !f.isSensitive && f.isVisible !== false)\n .filter(f => !isNumericSqlType(f.sqlDataType) || /anahtar|key|kod|code|grup|group|tip|type|tur|tür/i.test(normalizeText([f.sqlColumnName, f.displayName, f.captionKey].join(' '))))\n .map(f => ({ field: f, score: scoreGroupField(f, qNorm) }))\n .sort((a, b) => b.score - a.score || a.field.listOrderNo - b.field.listOrderNo);\n\n const explicit = candidates.filter(x => x.score >= 20).slice(0, maxCount).map(x => x.field);\n if (explicit.length) return explicit;\n\n // Varsayılan: görünen ilk metin/kod alanı\n const fallback = fields.find(f =>\n f.sqlColumnName &&\n !f.isSensitive &&\n f.isVisible !== false &&\n !/^id$/i.test(f.sqlColumnName) &&\n !isNumericSqlType(f.sqlDataType)\n );\n\n return fallback ? [fallback] : fields.filter(f => f.sqlColumnName && !f.isSensitive).slice(0, 1);\n}\n\nfunction buildDirectSql(form, questionText) {\n if (!form || !form.tableName) return '';\n\n const baseAlias = '[T]';\n const qNorm = normalizeText(questionText);\n const resultMode = detectResultMode(questionText);\n\n const fields = (form.fields || [])\n .filter(f => f.sqlColumnName && !f.isSensitive && f.isVisible !== false)\n .sort((a, b) => a.listOrderNo - b.listOrderNo);\n\n const where = [];\n const activeField = fields.find(f => f.isActiveStatusColumn) || fields.find(f => normalizeText(f.sqlColumnName) === 'isactive');\n const deletedField = fields.find(f => f.isDeletedStatusColumn) || fields.find(f => normalizeText(f.sqlColumnName) === 'isdeleted');\n\n const asksActive = qNorm.split(' ').includes('aktif') || qNorm.split(' ').includes('active') || qNorm.split(' ').includes('etkin');\n const asksPassive = qNorm.split(' ').includes('pasif') || qNorm.split(' ').includes('inactive');\n\n if (activeField && asksActive) where.push(`${baseAlias}.${bracketIdentifier(activeField.sqlColumnName)} = 1`);\n if (activeField && asksPassive) where.push(`${baseAlias}.${bracketIdentifier(activeField.sqlColumnName)} = 0`);\n if (deletedField) where.push(`${baseAlias}.${bracketIdentifier(deletedField.sqlColumnName)} = 0`);\n\n // ÖZET / PIVOT / GROUPING: SELECT * asla kullanılmaz. GROUP BY alan + COUNT(1) döner.\n if (resultMode === 'summary' || resultMode === 'pivot') {\n const groupFields = pickGroupFields(fields, questionText, 2);\n\n if (groupFields.length > 0) {\n const groupExpressions = groupFields.map(f => `${baseAlias}.${bracketIdentifier(f.sqlColumnName)}`);\n const selectParts = groupFields.map(f => `${baseAlias}.${bracketIdentifier(f.sqlColumnName)} AS ${bracketAlias(f.displayName, f.sqlColumnName)}`);\n\n selectParts.push(`COUNT(1) AS [Adet]`);\n\n // Kullanıcı açıkça toplam isterse, ilk uygun sayısal kolonları da topla.\n const wantsTotal = qNorm.split(' ').includes('toplam') || qNorm.split(' ').includes('sum');\n if (wantsTotal) {\n const numericFields = fields\n .filter(f => isNumericSqlType(f.sqlDataType))\n .filter(f => !/^id$/i.test(f.sqlColumnName))\n .slice(0, 3);\n\n for (const nf of numericFields) {\n selectParts.push(`SUM(TRY_CONVERT(DECIMAL(38, 6), ${baseAlias}.${bracketIdentifier(nf.sqlColumnName)})) AS ${bracketAlias((nf.displayName || nf.sqlColumnName) + ' Toplam', nf.sqlColumnName + ' Toplam')}`);\n }\n }\n\n return `SELECT ${selectParts.join(', ')} FROM [${tenantName}].[dbo].${bracketIdentifier(form.tableName)} AS ${baseAlias}` +\n (where.length ? ' WHERE ' + where.join(' AND ') : '') +\n ` GROUP BY ${groupExpressions.join(', ')} ORDER BY [Adet] DESC`;\n }\n\n return `SELECT COUNT(1) AS [Adet] FROM [${tenantName}].[dbo].${bracketIdentifier(form.tableName)} AS ${baseAlias}` +\n (where.length ? ' WHERE ' + where.join(' AND ') : '');\n }\n\n let selectedFields = fields.filter(f => f.isDefaultSelectColumn);\n if (selectedFields.length === 0) selectedFields = fields.slice(0, 12);\n\n selectedFields = selectedFields\n .filter(f => !/^id$/i.test(f.sqlColumnName))\n .slice(0, Math.max(1, Math.min(Number(body.maxSelectColumns || 10), 20)));\n\n const joins = [];\n const lookupAliasByColumn = new Map();\n let joinIndex = 1;\n\n for (const field of selectedFields) {\n if (!field.hasLookup || !field.lookup?.tableName || !field.lookup?.valueColumn || !field.lookup?.displayColumn) continue;\n\n const lookupAlias = aliasFor(joinIndex++);\n lookupAliasByColumn.set(field.sqlColumnName, lookupAlias);\n\n const baseColumn = bracketIdentifier(field.sqlColumnName);\n const valueColumn = bracketIdentifier(field.lookup.valueColumn);\n const lookupTable = bracketIdentifier(field.lookup.tableName);\n\n const joinCondition = field.lookup.joinMode === 'direct-string'\n ? `${baseAlias}.${baseColumn} = ${lookupAlias}.${valueColumn}`\n : `CONVERT(NVARCHAR(100), ${baseAlias}.${baseColumn}) = CONVERT(NVARCHAR(100), ${lookupAlias}.${valueColumn})`;\n\n joins.push(`LEFT JOIN [${tenantName}].[dbo].${lookupTable} AS ${lookupAlias} ON ${joinCondition}`);\n }\n\n const selectParts = selectedFields.map(field =>\n sqlExpressionForField(baseAlias, lookupAliasByColumn.get(field.sqlColumnName) || '[L0]', field)\n );\n\n if (selectParts.length === 0) {\n selectParts.push(`${baseAlias}.${bracketIdentifier(form.primaryKeyColumn || 'Id')} AS [Id]`);\n }\n\n return `SELECT ${selectParts.join(', ')} FROM [${tenantName}].[dbo].${bracketIdentifier(form.tableName)} AS ${baseAlias}` +\n (joins.length ? ' ' + joins.join(' ') : '') +\n (where.length ? ' WHERE ' + where.join(' AND ') : '');\n}\n\n\nconst directSql = buildDirectSql(selectedForm, question);\n\nconst systemPrompt = `Sen Microsoft SQL Server T-SQL üretim motorusun.\nSadece SELECT sorgusu döndür. Açıklama, özet, yorum, JSON açıklaması, markdown veya kod bloğu yazma.\nİlk kelime kesinlikle SELECT olmalı.\nSadece verilen JSON veri sözlüğündeki form, tablo, kolon ve lookup bilgilerini kullan.\nTablo referansları [${tenantName}].[dbo].[TableName] formatında olmalı.\nSELECT * kullanma.\nINSERT, UPDATE, DELETE, MERGE, DROP, ALTER, TRUNCATE, CREATE, EXEC, EXECUTE üretme.\nLookup alanlarda kullanıcı ID istemedikçe lookup.displayColumn değerini LEFT JOIN ile göster.\nlookup.valueColumn değerini kolon adı olarak aynen kullan. \"LookupQuery:\" gibi metinler kolon adı değildir.\nlookup.joinMode direct-string ise ON [T].[baseColumn] = [Lx].[lookup.valueColumn] kullan.\nlookup.joinMode convert-nvarchar-safe ise ON CONVERT(NVARCHAR(100), [T].[baseColumn]) = CONVERT(NVARCHAR(100), [Lx].[lookup.valueColumn]) kullan.\nBoolean alanları SELECT çıktısında CASE ile Yes/No olarak göster.\nAktif/pasif filtreleri yalnızca veri sözlüğünde uygun aktiflik kolonu varsa uygulanır.\nHassas alanları döndürme.\nKullanıcı listele/getir/göster/select derse normal satır listesi üret.\nKullanıcı özetle/özet/toplam/adet/kaç/sayısı derse aggregate üret: SELECT grupKolonu, COUNT(1) AS [Adet] ... GROUP BY grupKolonu.\nKullanıcı bazında/kırılım/gruplandır/grouping/pivot derse ilgili alanı GROUP BY yap ve COUNT(1) AS [Adet] döndür.\nGROUP BY varsa SELECT * kullanmak kesinlikle yasaktır.\nGROUP BY varsa SELECT içindeki her düz kolon GROUP BY içinde de olmalı veya aggregate fonksiyonda olmalı.\nSQL Server PIVOT için değerleri bilmiyorsan PIVOT operatörü kullanma; GROUP BY ile pivotlanabilir sonuç üret.`;\n\nconst userContent = `Kullanıcı isteği: ${question}\n\nADAY_VERI_SOZLUGU_JSON:\n${JSON.stringify({ listForms: compactForms }, null, 2)}\n\nDETERMINISTIK_SQL:\n${directSql}\n\nDeterministik SQL kullanıcı isteğini karşılıyorsa aynen döndür.`;\n\nreturn [{\n json: {\n valid: true,\n question,\n tenantName,\n dictionaryMeta: {\n cacheKey: row.CacheKey,\n refreshedAt: row.RefreshedAt,\n formCount: row.FormCount,\n fieldCount: row.FieldCount,\n jsonLength: row.JsonLength\n },\n candidateFormCodes: compactForms.map(f => f.formCode),\n formScores: scored.slice(0, maxCandidateForms).map(x => ({ formCode: x.form.formCode, tableName: x.form.tableName, score: x.score })),\n selectedFormCode: selectedForm?.formCode || null,\n selectedTableName: selectedForm?.tableName || null,\n directSql,\n compactDictionary: { listForms: compactForms },\n allowedTables: [...allowedTables],\n ollamaBody: {\n model: body.sqlModel || 'gemma3:4b',\n stream: false,\n options: {\n temperature: 0,\n top_p: 0.05,\n num_ctx: Math.max(1024, Math.min(Number(body.numCtx || 2048), 4096)),\n num_predict: Math.max(256, Math.min(Number(body.numPredict || 600), 900)),\n stop: [';\\n\\n', 'Açıklama:', 'Explanation:', 'Not:', '```']\n },\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userContent }\n ]\n }\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
- -31344,
- -3360
+ -37808,
+ -3248
],
- "id": "7b44191c-172d-464b-ba11-bdd5728d25fc",
+ "id": "61007e7c-6f51-4572-ad61-13d9f0bffa4c",
"name": "Build SQL Request"
},
{
@@ -201,23 +201,23 @@
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
- -31104,
- -3360
+ -37568,
+ -3248
],
- "id": "4ca40c6f-3605-4e7c-8beb-2d25784094d8",
+ "id": "2b6630c2-f520-472a-8733-06e362264849",
"name": "SQL Ollama"
},
{
"parameters": {
- "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(/"/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(/"/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(/"/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 parts = String(m[1]).split('.');\n tables.push(cleanName(parts[parts.length - 1]));\n }\n return tables;\n}\n\nfunction validateCandidate(sqlText, sourceName) {\n let sql = cleanSql(sqlText);\n if (!sql) return { ok: false, error: 'SELECT bulunamadı' };\n if (!/^SELECT\\b/i.test(sql)) return { ok: false, error: 'Üretilen çıktı SELECT ile başlamıyor.' };\n\n const forbidden = /\\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|TRUNCATE|CREATE|EXEC|EXECUTE|GRANT|REVOKE|BACKUP|RESTORE)\\b/i;\n if (forbidden.test(sql)) {\n return { ok: false, error: 'Güvenlik nedeniyle sadece SELECT sorgularına izin veriliyor.' };\n }\n\n sql = qualifyTables(sql);\n sql = cleanSql(sql);\n\n if (/LookupQuery\\s*:/i.test(sql)) {\n return { ok: false, error: 'SQL içinde hatalı LookupQuery kolon formatı var.', cleanedText: sql };\n }\n\n const doubleDbRe = new RegExp('\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[dbo\\\\]', 'i');\n if (doubleDbRe.test(sql)) {\n return { ok: false, error: 'SQL içinde çift database formatı var.', cleanedText: sql };\n }\n\n const usedTables = extractTables(sql);\n const unknown = usedTables.filter(t => !allowedTables.has(cleanName(t)));\n if (unknown.length) {\n return { ok: false, error: 'Veri sözlüğünde olmayan tablo kullanıldı.', unknownTables: unknown, usedTables, cleanedText: sql };\n }\n\n return { ok: true, sql, usedTables, sourceName };\n}\n\n// Önemli: Model bazen doğru SQL'i yorumluyor/eleştiriyor. Bu yüzden önce deterministik directSql kullanılır.\nlet candidate = build.directSql\n ? validateCandidate(build.directSql, 'directSql')\n : { ok: false, error: 'directSql yok' };\n\n// directSql yoksa veya geçersizse Ollama çıktısı denenir.\nif (!candidate.ok) {\n candidate = validateCandidate(raw, 'ollama');\n}\n\nif (!candidate.ok) {\n return [{\n json: {\n valid: false,\n type: 'sql_error',\n question,\n error: candidate.error || 'SQL doğrulanamadı.',\n unknownTables: candidate.unknownTables || null,\n allowedTables: Array.from(allowedTables),\n generatedText: raw,\n cleanedText: candidate.cleanedText || null,\n directSql: build.directSql || null,\n selectedFormCode: build.selectedFormCode,\n candidateFormCodes: build.candidateFormCodes\n }\n }];\n}\n\nreturn [{\n json: {\n valid: true,\n type: 'sql',\n question,\n tenantName,\n sql: candidate.sql,\n sqlSource: candidate.sourceName,\n generatedText: raw,\n directSql: build.directSql,\n usedTables: candidate.usedTables,\n dictionaryMeta: build.dictionaryMeta,\n candidateFormCodes: build.candidateFormCodes,\n selectedFormCode: build.selectedFormCode\n }\n}];\n"
+ "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(/"/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(/"/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(/"/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 parts = String(m[1]).split('.');\n tables.push(cleanName(parts[parts.length - 1]));\n }\n return tables;\n}\n\n\nfunction splitSqlListPreserve(text) {\n const s = String(text || '');\n const parts = [];\n let cur = '';\n let depth = 0;\n let bracket = 0;\n let quote = null;\n\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n const next = s[i + 1];\n\n if (quote) {\n cur += ch;\n if (ch === quote) {\n if (next === quote) {\n cur += next;\n i++;\n } else {\n quote = null;\n }\n }\n continue;\n }\n\n if (ch === \"'\" || ch === '\"') {\n quote = ch;\n cur += ch;\n continue;\n }\n\n if (ch === '[') bracket++;\n if (ch === ']') bracket = Math.max(0, bracket - 1);\n if (ch === '(') depth++;\n if (ch === ')') depth = Math.max(0, depth - 1);\n\n if (ch === ',' && depth === 0 && bracket === 0) {\n parts.push(cur.trim());\n cur = '';\n continue;\n }\n\n cur += ch;\n }\n\n if (cur.trim()) parts.push(cur.trim());\n return parts;\n}\n\nfunction aliasFromGroupExpression(expr) {\n let s = String(expr || '').trim();\n s = s.replace(/\\s+ASC$|\\s+DESC$/i, '').trim();\n const parts = s.split('.');\n s = parts[parts.length - 1] || s;\n s = cleanName(s) || 'Grup';\n return '[' + s.replace(/]/g, ']]') + ']';\n}\n\nfunction repairGroupByStar(sql) {\n let q = String(sql || '').trim();\n\n if (!/\\bGROUP\\s+BY\\b/i.test(q)) return q;\n if (!/^SELECT\\s+\\*/i.test(q)) return q;\n\n const m = q.match(/^SELECT\\s+\\*\\s+FROM\\s+(.+?)\\s+GROUP\\s+BY\\s+(.+?)(?:\\s+ORDER\\s+BY\\s+.+)?$/i);\n if (!m) return q;\n\n const fromPart = m[1].trim();\n const groupPart = m[2].trim();\n const groupExprs = splitSqlListPreserve(groupPart);\n\n if (!groupExprs.length) return q;\n\n const selectParts = groupExprs.map(expr => `${expr} AS ${aliasFromGroupExpression(expr)}`);\n selectParts.push('COUNT(1) AS [Adet]');\n\n return `SELECT ${selectParts.join(', ')} FROM ${fromPart} GROUP BY ${groupExprs.join(', ')} ORDER BY [Adet] DESC`;\n}\n\nfunction validateGroupByRules(sql) {\n if (!/\\bGROUP\\s+BY\\b/i.test(sql)) return { ok: true };\n\n if (/^SELECT\\s+\\*/i.test(sql)) {\n return { ok: false, error: 'GROUP BY ile SELECT * kullanılamaz.' };\n }\n\n return { ok: true };\n}\n\n\nfunction validateCandidate(sqlText, sourceName) {\n let sql = cleanSql(sqlText);\n if (!sql) return { ok: false, error: 'SELECT bulunamadı' };\n if (!/^SELECT\\b/i.test(sql)) return { ok: false, error: 'Üretilen çıktı SELECT ile başlamıyor.' };\n\n const forbidden = /\\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|TRUNCATE|CREATE|EXEC|EXECUTE|GRANT|REVOKE|BACKUP|RESTORE)\\b/i;\n if (forbidden.test(sql)) {\n return { ok: false, error: 'Güvenlik nedeniyle sadece SELECT sorgularına izin veriliyor.' };\n }\n\n sql = qualifyTables(sql);\n sql = cleanSql(sql);\n\n sql = repairGroupByStar(sql);\n sql = qualifyTables(sql);\n sql = cleanSql(sql);\n\n const groupCheck = validateGroupByRules(sql);\n if (!groupCheck.ok) {\n return { ok: false, error: groupCheck.error, cleanedText: sql };\n }\n\n if (/LookupQuery\\s*:/i.test(sql)) {\n return { ok: false, error: 'SQL içinde hatalı LookupQuery kolon formatı var.', cleanedText: sql };\n }\n\n const doubleDbRe = new RegExp('\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[' + escapeRegExp(tenantName) + '\\\\]\\\\.\\\\[dbo\\\\]', 'i');\n if (doubleDbRe.test(sql)) {\n return { ok: false, error: 'SQL içinde çift database formatı var.', cleanedText: sql };\n }\n\n const usedTables = extractTables(sql);\n const unknown = usedTables.filter(t => !allowedTables.has(cleanName(t)));\n if (unknown.length) {\n return { ok: false, error: 'Veri sözlüğünde olmayan tablo kullanıldı.', unknownTables: unknown, usedTables, cleanedText: sql };\n }\n\n return { ok: true, sql, usedTables, sourceName };\n}\n\n// Önemli: Model bazen doğru SQL'i yorumluyor/eleştiriyor. Bu yüzden önce deterministik directSql kullanılır.\nlet candidate = build.directSql\n ? validateCandidate(build.directSql, 'directSql')\n : { ok: false, error: 'directSql yok' };\n\n// directSql yoksa veya geçersizse Ollama çıktısı denenir.\nif (!candidate.ok) {\n candidate = validateCandidate(raw, 'ollama');\n}\n\nif (!candidate.ok) {\n return [{\n json: {\n valid: false,\n type: 'sql_error',\n question,\n error: candidate.error || 'SQL doğrulanamadı.',\n unknownTables: candidate.unknownTables || null,\n allowedTables: Array.from(allowedTables),\n generatedText: raw,\n cleanedText: candidate.cleanedText || null,\n directSql: build.directSql || null,\n selectedFormCode: build.selectedFormCode,\n candidateFormCodes: build.candidateFormCodes\n }\n }];\n}\n\nreturn [{\n json: {\n valid: true,\n type: 'sql',\n question,\n tenantName,\n sql: candidate.sql,\n sqlSource: candidate.sourceName,\n generatedText: raw,\n directSql: build.directSql,\n usedTables: candidate.usedTables,\n dictionaryMeta: build.dictionaryMeta,\n candidateFormCodes: build.candidateFormCodes,\n selectedFormCode: build.selectedFormCode\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
- -30864,
- -3360
+ -37328,
+ -3248
],
- "id": "8d752e56-0e49-4bc9-8af4-4dade50bc690",
+ "id": "2a9ba43b-f687-4c27-a00b-841d912cc9a1",
"name": "Validate SQL"
},
{
@@ -248,10 +248,10 @@
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
- -30640,
- -3360
+ -37104,
+ -3248
],
- "id": "8a653bdf-3fc8-4cba-af8d-d3c827dfd739",
+ "id": "4445ba0d-c494-4389-be21-1b2468fade63",
"name": "SQL Valid?"
},
{
@@ -262,10 +262,10 @@
"type": "n8n-nodes-base.microsoftSql",
"typeVersion": 1.1,
"position": [
- -30416,
- -3456
+ -36880,
+ -3344
],
- "id": "13a61ae4-27e2-4b0b-ab81-636281d81812",
+ "id": "ec109600-5013-4b38-aeee-be8f2b654864",
"name": "Execute Generated SQL",
"credentials": {
"microsoftSql": {
@@ -276,16 +276,16 @@
},
{
"parameters": {
- "jsCode": "\nconst rows = $input.all().map(item => item.json);\nconst v = $('Validate SQL').first().json || {};\nreturn [{ json: {\n type: 'sql_result',\n question: v.question,\n sql: v.sql,\n rowCount: rows.length,\n columns: rows.length > 0 ? Object.keys(rows[0]) : [],\n rows,\n debug: { usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n}}];\n"
+ "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 }\n }\n\n const pivotRows = Object.values(map).sort((a, b) => (b.Adet || 0) - (a.Adet || 0));\n\n return [{\n json: {\n type: 'sql_result',\n mode: 'pivot',\n question: v.question,\n sql: v.sql,\n groupColumn,\n valueColumn,\n rowCount: pivotRows.length,\n columns: Object.keys(pivotRows[0] || {}),\n rows: pivotRows,\n debug: { sqlSource: v.sqlSource, usedTables: v.usedTables, candidateFormCodes: v.candidateFormCodes, dictionaryMeta: v.dictionaryMeta }\n }\n }];\n}\n\nreturn [{\n json: {\n type: 'sql_result',\n mode: 'list',\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"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
- -30176,
- -3456
+ -36640,
+ -3344
],
- "id": "c6325122-ac66-4ecc-845b-61d8b8b9a0c2",
- "name": "Format SQL Result"
+ "id": "4797a41c-acb6-4a8b-b66c-e3cebc230dae",
+ "name": "Format Smart SQL Result"
},
{
"parameters": {
@@ -294,10 +294,10 @@
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
- -30416,
- -3216
+ -36880,
+ -3104
],
- "id": "6969c1e6-1c67-4305-a4c3-937e66ce3dea",
+ "id": "ce66daea-a390-43ae-bd7d-0a00d80c86be",
"name": "SQL Error Response"
}
],
@@ -464,7 +464,7 @@
"main": [
[
{
- "node": "Format SQL Result",
+ "node": "Format Smart SQL Result",
"type": "main",
"index": 0
}
@@ -479,7 +479,7 @@
"timeSavedMode": "fixed",
"callerPolicy": "workflowsFromSameOwner"
},
- "versionId": "22ff5d0b-ab86-47fb-8251-c38258384419",
+ "versionId": "39108360-6275-4878-8546-69b1baa0c871",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "1d288821beaaeeada5e8dce6f282c802098a0c83ef6ddb35a174b09a9d43850e"
diff --git a/configs/ai/Kurulum.txt b/configs/ai/Kurulum.txt
new file mode 100644
index 0000000..f0d9983
--- /dev/null
+++ b/configs/ai/Kurulum.txt
@@ -0,0 +1,3 @@
+docker exec -it ollama ollama list
+docker exec -it ollama ollama pull qwen2.5:3b
+docker exec -it ollama ollama pull gemma3:4b
\ No newline at end of file
diff --git a/ui/src/views/ai/Assistant.tsx b/ui/src/views/ai/Assistant.tsx
index 32f48b3..a60150a 100644
--- a/ui/src/views/ai/Assistant.tsx
+++ b/ui/src/views/ai/Assistant.tsx
@@ -428,7 +428,7 @@ const Assistant = () => {
+
-
+
{cleanedSql}
@@ -439,7 +439,7 @@ const Assistant = () => {
Sonuç bulunamadı.
) : (
-
+
@@ -730,7 +730,7 @@ const Assistant = () => {
)}
{renderMessageContent(msg)}