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.
- package/.idea/epa_mcp.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/vcs.xml +6 -0
- package/AGENTS.md +10 -0
- package/README.md +339 -0
- package/dist/agent/agentHelpers.js +21 -0
- package/dist/agent/openaiAgent.js +170 -0
- package/dist/agent/slidingWindow.js +13 -0
- package/dist/api/epaApiClient.js +59 -0
- package/dist/cli/index.js +47 -0
- package/dist/config/credentialStore.js +92 -0
- package/dist/config/ensureCliAuth.js +127 -0
- package/dist/config/loadConfig.js +40 -0
- package/dist/config/setup.js +23 -0
- package/dist/core/createReferenceTool.js +12 -0
- package/dist/core/createTool.js +32 -0
- package/dist/mocks/requestMocks.js +47 -0
- package/dist/server/server.js +41 -0
- package/dist/server/stdioServer.js +3 -0
- package/dist/services/request/requestFilters.js +1 -0
- package/dist/services/requestService.js +81 -0
- package/dist/services/teamService.js +18 -0
- package/dist/sql/createSqlConnection.js +13 -0
- package/dist/sql/fetchSchemaSummary.js +38 -0
- package/dist/sql/generateSqlFromQuestion.js +83 -0
- package/dist/sql/generateSqlPlan.js +111 -0
- package/dist/sql/getSqlAgentErrorMessage.js +15 -0
- package/dist/sql/loadSqlAgentConfig.js +24 -0
- package/dist/sql/loadSqlConfig.js +43 -0
- package/dist/sql/parseSqlQuestionHints.js +29 -0
- package/dist/sql/runSqlAgentCli.js +163 -0
- package/dist/sql/runSqlCli.js +34 -0
- package/dist/sql/selectRelevantTables.js +136 -0
- package/dist/sql/sqlGuard.js +21 -0
- package/dist/sql/sqlPlan.js +16 -0
- package/dist/tests/requestService.test.js +110 -0
- package/dist/tools/analytics/teamReport.draft.js +16 -0
- package/dist/tools/loadTools.js +40 -0
- package/dist/tools/requests/assignees.js +50 -0
- package/dist/tools/requests/clients.draft.js +10 -0
- package/dist/tools/requests/create.draft.js +51 -0
- package/dist/tools/requests/list.js +50 -0
- package/dist/tools/requests/priorities.js +2 -0
- package/dist/tools/requests/services.draft.js +20 -0
- package/dist/tools/requests/types.js +16 -0
- package/dist/tools/requests/units.draft.js +10 -0
- package/dist/tools/requests/view.draft.js +20 -0
- package/dist/utils/buildDateRange.js +18 -0
- package/dist/utils/findIdByDescription.js +4 -0
- package/dist/utils/resolveAssigneeId.js +14 -0
- package/dist/utils/toolNameMaps.js +23 -0
- package/package.json +31 -0
- package/src/agent/agentHelpers.ts +25 -0
- package/src/agent/openaiAgent.ts +205 -0
- package/src/agent/slidingWindow.ts +17 -0
- package/src/api/epaApiClient.ts +82 -0
- package/src/cli/index.ts +61 -0
- package/src/config/credentialStore.ts +130 -0
- package/src/config/ensureCliAuth.ts +152 -0
- package/src/config/loadConfig.ts +62 -0
- package/src/config/setup.ts +35 -0
- package/src/core/createReferenceTool.ts +17 -0
- package/src/core/createTool.ts +51 -0
- package/src/mocks/requestMocks.ts +52 -0
- package/src/server/server.ts +61 -0
- package/src/server/stdioServer.ts +5 -0
- package/src/services/request/requestFilters.ts +12 -0
- package/src/services/requestService.ts +126 -0
- package/src/services/teamService.ts +27 -0
- package/src/sql/createSqlConnection.ts +15 -0
- package/src/sql/fetchSchemaSummary.ts +64 -0
- package/src/sql/generateSqlFromQuestion.ts +105 -0
- package/src/sql/generateSqlPlan.ts +133 -0
- package/src/sql/getSqlAgentErrorMessage.ts +24 -0
- package/src/sql/loadSqlAgentConfig.ts +33 -0
- package/src/sql/loadSqlConfig.ts +75 -0
- package/src/sql/parseSqlQuestionHints.ts +46 -0
- package/src/sql/runSqlAgentCli.ts +204 -0
- package/src/sql/runSqlCli.ts +40 -0
- package/src/sql/selectRelevantTables.ts +184 -0
- package/src/sql/sqlGuard.ts +28 -0
- package/src/sql/sqlPlan.ts +28 -0
- package/src/tests/requestService.test.ts +152 -0
- package/src/tools/analytics/teamReport.draft.ts +25 -0
- package/src/tools/loadTools.ts +59 -0
- package/src/tools/requests/assignees.ts +59 -0
- package/src/tools/requests/clients.draft.ts +18 -0
- package/src/tools/requests/create.draft.ts +59 -0
- package/src/tools/requests/list.ts +57 -0
- package/src/tools/requests/priorities.ts +6 -0
- package/src/tools/requests/services.draft.ts +24 -0
- package/src/tools/requests/types.ts +18 -0
- package/src/tools/requests/units.draft.ts +18 -0
- package/src/tools/requests/view.draft.ts +27 -0
- package/src/utils/buildDateRange.ts +22 -0
- package/src/utils/findIdByDescription.ts +10 -0
- package/src/utils/resolveAssigneeId.ts +24 -0
- package/src/utils/toolNameMaps.ts +33 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
|
|
5
|
+
export async function setupConfig() {
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const ask = (q: string) =>
|
|
13
|
+
new Promise<string>((resolve) => rl.question(q, resolve));
|
|
14
|
+
|
|
15
|
+
const apiUrl = await ask("EPA API URL: ");
|
|
16
|
+
|
|
17
|
+
rl.close();
|
|
18
|
+
|
|
19
|
+
const config = {
|
|
20
|
+
epaApiUrl: apiUrl,
|
|
21
|
+
epaApiTimeoutMs: 15000
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const configDir = path.join(process.cwd(), "config");
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(configDir)) {
|
|
27
|
+
fs.mkdirSync(configDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const configPath = path.join(configDir, "config.json");
|
|
31
|
+
|
|
32
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
33
|
+
|
|
34
|
+
console.log("Config salvo em:", configPath);
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createTool } from "./createTool.js"
|
|
2
|
+
import { EpaApiClient } from "../api/epaApiClient.js"
|
|
3
|
+
|
|
4
|
+
const api = new EpaApiClient()
|
|
5
|
+
|
|
6
|
+
export function createReferenceTool(name:string, endpoint:string) {
|
|
7
|
+
|
|
8
|
+
return createTool({
|
|
9
|
+
name,
|
|
10
|
+
description: `Lista dados de referência: ${name}`,
|
|
11
|
+
handler: async () => {
|
|
12
|
+
return api.get(endpoint)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ZodTypeAny } from "zod"
|
|
2
|
+
|
|
3
|
+
export interface ToolDefinition {
|
|
4
|
+
name: string
|
|
5
|
+
description: string
|
|
6
|
+
schema?: any
|
|
7
|
+
validator?: ZodTypeAny
|
|
8
|
+
handler: (args: any) => Promise<any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createTool(def: ToolDefinition) {
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
name: def.name,
|
|
15
|
+
description: def.description,
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: def.schema || {},
|
|
19
|
+
},
|
|
20
|
+
async execute(args: any) {
|
|
21
|
+
|
|
22
|
+
let parsedArgs = args ?? {}
|
|
23
|
+
|
|
24
|
+
if (def.validator) {
|
|
25
|
+
const parsed = def.validator.safeParse(parsedArgs)
|
|
26
|
+
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
const issues = parsed.error.issues
|
|
29
|
+
.map((issue) => `${issue.path.join(".") || "args"}: ${issue.message}`)
|
|
30
|
+
.join("; ")
|
|
31
|
+
throw new Error(`Parametros invalidos para "${def.name}": ${issues}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
parsedArgs = parsed.data
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await def.handler(parsedArgs);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: JSON.stringify(result, null, 2)
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const OS_TIPO = [
|
|
2
|
+
{ "id": 1009, "descricao": "Abrir Lojas" },
|
|
3
|
+
{ "id": 1005, "descricao": "Apoio Tecnológico" },
|
|
4
|
+
{ "id": 1008, "descricao": "Chamados de TI" },
|
|
5
|
+
{ "id": 1003, "descricao": "Desenvolvimento de Software" },
|
|
6
|
+
{ "id": 1019, "descricao": "Engenharia Clínica" },
|
|
7
|
+
{ "id": 999, "descricao": "Gestão da Qualidade" },
|
|
8
|
+
{ "id": 1013, "descricao": "Gestão de Pessoas / DP" },
|
|
9
|
+
{ "id": 1012, "descricao": "Instalação" },
|
|
10
|
+
{ "id": 1010, "descricao": "Manutenção" },
|
|
11
|
+
{ "id": 1002, "descricao": "Manutenção Preventiva" },
|
|
12
|
+
{ "id": 1001, "descricao": "Ouvidoria" },
|
|
13
|
+
{ "id": 1020, "descricao": "Qualidade - envio de documentos" },
|
|
14
|
+
{ "id": 2, "descricao": "Reclamação" },
|
|
15
|
+
{ "id": 1000, "descricao": "Recursos Humanos" },
|
|
16
|
+
{ "id": 1015, "descricao": "Reembolso de despesas" },
|
|
17
|
+
{ "id": 1007, "descricao": "Reembolso de Despesa_" },
|
|
18
|
+
{ "id": 1018, "descricao": "Suporte Técnico de TI" },
|
|
19
|
+
{ "id": 1014, "descricao": "Teste" },
|
|
20
|
+
{ "id": 1017, "descricao": "teste" },
|
|
21
|
+
{ "id": 1016, "descricao": "teste 123" },
|
|
22
|
+
{ "id": 1, "descricao": "TI" }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export const OS_TIPO_SERVICO = [
|
|
26
|
+
{ "id": 1005, "descricao": "Analisar documento" },
|
|
27
|
+
{ "id": 1007, "descricao": "Formatar padrão" },
|
|
28
|
+
{ "id": 1016, "descricao": "Adicionar Campo" },
|
|
29
|
+
{ "id": 1015, "descricao": "Criar Formulário" },
|
|
30
|
+
{ "id": 1017, "descricao": "Criar Função" },
|
|
31
|
+
{ "id": 995, "descricao": "Geração de Backup" }
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export const UNIDADE_GERENCIAL = [
|
|
35
|
+
{ "id": 18, "descricao": "DESEMPENHO PLANEJAMENTO > TI" }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export const RESPONSAVEL = [
|
|
39
|
+
{ "id": 1, "descricao": "suporte.simeon" },
|
|
40
|
+
{ "id": 577, "descricao": "maely.superusuario - Maely Teste Super Usuário" }
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export const CLIENTE = [
|
|
44
|
+
{ "id": 1, "descricao": "Suporte Simeon" },
|
|
45
|
+
{ "id": 349, "descricao": "Comercial-Simeon" }
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export const PRIORIDADE = [
|
|
49
|
+
{ "id": 6, "descricao": "Média" },
|
|
50
|
+
{ "id": 7, "descricao": "Alta" },
|
|
51
|
+
{ "id": 8, "descricao": "Baixa" }
|
|
52
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { loadTools } from "../tools/loadTools.js";
|
|
5
|
+
import { buildToolNameMaps } from "../utils/toolNameMaps.js";
|
|
6
|
+
|
|
7
|
+
export async function createMcpServer() {
|
|
8
|
+
|
|
9
|
+
const server = new Server(
|
|
10
|
+
{
|
|
11
|
+
name: "epa-mcp",
|
|
12
|
+
version: "0.1.0"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const tools = await loadTools();
|
|
22
|
+
const toolNames = tools.map((tool) => tool.name);
|
|
23
|
+
const { internalToExternal, externalToInternal } = buildToolNameMaps(toolNames);
|
|
24
|
+
|
|
25
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
tools: tools.map((tool) => ({
|
|
29
|
+
name: internalToExternal.get(tool.name) ?? tool.name,
|
|
30
|
+
description: tool.description,
|
|
31
|
+
inputSchema: tool.inputSchema
|
|
32
|
+
}))
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
38
|
+
|
|
39
|
+
const internalName = externalToInternal.get(request.params.name) ?? request.params.name;
|
|
40
|
+
const tool = tools.find((t) => t.name === internalName);
|
|
41
|
+
|
|
42
|
+
if (!tool) {
|
|
43
|
+
throw new Error("Tool não encontrada");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return await tool.execute(request.params.arguments);
|
|
47
|
+
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function startServer() {
|
|
54
|
+
|
|
55
|
+
const server = await createMcpServer()
|
|
56
|
+
|
|
57
|
+
const transport = new StdioServerTransport();
|
|
58
|
+
|
|
59
|
+
await server.connect(transport);
|
|
60
|
+
|
|
61
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { EpaApiClient } from "../api/epaApiClient.js"
|
|
2
|
+
import type { AssigneeSearchOptions, ListFilters } from "./request/requestFilters.js"
|
|
3
|
+
import { buildDateRange } from "../utils/buildDateRange.js"
|
|
4
|
+
|
|
5
|
+
export class RequestService {
|
|
6
|
+
|
|
7
|
+
private api: EpaApiClient
|
|
8
|
+
|
|
9
|
+
constructor(apiClient?: EpaApiClient) {
|
|
10
|
+
this.api = apiClient ?? new EpaApiClient()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getTypes() {
|
|
14
|
+
return this.api.get("/epa_os/ajax.php", {
|
|
15
|
+
action: "get",
|
|
16
|
+
controller: "TipoSolicitacao"
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getServices(typeId: number) {
|
|
21
|
+
return this.api.get("/api/tipos_servico", {
|
|
22
|
+
tipo_id: typeId
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getAssignees(term: string, options: AssigneeSearchOptions = {}) {
|
|
27
|
+
|
|
28
|
+
const params: Record<string, string | number> = {
|
|
29
|
+
term,
|
|
30
|
+
_type: "query",
|
|
31
|
+
q: options.q ?? term
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.unitValue !== undefined && options.unitValue !== null) {
|
|
35
|
+
params["filters[unidade_gerencial][type]"] = options.unitType ?? "pertenco"
|
|
36
|
+
params["filters[unidade_gerencial][value]"] = options.unitValue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await this.api.get("/api/api/usuarios/search", params)
|
|
40
|
+
|
|
41
|
+
if (response && Array.isArray((response as any).results)) {
|
|
42
|
+
return (response as any).results
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getClients() {
|
|
49
|
+
return this.api.get("/api/clientes")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getUnits() {
|
|
53
|
+
return this.api.get("/api/unidades_gerenciais")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async list(filters: number | ListFilters = {}) {
|
|
57
|
+
|
|
58
|
+
const normalizedFilters: ListFilters =
|
|
59
|
+
typeof filters === "number" ? { assigneeId: filters } : filters
|
|
60
|
+
|
|
61
|
+
const params: any = {
|
|
62
|
+
data_inclusao: buildDateRange(normalizedFilters.period)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (normalizedFilters.assigneeId) {
|
|
66
|
+
params.responsavel = normalizedFilters.assigneeId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const response = await this.api.post(
|
|
70
|
+
"/api/api/os/listar",
|
|
71
|
+
{},
|
|
72
|
+
params
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!response || response.length === 0) {
|
|
76
|
+
return "Nenhuma solicitacao encontrada."
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return response
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async view(id: number) {
|
|
84
|
+
|
|
85
|
+
const response = await this.api.post(
|
|
86
|
+
"/api/api/os/listar",
|
|
87
|
+
{},
|
|
88
|
+
{ codigo: id }
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if (!response || response.length === 0) {
|
|
92
|
+
return `Solicitacao ${id} nao encontrada.`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(response)) {
|
|
96
|
+
const found = response.find((item: any) => Number(item.codigo ?? item.id) === id)
|
|
97
|
+
return found ?? response[0]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async create(data: any) {
|
|
105
|
+
|
|
106
|
+
const payload = {
|
|
107
|
+
os_tipo: data.os_tipo,
|
|
108
|
+
os_tipo_servico: data.os_tipo_servico,
|
|
109
|
+
unidade_gerencial_executora: data.unidade_gerencial_executora,
|
|
110
|
+
responsavel: data.responsavel,
|
|
111
|
+
titulo: data.titulo,
|
|
112
|
+
cliente: data.cliente,
|
|
113
|
+
data_desejavel: data.data_desejavel,
|
|
114
|
+
prioridade: data.prioridade,
|
|
115
|
+
observacao: data.observacao
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await this.api.post(
|
|
119
|
+
"/api/api/solicitacao_rapida",
|
|
120
|
+
payload
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EpaApiClient } from "../api/epaApiClient.js";
|
|
2
|
+
|
|
3
|
+
export class TeamService {
|
|
4
|
+
|
|
5
|
+
private api: EpaApiClient;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
this.api = new EpaApiClient();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async teamReport(name: string) {
|
|
12
|
+
|
|
13
|
+
const requests = await this.api.get("/solicitacoes", { responsavel: name });
|
|
14
|
+
|
|
15
|
+
const total = requests.length;
|
|
16
|
+
|
|
17
|
+
const overdue = requests.filter((request: any) => request.vencida);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
desenvolvedor: name,
|
|
21
|
+
total,
|
|
22
|
+
vencidas: overdue.length,
|
|
23
|
+
solicitacoes: requests
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import mysql from "mysql2/promise"
|
|
2
|
+
import { loadSqlConfig } from "./loadSqlConfig.js"
|
|
3
|
+
|
|
4
|
+
export async function createSqlConnection() {
|
|
5
|
+
const sqlConfig = loadSqlConfig()
|
|
6
|
+
|
|
7
|
+
return mysql.createConnection({
|
|
8
|
+
host: sqlConfig.host,
|
|
9
|
+
port: sqlConfig.port,
|
|
10
|
+
database: sqlConfig.database,
|
|
11
|
+
user: sqlConfig.user,
|
|
12
|
+
password: sqlConfig.password,
|
|
13
|
+
ssl: sqlConfig.ssl ? {} : undefined
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RowDataPacket } from "mysql2"
|
|
2
|
+
import type { Connection } from "mysql2/promise"
|
|
3
|
+
import { loadSqlConfig } from "./loadSqlConfig.js"
|
|
4
|
+
|
|
5
|
+
type TableRow = RowDataPacket & {
|
|
6
|
+
TABLE_NAME: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type ColumnRow = RowDataPacket & {
|
|
10
|
+
TABLE_NAME: string
|
|
11
|
+
COLUMN_NAME: string
|
|
12
|
+
DATA_TYPE: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MAX_COLUMNS_PER_TABLE = 20
|
|
16
|
+
|
|
17
|
+
export async function fetchAvailableTableNames(connection: Connection): Promise<string[]> {
|
|
18
|
+
const sqlConfig = loadSqlConfig()
|
|
19
|
+
|
|
20
|
+
const [tableRows] = await connection.query<TableRow[]>(
|
|
21
|
+
`
|
|
22
|
+
SELECT TABLE_NAME
|
|
23
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
24
|
+
WHERE TABLE_SCHEMA = ?
|
|
25
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
26
|
+
ORDER BY TABLE_NAME
|
|
27
|
+
`,
|
|
28
|
+
[sqlConfig.database]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return tableRows.map((row) => row.TABLE_NAME)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchSchemaSummary(connection: Connection, tableNames: string[]): Promise<string> {
|
|
35
|
+
const sqlConfig = loadSqlConfig()
|
|
36
|
+
const placeholders = tableNames.map(() => "?").join(", ")
|
|
37
|
+
const [columnRows] = await connection.query<ColumnRow[]>(
|
|
38
|
+
`
|
|
39
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
|
|
40
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
41
|
+
WHERE TABLE_SCHEMA = ?
|
|
42
|
+
AND TABLE_NAME IN (${placeholders})
|
|
43
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
44
|
+
`,
|
|
45
|
+
[sqlConfig.database, ...tableNames]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const columnsByTable = new Map<string, string[]>()
|
|
49
|
+
|
|
50
|
+
for (const row of columnRows) {
|
|
51
|
+
const columns = columnsByTable.get(row.TABLE_NAME) ?? []
|
|
52
|
+
if (columns.length < MAX_COLUMNS_PER_TABLE) {
|
|
53
|
+
columns.push(`${row.COLUMN_NAME} (${row.DATA_TYPE})`)
|
|
54
|
+
}
|
|
55
|
+
columnsByTable.set(row.TABLE_NAME, columns)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return tableNames
|
|
59
|
+
.map((tableName) => {
|
|
60
|
+
const columns = columnsByTable.get(tableName) ?? []
|
|
61
|
+
return `Tabela ${tableName}: ${columns.join(", ")}`
|
|
62
|
+
})
|
|
63
|
+
.join("\n")
|
|
64
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import OpenAI from "openai"
|
|
2
|
+
import { loadSqlAgentConfig } from "./loadSqlAgentConfig.js"
|
|
3
|
+
import type { SqlQuestionHints } from "./parseSqlQuestionHints.js"
|
|
4
|
+
import type { SqlQueryPlan } from "./sqlPlan.js"
|
|
5
|
+
|
|
6
|
+
function extractSql(content: string): string {
|
|
7
|
+
const fencedMatch = content.match(/```(?:sql)?\s*([\s\S]*?)```/i)
|
|
8
|
+
if (fencedMatch) {
|
|
9
|
+
return fencedMatch[1].trim()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return content.trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildHintsSummary(hints: SqlQuestionHints) {
|
|
16
|
+
const lines: string[] = []
|
|
17
|
+
|
|
18
|
+
if (hints.tableNames.length > 0) {
|
|
19
|
+
lines.push(`Tabelas indicadas pelo usuario: ${hints.tableNames.join(", ")}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (hints.fieldNames.length > 0) {
|
|
23
|
+
lines.push(`Campos indicados pelo usuario: ${hints.fieldNames.join(", ")}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (hints.filters.length > 0) {
|
|
27
|
+
lines.push(`Filtros indicados pelo usuario: ${hints.filters.join(", ")}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (hints.orderBy.length > 0) {
|
|
31
|
+
lines.push(`Ordenacao indicada pelo usuario: ${hints.orderBy.join(", ")}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hints.limit) {
|
|
35
|
+
lines.push(`Limite indicado pelo usuario: ${hints.limit}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return lines.length > 0 ? lines.join("\n") : "Nenhuma pista explicita informada."
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function generateSqlFromQuestion(
|
|
42
|
+
question: string,
|
|
43
|
+
schemaSummary: string,
|
|
44
|
+
hints: SqlQuestionHints,
|
|
45
|
+
plan: SqlQueryPlan,
|
|
46
|
+
additionalGuidance?: string
|
|
47
|
+
) {
|
|
48
|
+
const sqlAgentConfig = loadSqlAgentConfig()
|
|
49
|
+
const openai = new OpenAI({ apiKey: sqlAgentConfig.openaiApiKey })
|
|
50
|
+
|
|
51
|
+
const completion = await openai.chat.completions.create({
|
|
52
|
+
model: sqlAgentConfig.model,
|
|
53
|
+
temperature: 0,
|
|
54
|
+
messages: [
|
|
55
|
+
{
|
|
56
|
+
role: "system",
|
|
57
|
+
content: `
|
|
58
|
+
Voce e um assistente tecnico que gera SQL para MySQL/MariaDB.
|
|
59
|
+
|
|
60
|
+
Regras obrigatorias:
|
|
61
|
+
- gere apenas uma consulta SQL
|
|
62
|
+
- responda somente com SQL, sem explicacoes
|
|
63
|
+
- use apenas SELECT
|
|
64
|
+
- nao finalize a resposta com ponto e virgula
|
|
65
|
+
- nunca use INSERT, UPDATE, DELETE, ALTER, DROP, TRUNCATE, CREATE, REPLACE, GRANT, REVOKE, EXECUTE ou CALL
|
|
66
|
+
- quando o pedido for listar registros e o usuario nao informar quantidade, aplique LIMIT 10
|
|
67
|
+
- use somente tabelas e colunas presentes no schema informado
|
|
68
|
+
- priorize tabelas com nome diretamente relacionado ao pedido do usuario
|
|
69
|
+
- se houver tabela com nome muito proximo ao termo pedido, prefira essa tabela em vez de nomes genericos ou sem relacao clara
|
|
70
|
+
- quando o usuario informar explicitamente nomes de tabelas ou campos, trate essa informacao como instrucao prioritaria
|
|
71
|
+
- 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
|
|
72
|
+
- quando o usuario indicar filtros, ordenacao ou limite, respeite essas instrucoes na SQL final
|
|
73
|
+
- siga o plano de consulta aprovado pelo usuario
|
|
74
|
+
`.trim()
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
role: "user",
|
|
78
|
+
content: `
|
|
79
|
+
Schema disponivel:
|
|
80
|
+
${schemaSummary}
|
|
81
|
+
|
|
82
|
+
Pistas explicitas do usuario:
|
|
83
|
+
${buildHintsSummary(hints)}
|
|
84
|
+
|
|
85
|
+
Plano aprovado:
|
|
86
|
+
${JSON.stringify(plan, null, 2)}
|
|
87
|
+
|
|
88
|
+
Orientacoes adicionais:
|
|
89
|
+
${additionalGuidance?.trim() || "Nenhuma orientacao adicional."}
|
|
90
|
+
|
|
91
|
+
Pedido do usuario:
|
|
92
|
+
${question}
|
|
93
|
+
`.trim()
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const content = completion.choices[0]?.message?.content ?? ""
|
|
99
|
+
|
|
100
|
+
if (!content.trim()) {
|
|
101
|
+
throw new Error("A LLM nao retornou SQL para o pedido informado.")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return extractSql(content)
|
|
105
|
+
}
|