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,133 @@
|
|
|
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 buildHintsSummary(hints: SqlQuestionHints) {
|
|
7
|
+
const lines: string[] = []
|
|
8
|
+
|
|
9
|
+
if (hints.tableNames.length > 0) {
|
|
10
|
+
lines.push(`Tabelas indicadas pelo usuario: ${hints.tableNames.join(", ")}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (hints.fieldNames.length > 0) {
|
|
14
|
+
lines.push(`Campos indicados pelo usuario: ${hints.fieldNames.join(", ")}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (hints.filters.length > 0) {
|
|
18
|
+
lines.push(`Filtros indicados pelo usuario: ${hints.filters.join(", ")}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hints.orderBy.length > 0) {
|
|
22
|
+
lines.push(`Ordenacao indicada pelo usuario: ${hints.orderBy.join(", ")}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (hints.limit) {
|
|
26
|
+
lines.push(`Limite indicado pelo usuario: ${hints.limit}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return lines.length > 0 ? lines.join("\n") : "Nenhuma pista explicita informada."
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractJson(content: string) {
|
|
33
|
+
const fencedMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
34
|
+
if (fencedMatch) {
|
|
35
|
+
return fencedMatch[1].trim()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return content.trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizePlan(rawPlan: Partial<SqlQueryPlan>): SqlQueryPlan {
|
|
42
|
+
return {
|
|
43
|
+
goal: String(rawPlan.goal ?? "").trim(),
|
|
44
|
+
mainTable: String(rawPlan.mainTable ?? "").trim(),
|
|
45
|
+
relatedTables: Array.isArray(rawPlan.relatedTables)
|
|
46
|
+
? rawPlan.relatedTables.map((value) => String(value).trim()).filter(Boolean)
|
|
47
|
+
: [],
|
|
48
|
+
relationshipFields: Array.isArray(rawPlan.relationshipFields)
|
|
49
|
+
? rawPlan.relationshipFields.map((value) => String(value).trim()).filter(Boolean)
|
|
50
|
+
: [],
|
|
51
|
+
selectedFields: Array.isArray(rawPlan.selectedFields)
|
|
52
|
+
? rawPlan.selectedFields.map((value) => String(value).trim()).filter(Boolean)
|
|
53
|
+
: [],
|
|
54
|
+
filters: Array.isArray(rawPlan.filters)
|
|
55
|
+
? rawPlan.filters.map((value) => String(value).trim()).filter(Boolean)
|
|
56
|
+
: [],
|
|
57
|
+
orderBy: Array.isArray(rawPlan.orderBy)
|
|
58
|
+
? rawPlan.orderBy.map((value) => String(value).trim()).filter(Boolean)
|
|
59
|
+
: [],
|
|
60
|
+
limit: Number.isFinite(Number(rawPlan.limit)) && Number(rawPlan.limit) > 0
|
|
61
|
+
? Number(rawPlan.limit)
|
|
62
|
+
: 10
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function generateSqlPlan(
|
|
67
|
+
question: string,
|
|
68
|
+
schemaSummary: string,
|
|
69
|
+
hints: SqlQuestionHints,
|
|
70
|
+
additionalGuidance?: string
|
|
71
|
+
) {
|
|
72
|
+
const sqlAgentConfig = loadSqlAgentConfig()
|
|
73
|
+
const openai = new OpenAI({ apiKey: sqlAgentConfig.openaiApiKey })
|
|
74
|
+
|
|
75
|
+
const completion = await openai.chat.completions.create({
|
|
76
|
+
model: sqlAgentConfig.model,
|
|
77
|
+
temperature: 0,
|
|
78
|
+
messages: [
|
|
79
|
+
{
|
|
80
|
+
role: "system",
|
|
81
|
+
content: `
|
|
82
|
+
Voce e um assistente tecnico que monta um plano de consulta para MySQL/MariaDB antes da SQL.
|
|
83
|
+
|
|
84
|
+
Regras obrigatorias:
|
|
85
|
+
- responda somente com JSON valido
|
|
86
|
+
- use apenas tabelas e colunas presentes no schema informado
|
|
87
|
+
- respeite pistas explicitas do usuario como prioridade
|
|
88
|
+
- quando o usuario indicar tabelas, campos ou relacionamento, reflita isso no plano
|
|
89
|
+
- quando o usuario indicar filtros, ordenacao ou limite, reflita isso no plano
|
|
90
|
+
- preencha o limite com 10 quando o pedido for listar registros sem quantidade especifica
|
|
91
|
+
|
|
92
|
+
Formato do JSON:
|
|
93
|
+
{
|
|
94
|
+
"goal": "string",
|
|
95
|
+
"mainTable": "string",
|
|
96
|
+
"relatedTables": ["string"],
|
|
97
|
+
"relationshipFields": ["string"],
|
|
98
|
+
"selectedFields": ["string"],
|
|
99
|
+
"filters": ["string"],
|
|
100
|
+
"orderBy": ["string"],
|
|
101
|
+
"limit": 10
|
|
102
|
+
}
|
|
103
|
+
`.trim()
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
role: "user",
|
|
107
|
+
content: `
|
|
108
|
+
Schema disponivel:
|
|
109
|
+
${schemaSummary}
|
|
110
|
+
|
|
111
|
+
Pistas explicitas do usuario:
|
|
112
|
+
${buildHintsSummary(hints)}
|
|
113
|
+
|
|
114
|
+
Orientacoes adicionais:
|
|
115
|
+
${additionalGuidance?.trim() || "Nenhuma orientacao adicional."}
|
|
116
|
+
|
|
117
|
+
Pedido do usuario:
|
|
118
|
+
${question}
|
|
119
|
+
`.trim()
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const content = completion.choices[0]?.message?.content ?? ""
|
|
125
|
+
|
|
126
|
+
if (!content.trim()) {
|
|
127
|
+
throw new Error("A LLM nao retornou um plano de consulta.")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const json = extractJson(content)
|
|
131
|
+
const parsedPlan = JSON.parse(json) as Partial<SqlQueryPlan>
|
|
132
|
+
return normalizePlan(parsedPlan)
|
|
133
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function getSqlAgentErrorMessage(error: unknown): string {
|
|
2
|
+
if (!(error instanceof Error)) {
|
|
3
|
+
return `Erro ao executar SQL Agent: ${String(error)}`
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const openAiError = error as Error & {
|
|
7
|
+
status?: number
|
|
8
|
+
code?: string
|
|
9
|
+
type?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
openAiError.status === 429 ||
|
|
14
|
+
openAiError.code === "insufficient_quota" ||
|
|
15
|
+
openAiError.type === "insufficient_quota"
|
|
16
|
+
) {
|
|
17
|
+
return [
|
|
18
|
+
"Nao foi possivel gerar a SQL porque a chave da OpenAI esta sem cota disponivel.",
|
|
19
|
+
'Verifique billing/limites da conta ou configure outra chave em "OPENAI_API_KEY" ou na CLI.'
|
|
20
|
+
].join(" ")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `Erro ao executar SQL Agent: ${error.message}`
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import dotenv from "dotenv"
|
|
2
|
+
import { readStoredCredentials } from "../config/credentialStore.js"
|
|
3
|
+
|
|
4
|
+
dotenv.config()
|
|
5
|
+
|
|
6
|
+
function getEnvValue(...keys: string[]): string | undefined {
|
|
7
|
+
for (const key of keys) {
|
|
8
|
+
const value = process.env[key]
|
|
9
|
+
if (value && value.trim()) {
|
|
10
|
+
return value.trim()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadSqlAgentConfig() {
|
|
18
|
+
const storedCredentials = readStoredCredentials()
|
|
19
|
+
const openaiApiKey =
|
|
20
|
+
getEnvValue("OPENAI_API_KEY", "openaiApiKey") ??
|
|
21
|
+
storedCredentials.openaiApiKey
|
|
22
|
+
|
|
23
|
+
if (!openaiApiKey) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'OpenAI API KEY nao encontrada. Defina "OPENAI_API_KEY" no ambiente ou configure a chave pela CLI.'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
openaiApiKey,
|
|
31
|
+
model: getEnvValue("SQL_AGENT_MODEL") ?? "gpt-4o-mini"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import dotenv from "dotenv"
|
|
4
|
+
|
|
5
|
+
dotenv.config()
|
|
6
|
+
|
|
7
|
+
type SqlConfigFile = {
|
|
8
|
+
sql?: {
|
|
9
|
+
host?: string
|
|
10
|
+
port?: number
|
|
11
|
+
database?: string
|
|
12
|
+
user?: string
|
|
13
|
+
password?: string
|
|
14
|
+
ssl?: boolean
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SqlConfig {
|
|
19
|
+
host: string
|
|
20
|
+
port: number
|
|
21
|
+
database: string
|
|
22
|
+
user: string
|
|
23
|
+
password: string
|
|
24
|
+
ssl: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readSqlConfigFile(): SqlConfigFile["sql"] {
|
|
28
|
+
const configPath = path.join(process.cwd(), "config", "config.json")
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(configPath)) {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const raw = fs.readFileSync(configPath, "utf-8")
|
|
35
|
+
const json = JSON.parse(raw) as SqlConfigFile
|
|
36
|
+
return json.sql
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getEnvValue(...keys: string[]): string | undefined {
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
const value = process.env[key]
|
|
42
|
+
if (value && value.trim()) {
|
|
43
|
+
return value.trim()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function loadSqlConfig(): SqlConfig {
|
|
51
|
+
const fileConfig = readSqlConfigFile()
|
|
52
|
+
|
|
53
|
+
const host = getEnvValue("DB_HOST", "MYSQL_HOST") ?? fileConfig?.host
|
|
54
|
+
const database = getEnvValue("DB_NAME", "MYSQL_DATABASE") ?? fileConfig?.database
|
|
55
|
+
const user = getEnvValue("DB_USER", "MYSQL_USER") ?? fileConfig?.user
|
|
56
|
+
const password = getEnvValue("DB_PASSWORD", "MYSQL_PASSWORD") ?? fileConfig?.password
|
|
57
|
+
const portRaw = getEnvValue("DB_PORT", "MYSQL_PORT") ?? String(fileConfig?.port ?? "3306")
|
|
58
|
+
const sslRaw = String(getEnvValue("DB_SSL", "MYSQL_SSL") ?? fileConfig?.ssl ?? "false").toLowerCase()
|
|
59
|
+
const port = Number(portRaw)
|
|
60
|
+
|
|
61
|
+
if (!host || !database || !user || !password) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'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.'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
host,
|
|
69
|
+
port: Number.isFinite(port) && port > 0 ? port : 3306,
|
|
70
|
+
database,
|
|
71
|
+
user,
|
|
72
|
+
password,
|
|
73
|
+
ssl: sslRaw === "true" || sslRaw === "1"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type SqlQuestionHints = {
|
|
2
|
+
tableNames: string[]
|
|
3
|
+
fieldNames: string[]
|
|
4
|
+
filters: string[]
|
|
5
|
+
orderBy: string[]
|
|
6
|
+
limit?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function unique(values: string[]) {
|
|
10
|
+
return [...new Set(values)]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extractMatches(question: string, pattern: RegExp, captureIndex = 1) {
|
|
14
|
+
const matches: string[] = []
|
|
15
|
+
|
|
16
|
+
for (const match of question.matchAll(pattern)) {
|
|
17
|
+
const value = match[captureIndex]?.trim()
|
|
18
|
+
if (value) {
|
|
19
|
+
matches.push(value)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return matches
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseSqlQuestionHints(question: string): SqlQuestionHints {
|
|
27
|
+
const explicitTables = extractMatches(question, /tabela\s*:\s*([a-zA-Z0-9_]+)/gi)
|
|
28
|
+
const relationshipTables = extractMatches(
|
|
29
|
+
question,
|
|
30
|
+
/(vincular com tabela|relacionar com tabela|join com tabela)\s*:?\s*([a-zA-Z0-9_]+)/gi,
|
|
31
|
+
2
|
|
32
|
+
)
|
|
33
|
+
const explicitFields = extractMatches(question, /campo\s*:\s*([a-zA-Z0-9_]+)/gi)
|
|
34
|
+
const explicitFilters = extractMatches(question, /filtro\s*:\s*([^\n\r]+)/gi)
|
|
35
|
+
const explicitOrderBy = extractMatches(question, /ordenar por\s*:\s*([^\n\r]+)/gi)
|
|
36
|
+
const explicitLimitRaw = extractMatches(question, /limite\s*:\s*(\d+)/gi)
|
|
37
|
+
const explicitLimit = explicitLimitRaw.length > 0 ? Number(explicitLimitRaw[0]) : undefined
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
tableNames: unique([...explicitTables, ...relationshipTables]),
|
|
41
|
+
fieldNames: unique(explicitFields),
|
|
42
|
+
filters: unique(explicitFilters),
|
|
43
|
+
orderBy: unique(explicitOrderBy),
|
|
44
|
+
limit: Number.isFinite(explicitLimit) && explicitLimit && explicitLimit > 0 ? explicitLimit : undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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, type RankedTable } from "./selectRelevantTables.js"
|
|
9
|
+
import { validateReadOnlySql } from "./sqlGuard.js"
|
|
10
|
+
import { formatSqlPlan } from "./sqlPlan.js"
|
|
11
|
+
|
|
12
|
+
function askQuestion(rl: readline.Interface, question: string): Promise<string> {
|
|
13
|
+
return new Promise((resolve) => rl.question(question, resolve))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeQuestion(rawQuestion?: string): string {
|
|
17
|
+
return (rawQuestion ?? "").trim()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function askForTableSelection(candidateTables: RankedTable[]) {
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
console.log("Encontrei mais de uma tabela possivel para esse pedido:")
|
|
28
|
+
candidateTables.forEach((table, index) => {
|
|
29
|
+
console.log(`${index + 1}. ${table.name}`)
|
|
30
|
+
})
|
|
31
|
+
console.log("M. Informar manualmente o nome da tabela")
|
|
32
|
+
console.log("")
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const answer = (await askQuestion(rl, "Escolha o numero da tabela correta ou M para informar manualmente: ")).trim()
|
|
36
|
+
|
|
37
|
+
if (answer.toLowerCase() === "m") {
|
|
38
|
+
const manualTableName = (await askQuestion(rl, "Digite o nome exato da tabela: ")).trim()
|
|
39
|
+
|
|
40
|
+
if (manualTableName) {
|
|
41
|
+
return manualTableName
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log("Informe um nome de tabela valido.")
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const selectedIndex = Number(answer)
|
|
49
|
+
|
|
50
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= candidateTables.length) {
|
|
51
|
+
return candidateTables[selectedIndex - 1].name
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log("Opcao invalida. Informe um numero da lista.")
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
rl.close()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function askForPlanAction() {
|
|
62
|
+
const rl = readline.createInterface({
|
|
63
|
+
input: process.stdin,
|
|
64
|
+
output: process.stdout
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
console.log("")
|
|
69
|
+
console.log("1. Executar esse plano")
|
|
70
|
+
console.log("2. Corrigir o plano")
|
|
71
|
+
console.log("3. Cancelar")
|
|
72
|
+
console.log("")
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
const answer = (await askQuestion(rl, "Escolha uma opcao: ")).trim()
|
|
76
|
+
|
|
77
|
+
if (answer === "1" || answer === "2" || answer === "3") {
|
|
78
|
+
return answer
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("Opcao invalida. Escolha 1, 2 ou 3.")
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
rl.close()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function askForPlanCorrection() {
|
|
89
|
+
const rl = readline.createInterface({
|
|
90
|
+
input: process.stdin,
|
|
91
|
+
output: process.stdout
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return normalizeQuestion(
|
|
96
|
+
await askQuestion(rl, "Descreva a correcao do plano: ")
|
|
97
|
+
)
|
|
98
|
+
} finally {
|
|
99
|
+
rl.close()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function runSqlAgentCli(initialQuestion?: string) {
|
|
104
|
+
let question = normalizeQuestion(initialQuestion)
|
|
105
|
+
let additionalGuidance = ""
|
|
106
|
+
|
|
107
|
+
if (!question) {
|
|
108
|
+
const rl = readline.createInterface({
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
question = normalizeQuestion(await askQuestion(rl, "Pergunta para o SQL Agent: "))
|
|
115
|
+
} finally {
|
|
116
|
+
rl.close()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let connection: Awaited<ReturnType<typeof createSqlConnection>> | undefined
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (!question) {
|
|
124
|
+
throw new Error("Informe uma pergunta para o SQL Agent.")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
connection = await createSqlConnection()
|
|
128
|
+
|
|
129
|
+
const allTableNames = await fetchAvailableTableNames(connection)
|
|
130
|
+
|
|
131
|
+
if (allTableNames.length === 0) {
|
|
132
|
+
throw new Error("Nenhuma tabela foi encontrada no schema configurado.")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
const composedQuestion = additionalGuidance
|
|
137
|
+
? `${question}\nOrientacoes adicionais do usuario:\n${additionalGuidance}`
|
|
138
|
+
: question
|
|
139
|
+
|
|
140
|
+
const hints = parseSqlQuestionHints(composedQuestion)
|
|
141
|
+
const tableSelection = selectRelevantTables(composedQuestion, allTableNames, 12, hints.tableNames)
|
|
142
|
+
const selectedTableNames = tableSelection.requiresConfirmation
|
|
143
|
+
? [await askForTableSelection(tableSelection.candidateTables)]
|
|
144
|
+
: tableSelection.suggestedTables
|
|
145
|
+
|
|
146
|
+
const schemaSummary = await fetchSchemaSummary(connection, selectedTableNames)
|
|
147
|
+
const plan = await generateSqlPlan(question, schemaSummary, hints, additionalGuidance)
|
|
148
|
+
|
|
149
|
+
console.log("")
|
|
150
|
+
console.log(formatSqlPlan(plan))
|
|
151
|
+
|
|
152
|
+
const action = await askForPlanAction()
|
|
153
|
+
|
|
154
|
+
if (action === "3") {
|
|
155
|
+
console.log("Execucao cancelada.")
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (action === "2") {
|
|
160
|
+
const correction = await askForPlanCorrection()
|
|
161
|
+
|
|
162
|
+
if (!correction) {
|
|
163
|
+
console.log("Nenhuma correcao informada. Mantendo plano atual.")
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
additionalGuidance = additionalGuidance
|
|
168
|
+
? `${additionalGuidance}\n${correction}`
|
|
169
|
+
: correction
|
|
170
|
+
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const generatedSql = await generateSqlFromQuestion(
|
|
175
|
+
question,
|
|
176
|
+
schemaSummary,
|
|
177
|
+
hints,
|
|
178
|
+
plan,
|
|
179
|
+
additionalGuidance
|
|
180
|
+
)
|
|
181
|
+
const sql = validateReadOnlySql(generatedSql)
|
|
182
|
+
|
|
183
|
+
const [rows] = await connection.query(sql)
|
|
184
|
+
|
|
185
|
+
console.log("")
|
|
186
|
+
console.log("Pergunta:")
|
|
187
|
+
console.log(question)
|
|
188
|
+
console.log("")
|
|
189
|
+
console.log("SQL gerada:")
|
|
190
|
+
console.log(sql)
|
|
191
|
+
console.log("")
|
|
192
|
+
console.log("Resultado:")
|
|
193
|
+
console.log(JSON.stringify(rows, null, 2))
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(getSqlAgentErrorMessage(error))
|
|
198
|
+
process.exitCode = 1
|
|
199
|
+
} finally {
|
|
200
|
+
if (connection) {
|
|
201
|
+
await connection.end()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import readline from "readline"
|
|
2
|
+
import { createSqlConnection } from "./createSqlConnection.js"
|
|
3
|
+
import { validateReadOnlySql } from "./sqlGuard.js"
|
|
4
|
+
|
|
5
|
+
function askQuestion(rl: readline.Interface, question: string): Promise<string> {
|
|
6
|
+
return new Promise((resolve) => rl.question(question, resolve))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeQuery(rawQuery?: string): string {
|
|
10
|
+
return (rawQuery ?? "").trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runSqlCli(initialQuery?: string) {
|
|
14
|
+
const query = normalizeQuery(initialQuery)
|
|
15
|
+
let finalQuery = query
|
|
16
|
+
|
|
17
|
+
if (!finalQuery) {
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
finalQuery = normalizeQuery(await askQuestion(rl, "SQL (somente SELECT): "))
|
|
25
|
+
} finally {
|
|
26
|
+
rl.close()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateReadOnlySql(finalQuery)
|
|
31
|
+
|
|
32
|
+
const connection = await createSqlConnection()
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const [rows] = await connection.query(finalQuery)
|
|
36
|
+
console.log(JSON.stringify(rows, null, 2))
|
|
37
|
+
} finally {
|
|
38
|
+
await connection.end()
|
|
39
|
+
}
|
|
40
|
+
}
|