@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (142) hide show
  1. package/README.md +68 -6
  2. package/docs/guides/add-channel.md +189 -7
  3. package/docs/guides/add-knowledge.md +144 -0
  4. package/docs/guides/add-managed-composio.md +163 -0
  5. package/docs/guides/add-tool.md +1 -1
  6. package/docs/guides/channel-security.md +128 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +78 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +126 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +112 -8
  13. package/docs/guides/create-agent.md +45 -4
  14. package/docs/guides/debug-channel.md +147 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +47 -17
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +147 -20
  19. package/docs/guides/security-rules.md +7 -6
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-provider.md +27 -3
  22. package/docs/llms-full.txt +348 -55
  23. package/docs/llms.txt +62 -7
  24. package/package.json +2 -5
  25. package/src/cli/args.ts +57 -0
  26. package/src/cli/cloud-client.ts +377 -0
  27. package/src/cli/commands/channels.ts +1586 -0
  28. package/src/cli/commands/feedback.ts +438 -0
  29. package/src/cli/commands/knowledge.ts +136 -0
  30. package/src/cli/commands/transcribe.ts +171 -0
  31. package/src/cli/constants.ts +4 -0
  32. package/src/cli/deploy-chat-ui.ts +535 -0
  33. package/src/cli/deploy-readiness.ts +481 -0
  34. package/src/cli/flags.ts +162 -0
  35. package/src/cli/help.ts +236 -0
  36. package/src/cli/index.ts +1167 -1005
  37. package/src/cli/process.ts +31 -0
  38. package/src/cloud/artifact.ts +139 -0
  39. package/src/cloud/client.ts +80 -0
  40. package/src/cloud/contracts.ts +63 -0
  41. package/src/cloud/index.ts +3 -0
  42. package/src/create-project.ts +21 -6
  43. package/src/index.ts +517 -8
  44. package/src/providers/pi.ts +70 -16
  45. package/src/providers/test.ts +88 -1
  46. package/src/providers/types.ts +7 -0
  47. package/src/runtime/channel-buffer.ts +30 -0
  48. package/src/runtime/channel-test-harness.ts +21 -1
  49. package/src/runtime/channels/discord.ts +896 -0
  50. package/src/runtime/channels/generic-webhook.ts +225 -0
  51. package/src/runtime/channels/slack.ts +646 -0
  52. package/src/runtime/channels/telegram.ts +466 -23
  53. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  54. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  55. package/src/runtime/channels/whatsapp-uazapi.ts +1327 -0
  56. package/src/runtime/channels/whatsapp-zapster.ts +677 -40
  57. package/src/runtime/channels.ts +87 -4
  58. package/src/runtime/chat.ts +130 -38
  59. package/src/runtime/config.ts +519 -19
  60. package/src/runtime/core/manifest.ts +103 -5
  61. package/src/runtime/core/targets.ts +5 -5
  62. package/src/runtime/database.ts +93 -2
  63. package/src/runtime/db-commands.ts +9 -0
  64. package/src/runtime/deploy-readiness.ts +46 -4
  65. package/src/runtime/deploy.ts +1 -1
  66. package/src/runtime/dev-server.ts +779 -45
  67. package/src/runtime/env.ts +8 -3
  68. package/src/runtime/evals.ts +589 -43
  69. package/src/runtime/improve.ts +868 -0
  70. package/src/runtime/inspect.ts +194 -4
  71. package/src/runtime/integrations/composio.ts +423 -0
  72. package/src/runtime/knowledge/chunk.ts +333 -0
  73. package/src/runtime/knowledge/config.ts +135 -0
  74. package/src/runtime/knowledge/embeddings.ts +133 -0
  75. package/src/runtime/knowledge/ingest.ts +521 -0
  76. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  77. package/src/runtime/knowledge/retrieve.ts +303 -0
  78. package/src/runtime/knowledge/schema.ts +100 -0
  79. package/src/runtime/knowledge/tool.ts +64 -0
  80. package/src/runtime/knowledge/vector.ts +258 -0
  81. package/src/runtime/prompt-context.ts +141 -0
  82. package/src/runtime/runtime-contract.ts +86 -8
  83. package/src/runtime/skills.ts +95 -0
  84. package/src/runtime/spec.ts +152 -0
  85. package/src/runtime/sync.ts +144 -0
  86. package/src/runtime/targets/cloudflare/build.ts +1468 -203
  87. package/src/runtime/targets/container/server.ts +1 -1
  88. package/src/runtime/targets/vps/deploy.ts +26 -9
  89. package/src/runtime/tool-runner.ts +9 -1
  90. package/src/runtime/tools.ts +128 -2
  91. package/src/runtime/traces.ts +41 -0
  92. package/src/runtime/transcription.ts +483 -0
  93. package/src/storage/sqlite.ts +149 -3
  94. package/src/templates/blank.ts +76 -17
  95. package/src/templates/dentista.ts +1011 -0
  96. package/src/templates/index.ts +2 -0
  97. package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
  98. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
  99. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  100. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  101. package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
  102. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  103. package/src/templates/skills/agentkit-channels/SKILL.md +127 -0
  104. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
  105. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
  106. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  107. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  108. package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
  109. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  110. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +61 -0
  111. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
  112. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  113. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  114. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  115. package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
  116. package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
  117. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
  118. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
  119. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
  120. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
  121. package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
  122. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  123. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  124. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  125. package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
  126. package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
  127. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  128. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  129. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  130. package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
  131. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  132. package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
  133. package/src/templates/skills/agentkit-security/SKILL.md +56 -0
  134. package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
  135. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  136. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  137. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  138. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
  139. package/src/templates/support.ts +77 -18
  140. package/docs/guides/channels-production-handoff.md +0 -99
  141. package/docs/portable-deploy-release-checklist.md +0 -41
  142. package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
@@ -0,0 +1,1011 @@
1
+ import type { AgentTemplate } from ".";
2
+
3
+ export const dentistaTemplate: AgentTemplate = {
4
+ name: "dentista",
5
+ files(projectName: string, context = {}) {
6
+ return [
7
+ {
8
+ path: "package.json",
9
+ contents: `${JSON.stringify(
10
+ {
11
+ name: projectName,
12
+ private: true,
13
+ type: "module",
14
+ scripts: {
15
+ agentkit: "agentkit",
16
+ dev: "agentkit dev",
17
+ chat: "agentkit chat",
18
+ eval: "agentkit eval run",
19
+ typecheck: "tsc --noEmit",
20
+ },
21
+ dependencies: {
22
+ "@andreprado/agentkit": context.agentkitDependency ?? "workspace:*",
23
+ },
24
+ devDependencies: {
25
+ "@types/node": "^24.12.4",
26
+ typescript: "^5.9.3",
27
+ },
28
+ },
29
+ null,
30
+ 2,
31
+ )}
32
+ `,
33
+ },
34
+ {
35
+ path: "tsconfig.json",
36
+ contents: `${JSON.stringify(
37
+ {
38
+ compilerOptions: {
39
+ target: "ES2022",
40
+ module: "ESNext",
41
+ moduleResolution: "Bundler",
42
+ strict: true,
43
+ skipLibCheck: true,
44
+ noEmit: true,
45
+ types: ["node"],
46
+ },
47
+ include: ["**/*.ts"],
48
+ },
49
+ null,
50
+ 2,
51
+ )}
52
+ `,
53
+ },
54
+ {
55
+ path: ".gitignore",
56
+ contents: `.env
57
+ .agentkit/
58
+ node_modules/
59
+ `,
60
+ },
61
+ {
62
+ path: ".env.schema",
63
+ contents: `# Opcional: adicione chaves reais depois de trocar o provider test/fake.
64
+ OPENAI_API_KEY=
65
+ ANTHROPIC_API_KEY=
66
+ OPENROUTER_API_KEY=
67
+ `,
68
+ },
69
+ {
70
+ path: "agentkit.config.ts",
71
+ contents: `import { defineAgent } from "@andreprado/agentkit";
72
+ import {
73
+ agendarConsulta,
74
+ alterarConsulta,
75
+ consultarConsulta,
76
+ listarHorariosDisponiveis,
77
+ } from "./tools/agenda";
78
+
79
+ export default defineAgent({
80
+ name: "Clara",
81
+ runtime: "edge",
82
+ provider: {
83
+ name: "test",
84
+ model: "fake",
85
+ },
86
+ timeZone: "America/Sao_Paulo",
87
+ instructions: "./prompts/instructions.md",
88
+ secrets: [],
89
+ tools: [listarHorariosDisponiveis, consultarConsulta, agendarConsulta, alterarConsulta],
90
+ access: {
91
+ mode: "private",
92
+ },
93
+ storage: {
94
+ driver: "agentkit",
95
+ path: ".agentkit/agentkit.db",
96
+ database: {
97
+ driver: "turso",
98
+ schema: "./schema.sql",
99
+ },
100
+ },
101
+ });
102
+ `,
103
+ },
104
+ {
105
+ path: "schema.sql",
106
+ contents: `CREATE TABLE IF NOT EXISTS consultas_dentista (
107
+ id TEXT PRIMARY KEY,
108
+ nome TEXT NOT NULL,
109
+ email TEXT NOT NULL,
110
+ telefone TEXT NOT NULL,
111
+ data TEXT NOT NULL,
112
+ horario TEXT NOT NULL,
113
+ status TEXT NOT NULL DEFAULT 'agendada',
114
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
115
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
+ UNIQUE (email, telefone),
117
+ UNIQUE (data, horario)
118
+ );
119
+
120
+ CREATE INDEX IF NOT EXISTS consultas_dentista_cliente_idx
121
+ ON consultas_dentista (email, telefone);
122
+
123
+ CREATE INDEX IF NOT EXISTS consultas_dentista_data_idx
124
+ ON consultas_dentista (data, horario);
125
+ `,
126
+ },
127
+ {
128
+ path: "src/agent.ts",
129
+ contents: `export { default } from "../agentkit.config";
130
+ `,
131
+ },
132
+ {
133
+ path: "tools/agenda.ts",
134
+ contents: `import { defineTool, type DatabaseRunner } from "@andreprado/agentkit";
135
+
136
+ const CLINIC_TIME_ZONE = "America/Sao_Paulo";
137
+ const ALL_SLOTS = [
138
+ "08:00",
139
+ "08:30",
140
+ "09:00",
141
+ "09:30",
142
+ "10:00",
143
+ "10:30",
144
+ "11:00",
145
+ "11:30",
146
+ "13:30",
147
+ "14:00",
148
+ "14:30",
149
+ "15:00",
150
+ "15:30",
151
+ "16:00",
152
+ "16:30",
153
+ "17:00",
154
+ "17:30",
155
+ ];
156
+ const SLOT_SET = new Set(ALL_SLOTS);
157
+
158
+ type ConsultaRow = {
159
+ id: string;
160
+ nome: string;
161
+ email: string;
162
+ telefone: string;
163
+ data: string;
164
+ horario: string;
165
+ };
166
+
167
+ type ListarHorariosInput = {
168
+ data: string;
169
+ };
170
+
171
+ type ConsultarConsultaInput = {
172
+ email: string;
173
+ telefone: string;
174
+ };
175
+
176
+ type AgendarConsultaInput = {
177
+ nome: string;
178
+ email: string;
179
+ telefone: string;
180
+ data: string;
181
+ horario: string;
182
+ confirmadoPeloCliente: boolean;
183
+ };
184
+
185
+ type AlterarConsultaInput = {
186
+ email: string;
187
+ telefone: string;
188
+ novaData: string;
189
+ novoHorario: string;
190
+ confirmadoPeloCliente: boolean;
191
+ };
192
+
193
+ type ConsultaOutput = {
194
+ encontrada: boolean;
195
+ id: string;
196
+ nome: string;
197
+ email: string;
198
+ telefone: string;
199
+ data: string;
200
+ horario: string;
201
+ mensagem: string;
202
+ };
203
+
204
+ type AgendaOutput = {
205
+ ok: boolean;
206
+ id: string;
207
+ nome: string;
208
+ email: string;
209
+ telefone: string;
210
+ data: string;
211
+ horario: string;
212
+ disponiveis: string[];
213
+ mensagem: string;
214
+ };
215
+
216
+ const consultaOutputSchema = {
217
+ type: "object",
218
+ properties: {
219
+ encontrada: { type: "boolean" },
220
+ id: { type: "string" },
221
+ nome: { type: "string" },
222
+ email: { type: "string" },
223
+ telefone: { type: "string" },
224
+ data: { type: "string" },
225
+ horario: { type: "string" },
226
+ mensagem: { type: "string" },
227
+ },
228
+ required: ["encontrada", "id", "nome", "email", "telefone", "data", "horario", "mensagem"],
229
+ additionalProperties: false,
230
+ };
231
+
232
+ const agendaOutputSchema = {
233
+ type: "object",
234
+ properties: {
235
+ ok: { type: "boolean" },
236
+ id: { type: "string" },
237
+ nome: { type: "string" },
238
+ email: { type: "string" },
239
+ telefone: { type: "string" },
240
+ data: { type: "string" },
241
+ horario: { type: "string" },
242
+ disponiveis: {
243
+ type: "array",
244
+ items: { type: "string" },
245
+ },
246
+ mensagem: { type: "string" },
247
+ },
248
+ required: ["ok", "id", "nome", "email", "telefone", "data", "horario", "disponiveis", "mensagem"],
249
+ additionalProperties: false,
250
+ };
251
+
252
+ export const listarHorariosDisponiveis = defineTool<ListarHorariosInput, { data: string; disponiveis: string[]; ocupados: string[]; mensagem: string }>({
253
+ name: "listar_horarios_disponiveis",
254
+ description: "Lista os slots de 30 minutos disponiveis para uma data no consultorio odontologico.",
255
+ visibility: "user",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: {
259
+ data: {
260
+ type: "string",
261
+ description: "Data desejada no formato YYYY-MM-DD.",
262
+ },
263
+ },
264
+ required: ["data"],
265
+ additionalProperties: false,
266
+ },
267
+ outputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ data: { type: "string" },
271
+ disponiveis: {
272
+ type: "array",
273
+ items: { type: "string" },
274
+ },
275
+ ocupados: {
276
+ type: "array",
277
+ items: { type: "string" },
278
+ },
279
+ mensagem: { type: "string" },
280
+ },
281
+ required: ["data", "disponiveis", "ocupados", "mensagem"],
282
+ additionalProperties: false,
283
+ },
284
+ async execute(input, ctx) {
285
+ const date = normalizeDate(input.data);
286
+ const dateError = validateAppointmentDate(date, ctx.clock.now);
287
+
288
+ if (dateError) {
289
+ return {
290
+ data: date,
291
+ disponiveis: [],
292
+ ocupados: [],
293
+ mensagem: dateError,
294
+ };
295
+ }
296
+
297
+ const occupied = await occupiedSlots(ctx.db, date);
298
+ const disponiveis = ALL_SLOTS.filter((slot) => !occupied.has(slot));
299
+
300
+ return {
301
+ data: date,
302
+ disponiveis,
303
+ ocupados: Array.from(occupied).sort(),
304
+ mensagem:
305
+ disponiveis.length > 0
306
+ ? "Estes horarios estao disponiveis para consulta."
307
+ : "Nao ha horarios disponiveis nesta data.",
308
+ };
309
+ },
310
+ });
311
+
312
+ export const consultarConsulta = defineTool<ConsultarConsultaInput, ConsultaOutput>({
313
+ name: "consultar_consulta",
314
+ description: "Consulta o horario agendado de um cliente usando email e telefone.",
315
+ visibility: "user",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ email: { type: "string" },
320
+ telefone: { type: "string" },
321
+ },
322
+ required: ["email", "telefone"],
323
+ additionalProperties: false,
324
+ },
325
+ outputSchema: consultaOutputSchema,
326
+ async execute(input, ctx) {
327
+ const contact = normalizeContact(input.email, input.telefone);
328
+
329
+ if (!contact.ok) {
330
+ return emptyConsulta(contact.mensagem);
331
+ }
332
+
333
+ const appointment = await findAppointmentByContact(ctx.db, contact.email, contact.telefone);
334
+
335
+ if (!appointment) {
336
+ return emptyConsulta("Nao encontrei consulta agendada para este email e telefone.");
337
+ }
338
+
339
+ return {
340
+ encontrada: true,
341
+ id: appointment.id,
342
+ nome: appointment.nome,
343
+ email: appointment.email,
344
+ telefone: appointment.telefone,
345
+ data: appointment.data,
346
+ horario: appointment.horario,
347
+ mensagem: \`Consulta encontrada para \${appointment.data} as \${appointment.horario}.\`,
348
+ };
349
+ },
350
+ });
351
+
352
+ export const agendarConsulta = defineTool<AgendarConsultaInput, AgendaOutput>({
353
+ name: "agendar_consulta",
354
+ description: "Agenda uma consulta odontologica depois que o cliente confirma explicitamente data e horario.",
355
+ visibility: "user",
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ nome: { type: "string" },
360
+ email: { type: "string" },
361
+ telefone: { type: "string" },
362
+ data: {
363
+ type: "string",
364
+ description: "Data confirmada no formato YYYY-MM-DD.",
365
+ },
366
+ horario: {
367
+ type: "string",
368
+ description: "Horario confirmado, em slot de 30 minutos, no formato HH:MM.",
369
+ },
370
+ confirmadoPeloCliente: {
371
+ type: "boolean",
372
+ description: "Use true somente depois que o cliente confirmar explicitamente a data e o horario.",
373
+ },
374
+ },
375
+ required: ["nome", "email", "telefone", "data", "horario", "confirmadoPeloCliente"],
376
+ additionalProperties: false,
377
+ },
378
+ outputSchema: agendaOutputSchema,
379
+ async execute(input, ctx) {
380
+ const nome = input.nome.trim();
381
+ const contact = normalizeContact(input.email, input.telefone);
382
+ const data = normalizeDate(input.data);
383
+ const horario = normalizeTime(input.horario);
384
+
385
+ if (!nome) {
386
+ return agendaFailure("Preciso do nome do cliente para agendar.", { data, horario });
387
+ }
388
+
389
+ if (!contact.ok) {
390
+ return agendaFailure(contact.mensagem, { nome, data, horario });
391
+ }
392
+
393
+ const existingClient = await findAppointmentByContact(ctx.db, contact.email, contact.telefone);
394
+
395
+ if (existingClient) {
396
+ return {
397
+ ok: false,
398
+ id: existingClient.id,
399
+ nome: existingClient.nome,
400
+ email: existingClient.email,
401
+ telefone: existingClient.telefone,
402
+ data: existingClient.data,
403
+ horario: existingClient.horario,
404
+ disponiveis: await availableSlots(ctx.db, existingClient.data),
405
+ mensagem: "Este cliente ja tem uma consulta agendada. Use alterar_consulta se ele quiser remarcar.",
406
+ };
407
+ }
408
+
409
+ const validation = await validateScheduleRequest(ctx.db, {
410
+ data,
411
+ horario,
412
+ confirmadoPeloCliente: input.confirmadoPeloCliente,
413
+ now: ctx.clock.now,
414
+ });
415
+
416
+ if (!validation.ok) {
417
+ return agendaFailure(validation.mensagem, { nome, email: contact.email, telefone: contact.telefone, data, horario, disponiveis: validation.disponiveis });
418
+ }
419
+
420
+ const id = crypto.randomUUID();
421
+
422
+ try {
423
+ await ctx.db.execute(
424
+ "INSERT INTO consultas_dentista (id, nome, email, telefone, data, horario) VALUES (?, ?, ?, ?, ?, ?)",
425
+ [id, nome, contact.email, contact.telefone, data, horario],
426
+ );
427
+ } catch {
428
+ const disponiveis = await availableSlots(ctx.db, data);
429
+ return agendaFailure("Esse horario acabou de ficar indisponivel. Ofereca outro horario ao cliente.", {
430
+ nome,
431
+ email: contact.email,
432
+ telefone: contact.telefone,
433
+ data,
434
+ horario,
435
+ disponiveis,
436
+ });
437
+ }
438
+
439
+ return {
440
+ ok: true,
441
+ id,
442
+ nome,
443
+ email: contact.email,
444
+ telefone: contact.telefone,
445
+ data,
446
+ horario,
447
+ disponiveis: validation.disponiveis,
448
+ mensagem: \`Consulta agendada para \${data} as \${horario}.\`,
449
+ };
450
+ },
451
+ });
452
+
453
+ export const alterarConsulta = defineTool<AlterarConsultaInput, AgendaOutput>({
454
+ name: "alterar_consulta",
455
+ description: "Altera a propria consulta do cliente usando email e telefone, depois de confirmar o novo horario.",
456
+ visibility: "user",
457
+ inputSchema: {
458
+ type: "object",
459
+ properties: {
460
+ email: { type: "string" },
461
+ telefone: { type: "string" },
462
+ novaData: {
463
+ type: "string",
464
+ description: "Nova data confirmada no formato YYYY-MM-DD.",
465
+ },
466
+ novoHorario: {
467
+ type: "string",
468
+ description: "Novo horario confirmado, em slot de 30 minutos, no formato HH:MM.",
469
+ },
470
+ confirmadoPeloCliente: {
471
+ type: "boolean",
472
+ description: "Use true somente depois que o cliente confirmar explicitamente a nova data e o novo horario.",
473
+ },
474
+ },
475
+ required: ["email", "telefone", "novaData", "novoHorario", "confirmadoPeloCliente"],
476
+ additionalProperties: false,
477
+ },
478
+ outputSchema: agendaOutputSchema,
479
+ async execute(input, ctx) {
480
+ const contact = normalizeContact(input.email, input.telefone);
481
+ const data = normalizeDate(input.novaData);
482
+ const horario = normalizeTime(input.novoHorario);
483
+ const validation = await validateScheduleRequest(ctx.db, {
484
+ data,
485
+ horario,
486
+ confirmadoPeloCliente: input.confirmadoPeloCliente,
487
+ now: ctx.clock.now,
488
+ });
489
+
490
+ if (!contact.ok) {
491
+ return agendaFailure(contact.mensagem, { data, horario });
492
+ }
493
+
494
+ const appointment = await findAppointmentByContact(ctx.db, contact.email, contact.telefone);
495
+
496
+ if (!appointment) {
497
+ return agendaFailure("Nao encontrei consulta agendada para este email e telefone.", {
498
+ email: contact.email,
499
+ telefone: contact.telefone,
500
+ data,
501
+ horario,
502
+ });
503
+ }
504
+
505
+ if (!validation.ok) {
506
+ return agendaFailure(validation.mensagem, {
507
+ id: appointment.id,
508
+ nome: appointment.nome,
509
+ email: appointment.email,
510
+ telefone: appointment.telefone,
511
+ data: appointment.data,
512
+ horario: appointment.horario,
513
+ disponiveis: validation.disponiveis,
514
+ });
515
+ }
516
+
517
+ if (appointment.data === data && appointment.horario === horario) {
518
+ return {
519
+ ok: true,
520
+ id: appointment.id,
521
+ nome: appointment.nome,
522
+ email: appointment.email,
523
+ telefone: appointment.telefone,
524
+ data,
525
+ horario,
526
+ disponiveis: validation.disponiveis,
527
+ mensagem: "A consulta ja estava marcada nesse horario.",
528
+ };
529
+ }
530
+
531
+ const conflicting = await findAppointmentBySlot(ctx.db, data, horario);
532
+
533
+ if (conflicting && conflicting.id !== appointment.id) {
534
+ const disponiveis = await availableSlots(ctx.db, data);
535
+ return agendaFailure("Esse horario nao esta mais disponivel. Ofereca outro horario ao cliente.", {
536
+ id: appointment.id,
537
+ nome: appointment.nome,
538
+ email: appointment.email,
539
+ telefone: appointment.telefone,
540
+ data: appointment.data,
541
+ horario: appointment.horario,
542
+ disponiveis,
543
+ });
544
+ }
545
+
546
+ try {
547
+ await ctx.db.execute("UPDATE consultas_dentista SET data = ?, horario = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [
548
+ data,
549
+ horario,
550
+ appointment.id,
551
+ ]);
552
+ } catch {
553
+ const disponiveis = await availableSlots(ctx.db, data);
554
+ return agendaFailure("Esse horario acabou de ficar indisponivel. Ofereca outro horario ao cliente.", {
555
+ id: appointment.id,
556
+ nome: appointment.nome,
557
+ email: appointment.email,
558
+ telefone: appointment.telefone,
559
+ data: appointment.data,
560
+ horario: appointment.horario,
561
+ disponiveis,
562
+ });
563
+ }
564
+
565
+ return {
566
+ ok: true,
567
+ id: appointment.id,
568
+ nome: appointment.nome,
569
+ email: appointment.email,
570
+ telefone: appointment.telefone,
571
+ data,
572
+ horario,
573
+ disponiveis: validation.disponiveis,
574
+ mensagem: \`Consulta alterada para \${data} as \${horario}.\`,
575
+ };
576
+ },
577
+ });
578
+
579
+ async function validateScheduleRequest(
580
+ db: DatabaseRunner,
581
+ input: { data: string; horario: string; confirmadoPeloCliente: boolean; now: Date },
582
+ ): Promise<{ ok: boolean; mensagem: string; disponiveis: string[] }> {
583
+ const dateError = validateAppointmentDate(input.data, input.now);
584
+
585
+ if (dateError) {
586
+ return {
587
+ ok: false,
588
+ mensagem: dateError,
589
+ disponiveis: [],
590
+ };
591
+ }
592
+
593
+ const disponiveis = await availableSlots(db, input.data);
594
+
595
+ if (!input.confirmadoPeloCliente) {
596
+ return {
597
+ ok: false,
598
+ mensagem: "Antes de salvar, confirme explicitamente a data e o horario com o cliente.",
599
+ disponiveis,
600
+ };
601
+ }
602
+
603
+ if (!SLOT_SET.has(input.horario)) {
604
+ return {
605
+ ok: false,
606
+ mensagem: "Horario invalido. Use apenas slots de 30 minutos entre 08:00 e 11:30 ou entre 13:30 e 17:30.",
607
+ disponiveis,
608
+ };
609
+ }
610
+
611
+ if (!disponiveis.includes(input.horario)) {
612
+ return {
613
+ ok: false,
614
+ mensagem: "Esse horario nao esta disponivel. Ofereca um dos horarios disponiveis.",
615
+ disponiveis,
616
+ };
617
+ }
618
+
619
+ return {
620
+ ok: true,
621
+ mensagem: "Horario valido.",
622
+ disponiveis,
623
+ };
624
+ }
625
+
626
+ async function availableSlots(db: DatabaseRunner, data: string): Promise<string[]> {
627
+ const occupied = await occupiedSlots(db, data);
628
+ return ALL_SLOTS.filter((slot) => !occupied.has(slot));
629
+ }
630
+
631
+ async function occupiedSlots(db: DatabaseRunner, data: string): Promise<Set<string>> {
632
+ const result = await db.query("SELECT horario FROM consultas_dentista WHERE data = ? AND status = 'agendada'", [data]);
633
+ return new Set(result.rows.map((row) => String(row.horario)));
634
+ }
635
+
636
+ async function findAppointmentByContact(db: DatabaseRunner, email: string, telefone: string): Promise<ConsultaRow | null> {
637
+ const result = await db.query(
638
+ "SELECT id, nome, email, telefone, data, horario FROM consultas_dentista WHERE email = ? AND telefone = ? AND status = 'agendada' LIMIT 1",
639
+ [email, telefone],
640
+ );
641
+ return rowToAppointment(result.rows[0]);
642
+ }
643
+
644
+ async function findAppointmentBySlot(db: DatabaseRunner, data: string, horario: string): Promise<ConsultaRow | null> {
645
+ const result = await db.query(
646
+ "SELECT id, nome, email, telefone, data, horario FROM consultas_dentista WHERE data = ? AND horario = ? AND status = 'agendada' LIMIT 1",
647
+ [data, horario],
648
+ );
649
+ return rowToAppointment(result.rows[0]);
650
+ }
651
+
652
+ function rowToAppointment(row: Record<string, unknown> | undefined): ConsultaRow | null {
653
+ if (!row) {
654
+ return null;
655
+ }
656
+
657
+ return {
658
+ id: String(row.id),
659
+ nome: String(row.nome),
660
+ email: String(row.email),
661
+ telefone: String(row.telefone),
662
+ data: String(row.data),
663
+ horario: String(row.horario),
664
+ };
665
+ }
666
+
667
+ function normalizeContact(emailInput: string, telefoneInput: string): { ok: true; email: string; telefone: string } | { ok: false; mensagem: string } {
668
+ const email = emailInput.trim().toLowerCase();
669
+ const telefone = telefoneInput.replace(/\\D/g, "");
670
+
671
+ if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) {
672
+ return { ok: false, mensagem: "Preciso de um email valido para localizar ou salvar a consulta." };
673
+ }
674
+
675
+ if (telefone.length < 8) {
676
+ return { ok: false, mensagem: "Preciso de um telefone valido para localizar ou salvar a consulta." };
677
+ }
678
+
679
+ return { ok: true, email, telefone };
680
+ }
681
+
682
+ function normalizeDate(input: string): string {
683
+ return input.trim();
684
+ }
685
+
686
+ function normalizeTime(input: string): string {
687
+ const trimmed = input.trim().toLowerCase().replace(/\\s+/g, "");
688
+ const hourOnly = /^(\\d{1,2})h$/.exec(trimmed);
689
+
690
+ if (hourOnly) {
691
+ return \`\${hourOnly[1].padStart(2, "0")}:00\`;
692
+ }
693
+
694
+ const normalized = trimmed.replace("h", ":");
695
+ const match = /^(\\d{1,2}):(\\d{2})$/.exec(normalized);
696
+
697
+ if (!match) {
698
+ return input.trim();
699
+ }
700
+
701
+ return \`\${match[1].padStart(2, "0")}:\${match[2]}\`;
702
+ }
703
+
704
+ function validateAppointmentDate(data: string, now: Date): string | null {
705
+ if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(data)) {
706
+ return "Use a data no formato YYYY-MM-DD.";
707
+ }
708
+
709
+ const [year, month, day] = data.split("-").map(Number);
710
+ const parsed = new Date(Date.UTC(year, month - 1, day));
711
+
712
+ if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
713
+ return "Esta data nao existe. Confirme a data com o cliente.";
714
+ }
715
+
716
+ if (data < todayInClinicTimezone(now)) {
717
+ return "Nao agende consultas em datas passadas.";
718
+ }
719
+
720
+ return null;
721
+ }
722
+
723
+ function todayInClinicTimezone(now: Date): string {
724
+ const parts = new Intl.DateTimeFormat("en-US", {
725
+ timeZone: CLINIC_TIME_ZONE,
726
+ year: "numeric",
727
+ month: "2-digit",
728
+ day: "2-digit",
729
+ }).formatToParts(now);
730
+ const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
731
+ return \`\${byType.year}-\${byType.month}-\${byType.day}\`;
732
+ }
733
+
734
+ function emptyConsulta(mensagem: string): ConsultaOutput {
735
+ return {
736
+ encontrada: false,
737
+ id: "",
738
+ nome: "",
739
+ email: "",
740
+ telefone: "",
741
+ data: "",
742
+ horario: "",
743
+ mensagem,
744
+ };
745
+ }
746
+
747
+ function agendaFailure(
748
+ mensagem: string,
749
+ partial: Partial<Omit<AgendaOutput, "ok" | "mensagem">> = {},
750
+ ): AgendaOutput {
751
+ return {
752
+ ok: false,
753
+ id: partial.id ?? "",
754
+ nome: partial.nome ?? "",
755
+ email: partial.email ?? "",
756
+ telefone: partial.telefone ?? "",
757
+ data: partial.data ?? "",
758
+ horario: partial.horario ?? "",
759
+ disponiveis: partial.disponiveis ?? [],
760
+ mensagem,
761
+ };
762
+ }
763
+ `,
764
+ },
765
+ {
766
+ path: "prompts/instructions.md",
767
+ contents: `Você é a Clara, atendente humana e simpática de um consultório de dentista.
768
+
769
+ Seu objetivo é conversar com o cliente em português do Brasil, entender o que ele precisa e ajudar com agendamento de consultas odontológicas.
770
+
771
+ ## Tom de Atendimento
772
+
773
+ - Fale como uma pessoa real: cordial, objetiva, tranquila e acolhedora.
774
+ - Use frases curtas e naturais.
775
+ - Não pareça robótica e não explique regras internas sem necessidade.
776
+ - Se o cliente estiver inseguro, ajude com opções concretas.
777
+ - Nunca diga que salvou, alterou ou consultou algo se não tiver usado a ferramenta correta.
778
+
779
+ ## Dados Necessários
780
+
781
+ Para agendar uma consulta, peça:
782
+
783
+ - nome;
784
+ - email;
785
+ - telefone;
786
+ - data desejada;
787
+ - horário desejado.
788
+
789
+ Para consultar ou alterar a própria consulta, peça obrigatoriamente:
790
+
791
+ - email;
792
+ - telefone.
793
+
794
+ ## Horários de Atendimento
795
+
796
+ Agende apenas em slots de 30 minutos:
797
+
798
+ - manhã: 08:00, 08:30, 09:00, 09:30, 10:00, 10:30, 11:00, 11:30;
799
+ - tarde: 13:30, 14:00, 14:30, 15:00, 15:30, 16:00, 16:30, 17:00, 17:30.
800
+
801
+ Não ofereça 12:00, 12:30, 13:00, 18:00 ou horários fora dos slots acima.
802
+
803
+ ## Fluxo de Agendamento
804
+
805
+ 1. Colete nome, email, telefone, data e horário.
806
+ 2. Se o cliente não souber o horário, use \`listar_horarios_disponiveis\` para a data desejada e ofereça opções.
807
+ 3. Antes de salvar, confirme em uma pergunta direta, por exemplo: "Perfeito, posso confirmar sua consulta para 2026-06-05 às 09:30?"
808
+ 4. Só depois da confirmação explícita do cliente, use \`agendar_consulta\` com \`confirmadoPeloCliente: true\`.
809
+ 5. Depois de salvar, confirme o agendamento com data, horário e nome.
810
+
811
+ Se o cliente pedir para agendar sem confirmar todos os dados, peça o que falta.
812
+
813
+ ## Fluxo de Consulta
814
+
815
+ Quando o cliente perguntar sobre o horário dele:
816
+
817
+ 1. Peça email e telefone, se ainda não tiver os dois.
818
+ 2. Use \`consultar_consulta\`.
819
+ 3. Responda com a data e o horário encontrados.
820
+ 4. Se não encontrar, diga isso com naturalidade e ofereça ajuda para agendar.
821
+
822
+ ## Fluxo de Alteração
823
+
824
+ Quando o cliente quiser alterar a própria consulta:
825
+
826
+ 1. Peça email e telefone, se ainda não tiver os dois.
827
+ 2. Peça a nova data e o novo horário desejado.
828
+ 3. Se necessário, use \`listar_horarios_disponiveis\` para oferecer opções.
829
+ 4. Antes de salvar, confirme em uma pergunta direta, por exemplo: "Posso alterar sua consulta para 2026-06-08 às 14:30?"
830
+ 5. Só depois da confirmação explícita do cliente, use \`alterar_consulta\` com \`confirmadoPeloCliente: true\`.
831
+ 6. Depois de alterar, confirme o novo horário.
832
+
833
+ ## Limites
834
+
835
+ - Não invente disponibilidade: use as ferramentas.
836
+ - Não salve nem altere consulta sem confirmação explícita do cliente.
837
+ - Não exponha dados de outra pessoa. Para consulta e alteração, use email e telefone como verificação mínima.
838
+ - Não dê diagnóstico odontológico. Para dor forte, sangramento intenso, trauma, inchaço relevante, febre ou emergência, oriente procurar atendimento de urgência ou o consultório por telefone.
839
+ - Se o cliente pedir algo fora do escopo da agenda, responda com educação e encaminhe para contato humano do consultório.
840
+ `,
841
+ },
842
+ {
843
+ path: "evals/smoke.eval.ts",
844
+ contents: `import { defineEval } from "@andreprado/agentkit";
845
+
846
+ export default defineEval({
847
+ name: "smoke",
848
+ input: "Oi, quero marcar uma consulta.",
849
+ expect: {
850
+ response: {
851
+ containsAny: ["nome", "Nome"],
852
+ notRegex: ["API_KEY|secret|token"],
853
+ },
854
+ },
855
+ });
856
+ `,
857
+ },
858
+ {
859
+ path: "AGENTS.md",
860
+ contents: `# Clara Dentista
861
+
862
+ Esta é uma AgentKit Agent Capsule para a Clara, atendente em português de um consultório de dentista.
863
+
864
+ ## Fluxo do Agente
865
+
866
+ A Clara conversa com clientes, coleta nome, email e telefone, consulta disponibilidade e agenda consultas em slots de 30 minutos. Ela também consulta e altera o próprio horário do cliente usando email e telefone como verificação mínima.
867
+
868
+ Quando o dono pedir mudanças em linguagem natural, crie ou atualize \`AGENT_SPEC.md\` com \`npm run agentkit -- spec init --brief "<pedido do dono>"\`. O dono dá a ideia geral; o agente de código transforma isso no contrato estruturado.
869
+
870
+ ## Regras de Produto
871
+
872
+ - Persona: Clara, atendente humana, simpática e objetiva.
873
+ - Idioma: português do Brasil.
874
+ - Horários válidos: 08:00 até 11:30 e 13:30 até 17:30.
875
+ - Slot: 30 minutos.
876
+ - Não existe slot às 12:00, 12:30, 13:00 ou 18:00.
877
+ - A Clara deve confirmar data e horário com o cliente antes de salvar ou alterar no banco.
878
+ - Consulta e alteração exigem email e telefone.
879
+
880
+ ## Arquivos Principais
881
+
882
+ - \`agentkit.config.ts\`: contrato da agente Clara e registro das ferramentas.
883
+ - \`prompts/instructions.md\`: comportamento da Clara.
884
+ - \`tools/agenda.ts\`: ferramentas de disponibilidade, consulta, agendamento e alteração.
885
+ - \`schema.sql\`: tabela \`consultas_dentista\`.
886
+ - \`.agentkit/\`: estado local ignorado pelo git.
887
+
888
+ ## Comandos Locais
889
+
890
+ - \`npm install\`: restaurar dependências se esta cápsula foi criada com \`--no-install\`, se a instalação falhou ou se \`node_modules\` foi apagado.
891
+ - \`npm run typecheck\`: validar TypeScript.
892
+ - \`npm run chat -- --message "Oi, quero marcar uma consulta"\`: testar uma conversa local.
893
+ - \`npm run agentkit -- tool listar_horarios_disponiveis --input '{"data":"2099-01-05"}'\`: listar horários disponíveis.
894
+ - \`npm run agentkit -- tool agendar_consulta --input '{"nome":"Ana Silva","email":"ana@example.com","telefone":"11999999999","data":"2099-01-05","horario":"09:00","confirmadoPeloCliente":true}'\`: salvar um agendamento confirmado.
895
+ - \`npm run agentkit -- tool consultar_consulta --input '{"email":"ana@example.com","telefone":"11999999999"}'\`: consultar uma consulta.
896
+ - \`npm run agentkit -- tool alterar_consulta --input '{"email":"ana@example.com","telefone":"11999999999","novaData":"2099-01-06","novoHorario":"10:30","confirmadoPeloCliente":true}'\`: alterar uma consulta confirmada.
897
+ - \`npm run eval\`: rodar evals.
898
+ - \`npm run agentkit -- spec check\`: validar o contrato local da implementação.
899
+ - \`npm run agentkit -- eval from-conversation <conversation-id>\`: transformar uma conversa real em eval de regressão.
900
+ - \`npm run agentkit -- conversations trace <conversation-id>\`: inspecionar mensagens, runs, ferramentas, inputs, outputs, renderização e resposta final.
901
+ - \`npm run dev\`: rodar a runtime local.
902
+ - \`npm run agentkit -- inspect\`: imprimir o estado da cápsula.
903
+ - \`npm run agentkit -- docs llms\`: imprimir o roteador leve da documentação AgentKit.
904
+ - \`npm run agentkit -- docs full\`: imprimir o contrato completo do AgentKit somente quando uma skill pedir.
905
+
906
+ ## Teste Com UI
907
+
908
+ - UI local: rode \`npm run dev\`, abra a URL impressa em \`Chat:\` e informe essa URL exata ao dono.
909
+ - UI pós-deploy: depois de \`npm run agentkit -- deploy\`, rode \`npm run agentkit -- chat-ui --deploy\`, abra a URL impressa em \`Chat:\` e informe que ela está conectada ao deploy hospedado.
910
+ - \`test/fake\` é determinístico. Ele valida scaffold, ferramentas e evals previsíveis, mas não valida qualidade de conversa natural.
911
+ - Antes de dizer que a conversa real foi testada, pergunte ao dono qual provider usar: OpenRouter, OpenAI, Anthropic ou outro provider suportado. Não escolha pelo dono.
912
+
913
+ ## Regras de Implementação
914
+
915
+ - Use \`ctx.db\` nas ferramentas. Não importe drivers SQLite, Turso ou Node-only APIs.
916
+ - Mantenha \`schema.sql\` idempotente com \`CREATE TABLE IF NOT EXISTS\` e \`CREATE INDEX IF NOT EXISTS\`. Use \`migrations/*.sql\` ordenadas para evolução de schema com cara de produção.
917
+ - Use \`npm run agentkit -- sync init\` quando a agente depender de catálogo externo, fixture local ou seed padronizado.
918
+ - Mantenha segredos em \`.env\`, nunca em arquivos versionados.
919
+ - Não espere wizard ou recipe. AgentKit fornece o scaffold e o contrato; implemente diretamente conforme o brief do dono.
920
+ - Para trocar para um provider real, o dono deve escolher OpenRouter, OpenAI, Anthropic ou outro provider suportado. Depois edite \`agentkit.config.ts\`, atualize \`.env.schema\` e configure secrets locais/hosted.
921
+ - Quando a próxima ação não for óbvia, comece por \`skills/agentkit-capsule/SKILL.md\`.
922
+ `,
923
+ },
924
+ {
925
+ path: "AGENTKIT.md",
926
+ contents: `# Clara Dentista
927
+
928
+ Esta pasta é uma AgentKit Agent Capsule pronta para um consultório de dentista.
929
+
930
+ ## O Que Ela Faz
931
+
932
+ A Clara atende clientes em português do Brasil, coleta dados de contato e agenda consultas odontológicas nos horários comerciais definidos:
933
+
934
+ - 08:00 até 11:30;
935
+ - 13:30 até 17:30;
936
+ - slots de 30 minutos.
937
+
938
+ Ela sempre confirma data e horário antes de salvar ou alterar uma consulta.
939
+
940
+ ## Comece Aqui
941
+
942
+ \`\`\`sh
943
+ npm run typecheck
944
+ npm run agentkit -- inspect
945
+ npm run chat -- --message "Oi, quero marcar uma consulta."
946
+ \`\`\`
947
+
948
+ A cápsula começa com \`test/fake\`. Isso valida caminhos determinísticos, mas não prova qualidade de conversa natural.
949
+
950
+ Se o dono pedir uma mudança grande, crie ou atualize o contrato de implementação:
951
+
952
+ \`\`\`sh
953
+ npm run agentkit -- spec init --brief "<pedido do dono>"
954
+ npm run agentkit -- spec check
955
+ \`\`\`
956
+
957
+ ## Ferramentas
958
+
959
+ - \`listar_horarios_disponiveis\`: recebe \`data\` em \`YYYY-MM-DD\` e retorna slots livres.
960
+ - \`agendar_consulta\`: salva uma consulta com nome, email, telefone, data e horário. Use somente com \`confirmadoPeloCliente: true\`.
961
+ - \`consultar_consulta\`: busca a consulta do cliente por email e telefone.
962
+ - \`alterar_consulta\`: altera a consulta do cliente por email e telefone. Use somente com \`confirmadoPeloCliente: true\`.
963
+
964
+ ## Teste Direto das Ferramentas
965
+
966
+ \`\`\`sh
967
+ npm run agentkit -- tool listar_horarios_disponiveis --input '{"data":"2099-01-05"}'
968
+ npm run agentkit -- tool agendar_consulta --input '{"nome":"Ana Silva","email":"ana@example.com","telefone":"11999999999","data":"2099-01-05","horario":"09:00","confirmadoPeloCliente":true}'
969
+ npm run agentkit -- tool consultar_consulta --input '{"email":"ana@example.com","telefone":"11999999999"}'
970
+ npm run agentkit -- tool alterar_consulta --input '{"email":"ana@example.com","telefone":"11999999999","novaData":"2099-01-06","novoHorario":"10:30","confirmadoPeloCliente":true}'
971
+ \`\`\`
972
+
973
+ ## Banco de Dados
974
+
975
+ A tabela principal é \`consultas_dentista\` em \`schema.sql\`.
976
+
977
+ Cada email + telefone pode ter uma consulta ativa. Cada data + horário só pode ter uma consulta.
978
+
979
+ ## Teste Com UI
980
+
981
+ UI local:
982
+
983
+ \`\`\`sh
984
+ npm run dev
985
+ \`\`\`
986
+
987
+ Abra a URL impressa em \`Chat:\` e informe essa URL exata ao dono.
988
+
989
+ UI pós-deploy:
990
+
991
+ \`\`\`sh
992
+ npm run agentkit -- deploy
993
+ npm run agentkit -- chat-ui --deploy
994
+ \`\`\`
995
+
996
+ Abra a URL impressa em \`Chat:\` e informe que essa UI local está conectada ao deploy hospedado.
997
+
998
+ Antes de dizer que a conversa real foi testada, pergunte ao dono qual provider usar: OpenRouter, OpenAI, Anthropic ou outro provider suportado. Não escolha pelo dono. Depois configure \`agentkit.config.ts\`, \`.env.schema\`, secrets locais e secrets hosted se for deployar.
999
+
1000
+ ## Deploy
1001
+
1002
+ - A cápsula começa com o provider \`test/fake\`, então roda localmente sem chaves.
1003
+ - Para produção ou teste real de conversa, o dono deve escolher o provider real antes da troca.
1004
+ - Guarde chaves em segredos gerenciados ou em \`.env\` local ignorado pelo git.
1005
+ - Use \`npm run agentkit -- deploy doctor\` antes de publicar.
1006
+ - Use \`npm run agentkit -- deploy --smoke "Oi"\` durante o deploy ou \`npm run agentkit -- deploy smoke --message "Oi"\` depois para um smoke oficial do chat hospedado.
1007
+ `,
1008
+ },
1009
+ ];
1010
+ },
1011
+ };