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,184 @@
|
|
|
1
|
+
export type RankedTable = {
|
|
2
|
+
name: string
|
|
3
|
+
score: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type TableSelection = {
|
|
7
|
+
candidateTables: RankedTable[]
|
|
8
|
+
suggestedTables: string[]
|
|
9
|
+
requiresConfirmation: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const QUESTION_STOP_WORDS = new Set([
|
|
13
|
+
"a",
|
|
14
|
+
"as",
|
|
15
|
+
"ao",
|
|
16
|
+
"aos",
|
|
17
|
+
"com",
|
|
18
|
+
"da",
|
|
19
|
+
"das",
|
|
20
|
+
"de",
|
|
21
|
+
"do",
|
|
22
|
+
"dos",
|
|
23
|
+
"e",
|
|
24
|
+
"em",
|
|
25
|
+
"na",
|
|
26
|
+
"nas",
|
|
27
|
+
"no",
|
|
28
|
+
"nos",
|
|
29
|
+
"o",
|
|
30
|
+
"os",
|
|
31
|
+
"ou",
|
|
32
|
+
"para",
|
|
33
|
+
"por",
|
|
34
|
+
"quais",
|
|
35
|
+
"qual",
|
|
36
|
+
"quero",
|
|
37
|
+
"traga",
|
|
38
|
+
"trazer",
|
|
39
|
+
"listar",
|
|
40
|
+
"liste",
|
|
41
|
+
"mostrar",
|
|
42
|
+
"mostre",
|
|
43
|
+
"me",
|
|
44
|
+
"uma",
|
|
45
|
+
"um"
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
function normalizeText(value: string) {
|
|
49
|
+
return value
|
|
50
|
+
.normalize("NFD")
|
|
51
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildTokenVariants(token: string) {
|
|
56
|
+
const variants = new Set([token])
|
|
57
|
+
|
|
58
|
+
if (token.endsWith("es") && token.length > 4) {
|
|
59
|
+
variants.add(token.slice(0, -2))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (token.endsWith("s") && token.length > 3) {
|
|
63
|
+
variants.add(token.slice(0, -1))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...variants]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractRelevantTerms(question: string) {
|
|
70
|
+
const normalizedQuestion = normalizeText(question)
|
|
71
|
+
const rawTokens = normalizedQuestion.match(/[a-z0-9_]+/g) ?? []
|
|
72
|
+
|
|
73
|
+
return rawTokens
|
|
74
|
+
.filter((token) => token.length >= 3)
|
|
75
|
+
.filter((token) => !QUESTION_STOP_WORDS.has(token))
|
|
76
|
+
.flatMap(buildTokenVariants)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scoreTableName(tableName: string, terms: string[]) {
|
|
80
|
+
if (terms.length === 0) {
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const normalizedTableName = normalizeText(tableName)
|
|
85
|
+
let score = 0
|
|
86
|
+
|
|
87
|
+
for (const term of terms) {
|
|
88
|
+
if (normalizedTableName === term) {
|
|
89
|
+
score += 100
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (normalizedTableName.startsWith(`${term}_`) || normalizedTableName.endsWith(`_${term}`)) {
|
|
94
|
+
score += 70
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (normalizedTableName.includes(`_${term}_`)) {
|
|
99
|
+
score += 60
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (normalizedTableName.includes(term)) {
|
|
104
|
+
score += 40
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return score
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function shouldAskForConfirmation(candidateTables: RankedTable[]) {
|
|
112
|
+
if (candidateTables.length <= 1) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [topCandidate, secondCandidate] = candidateTables
|
|
117
|
+
|
|
118
|
+
if (!topCandidate || !secondCandidate) {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (topCandidate.score === 0) {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (topCandidate.score < 100) {
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return secondCandidate.score >= topCandidate.score - 30
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveExplicitTableNames(tableNames: string[], explicitTableNames: string[]) {
|
|
134
|
+
if (explicitTableNames.length === 0) {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const normalizedMap = new Map(
|
|
139
|
+
tableNames.map((tableName) => [normalizeText(tableName), tableName])
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return explicitTableNames
|
|
143
|
+
.map((tableName) => normalizedMap.get(normalizeText(tableName)) ?? tableName)
|
|
144
|
+
.filter((tableName, index, values) => values.indexOf(tableName) === index)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function selectRelevantTables(
|
|
148
|
+
question: string,
|
|
149
|
+
tableNames: string[],
|
|
150
|
+
maxRelevantTables = 12,
|
|
151
|
+
explicitTableNames: string[] = []
|
|
152
|
+
): TableSelection {
|
|
153
|
+
const terms = extractRelevantTerms(question)
|
|
154
|
+
const resolvedExplicitTables = resolveExplicitTableNames(tableNames, explicitTableNames)
|
|
155
|
+
|
|
156
|
+
const rankedTables: RankedTable[] = tableNames
|
|
157
|
+
.map((name) => ({
|
|
158
|
+
name,
|
|
159
|
+
score: scoreTableName(name, terms) + (resolvedExplicitTables.includes(name) ? 1000 : 0)
|
|
160
|
+
}))
|
|
161
|
+
.sort((left, right) => right.score - left.score || left.name.localeCompare(right.name))
|
|
162
|
+
|
|
163
|
+
const candidateTables = rankedTables
|
|
164
|
+
.filter((table) => table.score > 0)
|
|
165
|
+
.slice(0, maxRelevantTables)
|
|
166
|
+
|
|
167
|
+
const rankedSuggestedTables =
|
|
168
|
+
candidateTables.length > 0
|
|
169
|
+
? candidateTables.map((table) => table.name)
|
|
170
|
+
: rankedTables.slice(0, maxRelevantTables).map((table) => table.name)
|
|
171
|
+
|
|
172
|
+
const suggestedTables = [...resolvedExplicitTables]
|
|
173
|
+
for (const tableName of rankedSuggestedTables) {
|
|
174
|
+
if (!suggestedTables.includes(tableName)) {
|
|
175
|
+
suggestedTables.push(tableName)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
candidateTables,
|
|
181
|
+
suggestedTables: suggestedTables.slice(0, maxRelevantTables),
|
|
182
|
+
requiresConfirmation: resolvedExplicitTables.length === 0 && shouldAskForConfirmation(candidateTables)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
4
|
+
export function normalizeReadOnlySql(query: string) {
|
|
5
|
+
return query.trim().replace(/;\s*$/, "")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validateReadOnlySql(query: string) {
|
|
9
|
+
const normalizedQuery = normalizeReadOnlySql(query)
|
|
10
|
+
|
|
11
|
+
if (!normalizedQuery) {
|
|
12
|
+
throw new Error("Informe uma consulta SQL.")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!READ_ONLY_SQL_PATTERN.test(normalizedQuery)) {
|
|
16
|
+
throw new Error("Somente consultas SELECT sao permitidas neste modo tecnico.")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (FORBIDDEN_SQL_PATTERN.test(normalizedQuery)) {
|
|
20
|
+
throw new Error("A consulta contem comandos nao permitidos para modo read-only.")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (normalizedQuery.includes(";")) {
|
|
24
|
+
throw new Error("Envie apenas uma consulta por vez.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalizedQuery
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SqlQueryPlan = {
|
|
2
|
+
goal: string
|
|
3
|
+
mainTable: string
|
|
4
|
+
relatedTables: string[]
|
|
5
|
+
relationshipFields: string[]
|
|
6
|
+
selectedFields: string[]
|
|
7
|
+
filters: string[]
|
|
8
|
+
orderBy: string[]
|
|
9
|
+
limit: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatList(values: string[]) {
|
|
13
|
+
return values.length > 0 ? values.join(", ") : "-"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatSqlPlan(plan: SqlQueryPlan) {
|
|
17
|
+
return [
|
|
18
|
+
"Plano de consulta:",
|
|
19
|
+
`- objetivo: ${plan.goal || "-"}`,
|
|
20
|
+
`- tabela principal: ${plan.mainTable || "-"}`,
|
|
21
|
+
`- tabelas relacionadas: ${formatList(plan.relatedTables)}`,
|
|
22
|
+
`- campos de relacionamento: ${formatList(plan.relationshipFields)}`,
|
|
23
|
+
`- colunas de saida: ${formatList(plan.selectedFields)}`,
|
|
24
|
+
`- filtros: ${formatList(plan.filters)}`,
|
|
25
|
+
`- ordenacao: ${formatList(plan.orderBy)}`,
|
|
26
|
+
`- limite: ${plan.limit}`
|
|
27
|
+
].join("\n")
|
|
28
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import test from "node:test"
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import { RequestService } from "../services/requestService.js"
|
|
4
|
+
import { resolveAssigneeId } from "../utils/resolveAssigneeId.js"
|
|
5
|
+
|
|
6
|
+
type PostCall = {
|
|
7
|
+
endpoint: string
|
|
8
|
+
body: any
|
|
9
|
+
params?: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type GetCall = {
|
|
13
|
+
endpoint: string
|
|
14
|
+
params?: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class FakeApiClient {
|
|
18
|
+
public getCalls: GetCall[] = []
|
|
19
|
+
public postCalls: PostCall[] = []
|
|
20
|
+
private getResult: any
|
|
21
|
+
private postResult: any
|
|
22
|
+
|
|
23
|
+
constructor(results: { getResult?: any; postResult?: any }) {
|
|
24
|
+
this.getResult = results.getResult
|
|
25
|
+
this.postResult = results.postResult
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async get(endpoint: string, params?: any) {
|
|
29
|
+
this.getCalls.push({ endpoint, params })
|
|
30
|
+
return this.getResult
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async post(endpoint: string, body: any = {}, params?: any) {
|
|
34
|
+
this.postCalls.push({ endpoint, body, params })
|
|
35
|
+
return this.postResult
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("list usa /api/api/os/listar e aplica filtro de responsavel", async () => {
|
|
40
|
+
const fakeApi = new FakeApiClient({ postResult: [{ id: 1 }] })
|
|
41
|
+
const service = new RequestService(fakeApi as any)
|
|
42
|
+
|
|
43
|
+
await service.list(123)
|
|
44
|
+
|
|
45
|
+
assert.equal(fakeApi.postCalls.length, 1)
|
|
46
|
+
const call = fakeApi.postCalls[0]
|
|
47
|
+
|
|
48
|
+
assert.equal(call.endpoint, "/api/api/os/listar")
|
|
49
|
+
assert.deepEqual(call.body, {})
|
|
50
|
+
assert.equal(call.params?.responsavel, 123)
|
|
51
|
+
assert.match(
|
|
52
|
+
String(call.params?.data_inclusao),
|
|
53
|
+
/^\d{4}-01-01 00:00:00to\d{4}-12-31 23:59:59$/
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("list com last_12_months envia data_inclusao em formato valido", async () => {
|
|
58
|
+
const fakeApi = new FakeApiClient({ postResult: [{ id: 1 }] })
|
|
59
|
+
const service = new RequestService(fakeApi as any)
|
|
60
|
+
|
|
61
|
+
await service.list({ assigneeId: 1, period: "last_12_months" })
|
|
62
|
+
|
|
63
|
+
assert.equal(fakeApi.postCalls.length, 1)
|
|
64
|
+
const call = fakeApi.postCalls[0]
|
|
65
|
+
assert.equal(call.endpoint, "/api/api/os/listar")
|
|
66
|
+
assert.match(
|
|
67
|
+
String(call.params?.data_inclusao),
|
|
68
|
+
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}to\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("view usa /api/api/os/listar com codigo", async () => {
|
|
73
|
+
const fakeApi = new FakeApiClient({ postResult: [{ codigo: 999, titulo: "OS 999" }] })
|
|
74
|
+
const service = new RequestService(fakeApi as any)
|
|
75
|
+
|
|
76
|
+
const result = await service.view(999)
|
|
77
|
+
|
|
78
|
+
assert.equal(fakeApi.postCalls.length, 1)
|
|
79
|
+
const call = fakeApi.postCalls[0]
|
|
80
|
+
assert.equal(call.endpoint, "/api/api/os/listar")
|
|
81
|
+
assert.deepEqual(call.body, {})
|
|
82
|
+
assert.deepEqual(call.params, { codigo: 999 })
|
|
83
|
+
assert.deepEqual(result, { codigo: 999, titulo: "OS 999" })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("create usa /api/api/solicitacao_rapida com payload mapeado", async () => {
|
|
87
|
+
const fakeApi = new FakeApiClient({ postResult: { id: 321 } })
|
|
88
|
+
const service = new RequestService(fakeApi as any)
|
|
89
|
+
|
|
90
|
+
const payload = {
|
|
91
|
+
os_tipo: 1,
|
|
92
|
+
os_tipo_servico: 2,
|
|
93
|
+
unidade_gerencial_executora: 3,
|
|
94
|
+
responsavel: 4,
|
|
95
|
+
titulo: "Nova solicitacao",
|
|
96
|
+
cliente: 5,
|
|
97
|
+
data_desejavel: "10/03/2026",
|
|
98
|
+
prioridade: 6,
|
|
99
|
+
observacao: "observacao de teste"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await service.create(payload)
|
|
103
|
+
|
|
104
|
+
assert.equal(fakeApi.postCalls.length, 1)
|
|
105
|
+
const call = fakeApi.postCalls[0]
|
|
106
|
+
assert.equal(call.endpoint, "/api/api/solicitacao_rapida")
|
|
107
|
+
assert.deepEqual(call.body, payload)
|
|
108
|
+
assert.deepEqual(result, { id: 321 })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("getTypes usa endpoint real com action/controller", async () => {
|
|
112
|
+
const fakeApi = new FakeApiClient({ getResult: [{ codigo: 1009, descricao: "Abrir Lojas" }] })
|
|
113
|
+
const service = new RequestService(fakeApi as any)
|
|
114
|
+
|
|
115
|
+
const result = await service.getTypes()
|
|
116
|
+
|
|
117
|
+
assert.equal(fakeApi.getCalls.length, 1)
|
|
118
|
+
const call = fakeApi.getCalls[0]
|
|
119
|
+
assert.equal(call.endpoint, "/epa_os/ajax.php")
|
|
120
|
+
assert.deepEqual(call.params, { action: "get", controller: "TipoSolicitacao" })
|
|
121
|
+
assert.deepEqual(result, [{ codigo: 1009, descricao: "Abrir Lojas" }])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("getAssignees monta payload obrigatorio e filtro opcional", async () => {
|
|
125
|
+
const fakeApi = new FakeApiClient({
|
|
126
|
+
getResult: { results: [{ id: 1, text: "suporte.simeon - Suporte Simeon" }] }
|
|
127
|
+
})
|
|
128
|
+
const service = new RequestService(fakeApi as any)
|
|
129
|
+
|
|
130
|
+
const result = await service.getAssignees("suporte", { unitValue: 18 })
|
|
131
|
+
|
|
132
|
+
assert.equal(fakeApi.getCalls.length, 1)
|
|
133
|
+
const call = fakeApi.getCalls[0]
|
|
134
|
+
assert.equal(call.endpoint, "/api/api/usuarios/search")
|
|
135
|
+
assert.deepEqual(call.params, {
|
|
136
|
+
term: "suporte",
|
|
137
|
+
_type: "query",
|
|
138
|
+
q: "suporte",
|
|
139
|
+
"filters[unidade_gerencial][type]": "pertenco",
|
|
140
|
+
"filters[unidade_gerencial][value]": 18
|
|
141
|
+
})
|
|
142
|
+
assert.deepEqual(result, [{ id: 1, text: "suporte.simeon - Suporte Simeon" }])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("resolveAssigneeId resolve login com match exato", () => {
|
|
146
|
+
const assignees = [
|
|
147
|
+
{ id: 10, text: "suporte.outro - Outro" },
|
|
148
|
+
{ id: 1, text: "suporte.simeon - Suporte Simeon" }
|
|
149
|
+
]
|
|
150
|
+
const id = resolveAssigneeId(assignees, "suporte.simeon")
|
|
151
|
+
assert.equal(id, 1)
|
|
152
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createTool } from "../../core/createTool.js"
|
|
2
|
+
import { TeamService } from "../../services/teamService.js"
|
|
3
|
+
|
|
4
|
+
const service = new TeamService()
|
|
5
|
+
|
|
6
|
+
export default createTool({
|
|
7
|
+
|
|
8
|
+
name: "analytics.teamReport",
|
|
9
|
+
|
|
10
|
+
description: "Gera relatório de tarefas de um desenvolvedor",
|
|
11
|
+
|
|
12
|
+
schema: {
|
|
13
|
+
name: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Nome do desenvolvedor"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
handler: async ({ name }) => {
|
|
20
|
+
|
|
21
|
+
return service.teamReport(name)
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
6
|
+
const toolsDir = path.dirname(currentFilePath);
|
|
7
|
+
|
|
8
|
+
export async function loadTools() {
|
|
9
|
+
|
|
10
|
+
const tools: any[] = [];
|
|
11
|
+
|
|
12
|
+
async function scan(dir: string) {
|
|
13
|
+
|
|
14
|
+
const files = fs.readdirSync(dir);
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
|
|
18
|
+
const full = path.join(dir, file);
|
|
19
|
+
const stat = fs.statSync(full);
|
|
20
|
+
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
await scan(full);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (file.endsWith(".draft.ts") || file.endsWith(".draft.js")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (file === "loadTools.ts" || file === "loadTools.js") {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tool = await import(pathToFileURL(full).href);
|
|
39
|
+
|
|
40
|
+
if (tool.default) {
|
|
41
|
+
tools.push(tool.default);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (tool.tool) {
|
|
45
|
+
tools.push(tool.tool);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (tool.teamReportTool) {
|
|
49
|
+
tools.push(tool.teamReportTool);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await scan(toolsDir);
|
|
57
|
+
|
|
58
|
+
return tools;
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { createTool } from "../../core/createTool.js"
|
|
3
|
+
import { RequestService } from "../../services/requestService.js"
|
|
4
|
+
|
|
5
|
+
const service = new RequestService()
|
|
6
|
+
|
|
7
|
+
export default createTool({
|
|
8
|
+
|
|
9
|
+
name: "requests.assignees",
|
|
10
|
+
|
|
11
|
+
description: `
|
|
12
|
+
Busca responsáveis no EPA.
|
|
13
|
+
|
|
14
|
+
Campos obrigatórios:
|
|
15
|
+
- term: texto de pesquisa
|
|
16
|
+
- _type: sempre "query" (preenchido automaticamente)
|
|
17
|
+
- q: texto de pesquisa (usa term por padrão)
|
|
18
|
+
|
|
19
|
+
Filtro opcional preparado:
|
|
20
|
+
- unit_value: ID da unidade gerencial
|
|
21
|
+
- unit_type: padrão "pertenco"
|
|
22
|
+
`,
|
|
23
|
+
|
|
24
|
+
schema: {
|
|
25
|
+
term: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Texto de pesquisa (ex: suporte)"
|
|
28
|
+
},
|
|
29
|
+
q: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Texto de pesquisa alternativo (opcional; padrão: term)"
|
|
32
|
+
},
|
|
33
|
+
unit_value: {
|
|
34
|
+
type: "number",
|
|
35
|
+
description: "ID da unidade gerencial para filtro opcional"
|
|
36
|
+
},
|
|
37
|
+
unit_type: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Tipo do filtro de unidade (opcional; padrao: pertenco)"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
validator: z.object({
|
|
43
|
+
term: z.string().min(1),
|
|
44
|
+
q: z.string().min(1).optional(),
|
|
45
|
+
unit_value: z.number().int().positive().optional(),
|
|
46
|
+
unit_type: z.string().min(1).optional()
|
|
47
|
+
}).strict(),
|
|
48
|
+
|
|
49
|
+
handler: async ({ term, q, unit_value, unit_type }) => {
|
|
50
|
+
|
|
51
|
+
return service.getAssignees(term, {
|
|
52
|
+
q,
|
|
53
|
+
unitValue: unit_value,
|
|
54
|
+
unitType: unit_type
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createTool } from "../../core/createTool.js"
|
|
2
|
+
import { RequestService } from "../../services/requestService.js"
|
|
3
|
+
|
|
4
|
+
const service = new RequestService()
|
|
5
|
+
|
|
6
|
+
export default createTool({
|
|
7
|
+
|
|
8
|
+
name: "requests.clients",
|
|
9
|
+
|
|
10
|
+
description: "Lista clientes disponíveis",
|
|
11
|
+
|
|
12
|
+
handler: async () => {
|
|
13
|
+
|
|
14
|
+
return service.getClients()
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { createTool } from "../../core/createTool.js"
|
|
3
|
+
import { RequestService } from "../../services/requestService.js"
|
|
4
|
+
|
|
5
|
+
const service = new RequestService()
|
|
6
|
+
|
|
7
|
+
export default createTool({
|
|
8
|
+
name: "requests.create",
|
|
9
|
+
description: `
|
|
10
|
+
Cria uma nova solicitacao no EPA.
|
|
11
|
+
|
|
12
|
+
Antes de usar esta tool e recomendado consultar:
|
|
13
|
+
|
|
14
|
+
1. requests.types
|
|
15
|
+
2. requests.services
|
|
16
|
+
3. requests.units
|
|
17
|
+
4. requests.assignees
|
|
18
|
+
5. requests.clients
|
|
19
|
+
6. requests.priorities
|
|
20
|
+
|
|
21
|
+
Use os IDs retornados por essas ferramentas.
|
|
22
|
+
`,
|
|
23
|
+
schema: {
|
|
24
|
+
os_tipo: { type: "number", description: "ID do tipo retornado pela tool requests.types" },
|
|
25
|
+
os_tipo_servico: { type: "number", description: "ID de servico retornado pela tool requests.services" },
|
|
26
|
+
unidade_gerencial_executora: { type: "number", description: "ID da unidade retornado pela tool requests.units" },
|
|
27
|
+
responsavel: { type: "number", description: "ID do responsavel retornado pela tool requests.assignees" },
|
|
28
|
+
cliente: { type: "number", description: "ID do cliente retornado pela tool requests.clients" },
|
|
29
|
+
prioridade: { type: "number", description: "ID da prioridade retornado pela tool requests.priorities" },
|
|
30
|
+
|
|
31
|
+
titulo: { type: "string" },
|
|
32
|
+
observacao: { type: "string" },
|
|
33
|
+
data_desejavel: { type: "string", description: "Formato dd/mm/yyyy" }
|
|
34
|
+
},
|
|
35
|
+
validator: z.object({
|
|
36
|
+
os_tipo: z.number().int().positive(),
|
|
37
|
+
os_tipo_servico: z.number().int().positive(),
|
|
38
|
+
unidade_gerencial_executora: z.number().int().positive(),
|
|
39
|
+
responsavel: z.number().int().positive(),
|
|
40
|
+
cliente: z.number().int().positive(),
|
|
41
|
+
prioridade: z.number().int().positive(),
|
|
42
|
+
titulo: z.string().min(3),
|
|
43
|
+
observacao: z.string().optional(),
|
|
44
|
+
data_desejavel: z.string().regex(/^\d{2}\/\d{2}\/\d{4}$/)
|
|
45
|
+
}).strict(),
|
|
46
|
+
|
|
47
|
+
handler: async (args) => {
|
|
48
|
+
|
|
49
|
+
const result = await service.create(args)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
message: "Solicitacao criada com sucesso",
|
|
53
|
+
codigo: result.id,
|
|
54
|
+
link: `/exibir_solicitacao.php?codigo=${result.id}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { createTool } from "../../core/createTool.js"
|
|
3
|
+
import { RequestService } from "../../services/requestService.js"
|
|
4
|
+
import { resolveAssigneeId } from "../../utils/resolveAssigneeId.js"
|
|
5
|
+
|
|
6
|
+
const service = new RequestService()
|
|
7
|
+
|
|
8
|
+
export default createTool({
|
|
9
|
+
name: "requests.list",
|
|
10
|
+
description: `
|
|
11
|
+
Lista solicitacoes do EPA.
|
|
12
|
+
|
|
13
|
+
Pode filtrar por ID do responsavel ou pelo nome/login do responsavel.
|
|
14
|
+
Tambem permite definir periodo.
|
|
15
|
+
|
|
16
|
+
Para descobrir os responsaveis disponiveis use:
|
|
17
|
+
requests.assignees
|
|
18
|
+
`,
|
|
19
|
+
schema: {
|
|
20
|
+
assignee_id: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "ID do responsavel (retornado por requests.assignees)"
|
|
23
|
+
},
|
|
24
|
+
assignee_name: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Nome/login do responsavel (ex: suporte.simeon)"
|
|
27
|
+
},
|
|
28
|
+
period: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Periodo de busca: current_year (padrao) ou last_12_months"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
validator: z.object({
|
|
34
|
+
assignee_id: z.number().int().positive().optional(),
|
|
35
|
+
assignee_name: z.string().min(1).optional(),
|
|
36
|
+
period: z.enum(["current_year", "last_12_months"]).optional()
|
|
37
|
+
}).strict(),
|
|
38
|
+
|
|
39
|
+
handler: async ({ assignee_id, assignee_name, period }) => {
|
|
40
|
+
|
|
41
|
+
if (assignee_id) {
|
|
42
|
+
return service.list({ assigneeId: assignee_id, period })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (assignee_name) {
|
|
46
|
+
const assignees = await service.getAssignees(assignee_name, { q: assignee_name })
|
|
47
|
+
const assigneeId = resolveAssigneeId(assignees, assignee_name)
|
|
48
|
+
if (!assigneeId) {
|
|
49
|
+
return `Responsavel "${assignee_name}" nao encontrado.`
|
|
50
|
+
}
|
|
51
|
+
return service.list({ assigneeId, period })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return service.list({ period })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
})
|