epa-testeprojetoia 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.idea/epa_mcp.iml +8 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/php.xml +19 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/AGENTS.md +10 -0
  6. package/README.md +339 -0
  7. package/dist/agent/agentHelpers.js +21 -0
  8. package/dist/agent/openaiAgent.js +170 -0
  9. package/dist/agent/slidingWindow.js +13 -0
  10. package/dist/api/epaApiClient.js +59 -0
  11. package/dist/cli/index.js +47 -0
  12. package/dist/config/credentialStore.js +92 -0
  13. package/dist/config/ensureCliAuth.js +127 -0
  14. package/dist/config/loadConfig.js +40 -0
  15. package/dist/config/setup.js +23 -0
  16. package/dist/core/createReferenceTool.js +12 -0
  17. package/dist/core/createTool.js +32 -0
  18. package/dist/mocks/requestMocks.js +47 -0
  19. package/dist/server/server.js +41 -0
  20. package/dist/server/stdioServer.js +3 -0
  21. package/dist/services/request/requestFilters.js +1 -0
  22. package/dist/services/requestService.js +81 -0
  23. package/dist/services/teamService.js +18 -0
  24. package/dist/sql/createSqlConnection.js +13 -0
  25. package/dist/sql/fetchSchemaSummary.js +38 -0
  26. package/dist/sql/generateSqlFromQuestion.js +83 -0
  27. package/dist/sql/generateSqlPlan.js +111 -0
  28. package/dist/sql/getSqlAgentErrorMessage.js +15 -0
  29. package/dist/sql/loadSqlAgentConfig.js +24 -0
  30. package/dist/sql/loadSqlConfig.js +43 -0
  31. package/dist/sql/parseSqlQuestionHints.js +29 -0
  32. package/dist/sql/runSqlAgentCli.js +163 -0
  33. package/dist/sql/runSqlCli.js +34 -0
  34. package/dist/sql/selectRelevantTables.js +136 -0
  35. package/dist/sql/sqlGuard.js +21 -0
  36. package/dist/sql/sqlPlan.js +16 -0
  37. package/dist/tests/requestService.test.js +110 -0
  38. package/dist/tools/analytics/teamReport.draft.js +16 -0
  39. package/dist/tools/loadTools.js +40 -0
  40. package/dist/tools/requests/assignees.js +50 -0
  41. package/dist/tools/requests/clients.draft.js +10 -0
  42. package/dist/tools/requests/create.draft.js +51 -0
  43. package/dist/tools/requests/list.js +50 -0
  44. package/dist/tools/requests/priorities.js +2 -0
  45. package/dist/tools/requests/services.draft.js +20 -0
  46. package/dist/tools/requests/types.js +16 -0
  47. package/dist/tools/requests/units.draft.js +10 -0
  48. package/dist/tools/requests/view.draft.js +20 -0
  49. package/dist/utils/buildDateRange.js +18 -0
  50. package/dist/utils/findIdByDescription.js +4 -0
  51. package/dist/utils/resolveAssigneeId.js +14 -0
  52. package/dist/utils/toolNameMaps.js +23 -0
  53. package/package.json +31 -0
  54. package/src/agent/agentHelpers.ts +25 -0
  55. package/src/agent/openaiAgent.ts +205 -0
  56. package/src/agent/slidingWindow.ts +17 -0
  57. package/src/api/epaApiClient.ts +82 -0
  58. package/src/cli/index.ts +61 -0
  59. package/src/config/credentialStore.ts +130 -0
  60. package/src/config/ensureCliAuth.ts +152 -0
  61. package/src/config/loadConfig.ts +62 -0
  62. package/src/config/setup.ts +35 -0
  63. package/src/core/createReferenceTool.ts +17 -0
  64. package/src/core/createTool.ts +51 -0
  65. package/src/mocks/requestMocks.ts +52 -0
  66. package/src/server/server.ts +61 -0
  67. package/src/server/stdioServer.ts +5 -0
  68. package/src/services/request/requestFilters.ts +12 -0
  69. package/src/services/requestService.ts +126 -0
  70. package/src/services/teamService.ts +27 -0
  71. package/src/sql/createSqlConnection.ts +15 -0
  72. package/src/sql/fetchSchemaSummary.ts +64 -0
  73. package/src/sql/generateSqlFromQuestion.ts +105 -0
  74. package/src/sql/generateSqlPlan.ts +133 -0
  75. package/src/sql/getSqlAgentErrorMessage.ts +24 -0
  76. package/src/sql/loadSqlAgentConfig.ts +33 -0
  77. package/src/sql/loadSqlConfig.ts +75 -0
  78. package/src/sql/parseSqlQuestionHints.ts +46 -0
  79. package/src/sql/runSqlAgentCli.ts +204 -0
  80. package/src/sql/runSqlCli.ts +40 -0
  81. package/src/sql/selectRelevantTables.ts +184 -0
  82. package/src/sql/sqlGuard.ts +28 -0
  83. package/src/sql/sqlPlan.ts +28 -0
  84. package/src/tests/requestService.test.ts +152 -0
  85. package/src/tools/analytics/teamReport.draft.ts +25 -0
  86. package/src/tools/loadTools.ts +59 -0
  87. package/src/tools/requests/assignees.ts +59 -0
  88. package/src/tools/requests/clients.draft.ts +18 -0
  89. package/src/tools/requests/create.draft.ts +59 -0
  90. package/src/tools/requests/list.ts +57 -0
  91. package/src/tools/requests/priorities.ts +6 -0
  92. package/src/tools/requests/services.draft.ts +24 -0
  93. package/src/tools/requests/types.ts +18 -0
  94. package/src/tools/requests/units.draft.ts +18 -0
  95. package/src/tools/requests/view.draft.ts +27 -0
  96. package/src/utils/buildDateRange.ts +22 -0
  97. package/src/utils/findIdByDescription.ts +10 -0
  98. package/src/utils/resolveAssigneeId.ts +24 -0
  99. package/src/utils/toolNameMaps.ts +33 -0
  100. package/tsconfig.json +11 -0
@@ -0,0 +1,83 @@
1
+ import OpenAI from "openai";
2
+ import { loadSqlAgentConfig } from "./loadSqlAgentConfig.js";
3
+ function extractSql(content) {
4
+ const fencedMatch = content.match(/```(?:sql)?\s*([\s\S]*?)```/i);
5
+ if (fencedMatch) {
6
+ return fencedMatch[1].trim();
7
+ }
8
+ return content.trim();
9
+ }
10
+ function buildHintsSummary(hints) {
11
+ const lines = [];
12
+ if (hints.tableNames.length > 0) {
13
+ lines.push(`Tabelas indicadas pelo usuario: ${hints.tableNames.join(", ")}`);
14
+ }
15
+ if (hints.fieldNames.length > 0) {
16
+ lines.push(`Campos indicados pelo usuario: ${hints.fieldNames.join(", ")}`);
17
+ }
18
+ if (hints.filters.length > 0) {
19
+ lines.push(`Filtros indicados pelo usuario: ${hints.filters.join(", ")}`);
20
+ }
21
+ if (hints.orderBy.length > 0) {
22
+ lines.push(`Ordenacao indicada pelo usuario: ${hints.orderBy.join(", ")}`);
23
+ }
24
+ if (hints.limit) {
25
+ lines.push(`Limite indicado pelo usuario: ${hints.limit}`);
26
+ }
27
+ return lines.length > 0 ? lines.join("\n") : "Nenhuma pista explicita informada.";
28
+ }
29
+ export async function generateSqlFromQuestion(question, schemaSummary, hints, plan, additionalGuidance) {
30
+ const sqlAgentConfig = loadSqlAgentConfig();
31
+ const openai = new OpenAI({ apiKey: sqlAgentConfig.openaiApiKey });
32
+ const completion = await openai.chat.completions.create({
33
+ model: sqlAgentConfig.model,
34
+ temperature: 0,
35
+ messages: [
36
+ {
37
+ role: "system",
38
+ content: `
39
+ Voce e um assistente tecnico que gera SQL para MySQL/MariaDB.
40
+
41
+ Regras obrigatorias:
42
+ - gere apenas uma consulta SQL
43
+ - responda somente com SQL, sem explicacoes
44
+ - use apenas SELECT
45
+ - nao finalize a resposta com ponto e virgula
46
+ - nunca use INSERT, UPDATE, DELETE, ALTER, DROP, TRUNCATE, CREATE, REPLACE, GRANT, REVOKE, EXECUTE ou CALL
47
+ - quando o pedido for listar registros e o usuario nao informar quantidade, aplique LIMIT 10
48
+ - use somente tabelas e colunas presentes no schema informado
49
+ - priorize tabelas com nome diretamente relacionado ao pedido do usuario
50
+ - se houver tabela com nome muito proximo ao termo pedido, prefira essa tabela em vez de nomes genericos ou sem relacao clara
51
+ - quando o usuario informar explicitamente nomes de tabelas ou campos, trate essa informacao como instrucao prioritaria
52
+ - quando o usuario indicar um campo de relacionamento e outra tabela para vinculo, gere o JOIN usando essas pistas, desde que as colunas existam no schema informado
53
+ - quando o usuario indicar filtros, ordenacao ou limite, respeite essas instrucoes na SQL final
54
+ - siga o plano de consulta aprovado pelo usuario
55
+ `.trim()
56
+ },
57
+ {
58
+ role: "user",
59
+ content: `
60
+ Schema disponivel:
61
+ ${schemaSummary}
62
+
63
+ Pistas explicitas do usuario:
64
+ ${buildHintsSummary(hints)}
65
+
66
+ Plano aprovado:
67
+ ${JSON.stringify(plan, null, 2)}
68
+
69
+ Orientacoes adicionais:
70
+ ${additionalGuidance?.trim() || "Nenhuma orientacao adicional."}
71
+
72
+ Pedido do usuario:
73
+ ${question}
74
+ `.trim()
75
+ }
76
+ ]
77
+ });
78
+ const content = completion.choices[0]?.message?.content ?? "";
79
+ if (!content.trim()) {
80
+ throw new Error("A LLM nao retornou SQL para o pedido informado.");
81
+ }
82
+ return extractSql(content);
83
+ }
@@ -0,0 +1,111 @@
1
+ import OpenAI from "openai";
2
+ import { loadSqlAgentConfig } from "./loadSqlAgentConfig.js";
3
+ function buildHintsSummary(hints) {
4
+ const lines = [];
5
+ if (hints.tableNames.length > 0) {
6
+ lines.push(`Tabelas indicadas pelo usuario: ${hints.tableNames.join(", ")}`);
7
+ }
8
+ if (hints.fieldNames.length > 0) {
9
+ lines.push(`Campos indicados pelo usuario: ${hints.fieldNames.join(", ")}`);
10
+ }
11
+ if (hints.filters.length > 0) {
12
+ lines.push(`Filtros indicados pelo usuario: ${hints.filters.join(", ")}`);
13
+ }
14
+ if (hints.orderBy.length > 0) {
15
+ lines.push(`Ordenacao indicada pelo usuario: ${hints.orderBy.join(", ")}`);
16
+ }
17
+ if (hints.limit) {
18
+ lines.push(`Limite indicado pelo usuario: ${hints.limit}`);
19
+ }
20
+ return lines.length > 0 ? lines.join("\n") : "Nenhuma pista explicita informada.";
21
+ }
22
+ function extractJson(content) {
23
+ const fencedMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
24
+ if (fencedMatch) {
25
+ return fencedMatch[1].trim();
26
+ }
27
+ return content.trim();
28
+ }
29
+ function normalizePlan(rawPlan) {
30
+ return {
31
+ goal: String(rawPlan.goal ?? "").trim(),
32
+ mainTable: String(rawPlan.mainTable ?? "").trim(),
33
+ relatedTables: Array.isArray(rawPlan.relatedTables)
34
+ ? rawPlan.relatedTables.map((value) => String(value).trim()).filter(Boolean)
35
+ : [],
36
+ relationshipFields: Array.isArray(rawPlan.relationshipFields)
37
+ ? rawPlan.relationshipFields.map((value) => String(value).trim()).filter(Boolean)
38
+ : [],
39
+ selectedFields: Array.isArray(rawPlan.selectedFields)
40
+ ? rawPlan.selectedFields.map((value) => String(value).trim()).filter(Boolean)
41
+ : [],
42
+ filters: Array.isArray(rawPlan.filters)
43
+ ? rawPlan.filters.map((value) => String(value).trim()).filter(Boolean)
44
+ : [],
45
+ orderBy: Array.isArray(rawPlan.orderBy)
46
+ ? rawPlan.orderBy.map((value) => String(value).trim()).filter(Boolean)
47
+ : [],
48
+ limit: Number.isFinite(Number(rawPlan.limit)) && Number(rawPlan.limit) > 0
49
+ ? Number(rawPlan.limit)
50
+ : 10
51
+ };
52
+ }
53
+ export async function generateSqlPlan(question, schemaSummary, hints, additionalGuidance) {
54
+ const sqlAgentConfig = loadSqlAgentConfig();
55
+ const openai = new OpenAI({ apiKey: sqlAgentConfig.openaiApiKey });
56
+ const completion = await openai.chat.completions.create({
57
+ model: sqlAgentConfig.model,
58
+ temperature: 0,
59
+ messages: [
60
+ {
61
+ role: "system",
62
+ content: `
63
+ Voce e um assistente tecnico que monta um plano de consulta para MySQL/MariaDB antes da SQL.
64
+
65
+ Regras obrigatorias:
66
+ - responda somente com JSON valido
67
+ - use apenas tabelas e colunas presentes no schema informado
68
+ - respeite pistas explicitas do usuario como prioridade
69
+ - quando o usuario indicar tabelas, campos ou relacionamento, reflita isso no plano
70
+ - quando o usuario indicar filtros, ordenacao ou limite, reflita isso no plano
71
+ - preencha o limite com 10 quando o pedido for listar registros sem quantidade especifica
72
+
73
+ Formato do JSON:
74
+ {
75
+ "goal": "string",
76
+ "mainTable": "string",
77
+ "relatedTables": ["string"],
78
+ "relationshipFields": ["string"],
79
+ "selectedFields": ["string"],
80
+ "filters": ["string"],
81
+ "orderBy": ["string"],
82
+ "limit": 10
83
+ }
84
+ `.trim()
85
+ },
86
+ {
87
+ role: "user",
88
+ content: `
89
+ Schema disponivel:
90
+ ${schemaSummary}
91
+
92
+ Pistas explicitas do usuario:
93
+ ${buildHintsSummary(hints)}
94
+
95
+ Orientacoes adicionais:
96
+ ${additionalGuidance?.trim() || "Nenhuma orientacao adicional."}
97
+
98
+ Pedido do usuario:
99
+ ${question}
100
+ `.trim()
101
+ }
102
+ ]
103
+ });
104
+ const content = completion.choices[0]?.message?.content ?? "";
105
+ if (!content.trim()) {
106
+ throw new Error("A LLM nao retornou um plano de consulta.");
107
+ }
108
+ const json = extractJson(content);
109
+ const parsedPlan = JSON.parse(json);
110
+ return normalizePlan(parsedPlan);
111
+ }
@@ -0,0 +1,15 @@
1
+ export function getSqlAgentErrorMessage(error) {
2
+ if (!(error instanceof Error)) {
3
+ return `Erro ao executar SQL Agent: ${String(error)}`;
4
+ }
5
+ const openAiError = error;
6
+ if (openAiError.status === 429 ||
7
+ openAiError.code === "insufficient_quota" ||
8
+ openAiError.type === "insufficient_quota") {
9
+ return [
10
+ "Nao foi possivel gerar a SQL porque a chave da OpenAI esta sem cota disponivel.",
11
+ 'Verifique billing/limites da conta ou configure outra chave em "OPENAI_API_KEY" ou na CLI.'
12
+ ].join(" ");
13
+ }
14
+ return `Erro ao executar SQL Agent: ${error.message}`;
15
+ }
@@ -0,0 +1,24 @@
1
+ import dotenv from "dotenv";
2
+ import { readStoredCredentials } from "../config/credentialStore.js";
3
+ dotenv.config();
4
+ function getEnvValue(...keys) {
5
+ for (const key of keys) {
6
+ const value = process.env[key];
7
+ if (value && value.trim()) {
8
+ return value.trim();
9
+ }
10
+ }
11
+ return undefined;
12
+ }
13
+ export function loadSqlAgentConfig() {
14
+ const storedCredentials = readStoredCredentials();
15
+ const openaiApiKey = getEnvValue("OPENAI_API_KEY", "openaiApiKey") ??
16
+ storedCredentials.openaiApiKey;
17
+ if (!openaiApiKey) {
18
+ throw new Error('OpenAI API KEY nao encontrada. Defina "OPENAI_API_KEY" no ambiente ou configure a chave pela CLI.');
19
+ }
20
+ return {
21
+ openaiApiKey,
22
+ model: getEnvValue("SQL_AGENT_MODEL") ?? "gpt-4o-mini"
23
+ };
24
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import dotenv from "dotenv";
4
+ dotenv.config();
5
+ function readSqlConfigFile() {
6
+ const configPath = path.join(process.cwd(), "config", "config.json");
7
+ if (!fs.existsSync(configPath)) {
8
+ return undefined;
9
+ }
10
+ const raw = fs.readFileSync(configPath, "utf-8");
11
+ const json = JSON.parse(raw);
12
+ return json.sql;
13
+ }
14
+ function getEnvValue(...keys) {
15
+ for (const key of keys) {
16
+ const value = process.env[key];
17
+ if (value && value.trim()) {
18
+ return value.trim();
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+ export function loadSqlConfig() {
24
+ const fileConfig = readSqlConfigFile();
25
+ const host = getEnvValue("DB_HOST", "MYSQL_HOST") ?? fileConfig?.host;
26
+ const database = getEnvValue("DB_NAME", "MYSQL_DATABASE") ?? fileConfig?.database;
27
+ const user = getEnvValue("DB_USER", "MYSQL_USER") ?? fileConfig?.user;
28
+ const password = getEnvValue("DB_PASSWORD", "MYSQL_PASSWORD") ?? fileConfig?.password;
29
+ const portRaw = getEnvValue("DB_PORT", "MYSQL_PORT") ?? String(fileConfig?.port ?? "3306");
30
+ const sslRaw = String(getEnvValue("DB_SSL", "MYSQL_SSL") ?? fileConfig?.ssl ?? "false").toLowerCase();
31
+ const port = Number(portRaw);
32
+ if (!host || !database || !user || !password) {
33
+ throw new Error('Configuracao SQL incompleta. Defina DB_HOST, DB_NAME, DB_USER e DB_PASSWORD nas variaveis de ambiente ou preencha o bloco "sql" em config/config.json.');
34
+ }
35
+ return {
36
+ host,
37
+ port: Number.isFinite(port) && port > 0 ? port : 3306,
38
+ database,
39
+ user,
40
+ password,
41
+ ssl: sslRaw === "true" || sslRaw === "1"
42
+ };
43
+ }
@@ -0,0 +1,29 @@
1
+ function unique(values) {
2
+ return [...new Set(values)];
3
+ }
4
+ function extractMatches(question, pattern, captureIndex = 1) {
5
+ const matches = [];
6
+ for (const match of question.matchAll(pattern)) {
7
+ const value = match[captureIndex]?.trim();
8
+ if (value) {
9
+ matches.push(value);
10
+ }
11
+ }
12
+ return matches;
13
+ }
14
+ export function parseSqlQuestionHints(question) {
15
+ const explicitTables = extractMatches(question, /tabela\s*:\s*([a-zA-Z0-9_]+)/gi);
16
+ const relationshipTables = extractMatches(question, /(vincular com tabela|relacionar com tabela|join com tabela)\s*:?\s*([a-zA-Z0-9_]+)/gi, 2);
17
+ const explicitFields = extractMatches(question, /campo\s*:\s*([a-zA-Z0-9_]+)/gi);
18
+ const explicitFilters = extractMatches(question, /filtro\s*:\s*([^\n\r]+)/gi);
19
+ const explicitOrderBy = extractMatches(question, /ordenar por\s*:\s*([^\n\r]+)/gi);
20
+ const explicitLimitRaw = extractMatches(question, /limite\s*:\s*(\d+)/gi);
21
+ const explicitLimit = explicitLimitRaw.length > 0 ? Number(explicitLimitRaw[0]) : undefined;
22
+ return {
23
+ tableNames: unique([...explicitTables, ...relationshipTables]),
24
+ fieldNames: unique(explicitFields),
25
+ filters: unique(explicitFilters),
26
+ orderBy: unique(explicitOrderBy),
27
+ limit: Number.isFinite(explicitLimit) && explicitLimit && explicitLimit > 0 ? explicitLimit : undefined
28
+ };
29
+ }
@@ -0,0 +1,163 @@
1
+ import readline from "readline";
2
+ import { createSqlConnection } from "./createSqlConnection.js";
3
+ import { fetchAvailableTableNames, fetchSchemaSummary } from "./fetchSchemaSummary.js";
4
+ import { generateSqlPlan } from "./generateSqlPlan.js";
5
+ import { generateSqlFromQuestion } from "./generateSqlFromQuestion.js";
6
+ import { getSqlAgentErrorMessage } from "./getSqlAgentErrorMessage.js";
7
+ import { parseSqlQuestionHints } from "./parseSqlQuestionHints.js";
8
+ import { selectRelevantTables } from "./selectRelevantTables.js";
9
+ import { validateReadOnlySql } from "./sqlGuard.js";
10
+ import { formatSqlPlan } from "./sqlPlan.js";
11
+ function askQuestion(rl, question) {
12
+ return new Promise((resolve) => rl.question(question, resolve));
13
+ }
14
+ function normalizeQuestion(rawQuestion) {
15
+ return (rawQuestion ?? "").trim();
16
+ }
17
+ async function askForTableSelection(candidateTables) {
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout
21
+ });
22
+ try {
23
+ console.log("Encontrei mais de uma tabela possivel para esse pedido:");
24
+ candidateTables.forEach((table, index) => {
25
+ console.log(`${index + 1}. ${table.name}`);
26
+ });
27
+ console.log("M. Informar manualmente o nome da tabela");
28
+ console.log("");
29
+ while (true) {
30
+ const answer = (await askQuestion(rl, "Escolha o numero da tabela correta ou M para informar manualmente: ")).trim();
31
+ if (answer.toLowerCase() === "m") {
32
+ const manualTableName = (await askQuestion(rl, "Digite o nome exato da tabela: ")).trim();
33
+ if (manualTableName) {
34
+ return manualTableName;
35
+ }
36
+ console.log("Informe um nome de tabela valido.");
37
+ continue;
38
+ }
39
+ const selectedIndex = Number(answer);
40
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= candidateTables.length) {
41
+ return candidateTables[selectedIndex - 1].name;
42
+ }
43
+ console.log("Opcao invalida. Informe um numero da lista.");
44
+ }
45
+ }
46
+ finally {
47
+ rl.close();
48
+ }
49
+ }
50
+ async function askForPlanAction() {
51
+ const rl = readline.createInterface({
52
+ input: process.stdin,
53
+ output: process.stdout
54
+ });
55
+ try {
56
+ console.log("");
57
+ console.log("1. Executar esse plano");
58
+ console.log("2. Corrigir o plano");
59
+ console.log("3. Cancelar");
60
+ console.log("");
61
+ while (true) {
62
+ const answer = (await askQuestion(rl, "Escolha uma opcao: ")).trim();
63
+ if (answer === "1" || answer === "2" || answer === "3") {
64
+ return answer;
65
+ }
66
+ console.log("Opcao invalida. Escolha 1, 2 ou 3.");
67
+ }
68
+ }
69
+ finally {
70
+ rl.close();
71
+ }
72
+ }
73
+ async function askForPlanCorrection() {
74
+ const rl = readline.createInterface({
75
+ input: process.stdin,
76
+ output: process.stdout
77
+ });
78
+ try {
79
+ return normalizeQuestion(await askQuestion(rl, "Descreva a correcao do plano: "));
80
+ }
81
+ finally {
82
+ rl.close();
83
+ }
84
+ }
85
+ export async function runSqlAgentCli(initialQuestion) {
86
+ let question = normalizeQuestion(initialQuestion);
87
+ let additionalGuidance = "";
88
+ if (!question) {
89
+ const rl = readline.createInterface({
90
+ input: process.stdin,
91
+ output: process.stdout
92
+ });
93
+ try {
94
+ question = normalizeQuestion(await askQuestion(rl, "Pergunta para o SQL Agent: "));
95
+ }
96
+ finally {
97
+ rl.close();
98
+ }
99
+ }
100
+ let connection;
101
+ try {
102
+ if (!question) {
103
+ throw new Error("Informe uma pergunta para o SQL Agent.");
104
+ }
105
+ connection = await createSqlConnection();
106
+ const allTableNames = await fetchAvailableTableNames(connection);
107
+ if (allTableNames.length === 0) {
108
+ throw new Error("Nenhuma tabela foi encontrada no schema configurado.");
109
+ }
110
+ while (true) {
111
+ const composedQuestion = additionalGuidance
112
+ ? `${question}\nOrientacoes adicionais do usuario:\n${additionalGuidance}`
113
+ : question;
114
+ const hints = parseSqlQuestionHints(composedQuestion);
115
+ const tableSelection = selectRelevantTables(composedQuestion, allTableNames, 12, hints.tableNames);
116
+ const selectedTableNames = tableSelection.requiresConfirmation
117
+ ? [await askForTableSelection(tableSelection.candidateTables)]
118
+ : tableSelection.suggestedTables;
119
+ const schemaSummary = await fetchSchemaSummary(connection, selectedTableNames);
120
+ const plan = await generateSqlPlan(question, schemaSummary, hints, additionalGuidance);
121
+ console.log("");
122
+ console.log(formatSqlPlan(plan));
123
+ const action = await askForPlanAction();
124
+ if (action === "3") {
125
+ console.log("Execucao cancelada.");
126
+ return;
127
+ }
128
+ if (action === "2") {
129
+ const correction = await askForPlanCorrection();
130
+ if (!correction) {
131
+ console.log("Nenhuma correcao informada. Mantendo plano atual.");
132
+ continue;
133
+ }
134
+ additionalGuidance = additionalGuidance
135
+ ? `${additionalGuidance}\n${correction}`
136
+ : correction;
137
+ continue;
138
+ }
139
+ const generatedSql = await generateSqlFromQuestion(question, schemaSummary, hints, plan, additionalGuidance);
140
+ const sql = validateReadOnlySql(generatedSql);
141
+ const [rows] = await connection.query(sql);
142
+ console.log("");
143
+ console.log("Pergunta:");
144
+ console.log(question);
145
+ console.log("");
146
+ console.log("SQL gerada:");
147
+ console.log(sql);
148
+ console.log("");
149
+ console.log("Resultado:");
150
+ console.log(JSON.stringify(rows, null, 2));
151
+ return;
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.error(getSqlAgentErrorMessage(error));
156
+ process.exitCode = 1;
157
+ }
158
+ finally {
159
+ if (connection) {
160
+ await connection.end();
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,34 @@
1
+ import readline from "readline";
2
+ import { createSqlConnection } from "./createSqlConnection.js";
3
+ import { validateReadOnlySql } from "./sqlGuard.js";
4
+ function askQuestion(rl, question) {
5
+ return new Promise((resolve) => rl.question(question, resolve));
6
+ }
7
+ function normalizeQuery(rawQuery) {
8
+ return (rawQuery ?? "").trim();
9
+ }
10
+ export async function runSqlCli(initialQuery) {
11
+ const query = normalizeQuery(initialQuery);
12
+ let finalQuery = query;
13
+ if (!finalQuery) {
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout
17
+ });
18
+ try {
19
+ finalQuery = normalizeQuery(await askQuestion(rl, "SQL (somente SELECT): "));
20
+ }
21
+ finally {
22
+ rl.close();
23
+ }
24
+ }
25
+ validateReadOnlySql(finalQuery);
26
+ const connection = await createSqlConnection();
27
+ try {
28
+ const [rows] = await connection.query(finalQuery);
29
+ console.log(JSON.stringify(rows, null, 2));
30
+ }
31
+ finally {
32
+ await connection.end();
33
+ }
34
+ }
@@ -0,0 +1,136 @@
1
+ const QUESTION_STOP_WORDS = new Set([
2
+ "a",
3
+ "as",
4
+ "ao",
5
+ "aos",
6
+ "com",
7
+ "da",
8
+ "das",
9
+ "de",
10
+ "do",
11
+ "dos",
12
+ "e",
13
+ "em",
14
+ "na",
15
+ "nas",
16
+ "no",
17
+ "nos",
18
+ "o",
19
+ "os",
20
+ "ou",
21
+ "para",
22
+ "por",
23
+ "quais",
24
+ "qual",
25
+ "quero",
26
+ "traga",
27
+ "trazer",
28
+ "listar",
29
+ "liste",
30
+ "mostrar",
31
+ "mostre",
32
+ "me",
33
+ "uma",
34
+ "um"
35
+ ]);
36
+ function normalizeText(value) {
37
+ return value
38
+ .normalize("NFD")
39
+ .replace(/[\u0300-\u036f]/g, "")
40
+ .toLowerCase();
41
+ }
42
+ function buildTokenVariants(token) {
43
+ const variants = new Set([token]);
44
+ if (token.endsWith("es") && token.length > 4) {
45
+ variants.add(token.slice(0, -2));
46
+ }
47
+ if (token.endsWith("s") && token.length > 3) {
48
+ variants.add(token.slice(0, -1));
49
+ }
50
+ return [...variants];
51
+ }
52
+ function extractRelevantTerms(question) {
53
+ const normalizedQuestion = normalizeText(question);
54
+ const rawTokens = normalizedQuestion.match(/[a-z0-9_]+/g) ?? [];
55
+ return rawTokens
56
+ .filter((token) => token.length >= 3)
57
+ .filter((token) => !QUESTION_STOP_WORDS.has(token))
58
+ .flatMap(buildTokenVariants);
59
+ }
60
+ function scoreTableName(tableName, terms) {
61
+ if (terms.length === 0) {
62
+ return 0;
63
+ }
64
+ const normalizedTableName = normalizeText(tableName);
65
+ let score = 0;
66
+ for (const term of terms) {
67
+ if (normalizedTableName === term) {
68
+ score += 100;
69
+ continue;
70
+ }
71
+ if (normalizedTableName.startsWith(`${term}_`) || normalizedTableName.endsWith(`_${term}`)) {
72
+ score += 70;
73
+ continue;
74
+ }
75
+ if (normalizedTableName.includes(`_${term}_`)) {
76
+ score += 60;
77
+ continue;
78
+ }
79
+ if (normalizedTableName.includes(term)) {
80
+ score += 40;
81
+ }
82
+ }
83
+ return score;
84
+ }
85
+ function shouldAskForConfirmation(candidateTables) {
86
+ if (candidateTables.length <= 1) {
87
+ return false;
88
+ }
89
+ const [topCandidate, secondCandidate] = candidateTables;
90
+ if (!topCandidate || !secondCandidate) {
91
+ return false;
92
+ }
93
+ if (topCandidate.score === 0) {
94
+ return true;
95
+ }
96
+ if (topCandidate.score < 100) {
97
+ return true;
98
+ }
99
+ return secondCandidate.score >= topCandidate.score - 30;
100
+ }
101
+ function resolveExplicitTableNames(tableNames, explicitTableNames) {
102
+ if (explicitTableNames.length === 0) {
103
+ return [];
104
+ }
105
+ const normalizedMap = new Map(tableNames.map((tableName) => [normalizeText(tableName), tableName]));
106
+ return explicitTableNames
107
+ .map((tableName) => normalizedMap.get(normalizeText(tableName)) ?? tableName)
108
+ .filter((tableName, index, values) => values.indexOf(tableName) === index);
109
+ }
110
+ export function selectRelevantTables(question, tableNames, maxRelevantTables = 12, explicitTableNames = []) {
111
+ const terms = extractRelevantTerms(question);
112
+ const resolvedExplicitTables = resolveExplicitTableNames(tableNames, explicitTableNames);
113
+ const rankedTables = tableNames
114
+ .map((name) => ({
115
+ name,
116
+ score: scoreTableName(name, terms) + (resolvedExplicitTables.includes(name) ? 1000 : 0)
117
+ }))
118
+ .sort((left, right) => right.score - left.score || left.name.localeCompare(right.name));
119
+ const candidateTables = rankedTables
120
+ .filter((table) => table.score > 0)
121
+ .slice(0, maxRelevantTables);
122
+ const rankedSuggestedTables = candidateTables.length > 0
123
+ ? candidateTables.map((table) => table.name)
124
+ : rankedTables.slice(0, maxRelevantTables).map((table) => table.name);
125
+ const suggestedTables = [...resolvedExplicitTables];
126
+ for (const tableName of rankedSuggestedTables) {
127
+ if (!suggestedTables.includes(tableName)) {
128
+ suggestedTables.push(tableName);
129
+ }
130
+ }
131
+ return {
132
+ candidateTables,
133
+ suggestedTables: suggestedTables.slice(0, maxRelevantTables),
134
+ requiresConfirmation: resolvedExplicitTables.length === 0 && shouldAskForConfirmation(candidateTables)
135
+ };
136
+ }
@@ -0,0 +1,21 @@
1
+ const READ_ONLY_SQL_PATTERN = /^\s*select\b/i;
2
+ const FORBIDDEN_SQL_PATTERN = /\b(insert|update|delete|alter|drop|truncate|create|replace|grant|revoke|execute|call)\b/i;
3
+ export function normalizeReadOnlySql(query) {
4
+ return query.trim().replace(/;\s*$/, "");
5
+ }
6
+ export function validateReadOnlySql(query) {
7
+ const normalizedQuery = normalizeReadOnlySql(query);
8
+ if (!normalizedQuery) {
9
+ throw new Error("Informe uma consulta SQL.");
10
+ }
11
+ if (!READ_ONLY_SQL_PATTERN.test(normalizedQuery)) {
12
+ throw new Error("Somente consultas SELECT sao permitidas neste modo tecnico.");
13
+ }
14
+ if (FORBIDDEN_SQL_PATTERN.test(normalizedQuery)) {
15
+ throw new Error("A consulta contem comandos nao permitidos para modo read-only.");
16
+ }
17
+ if (normalizedQuery.includes(";")) {
18
+ throw new Error("Envie apenas uma consulta por vez.");
19
+ }
20
+ return normalizedQuery;
21
+ }
@@ -0,0 +1,16 @@
1
+ function formatList(values) {
2
+ return values.length > 0 ? values.join(", ") : "-";
3
+ }
4
+ export function formatSqlPlan(plan) {
5
+ return [
6
+ "Plano de consulta:",
7
+ `- objetivo: ${plan.goal || "-"}`,
8
+ `- tabela principal: ${plan.mainTable || "-"}`,
9
+ `- tabelas relacionadas: ${formatList(plan.relatedTables)}`,
10
+ `- campos de relacionamento: ${formatList(plan.relationshipFields)}`,
11
+ `- colunas de saida: ${formatList(plan.selectedFields)}`,
12
+ `- filtros: ${formatList(plan.filters)}`,
13
+ `- ordenacao: ${formatList(plan.orderBy)}`,
14
+ `- limite: ${plan.limit}`
15
+ ].join("\n");
16
+ }