botql 1.0.1 → 1.0.2

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 (2) hide show
  1. package/botql.browser.js +1921 -0
  2. package/package.json +1 -1
@@ -0,0 +1,1921 @@
1
+ "use strict";
2
+ var BotQL = (() => {
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ var __commonJS = (cb, mod) => function __require2() {
11
+ try {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ } catch (e) {
14
+ throw mod = 0, e;
15
+ }
16
+ };
17
+
18
+ // Parser.js
19
+ var require_Parser = __commonJS({
20
+ "Parser.js"(exports, module) {
21
+ "use strict";
22
+ var TokenType = {
23
+ KEYWORD: "KEYWORD",
24
+ STRING: "STRING",
25
+ NUMBER: "NUMBER",
26
+ IDENT: "IDENT",
27
+ SYMBOL: "SYMBOL",
28
+ EOF: "EOF"
29
+ };
30
+ var KEYWORDS = /* @__PURE__ */ new Set([
31
+ "CREATE",
32
+ "BOT",
33
+ "PLATFORM",
34
+ "CONNECT",
35
+ "TABLE",
36
+ "PREVENT",
37
+ "DEFAULT",
38
+ "ON",
39
+ "START",
40
+ "MESSAGE",
41
+ "FROM",
42
+ "WHEN",
43
+ "CONTAINS",
44
+ "OR",
45
+ "OTHERWISE",
46
+ "KEYWORDS",
47
+ "THINK",
48
+ "WAITING",
49
+ "REPLY",
50
+ "TO",
51
+ "FORWARD",
52
+ "PARSE",
53
+ "SEND",
54
+ "INSERT",
55
+ "INTO",
56
+ "VALUES",
57
+ "UPDATE",
58
+ "SET",
59
+ "WHERE",
60
+ "RUN",
61
+ "IMPORT",
62
+ "AS",
63
+ "RESPONSE"
64
+ ]);
65
+ var Token = class {
66
+ constructor(type, value, line) {
67
+ this.type = type;
68
+ this.value = value;
69
+ this.line = line;
70
+ }
71
+ };
72
+ var Tokenizer = class {
73
+ constructor(source) {
74
+ this.source = source;
75
+ this.pos = 0;
76
+ this.line = 1;
77
+ this.tokens = [];
78
+ }
79
+ error(msg) {
80
+ throw new Error(`syntax error: ${msg}`);
81
+ }
82
+ peekChar(offset = 0) {
83
+ return this.source[this.pos + offset];
84
+ }
85
+ tokenize() {
86
+ while (this.pos < this.source.length) {
87
+ const c = this.peekChar();
88
+ if (c === "\n") {
89
+ this.line++;
90
+ this.pos++;
91
+ continue;
92
+ }
93
+ if (/\s/.test(c)) {
94
+ this.pos++;
95
+ continue;
96
+ }
97
+ if (c === "-" && this.peekChar(1) === "-") {
98
+ while (this.pos < this.source.length && this.peekChar() !== "\n") this.pos++;
99
+ continue;
100
+ }
101
+ if (c === '"' || c === "'") {
102
+ this.tokens.push(this.readString(c));
103
+ continue;
104
+ }
105
+ if (/[0-9]/.test(c)) {
106
+ this.tokens.push(this.readNumber());
107
+ continue;
108
+ }
109
+ if ("{}(),.=+*;".includes(c)) {
110
+ this.tokens.push(new Token(TokenType.SYMBOL, c, this.line));
111
+ this.pos++;
112
+ continue;
113
+ }
114
+ if (/[A-Za-z_]/.test(c)) {
115
+ const word = this.readWord();
116
+ this.tokens.push(word);
117
+ continue;
118
+ }
119
+ this.error(`unexpected character: "${c}"`);
120
+ }
121
+ this.tokens.push(new Token(TokenType.EOF, null, this.line));
122
+ return this.tokens;
123
+ }
124
+ readString(quote) {
125
+ const startLine = this.line;
126
+ this.pos++;
127
+ let value = "";
128
+ while (this.pos < this.source.length && this.peekChar() !== quote) {
129
+ if (this.peekChar() === "\\") {
130
+ this.pos++;
131
+ value += this.peekChar();
132
+ this.pos++;
133
+ continue;
134
+ }
135
+ if (this.peekChar() === "\n") this.line++;
136
+ value += this.peekChar();
137
+ this.pos++;
138
+ }
139
+ if (this.peekChar() !== quote) {
140
+ this.error("unterminated string");
141
+ }
142
+ this.pos++;
143
+ return new Token(TokenType.STRING, value, startLine);
144
+ }
145
+ readNumber() {
146
+ const startLine = this.line;
147
+ let value = "";
148
+ while (this.pos < this.source.length && /[0-9.]/.test(this.peekChar())) {
149
+ value += this.peekChar();
150
+ this.pos++;
151
+ }
152
+ return new Token(TokenType.NUMBER, Number(value), startLine);
153
+ }
154
+ readWord() {
155
+ const startLine = this.line;
156
+ let value = "";
157
+ while (this.pos < this.source.length && /[A-Za-z0-9_]/.test(this.peekChar())) {
158
+ value += this.peekChar();
159
+ this.pos++;
160
+ }
161
+ const upper = value.toUpperCase();
162
+ if (KEYWORDS.has(upper) && value === upper) {
163
+ return new Token(TokenType.KEYWORD, upper, startLine);
164
+ }
165
+ return new Token(TokenType.IDENT, value, startLine);
166
+ }
167
+ };
168
+ var Parser = class {
169
+ constructor(source) {
170
+ this.tokens = new Tokenizer(source).tokenize();
171
+ this.pos = 0;
172
+ }
173
+ error(msg) {
174
+ const tok = this.current();
175
+ throw new Error(`syntax error: ${msg}, near "${tok ? tok.value : "EOF"}"`);
176
+ }
177
+ current() {
178
+ return this.tokens[this.pos];
179
+ }
180
+ at(type, value) {
181
+ const tok = this.current();
182
+ if (tok.type !== type) return false;
183
+ if (value !== void 0 && tok.value !== value) return false;
184
+ return true;
185
+ }
186
+ atKeyword(...values) {
187
+ return this.at(TokenType.KEYWORD) && values.includes(this.current().value);
188
+ }
189
+ // Casa por valor, independente do token ser KEYWORD ou IDENT.
190
+ // Necessário para palavras como "SIGNAL", que tanto introduz um
191
+ // evento (ON SIGNAL FROM) como é usada como identificador (SIGNAL.PAIR).
192
+ atWord(...values) {
193
+ const tok = this.current();
194
+ return (tok.type === TokenType.KEYWORD || tok.type === TokenType.IDENT) && values.includes(tok.value);
195
+ }
196
+ advance() {
197
+ const tok = this.current();
198
+ if (tok.type !== TokenType.EOF) this.pos++;
199
+ return tok;
200
+ }
201
+ expect(type, value) {
202
+ if (!this.at(type, value)) {
203
+ this.error(`expected ${value || type}`);
204
+ }
205
+ return this.advance();
206
+ }
207
+ expectKeyword(value) {
208
+ return this.expect(TokenType.KEYWORD, value);
209
+ }
210
+ isAtEnd() {
211
+ return this.at(TokenType.EOF);
212
+ }
213
+ // ---- Programa ----
214
+ parseProgram() {
215
+ const statements = [];
216
+ while (!this.isAtEnd()) {
217
+ statements.push(this.parseStatement());
218
+ }
219
+ return { type: "Program", body: statements };
220
+ }
221
+ // ---- Bloco { ... } ou statement único ----
222
+ // Regra da linguagem: bloco com mais de uma ação usa chaves,
223
+ // uma única ação não precisa.
224
+ parseBody() {
225
+ if (this.at(TokenType.SYMBOL, "{")) {
226
+ this.advance();
227
+ const statements = [];
228
+ while (!this.at(TokenType.SYMBOL, "}")) {
229
+ if (this.isAtEnd()) this.error('unclosed block "{"');
230
+ statements.push(this.parseStatement());
231
+ }
232
+ this.advance();
233
+ return statements;
234
+ }
235
+ return [this.parseStatement()];
236
+ }
237
+ // ---- Dispatch de statement ----
238
+ parseStatement() {
239
+ if (this.atKeyword("CREATE")) return this.parseCreate();
240
+ if (this.atKeyword("PLATFORM")) return this.parsePlatform();
241
+ if (this.atKeyword("CONNECT")) return this.parseConnect();
242
+ if (this.atKeyword("ON")) return this.parseOn();
243
+ if (this.atKeyword("WHEN")) return this.parseWhen();
244
+ if (this.atKeyword("OTHERWISE")) return this.parseOtherwise();
245
+ if (this.atKeyword("REPLY")) return this.parseReply();
246
+ if (this.atKeyword("THINK")) return this.parseThink();
247
+ if (this.atKeyword("FORWARD")) return this.parseForward();
248
+ if (this.atKeyword("PARSE")) return this.parseParseSignal();
249
+ if (this.atKeyword("SEND")) return this.parseSend();
250
+ if (this.atKeyword("INSERT")) return this.parseInsert();
251
+ if (this.atKeyword("UPDATE")) return this.parseUpdate();
252
+ if (this.atKeyword("RUN")) return this.parseRunBot();
253
+ if (this.atKeyword("IMPORT")) return this.parseImport();
254
+ this.error("unexpected statement");
255
+ }
256
+ // ---- CREATE BOT / CREATE TABLE ----
257
+ parseCreate() {
258
+ this.expectKeyword("CREATE");
259
+ if (this.atKeyword("BOT")) {
260
+ this.advance();
261
+ const name = this.expect(TokenType.STRING).value;
262
+ return { type: "CreateBot", name };
263
+ }
264
+ if (this.atKeyword("TABLE")) {
265
+ this.advance();
266
+ const name = this.expect(TokenType.IDENT).value;
267
+ const columns = this.parseColumnList();
268
+ const preventDefault = this.tryParsePreventDefault();
269
+ const defaultMessage = this.tryParseDefaultMessage();
270
+ return { type: "CreateTable", name, columns, preventDefault, defaultMessage };
271
+ }
272
+ this.error("expected BOT or TABLE after CREATE");
273
+ }
274
+ parseColumnList() {
275
+ this.expect(TokenType.SYMBOL, "(");
276
+ const columns = [];
277
+ while (!this.at(TokenType.SYMBOL, ")")) {
278
+ const colName = this.expect(TokenType.IDENT).value;
279
+ const constraints = [];
280
+ let colType = null;
281
+ while (this.at(TokenType.IDENT) && !this.at(TokenType.SYMBOL, ",") && !this.at(TokenType.SYMBOL, ")")) {
282
+ const word = this.advance().value;
283
+ if (!colType) colType = word;
284
+ else constraints.push(word);
285
+ }
286
+ columns.push({ name: colName, columnType: colType, constraints });
287
+ if (this.at(TokenType.SYMBOL, ",")) this.advance();
288
+ }
289
+ this.expect(TokenType.SYMBOL, ")");
290
+ return columns;
291
+ }
292
+ tryParsePreventDefault() {
293
+ if (this.atKeyword("PREVENT")) {
294
+ this.advance();
295
+ this.expectKeyword("DEFAULT");
296
+ return true;
297
+ }
298
+ return false;
299
+ }
300
+ // ---- DEFAULT MESSAGE "texto" ----
301
+ // ---- DEFAULT MESSAGE (ficheiro.txt, N) ----
302
+ //
303
+ // Modificador do CREATE TABLE, ao lado de PREVENT DEFAULT: saudação
304
+ // automática enviada só na primeira mensagem de cada client/sender
305
+ // (ver botql.js, receiveMessage). Aceita texto direto ou a mesma forma
306
+ // de ficheiro indexado do REPLY — por isso reaproveita isReplyFileForm
307
+ // para o lookahead. "DEFAULT" aqui não é o mesmo "DEFAULT" de "PREVENT
308
+ // DEFAULT" — só é reconhecido como início de DEFAULT MESSAGE quando NÃO
309
+ // vem logo a seguir a PREVENT (tryParsePreventDefault já consumiu esse
310
+ // caso antes de chegarmos aqui).
311
+ tryParseDefaultMessage() {
312
+ if (!this.atKeyword("DEFAULT")) return null;
313
+ const next = this.tokens[this.pos + 1];
314
+ if (!next || next.type !== TokenType.KEYWORD || next.value !== "MESSAGE") return null;
315
+ this.advance();
316
+ this.advance();
317
+ if (this.isReplyFileForm()) {
318
+ this.expect(TokenType.SYMBOL, "(");
319
+ const file = this.parseFileName();
320
+ this.expect(TokenType.SYMBOL, ",");
321
+ const index = this.parseExpression();
322
+ this.expect(TokenType.SYMBOL, ")");
323
+ return { file, index };
324
+ }
325
+ const value = this.parseExpression();
326
+ return { value };
327
+ }
328
+ // ---- PLATFORM ----
329
+ parsePlatform() {
330
+ this.expectKeyword("PLATFORM");
331
+ const value = this.advance().value;
332
+ return { type: "Platform", value };
333
+ }
334
+ // ---- CONNECT SERVIÇO TIPO "credencial" ----
335
+ // ---- CONNECT RESPONSE alias ----
336
+ //
337
+ // A segunda forma liga o bot a uma IA usando um valor já importado (o
338
+ // alias de um IMPORT {..., N} AS alias) — só estabelece a ligação,
339
+ // não dispara nada sozinho (quem dispara é Response()/Response(alias),
340
+ // dentro de uma ação). RESPONSE é keyword fixa aqui, por isso da para
341
+ // distinguir logo no primeiro token, sem lookahead: a forma normal
342
+ // nunca começa por essa palavra.
343
+ parseConnect() {
344
+ this.expectKeyword("CONNECT");
345
+ if (this.atKeyword("RESPONSE")) {
346
+ this.advance();
347
+ const alias = this.expect(TokenType.IDENT).value;
348
+ return { type: "ConnectResponse", alias };
349
+ }
350
+ const service = this.advance().value;
351
+ const kind = this.advance().value;
352
+ const credential = this.expect(TokenType.STRING).value;
353
+ return { type: "Connect", service, kind, credential };
354
+ }
355
+ // ---- ON START | ON MESSAGE | ON SIGNAL FROM "..." ----
356
+ parseOn() {
357
+ this.expectKeyword("ON");
358
+ if (this.atKeyword("START")) {
359
+ this.advance();
360
+ const body = this.parseBody();
361
+ return { type: "On", event: "START", body };
362
+ }
363
+ if (this.atKeyword("MESSAGE")) {
364
+ this.advance();
365
+ const body = this.parseBody();
366
+ return { type: "On", event: "MESSAGE", body };
367
+ }
368
+ if (this.atWord("SIGNAL")) {
369
+ this.advance();
370
+ this.expectKeyword("FROM");
371
+ const source = this.expect(TokenType.STRING).value;
372
+ const body = this.parseBody();
373
+ return { type: "On", event: "SIGNAL", source, body };
374
+ }
375
+ this.error("unexpected event after ON");
376
+ }
377
+ // ---- WHEN CONTAINS "..." [OR CONTAINS "..."]* ----
378
+ parseWhen() {
379
+ this.expectKeyword("WHEN");
380
+ const conditions = [this.parseCondition()];
381
+ while (this.atKeyword("OR")) {
382
+ this.advance();
383
+ conditions.push(this.parseCondition());
384
+ }
385
+ const body = this.parseBody();
386
+ return { type: "When", conditions, body };
387
+ }
388
+ parseCondition() {
389
+ this.expectKeyword("CONTAINS");
390
+ if (this.at(TokenType.SYMBOL, "(")) {
391
+ const values = this.parseStringList();
392
+ return { op: "CONTAINS_ANY", values };
393
+ }
394
+ if (this.atKeyword("KEYWORDS")) {
395
+ this.advance();
396
+ this.expect(TokenType.SYMBOL, "(");
397
+ const path = this.parseFileName();
398
+ this.expect(TokenType.SYMBOL, ")");
399
+ return { op: "CONTAINS_KEYWORDS_FILE", path };
400
+ }
401
+ const value = this.expect(TokenType.STRING).value;
402
+ return { op: "CONTAINS", value };
403
+ }
404
+ // Lista de strings entre parênteses: ("a", "b", "c")
405
+ parseStringList() {
406
+ this.expect(TokenType.SYMBOL, "(");
407
+ const values = [];
408
+ while (!this.at(TokenType.SYMBOL, ")")) {
409
+ values.push(this.expect(TokenType.STRING).value);
410
+ if (this.at(TokenType.SYMBOL, ",")) this.advance();
411
+ }
412
+ this.expect(TokenType.SYMBOL, ")");
413
+ return values;
414
+ }
415
+ // Nome de ficheiro sem aspas, ex: saudacoes.txt ou lista_bot.sql.
416
+ // O tokenizer separa "saudacoes" (IDENT) e ".txt" em IDENT + SYMBOL('.')
417
+ // + IDENT, por isso remontamos aqui em vez de pedir um STRING.
418
+ parseFileName() {
419
+ let name = this.expect(TokenType.IDENT).value;
420
+ while (this.at(TokenType.SYMBOL, ".")) {
421
+ this.advance();
422
+ name += "." + this.expect(TokenType.IDENT).value;
423
+ }
424
+ return name;
425
+ }
426
+ // ---- OTHERWISE ----
427
+ parseOtherwise() {
428
+ this.expectKeyword("OTHERWISE");
429
+ const body = this.parseBody();
430
+ return { type: "Otherwise", body };
431
+ }
432
+ // ---- REPLY [TO alvo] expressão ----
433
+ // ---- REPLY [TO alvo] (ficheiro.txt, N) ----
434
+ //
435
+ // A segunda forma referencia uma entrada numerada de um ficheiro de
436
+ // respostas (uma linha "N- texto" por entrada). Distingue-se da
437
+ // expressão comum porque começa por "(" seguido de um nome de
438
+ // ficheiro (IDENT + "." + IDENT), e não por uma STRING/NUMBER/IDENT
439
+ // isolado — daí o lookahead antes de decidir qual ramo seguir.
440
+ // ---- THINK(ficheiro.txt) ----
441
+ // ---- THINK(ficheiro.txt) OR REPLY (fallback.txt, N) ----
442
+ // ---- THINK(ficheiro.txt) OR REPLY "texto fixo" ----
443
+ //
444
+ // Diferente de REPLY: THINK não responde com um texto já mapeado, faz
445
+ // retrieval local sobre um ficheiro de conhecimento (ver RAG.js) e só
446
+ // responde se achar um bloco com confiança suficiente. Antes de
447
+ // procurar, sinaliza onThinking (para a plataforma poder mostrar
448
+ // "Pensando..." enquanto isso, já que a busca — em ficheiros grandes —
449
+ // não é instantânea como um REPLY comum).
450
+ //
451
+ // O "OR REPLY ..." é o fallback: só corre se THINK não encontrar nada
452
+ // com confiança suficiente. Fica preso ao mesmo statement (não é um
453
+ // segundo statement solto no corpo do WHEN) porque só faz sentido
454
+ // junto — um REPLY fallback sem THINK antes seria só um REPLY normal.
455
+ parseThink() {
456
+ this.expectKeyword("THINK");
457
+ this.expect(TokenType.SYMBOL, "(");
458
+ const file = this.parseFileName();
459
+ this.expect(TokenType.SYMBOL, ")");
460
+ const waiting = this.tryParseWaiting();
461
+ let fallback = null;
462
+ if (this.atKeyword("OR")) {
463
+ this.advance();
464
+ fallback = this.parseReply();
465
+ }
466
+ return { type: "Think", file, waiting, fallback };
467
+ }
468
+ // ---- WAITING() | WAITING(ficheiro.txt) | WAITING(ficheiro.txt, N) ----
469
+ //
470
+ // Modificador do THINK, sempre logo a seguir a ele, antes do OR (ver
471
+ // parseThink acima). Os dois argumentos são opcionais e independentes:
472
+ // sem nenhum, o interpretador usa texto e tempo mínimo por omissão
473
+ // ("Pensando...", 3 segundos) — ver botql.js, resolveWaitingConfig.
474
+ tryParseWaiting() {
475
+ if (!this.atKeyword("WAITING")) return null;
476
+ this.advance();
477
+ this.expect(TokenType.SYMBOL, "(");
478
+ let file = null;
479
+ let seconds = null;
480
+ if (!this.at(TokenType.SYMBOL, ")")) {
481
+ file = this.parseFileName();
482
+ if (this.at(TokenType.SYMBOL, ",")) {
483
+ this.advance();
484
+ seconds = this.parseExpression();
485
+ }
486
+ }
487
+ this.expect(TokenType.SYMBOL, ")");
488
+ return { file, seconds };
489
+ }
490
+ parseReply() {
491
+ this.expectKeyword("REPLY");
492
+ let target = null;
493
+ if (this.atKeyword("TO")) {
494
+ this.advance();
495
+ target = this.advance().value;
496
+ }
497
+ if (this.isReplyFileForm()) {
498
+ this.expect(TokenType.SYMBOL, "(");
499
+ const file = this.parseFileName();
500
+ this.expect(TokenType.SYMBOL, ",");
501
+ const index = this.parseExpression();
502
+ this.expect(TokenType.SYMBOL, ")");
503
+ return { type: "Reply", target, file, index };
504
+ }
505
+ const value = this.parseExpression();
506
+ return { type: "Reply", target, value };
507
+ }
508
+ // Lookahead: "(" IDENT "." IDENT "," ... — só a forma de ficheiro
509
+ // tem "." logo a seguir ao primeiro identificador dentro dos
510
+ // parênteses. Uma chamada de função normal, ex: (NOW()), nunca
511
+ // bate aqui.
512
+ isReplyFileForm() {
513
+ if (!this.at(TokenType.SYMBOL, "(")) return false;
514
+ const next = this.tokens[this.pos + 1];
515
+ const afterNext = this.tokens[this.pos + 2];
516
+ return !!next && next.type === TokenType.IDENT && !!afterNext && afterNext.type === TokenType.SYMBOL && afterNext.value === ".";
517
+ }
518
+ // ---- FORWARD TO "contacto" ----
519
+ parseForward() {
520
+ this.expectKeyword("FORWARD");
521
+ this.expectKeyword("TO");
522
+ const target = this.parseExpression();
523
+ return { type: "ForwardTo", target };
524
+ }
525
+ // ---- PARSE SIGNAL ----
526
+ parseParseSignal() {
527
+ this.expectKeyword("PARSE");
528
+ if (!this.atWord("SIGNAL")) this.error("expected SIGNAL after PARSE");
529
+ this.advance();
530
+ return { type: "ParseSignal" };
531
+ }
532
+ // ---- SEND TO destino ----
533
+ parseSend() {
534
+ this.expectKeyword("SEND");
535
+ this.expectKeyword("TO");
536
+ const target = this.advance().value;
537
+ return { type: "SendTo", target };
538
+ }
539
+ // ---- INSERT INTO tabela [(cols)] [VALUES (vals)] ----
540
+ parseInsert() {
541
+ this.expectKeyword("INSERT");
542
+ this.expectKeyword("INTO");
543
+ const table = this.expect(TokenType.IDENT).value;
544
+ let columns = null;
545
+ if (this.at(TokenType.SYMBOL, "(")) {
546
+ columns = this.parseExpressionList();
547
+ }
548
+ let values = null;
549
+ if (this.atKeyword("VALUES")) {
550
+ this.advance();
551
+ values = this.parseExpressionList();
552
+ }
553
+ return { type: "Insert", table, columns, values };
554
+ }
555
+ parseExpressionList() {
556
+ this.expect(TokenType.SYMBOL, "(");
557
+ const items = [];
558
+ while (!this.at(TokenType.SYMBOL, ")")) {
559
+ items.push(this.parseExpression());
560
+ if (this.at(TokenType.SYMBOL, ",")) this.advance();
561
+ }
562
+ this.expect(TokenType.SYMBOL, ")");
563
+ return items;
564
+ }
565
+ // ---- UPDATE tabela SET col = expr WHERE expr ----
566
+ parseUpdate() {
567
+ this.expectKeyword("UPDATE");
568
+ const table = this.expect(TokenType.IDENT).value;
569
+ this.expectKeyword("SET");
570
+ const column = this.expect(TokenType.IDENT).value;
571
+ this.expect(TokenType.SYMBOL, "=");
572
+ const value = this.parseExpression();
573
+ let where = null;
574
+ if (this.atKeyword("WHERE")) {
575
+ this.advance();
576
+ where = this.parseComparison();
577
+ }
578
+ return { type: "Update", table, set: { column, value }, where };
579
+ }
580
+ // ---- IMPORT {ficheiro.sql} ----
581
+ // ---- IMPORT {ficheiro.txt, N} ----
582
+ // ---- IMPORT {ficheiro.txt, N} AS alias ----
583
+ //
584
+ // A segunda forma importa só a entrada N de um ficheiro de valores
585
+ // (o mesmo formato "N- texto" usado por REPLY (ficheiro, N)), em vez
586
+ // do ficheiro inteiro. O índice é opcional, tal como em REPLY.
587
+ //
588
+ // AS alias só faz sentido junto com o índice (dá nome ao valor lido,
589
+ // para CONNECT RESPONSE/Response(...) e qualquer outra expressão
590
+ // referenciarem por esse nome em vez do nome generico "env"). Sem AS,
591
+ // mantém-se o comportamento original: o valor cai em env.NOME_FICHEIRO.
592
+ parseImport() {
593
+ this.expectKeyword("IMPORT");
594
+ this.expect(TokenType.SYMBOL, "{");
595
+ const path = this.parseFileName();
596
+ let index = null;
597
+ if (this.at(TokenType.SYMBOL, ",")) {
598
+ this.advance();
599
+ index = this.parseExpression();
600
+ }
601
+ this.expect(TokenType.SYMBOL, "}");
602
+ let alias = null;
603
+ if (this.atKeyword("AS")) {
604
+ this.advance();
605
+ alias = this.expect(TokenType.IDENT).value;
606
+ }
607
+ return { type: "Import", path, index, alias };
608
+ }
609
+ // ---- RUN BOT ----
610
+ parseRunBot() {
611
+ this.expectKeyword("RUN");
612
+ this.expectKeyword("BOT");
613
+ return { type: "RunBot" };
614
+ }
615
+ // ---- Expressões: literais, identificadores, chamadas, acesso a
616
+ // propriedade (obj.prop) e concatenação com "+" ----
617
+ parseExpression() {
618
+ let node = this.parsePrimary();
619
+ while (this.at(TokenType.SYMBOL, "+")) {
620
+ this.advance();
621
+ const right = this.parsePrimary();
622
+ node = { type: "Binary", op: "+", left: node, right };
623
+ }
624
+ return node;
625
+ }
626
+ // Igualdade, usada em WHERE (ex: id = LAST_INSERT_ID())
627
+ parseComparison() {
628
+ const left = this.parseExpression();
629
+ if (this.at(TokenType.SYMBOL, "=")) {
630
+ this.advance();
631
+ const right = this.parseExpression();
632
+ return { type: "Binary", op: "=", left, right };
633
+ }
634
+ return left;
635
+ }
636
+ parsePrimary() {
637
+ const tok = this.current();
638
+ if (tok.type === TokenType.STRING) {
639
+ this.advance();
640
+ return { type: "Literal", value: tok.value };
641
+ }
642
+ if (tok.type === TokenType.NUMBER) {
643
+ this.advance();
644
+ return { type: "Literal", value: tok.value };
645
+ }
646
+ if (tok.type === TokenType.IDENT) {
647
+ this.advance();
648
+ let node = { type: "Identifier", name: tok.value };
649
+ if (this.at(TokenType.SYMBOL, "(")) {
650
+ const args = this.parseExpressionList();
651
+ node = { type: "Call", callee: node.name, args };
652
+ }
653
+ while (this.at(TokenType.SYMBOL, ".")) {
654
+ this.advance();
655
+ const prop = this.expect(TokenType.IDENT).value;
656
+ node = { type: "Member", object: node, property: prop };
657
+ }
658
+ return node;
659
+ }
660
+ this.error("invalid expression");
661
+ }
662
+ };
663
+ module.exports = { Parser, Tokenizer, TokenType, KEYWORDS };
664
+ }
665
+ });
666
+
667
+ // Database.js
668
+ var require_Database = __commonJS({
669
+ "Database.js"(exports, module) {
670
+ "use strict";
671
+ var CONTEXT_SCHEMA = [
672
+ { name: "id", columnType: "INT", constraints: ["PRIMARY", "KEY", "AUTO_INCREMENT"] },
673
+ { name: "client", columnType: "TEXT", constraints: [] },
674
+ { name: "message", columnType: "TEXT", constraints: [] },
675
+ { name: "reply", columnType: "TEXT", constraints: [] },
676
+ { name: "created_at", columnType: "DATETIME", constraints: [] }
677
+ ];
678
+ var MemoryDatabase = class {
679
+ constructor() {
680
+ this.tables = /* @__PURE__ */ new Map();
681
+ this.autoIncrement = /* @__PURE__ */ new Map();
682
+ }
683
+ createTable(name, columns, preventDefault) {
684
+ const resolvedColumns = name === "Context" && columns.length === 0 ? CONTEXT_SCHEMA : columns;
685
+ if (this.tables.has(name)) {
686
+ if (preventDefault) return;
687
+ throw new Error(`runtime error: table "${name}" already exists`);
688
+ }
689
+ this.tables.set(name, { columns: resolvedColumns, rows: [] });
690
+ this.autoIncrement.set(name, 0);
691
+ }
692
+ insert(table, columnNames, values) {
693
+ const t = this.tables.get(table);
694
+ if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
695
+ const nextId = this.autoIncrement.get(table) + 1;
696
+ this.autoIncrement.set(table, nextId);
697
+ const row = { id: nextId };
698
+ columnNames.forEach((col, i) => {
699
+ row[col] = values[i];
700
+ });
701
+ t.rows.push(row);
702
+ return nextId;
703
+ }
704
+ update(table, column, value, whereColumn, whereValue) {
705
+ const t = this.tables.get(table);
706
+ if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
707
+ const row = whereColumn ? t.rows.find((r) => r[whereColumn] === whereValue) : t.rows[t.rows.length - 1];
708
+ if (row) row[column] = value;
709
+ return row || null;
710
+ }
711
+ getRows(table) {
712
+ const t = this.tables.get(table);
713
+ return t ? t.rows.slice() : [];
714
+ }
715
+ };
716
+ function mapColumnType(columnType) {
717
+ const type = (columnType || "").toUpperCase();
718
+ if (type === "INT" || type === "INTEGER") return "INTEGER";
719
+ if (type === "DATETIME" || type === "DATE") return "TEXT";
720
+ return "TEXT";
721
+ }
722
+ function buildColumnDef(col) {
723
+ const type = mapColumnType(col.columnType);
724
+ const isPrimary = col.constraints.includes("PRIMARY") && col.constraints.includes("KEY");
725
+ const isAutoIncrement = col.constraints.includes("AUTO_INCREMENT");
726
+ let def = `${col.name} ${type}`;
727
+ if (isPrimary) def += " PRIMARY KEY";
728
+ if (isPrimary && isAutoIncrement) def += " AUTOINCREMENT";
729
+ return def;
730
+ }
731
+ var SQLiteDatabase = class {
732
+ /**
733
+ * @param {string} filePath Caminho do ficheiro .sqlite, ou ":memory:" para um banco temporário.
734
+ */
735
+ constructor(filePath = ":memory:") {
736
+ const { DatabaseSync } = __require("node:sqlite");
737
+ this.driver = new DatabaseSync(filePath);
738
+ }
739
+ createTable(name, columns, preventDefault) {
740
+ const resolvedColumns = name === "Context" && columns.length === 0 ? CONTEXT_SCHEMA : columns;
741
+ const columnDefs = resolvedColumns.map(buildColumnDef).join(", ");
742
+ const ifNotExists = preventDefault ? "IF NOT EXISTS " : "";
743
+ this.driver.exec(`CREATE TABLE ${ifNotExists}${name} (${columnDefs})`);
744
+ }
745
+ insert(table, columnNames, values) {
746
+ const placeholders = columnNames.map(() => "?").join(", ");
747
+ const sql = `INSERT INTO ${table} (${columnNames.join(", ")}) VALUES (${placeholders})`;
748
+ const info = this.driver.prepare(sql).run(...values);
749
+ return Number(info.lastInsertRowid);
750
+ }
751
+ update(table, column, value, whereColumn, whereValue) {
752
+ if (whereColumn) {
753
+ const sql = `UPDATE ${table} SET ${column} = ? WHERE ${whereColumn} = ?`;
754
+ this.driver.prepare(sql).run(value, whereValue);
755
+ } else {
756
+ const sql = `UPDATE ${table} SET ${column} = ? WHERE id = (SELECT MAX(id) FROM ${table})`;
757
+ this.driver.prepare(sql).run(value);
758
+ }
759
+ }
760
+ getRows(table) {
761
+ return this.driver.prepare(`SELECT * FROM ${table}`).all();
762
+ }
763
+ close() {
764
+ this.driver.close();
765
+ }
766
+ };
767
+ module.exports = { MemoryDatabase, SQLiteDatabase };
768
+ }
769
+ });
770
+
771
+ // FileSystem.js
772
+ var require_FileSystem = __commonJS({
773
+ "FileSystem.js"(exports, module) {
774
+ "use strict";
775
+ var MemoryFileSystem = class {
776
+ constructor(files = {}) {
777
+ this.files = new Map(
778
+ Object.entries(files).map(([k, v]) => [this._normalize(k), v])
779
+ );
780
+ }
781
+ _normalize(p) {
782
+ return String(p).replace(/^\.\//, "").replace(/\/+/g, "/").trim();
783
+ }
784
+ setFile(path, content) {
785
+ this.files.set(this._normalize(path), content);
786
+ }
787
+ exists(path) {
788
+ return this.files.has(this._normalize(path));
789
+ }
790
+ readFile(path) {
791
+ const key = this._normalize(path);
792
+ if (!this.files.has(key)) {
793
+ throw new Error(`BotQL: ficheiro n\xE3o encontrado: "${path}"`);
794
+ }
795
+ return this.files.get(key);
796
+ }
797
+ // basePath e' ignorado de proposito: no browser nao ha nocao real de
798
+ // diretorio corrente, os ficheiros importados sao identificados pelo
799
+ // nome/caminho tal como o utilizador os registou.
800
+ resolve(basePath, relativePath) {
801
+ return this._normalize(relativePath);
802
+ }
803
+ dirname(filePath) {
804
+ const norm = this._normalize(filePath);
805
+ const idx = norm.lastIndexOf("/");
806
+ return idx === -1 ? "" : norm.slice(0, idx);
807
+ }
808
+ };
809
+ var NodeFileSystem = class {
810
+ constructor() {
811
+ this._fs = __require("fs");
812
+ this._path = __require("path");
813
+ }
814
+ exists(path) {
815
+ return this._fs.existsSync(path);
816
+ }
817
+ readFile(path) {
818
+ return this._fs.readFileSync(path, "utf8");
819
+ }
820
+ resolve(basePath, relativePath) {
821
+ return this._path.resolve(basePath, relativePath);
822
+ }
823
+ dirname(filePath) {
824
+ return this._path.dirname(filePath);
825
+ }
826
+ };
827
+ var isNode = typeof process !== "undefined" && process.versions && !!process.versions.node;
828
+ function createDefaultFileSystem() {
829
+ return isNode ? new NodeFileSystem() : new MemoryFileSystem();
830
+ }
831
+ var api = { MemoryFileSystem, NodeFileSystem, createDefaultFileSystem };
832
+ if (typeof module !== "undefined" && module.exports) {
833
+ module.exports = api;
834
+ } else {
835
+ exports.BotQLFileSystem = api;
836
+ }
837
+ }
838
+ });
839
+
840
+ // RAG.js
841
+ var require_RAG = __commonJS({
842
+ "RAG.js"(exports, module) {
843
+ "use strict";
844
+ var STOPWORDS = /* @__PURE__ */ new Set([
845
+ "a",
846
+ "o",
847
+ "as",
848
+ "os",
849
+ "de",
850
+ "da",
851
+ "do",
852
+ "das",
853
+ "dos",
854
+ "e",
855
+ "ou",
856
+ "que",
857
+ "um",
858
+ "uma",
859
+ "uns",
860
+ "umas",
861
+ "em",
862
+ "no",
863
+ "na",
864
+ "nos",
865
+ "nas",
866
+ "por",
867
+ "para",
868
+ "com",
869
+ "sem",
870
+ "se",
871
+ "foi",
872
+ "ser",
873
+ "sao",
874
+ "esta",
875
+ "estao",
876
+ "ao",
877
+ "aos",
878
+ "mas",
879
+ "como",
880
+ "tem",
881
+ "ter",
882
+ "nao",
883
+ "sim",
884
+ "meu",
885
+ "minha",
886
+ "seu",
887
+ "sua",
888
+ "eu",
889
+ "tu",
890
+ "ele",
891
+ "ela",
892
+ "nos",
893
+ "vos",
894
+ "eles",
895
+ "elas",
896
+ "isso",
897
+ "isto",
898
+ "aquilo",
899
+ "quando",
900
+ "onde",
901
+ "porque",
902
+ "qual",
903
+ "quais",
904
+ "muito",
905
+ "muita",
906
+ "ja",
907
+ "so"
908
+ ]);
909
+ function normalizar(texto) {
910
+ return texto.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
911
+ }
912
+ var SUFIXOS_ADJETIVO_ADVERBIO = ["issimamente", "issimo", "issima", "mente"];
913
+ var SUFIXOS_NOMINALIZACAO = ["acoes", "acao", "imentos", "imento", "idades", "idade"];
914
+ function stem(palavra) {
915
+ let p = palavra;
916
+ for (const suf of SUFIXOS_ADJETIVO_ADVERBIO) {
917
+ if (p.length > suf.length + 3 && p.endsWith(suf)) {
918
+ p = p.slice(0, -suf.length);
919
+ break;
920
+ }
921
+ }
922
+ for (const suf of SUFIXOS_NOMINALIZACAO) {
923
+ if (p.length > suf.length + 3 && p.endsWith(suf)) {
924
+ p = p.slice(0, -suf.length);
925
+ break;
926
+ }
927
+ }
928
+ if (p.length > 4 && p.endsWith("s") && !p.endsWith("ns")) {
929
+ p = p.slice(0, -1);
930
+ }
931
+ return p;
932
+ }
933
+ function tokenizar(texto) {
934
+ return normalizar(texto).toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((palavra) => palavra.length > 1 && !STOPWORDS.has(palavra)).map(stem);
935
+ }
936
+ function distanciaEdicao(a, b) {
937
+ if (a === b) return 0;
938
+ const la = a.length;
939
+ const lb = b.length;
940
+ if (la === 0) return lb;
941
+ if (lb === 0) return la;
942
+ let linhaAnterior = new Array(lb + 1);
943
+ for (let j = 0; j <= lb; j++) linhaAnterior[j] = j;
944
+ for (let i = 1; i <= la; i++) {
945
+ const linhaAtual = [i];
946
+ for (let j = 1; j <= lb; j++) {
947
+ const custo = a[i - 1] === b[j - 1] ? 0 : 1;
948
+ linhaAtual[j] = Math.min(
949
+ linhaAtual[j - 1] + 1,
950
+ linhaAnterior[j] + 1,
951
+ linhaAnterior[j - 1] + custo
952
+ );
953
+ }
954
+ linhaAnterior = linhaAtual;
955
+ }
956
+ return linhaAnterior[lb];
957
+ }
958
+ function distanciaMaximaTolerada(tamanho) {
959
+ if (tamanho <= 4) return 0;
960
+ if (tamanho <= 7) return 1;
961
+ return 2;
962
+ }
963
+ var BM25_K1 = 1.5;
964
+ var BM25_B = 0.75;
965
+ var PESO_BM25 = 1;
966
+ var PESO_FRASE = 2.5;
967
+ var PESO_FUZZY = 0.4;
968
+ var CONFIANCA_ALTA = 0.55;
969
+ var CONFIANCA_MINIMA = 0.15;
970
+ var MARGEM_MINIMA = 0.12;
971
+ var SIMILARIDADE_DUPLICADA = 0.75;
972
+ function extrairNomesProprios(texto) {
973
+ const palavras = texto.split(/\s+/);
974
+ const encontrados = [];
975
+ for (let i = 1; i < palavras.length; i++) {
976
+ const limpa = palavras[i].replace(/^[(["']+|[.,;:!?)\]"']+$/g, "");
977
+ if (/^[A-ZÀ-Ý][a-zà-ÿ]+$/.test(limpa)) {
978
+ encontrados.push(limpa);
979
+ }
980
+ }
981
+ return encontrados;
982
+ }
983
+ var KnowledgeIndex = class _KnowledgeIndex {
984
+ /**
985
+ * @param {string} sourceText Conteúdo do ficheiro de conhecimento.
986
+ * @param {Object} [options]
987
+ * @param {Record<string,string>} [options.synonyms] Mapa de termo -> termo
988
+ * canónico, para ligar manualmente palavras que o stemmer não junta
989
+ * sozinho (ex: { horas: 'horario' }).
990
+ */
991
+ constructor(sourceText, options = {}) {
992
+ this.synonyms = {};
993
+ for (const [de, para] of Object.entries(options.synonyms || {})) {
994
+ this.synonyms[stem(normalizar(de.toLowerCase()))] = stem(normalizar(para.toLowerCase()));
995
+ }
996
+ this.blocks = _KnowledgeIndex.parseBlocks(sourceText);
997
+ this._buildIndex();
998
+ }
999
+ static parseBlocks(sourceText) {
1000
+ return sourceText.split(/\n\s*\n/).map((bloco) => bloco.trim()).filter((bloco) => bloco.length > 0);
1001
+ }
1002
+ _aplicarSinonimos(tokens) {
1003
+ return tokens.map((t) => this.synonyms[t] || t);
1004
+ }
1005
+ _buildIndex() {
1006
+ this.docs = this.blocks.map((bloco) => tokenizar(bloco));
1007
+ this.docTextNormalizado = this.blocks.map((bloco) => normalizar(bloco).toLowerCase());
1008
+ this.docFreq = /* @__PURE__ */ new Map();
1009
+ this.vocabulario = /* @__PURE__ */ new Set();
1010
+ for (const tokens of this.docs) {
1011
+ const vistas = new Set(tokens);
1012
+ for (const palavra of vistas) {
1013
+ this.docFreq.set(palavra, (this.docFreq.get(palavra) || 0) + 1);
1014
+ this.vocabulario.add(palavra);
1015
+ }
1016
+ }
1017
+ this.listaVocabulario = Array.from(this.vocabulario);
1018
+ this.totalDocs = this.docs.length;
1019
+ this.avgDocLen = this.totalDocs === 0 ? 0 : this.docs.reduce((soma, tokens) => soma + tokens.length, 0) / this.totalDocs;
1020
+ }
1021
+ _idf(palavra) {
1022
+ const n = this.docFreq.get(palavra) || 0;
1023
+ if (n === 0) return 0;
1024
+ return Math.log(1 + (this.totalDocs - n + 0.5) / (n + 0.5));
1025
+ }
1026
+ _termoMaisProximo(termo) {
1027
+ const tolerancia = distanciaMaximaTolerada(termo.length);
1028
+ if (tolerancia === 0) return null;
1029
+ let melhor = null;
1030
+ let melhorDist = tolerancia + 1;
1031
+ for (const candidato of this.listaVocabulario) {
1032
+ if (Math.abs(candidato.length - termo.length) > tolerancia) continue;
1033
+ const d = distanciaEdicao(termo, candidato);
1034
+ if (d < melhorDist) {
1035
+ melhorDist = d;
1036
+ melhor = candidato;
1037
+ }
1038
+ }
1039
+ return melhorDist <= tolerancia ? melhor : null;
1040
+ }
1041
+ _scoreBM25(queryTokens, docIndex) {
1042
+ const tokens = this.docs[docIndex];
1043
+ if (tokens.length === 0) return 0;
1044
+ const termFreq = /* @__PURE__ */ new Map();
1045
+ for (const t of tokens) termFreq.set(t, (termFreq.get(t) || 0) + 1);
1046
+ let score = 0;
1047
+ for (const termo of queryTokens) {
1048
+ let tf = termFreq.get(termo) || 0;
1049
+ let idf = this._idf(termo);
1050
+ let peso = 1;
1051
+ if (tf === 0) {
1052
+ const proximo = this._termoMaisProximo(termo);
1053
+ if (proximo === null) continue;
1054
+ tf = termFreq.get(proximo) || 0;
1055
+ if (tf === 0) continue;
1056
+ idf = this._idf(proximo);
1057
+ peso = PESO_FUZZY;
1058
+ }
1059
+ const numerador = tf * (BM25_K1 + 1);
1060
+ const denominador = tf + BM25_K1 * (1 - BM25_B + BM25_B * (tokens.length / this.avgDocLen));
1061
+ score += peso * idf * (numerador / denominador);
1062
+ }
1063
+ return score;
1064
+ }
1065
+ _bonusFrase(message, docIndex) {
1066
+ const msgNorm = normalizar(message).toLowerCase().replace(/[^a-z0-9\s]/g, " ");
1067
+ const palavras = msgNorm.split(/\s+/).filter((p) => p.length > 1);
1068
+ if (palavras.length < 2) return 0;
1069
+ const textoBloco = this.docTextNormalizado[docIndex];
1070
+ let bonus = 0;
1071
+ for (let tamanho = Math.min(6, palavras.length); tamanho >= 2; tamanho--) {
1072
+ for (let i = 0; i + tamanho <= palavras.length; i++) {
1073
+ const frase = palavras.slice(i, i + tamanho).join(" ");
1074
+ if (textoBloco.includes(frase)) {
1075
+ bonus += tamanho * tamanho;
1076
+ }
1077
+ }
1078
+ }
1079
+ return bonus;
1080
+ }
1081
+ _scoreDoc(message, queryTokens, docIndex) {
1082
+ const bm25 = this._scoreBM25(queryTokens, docIndex);
1083
+ const frase = this._bonusFrase(message, docIndex);
1084
+ return PESO_BM25 * bm25 + PESO_FRASE * frase;
1085
+ }
1086
+ // Calcula o score de todos os blocos para a mensagem e devolve ordenado
1087
+ // do maior para o menor. Base partilhada por search() e analyze().
1088
+ _rankTodos(message) {
1089
+ const queryTokens = this._aplicarSinonimos(tokenizar(message));
1090
+ if (queryTokens.length === 0) return [];
1091
+ const resultados = [];
1092
+ for (let i = 0; i < this.totalDocs; i++) {
1093
+ resultados.push({ index: i, score: this._scoreDoc(message, queryTokens, i) });
1094
+ }
1095
+ resultados.sort((a, b) => b.score - a.score);
1096
+ return resultados;
1097
+ }
1098
+ /**
1099
+ * Procura o bloco mais relevante para a mensagem recebida.
1100
+ *
1101
+ * @returns {{text: string, score: number, confidence: number, index: number} | null}
1102
+ * `confidence` está sempre entre 0 e 1 — não é probabilidade
1103
+ * estatística real, é uma escala interpretável para decidir um
1104
+ * limiar no `.sql` (ex: "só responde se confidence > 0.3").
1105
+ */
1106
+ search(message) {
1107
+ if (this.totalDocs === 0) return null;
1108
+ const ranking = this._rankTodos(message);
1109
+ if (ranking.length === 0) return null;
1110
+ const melhor = ranking[0];
1111
+ if (melhor.score <= 0) return null;
1112
+ return {
1113
+ text: this.blocks[melhor.index],
1114
+ score: melhor.score,
1115
+ confidence: melhor.score / (melhor.score + 3),
1116
+ index: melhor.index
1117
+ };
1118
+ }
1119
+ // Jaccard sobre os tokens (já com stem aplicado) de dois blocos já
1120
+ // indexados. Usado só pra detetar "mesmo conteúdo repetido" (valor
1121
+ // alto) — dois blocos sobre o mesmo assunto mas com detalhes
1122
+ // diferentes normalmente NÃO têm jaccard alto (a maior parte da frase
1123
+ // difere), por isso não serve pra decidir se vale a pena tentar unir.
1124
+ _similaridadeBlocos(indexA, indexB) {
1125
+ const a = new Set(this.docs[indexA]);
1126
+ const b = new Set(this.docs[indexB]);
1127
+ if (a.size === 0 || b.size === 0) return 0;
1128
+ let intersecao = 0;
1129
+ for (const t of a) if (b.has(t)) intersecao++;
1130
+ const uniao = a.size + b.size - intersecao;
1131
+ return uniao === 0 ? 0 : intersecao / uniao;
1132
+ }
1133
+ // Quantos tokens (com stem) os dois blocos partilham, em termos
1134
+ // absolutos. Usado como gatilho pra tentar unir: basta partilharem UM
1135
+ // termo de assunto ("entregamos") — quem garante que a fusão é segura
1136
+ // não é isto, é a regra de "exatamente um nome próprio diferente em
1137
+ // cada bloco" dentro de _tentarUnir.
1138
+ _termosPartilhados(indexA, indexB) {
1139
+ const a = new Set(this.docs[indexA]);
1140
+ const b = this.docs[indexB];
1141
+ let count = 0;
1142
+ for (const t of b) if (a.has(t)) count++;
1143
+ return count;
1144
+ }
1145
+ // Compara dois blocos concorrentes pelo nome próprio que cada um
1146
+ // menciona, pra decidir se são "a mesma informação" ou "informação
1147
+ // complementar que dá pra unir":
1148
+ // - mesmo nome próprio nos dois (ex: os dois falam de Luanda)
1149
+ // -> { tipo: 'duplicado' }: é a mesma coisa dita de formas
1150
+ // diferentes, não há o que unir, usa qualquer um dos dois
1151
+ // - nomes próprios diferentes (ex: Luanda vs Huambo)
1152
+ // -> { tipo: 'unido', texto: '...' }: funde numa frase só
1153
+ // - não dá pra identificar com segurança (nenhum nome próprio, ou
1154
+ // mais de um em algum dos blocos) -> null: quem chama decide o
1155
+ // que fazer a seguir (normalmente: reanalisar, ou cair no fallback)
1156
+ _tentarUnir(textoA, textoB) {
1157
+ const entidadesA = extrairNomesProprios(textoA);
1158
+ const entidadesB = extrairNomesProprios(textoB);
1159
+ if (entidadesA.length !== 1 || entidadesB.length !== 1) return null;
1160
+ const entidadeA = entidadesA[0];
1161
+ const entidadeB = entidadesB[0];
1162
+ if (normalizar(entidadeA).toLowerCase() === normalizar(entidadeB).toLowerCase()) {
1163
+ return { tipo: "duplicado" };
1164
+ }
1165
+ const primeiraPalavra = textoA.trim().split(/\s+/)[0].replace(/[.,;:!?]+$/, "");
1166
+ return { tipo: "unido", texto: `${primeiraPalavra} em v\xE1rios lugares, como ${entidadeA} e ${entidadeB}.` };
1167
+ }
1168
+ // Segunda tentativa de ranking, usada só quando a primeira ficou
1169
+ // ambígua: descarta metade dos termos da query (os de menor IDF, ou
1170
+ // seja, os mais genéricos/comuns) e refaz o ranking só com os termos
1171
+ // mais raros/decisivos. Uma query mais focada às vezes desempata o
1172
+ // que uma query "cheia" deixa embolado.
1173
+ _reanalisarFocado(message) {
1174
+ const tokens = this._aplicarSinonimos(tokenizar(message));
1175
+ if (tokens.length <= 2) return null;
1176
+ const comIdf = tokens.map((t) => ({ termo: t, idf: this._idf(t) }));
1177
+ comIdf.sort((a, b) => b.idf - a.idf);
1178
+ const focados = comIdf.slice(0, Math.max(1, Math.ceil(tokens.length / 2))).map((x) => x.termo);
1179
+ const resultados = [];
1180
+ for (let i = 0; i < this.totalDocs; i++) {
1181
+ resultados.push({ index: i, score: this._scoreDoc(message, focados, i) });
1182
+ }
1183
+ resultados.sort((a, b) => b.score - a.score);
1184
+ return resultados;
1185
+ }
1186
+ /**
1187
+ * Versão completa da busca: além do melhor bloco, avalia a evidência
1188
+ * (melhor x segundo colocado) e devolve uma decisão explícita, em vez
1189
+ * de deixar o `.sql` decidir tudo com um único limiar de confidence.
1190
+ *
1191
+ * EvidenceEvaluator: compara o melhor resultado com o segundo. Se os
1192
+ * dois estão muito próximos, o motor não tem certeza de qual bloco
1193
+ * responde à pergunta — mesmo que o score absoluto seja alto.
1194
+ *
1195
+ * ConfidenceEngine: mesma fórmula de sempre (score / (score + 3)),
1196
+ * aplicada só ao melhor resultado.
1197
+ *
1198
+ * DecisionEngine: cruza confidence com margem para decidir entre:
1199
+ * - RESPONDER confidence alta e o melhor bloco se destaca do 2o,
1200
+ * OU os dois concorrentes foram reconciliados (ver
1201
+ * abaixo) — nesse caso `texto` já vem pronto pra usar
1202
+ * - REANALISAR os blocos concorrentes são sobre assuntos diferentes
1203
+ * demais pra reconciliar, e nem a retentativa focada
1204
+ * resolveu — o `.sql` decide o que fazer (normalmente
1205
+ * cair no fallback do OR REPLY)
1206
+ * - UNKNOWN confidence baixa demais, não há bloco que sirva
1207
+ *
1208
+ * Reconciliação (só entra quando o resultado não é decisivo de cara):
1209
+ * 1. Se o melhor e o segundo colocado são basicamente o mesmo
1210
+ * conteúdo (alta similaridade) — não há nada pra unir, usa o
1211
+ * melhor tal como está.
1212
+ * 2. Se são blocos diferentes mas do mesmo assunto, e cada um tem
1213
+ * exatamente um nome próprio diferente (ex: "entregamos em
1214
+ * Luanda" / "entregamos no Huambo") — tenta fundir numa frase só
1215
+ * ("entregamos em vários lugares, como Luanda e Huambo").
1216
+ * 3. Se nada disso se aplica, tenta de novo com uma versão mais
1217
+ * enxuta da pergunta (só os termos mais decisivos) antes de
1218
+ * desistir.
1219
+ *
1220
+ * @returns {{
1221
+ * decision: 'RESPONDER'|'REANALISAR'|'UNKNOWN',
1222
+ * confidence: number,
1223
+ * margem: number,
1224
+ * texto: string | null,
1225
+ * unificado: boolean,
1226
+ * reanalisado: boolean,
1227
+ * melhor: {text: string, score: number, index: number} | null,
1228
+ * segundo: {text: string, score: number, index: number} | null
1229
+ * }}
1230
+ */
1231
+ analyze(message) {
1232
+ const vazio = { decision: "UNKNOWN", confidence: 0, margem: 0, texto: null, unificado: false, reanalisado: false, melhor: null, segundo: null };
1233
+ if (this.totalDocs === 0) return vazio;
1234
+ const ranking = this._rankTodos(message);
1235
+ if (ranking.length === 0 || ranking[0].score <= 0) return vazio;
1236
+ const melhor = ranking[0];
1237
+ const segundo = ranking[1] || { index: -1, score: 0 };
1238
+ const margem = (melhor.score - segundo.score) / melhor.score;
1239
+ const confidence = melhor.score / (melhor.score + 3);
1240
+ const melhorInfo = { text: this.blocks[melhor.index], score: melhor.score, index: melhor.index };
1241
+ const segundoInfo = segundo.index >= 0 ? { text: this.blocks[segundo.index], score: segundo.score, index: segundo.index } : null;
1242
+ if (confidence < CONFIANCA_MINIMA) return vazio;
1243
+ if (confidence >= CONFIANCA_ALTA && margem >= MARGEM_MINIMA) {
1244
+ return {
1245
+ decision: "RESPONDER",
1246
+ confidence,
1247
+ margem,
1248
+ texto: melhorInfo.text,
1249
+ unificado: false,
1250
+ reanalisado: false,
1251
+ melhor: melhorInfo,
1252
+ segundo: segundoInfo
1253
+ };
1254
+ }
1255
+ if (segundoInfo) {
1256
+ const similaridade = this._similaridadeBlocos(melhor.index, segundo.index);
1257
+ if (similaridade >= SIMILARIDADE_DUPLICADA) {
1258
+ return {
1259
+ decision: "RESPONDER",
1260
+ confidence,
1261
+ margem,
1262
+ texto: melhorInfo.text,
1263
+ unificado: false,
1264
+ reanalisado: false,
1265
+ melhor: melhorInfo,
1266
+ segundo: segundoInfo
1267
+ };
1268
+ }
1269
+ const termosPartilhados = this._termosPartilhados(melhor.index, segundo.index);
1270
+ if (termosPartilhados >= 1) {
1271
+ const resultado = this._tentarUnir(melhorInfo.text, segundoInfo.text);
1272
+ if (resultado && resultado.tipo === "duplicado") {
1273
+ return {
1274
+ decision: "RESPONDER",
1275
+ confidence,
1276
+ margem,
1277
+ texto: melhorInfo.text,
1278
+ unificado: false,
1279
+ reanalisado: false,
1280
+ melhor: melhorInfo,
1281
+ segundo: segundoInfo
1282
+ };
1283
+ }
1284
+ if (resultado && resultado.tipo === "unido") {
1285
+ return {
1286
+ decision: "RESPONDER",
1287
+ confidence,
1288
+ margem,
1289
+ texto: resultado.texto,
1290
+ unificado: true,
1291
+ reanalisado: false,
1292
+ melhor: melhorInfo,
1293
+ segundo: segundoInfo
1294
+ };
1295
+ }
1296
+ }
1297
+ }
1298
+ const tentativa2 = this._reanalisarFocado(message);
1299
+ if (tentativa2 && tentativa2.length > 0 && tentativa2[0].score > 0) {
1300
+ const melhor2 = tentativa2[0];
1301
+ const segundo2 = tentativa2[1] || { index: -1, score: 0 };
1302
+ const margem2 = (melhor2.score - segundo2.score) / melhor2.score;
1303
+ const confidence2 = melhor2.score / (melhor2.score + 3);
1304
+ if (confidence2 >= CONFIANCA_ALTA && margem2 >= MARGEM_MINIMA) {
1305
+ return {
1306
+ decision: "RESPONDER",
1307
+ confidence: confidence2,
1308
+ margem: margem2,
1309
+ texto: this.blocks[melhor2.index],
1310
+ unificado: false,
1311
+ reanalisado: true,
1312
+ melhor: { text: this.blocks[melhor2.index], score: melhor2.score, index: melhor2.index },
1313
+ segundo: segundo2.index >= 0 ? { text: this.blocks[segundo2.index], score: segundo2.score, index: segundo2.index } : null
1314
+ };
1315
+ }
1316
+ }
1317
+ return {
1318
+ decision: "REANALISAR",
1319
+ confidence,
1320
+ margem,
1321
+ texto: null,
1322
+ unificado: false,
1323
+ reanalisado: false,
1324
+ melhor: melhorInfo,
1325
+ segundo: segundoInfo
1326
+ };
1327
+ }
1328
+ };
1329
+ var KnowledgeCache = class {
1330
+ constructor(fileSystem) {
1331
+ this.fileSystem = fileSystem;
1332
+ this.cache = /* @__PURE__ */ new Map();
1333
+ }
1334
+ get(resolvedPath, options) {
1335
+ if (this.cache.has(resolvedPath)) return this.cache.get(resolvedPath);
1336
+ if (!this.fileSystem.exists(resolvedPath)) {
1337
+ throw new Error(`BotQL/THINK: ficheiro de conhecimento n\xE3o encontrado: "${resolvedPath}"`);
1338
+ }
1339
+ const texto = this.fileSystem.readFile(resolvedPath);
1340
+ const index = new KnowledgeIndex(texto, options);
1341
+ this.cache.set(resolvedPath, index);
1342
+ return index;
1343
+ }
1344
+ };
1345
+ module.exports = { KnowledgeIndex, KnowledgeCache, tokenizar, normalizar, stem, distanciaEdicao };
1346
+ }
1347
+ });
1348
+
1349
+ // botql.js
1350
+ var require_botql = __commonJS({
1351
+ "botql.js"(exports, module) {
1352
+ var { Parser } = require_Parser();
1353
+ var { MemoryDatabase } = require_Database();
1354
+ var { createDefaultFileSystem } = require_FileSystem();
1355
+ var { KnowledgeCache } = require_RAG();
1356
+ var WAITING_TEXTO_PADRAO = "Pensando...";
1357
+ var WAITING_SEGUNDOS_PADRAO = 3;
1358
+ var BotQLInterpreter = class _BotQLInterpreter {
1359
+ constructor(options = {}) {
1360
+ this.db = options.db || new MemoryDatabase();
1361
+ this.onReply = options.onReply || null;
1362
+ this.onForward = options.onForward || null;
1363
+ this.onSend = options.onSend || null;
1364
+ this.signalParser = options.signalParser || ((raw) => raw);
1365
+ this.onThinking = options.onThinking || null;
1366
+ this.botName = null;
1367
+ this.platform = null;
1368
+ this.connections = [];
1369
+ this.handlers = { START: [], MESSAGE: [], SIGNAL: [] };
1370
+ this.running = false;
1371
+ this.responseConnections = /* @__PURE__ */ new Set();
1372
+ this.onResponse = options.onResponse || null;
1373
+ this.defaultMessageConfig = null;
1374
+ this.nativeFuncs = /* @__PURE__ */ new Map();
1375
+ this.registerFunction("NOW", () => (/* @__PURE__ */ new Date()).toISOString());
1376
+ this.fileSystem = options.fileSystem || createDefaultFileSystem();
1377
+ const defaultBasePath = typeof process !== "undefined" && process.cwd ? process.cwd() : "";
1378
+ this._basePathStack = [options.basePath || defaultBasePath];
1379
+ this._importedFiles = /* @__PURE__ */ new Set();
1380
+ this._rootBasePath = options.basePath || defaultBasePath;
1381
+ this._keywordsFileCache = /* @__PURE__ */ new Map();
1382
+ this._replyFileCache = /* @__PURE__ */ new Map();
1383
+ this.envValues = /* @__PURE__ */ new Map();
1384
+ this.knowledgeCache = new KnowledgeCache(this.fileSystem);
1385
+ }
1386
+ static fromSource(source, options = {}) {
1387
+ const ast = new Parser(source).parseProgram();
1388
+ const interpreter = new _BotQLInterpreter(options);
1389
+ interpreter.load(ast);
1390
+ return interpreter;
1391
+ }
1392
+ // Lê um ficheiro .sql do disco (ou de outro fileSystem injetado) e usa
1393
+ // a sua pasta como base para resolver IMPORTs relativos dentro dele.
1394
+ static fromFile(filePath, options = {}) {
1395
+ const fileSystem = options.fileSystem || createDefaultFileSystem();
1396
+ const resolved = fileSystem.resolve("", filePath);
1397
+ const source = fileSystem.readFile(resolved);
1398
+ return _BotQLInterpreter.fromSource(source, {
1399
+ ...options,
1400
+ fileSystem,
1401
+ basePath: fileSystem.dirname(resolved)
1402
+ });
1403
+ }
1404
+ registerFunction(name, fn) {
1405
+ this.nativeFuncs.set(name, fn);
1406
+ }
1407
+ // ---- Carregamento da AST: statements de topo, fora de eventos ----
1408
+ //
1409
+ // IMPORT é sempre resolvido antes de qualquer outro comando, independente
1410
+ // de onde aparece no ficheiro (tal como o SOURCE do MySQL ou o \i do
1411
+ // psql) — por isso o corpo é primeiro "achatado": todo o conteúdo vindo
1412
+ // de ficheiros importados entra ANTES dos statements do próprio ficheiro,
1413
+ // recursivamente, mesmo que o IMPORT apareça no meio ou no fim do texto.
1414
+ load(ast, basePath = this._basePathStack[this._basePathStack.length - 1]) {
1415
+ const flatBody = this._flattenImports(ast.body, basePath);
1416
+ for (const node of flatBody) {
1417
+ switch (node.type) {
1418
+ case "CreateBot":
1419
+ this.botName = node.name;
1420
+ break;
1421
+ case "Platform":
1422
+ this.platform = node.value;
1423
+ break;
1424
+ case "Connect":
1425
+ this.connections.push({
1426
+ service: node.service,
1427
+ kind: node.kind,
1428
+ credential: node.credential
1429
+ });
1430
+ break;
1431
+ case "ConnectResponse":
1432
+ if (!this.envValues.has(node.alias)) {
1433
+ throw new Error(`runtime error: CONNECT RESPONSE failed, alias "${node.alias}" was not imported (use IMPORT {..., N} AS ${node.alias})`);
1434
+ }
1435
+ this.responseConnections.add(node.alias);
1436
+ break;
1437
+ case "CreateTable":
1438
+ this.db.createTable(node.name, node.columns, node.preventDefault);
1439
+ if (node.defaultMessage) {
1440
+ const senderColumn = node.name === "Context" ? "client" : (node.columns.find((c) => c.name === "client" || c.name === "sender") || {}).name;
1441
+ if (!senderColumn) {
1442
+ throw new Error(`runtime error: DEFAULT MESSAGE on table "${node.name}" requires a "client" or "sender" column`);
1443
+ }
1444
+ this.defaultMessageConfig = { table: node.name, senderColumn, ...node.defaultMessage };
1445
+ }
1446
+ break;
1447
+ case "On":
1448
+ this.handlers[node.event].push(node);
1449
+ break;
1450
+ case "RunBot":
1451
+ this.running = true;
1452
+ break;
1453
+ default:
1454
+ throw new Error(`runtime error: unsupported top-level statement: ${node.type}`);
1455
+ }
1456
+ }
1457
+ }
1458
+ // Devolve um array de statements sem nenhum nó 'Import': o conteúdo de
1459
+ // cada ficheiro importado é lido, parseado e achatado recursivamente
1460
+ // primeiro (imports dentro de imports também respeitam a regra), e só
1461
+ // depois vêm os statements que pertencem a este próprio ficheiro.
1462
+ //
1463
+ // IMPORT {ficheiro.sql} (sem índice) — comportamento original: lê o
1464
+ // ficheiro inteiro como código BotQL e junta ao programa.
1465
+ //
1466
+ // IMPORT {ficheiro.txt, N} (com índice) — não é código: lê só a
1467
+ // entrada N do ficheiro (mesmo formato "N- valor" do REPLY indexado)
1468
+ // e guarda em this.envValues. Com AS alias, a chave é o alias (fica
1469
+ // acessível diretamente pelo nome, ex: K, CONNECT RESPONSE K); sem
1470
+ // AS, mantém-se o comportamento original — chave é o nome do
1471
+ // ficheiro sem extensão, só acessível via env.NOME_FICHEIRO. Um
1472
+ // ficheiro sem AS não pode ser usado por CONNECT RESPONSE (exige
1473
+ // alias explícito, para não colidir com outros IMPORTs do mesmo
1474
+ // ficheiro em índices diferentes).
1475
+ // Não produz nenhum statement — é resolvido e removido do AST aqui
1476
+ // mesmo, antes de correr qualquer evento.
1477
+ _flattenImports(body, basePath) {
1478
+ const fromImports = [];
1479
+ const ownStatements = [];
1480
+ for (const node of body) {
1481
+ if (node.type !== "Import") {
1482
+ ownStatements.push(node);
1483
+ continue;
1484
+ }
1485
+ if (node.index !== null && node.index !== void 0) {
1486
+ const index = this.evalExpr(node.index, this.createContext());
1487
+ const entries = this._loadIndexedFile(node.path, basePath);
1488
+ const value = entries.get(Number(index));
1489
+ if (value === void 0) {
1490
+ throw new Error(`runtime error: IMPORT failed, entry ${index} not found in "${node.path}"`);
1491
+ }
1492
+ const key = node.alias || node.path.replace(/\.[^.]+$/, "");
1493
+ this.envValues.set(key, value);
1494
+ continue;
1495
+ }
1496
+ const fullPath = this.fileSystem.resolve(basePath, node.path);
1497
+ if (this._importedFiles.has(fullPath)) continue;
1498
+ if (!this.fileSystem.exists(fullPath)) {
1499
+ throw new Error(`runtime error: IMPORT failed, file not found: "${fullPath}"`);
1500
+ }
1501
+ this._importedFiles.add(fullPath);
1502
+ const source = this.fileSystem.readFile(fullPath);
1503
+ const importedAst = new Parser(source).parseProgram();
1504
+ const nestedFlat = this._flattenImports(importedAst.body, this.fileSystem.dirname(fullPath));
1505
+ fromImports.push(...nestedFlat);
1506
+ }
1507
+ return [...fromImports, ...ownStatements];
1508
+ }
1509
+ // ---- Contexto de execução de um evento ----
1510
+ createContext(base = {}) {
1511
+ return {
1512
+ vars: {
1513
+ client: base.client || null,
1514
+ message: base.message || null,
1515
+ lastMsg: null,
1516
+ SIGNAL: null
1517
+ },
1518
+ rawSignal: base.rawSignal || null,
1519
+ lastInsertId: null
1520
+ };
1521
+ }
1522
+ // ---- Ciclo de vida do bot ----
1523
+ async start() {
1524
+ for (const handler of this.handlers.START) {
1525
+ const ctx = this.createContext();
1526
+ await this.run(handler.body, ctx);
1527
+ }
1528
+ }
1529
+ async receiveMessage(client, message) {
1530
+ const ctx = this.createContext({ client, message });
1531
+ const cumprimentou = await this.greetIfNew(client);
1532
+ if (cumprimentou) return ctx;
1533
+ for (const handler of this.handlers.MESSAGE) {
1534
+ await this.run(handler.body, ctx);
1535
+ }
1536
+ return ctx;
1537
+ }
1538
+ // Mostra a DEFAULT MESSAGE para este client, se ainda não for
1539
+ // conhecido — sem precisar de nenhuma mensagem recebida. Usado por
1540
+ // receiveMessage() (caso real, reativo) e também pode ser chamado
1541
+ // diretamente pelo editor/preview ao abrir o chat, já que aí não há a
1542
+ // restrição das plataformas reais de só poder responder depois do
1543
+ // utilizador escrever primeiro. Devolve true se cumprimentou (e por
1544
+ // isso nada mais deve correr nesse turno), false caso contrário —
1545
+ // incluindo quando não há DEFAULT MESSAGE nenhuma configurada.
1546
+ async greetIfNew(client) {
1547
+ if (!this.defaultMessageConfig) return false;
1548
+ const { table, senderColumn } = this.defaultMessageConfig;
1549
+ const jaConhecido = this.db.getRows(table).some((row) => row[senderColumn] === client);
1550
+ if (jaConhecido) return false;
1551
+ const ctx = this.createContext({ client });
1552
+ const text = this.resolveDefaultMessageText(this.defaultMessageConfig, ctx);
1553
+ ctx.vars.lastMsg = text;
1554
+ this.db.insert(table, [senderColumn], [client]);
1555
+ if (this.onReply) {
1556
+ await this.onReply({ target: null, text, client });
1557
+ }
1558
+ return true;
1559
+ }
1560
+ // Resolve o texto do DEFAULT MESSAGE: mesma dualidade texto-direto vs
1561
+ // ficheiro indexado que o REPLY já tem (ver resolveReplyFromFile).
1562
+ resolveDefaultMessageText(config, ctx) {
1563
+ if (config.file) {
1564
+ return this.resolveReplyFromFile(config.file, config.index, ctx);
1565
+ }
1566
+ return String(this.evalExpr(config.value, ctx));
1567
+ }
1568
+ async receiveSignal(source, rawSignal) {
1569
+ const ctx = this.createContext({ rawSignal });
1570
+ for (const handler of this.handlers.SIGNAL) {
1571
+ if (handler.source !== source) continue;
1572
+ await this.run(handler.body, ctx);
1573
+ }
1574
+ return ctx;
1575
+ }
1576
+ // ---- Execução de uma lista de statements (corpo de um bloco) ----
1577
+ async run(statements, ctx) {
1578
+ let groupMatched = false;
1579
+ for (let i = 0; i < statements.length; i++) {
1580
+ const stmt = statements[i];
1581
+ if (stmt.type === "When") {
1582
+ if (i === 0 || statements[i - 1].type !== "When") groupMatched = false;
1583
+ const isMatch = stmt.conditions.some((cond) => this.evalCondition(cond, ctx));
1584
+ if (isMatch) {
1585
+ await this.run(stmt.body, ctx);
1586
+ groupMatched = true;
1587
+ }
1588
+ continue;
1589
+ }
1590
+ if (stmt.type === "Otherwise") {
1591
+ if (!groupMatched) await this.run(stmt.body, ctx);
1592
+ groupMatched = false;
1593
+ continue;
1594
+ }
1595
+ await this.execStatement(stmt, ctx);
1596
+ }
1597
+ }
1598
+ evalCondition(cond, ctx) {
1599
+ if (typeof ctx.vars.message !== "string") return false;
1600
+ const message = ctx.vars.message.toLowerCase();
1601
+ if (cond.op === "CONTAINS") {
1602
+ return message.includes(cond.value.toLowerCase());
1603
+ }
1604
+ if (cond.op === "CONTAINS_ANY") {
1605
+ return cond.values.some((v) => message.includes(v.toLowerCase()));
1606
+ }
1607
+ if (cond.op === "CONTAINS_KEYWORDS_FILE") {
1608
+ const words = this._loadKeywordsFile(cond.path);
1609
+ return words.some((v) => message.includes(v.toLowerCase()));
1610
+ }
1611
+ throw new Error(`runtime error: unsupported condition: ${cond.op}`);
1612
+ }
1613
+ // Lê um ficheiro de keywords: uma palavra/frase por linha, nada mais.
1614
+ // Sem comentários nem qualquer outra sintaxe misturada de propósito —
1615
+ // um ficheiro só serve para um fim, evita ambiguidade sobre o que é
1616
+ // keyword e o que não é. Resultado fica em cache — chamado a cada
1617
+ // mensagem recebida, não pode reler disco/rede sempre.
1618
+ _loadKeywordsFile(path) {
1619
+ if (this._keywordsFileCache.has(path)) {
1620
+ return this._keywordsFileCache.get(path);
1621
+ }
1622
+ const fullPath = this.fileSystem.resolve(this._rootBasePath, path);
1623
+ if (!this.fileSystem.exists(fullPath)) {
1624
+ throw new Error(`runtime error: KEYWORDS failed, file not found: "${fullPath}"`);
1625
+ }
1626
+ const raw = this.fileSystem.readFile(fullPath);
1627
+ const words = raw.split("\n").map((linha) => linha.trim()).filter((linha) => linha.length > 0);
1628
+ this._keywordsFileCache.set(path, words);
1629
+ return words;
1630
+ }
1631
+ // Resolve o texto e o tempo mínimo do WAITING(...) de um THINK. Sem
1632
+ // WAITING nenhum no .sql (stmt.waiting === null), ou com WAITING()
1633
+ // vazio, usa os valores por omissão. O ficheiro do WAITING é lido tal
1634
+ // e qual (texto/HTML livre) — ao contrário do REPLY/DEFAULT MESSAGE
1635
+ // indexados, não é o formato "N- valor".
1636
+ resolveWaitingConfig(waiting, ctx) {
1637
+ if (!waiting) return { text: WAITING_TEXTO_PADRAO, seconds: WAITING_SEGUNDOS_PADRAO };
1638
+ let text = WAITING_TEXTO_PADRAO;
1639
+ if (waiting.file) {
1640
+ const fullPath = this.fileSystem.resolve(this._rootBasePath, waiting.file);
1641
+ if (!this.fileSystem.exists(fullPath)) {
1642
+ throw new Error(`runtime error: WAITING failed, file not found: "${fullPath}"`);
1643
+ }
1644
+ text = this.fileSystem.readFile(fullPath).trim();
1645
+ }
1646
+ let seconds = WAITING_SEGUNDOS_PADRAO;
1647
+ if (waiting.seconds) {
1648
+ seconds = Number(this.evalExpr(waiting.seconds, ctx));
1649
+ }
1650
+ return { text, seconds };
1651
+ }
1652
+ // Resolve REPLY (ficheiro.txt, indice): lê o índice (número ou
1653
+ // expressão que resolve a número) e devolve o texto da entrada
1654
+ // correspondente do ficheiro de respostas.
1655
+ resolveReplyFromFile(file, indexNode, ctx) {
1656
+ const index = this.evalExpr(indexNode, ctx);
1657
+ const entries = this._loadIndexedFile(file, this._rootBasePath);
1658
+ const text = entries.get(Number(index));
1659
+ if (text === void 0) {
1660
+ throw new Error(`runtime error: REPLY failed, entry ${index} not found in "${file}"`);
1661
+ }
1662
+ return text;
1663
+ }
1664
+ // Lê um ficheiro de entradas numeradas. Duas formas, na mesma
1665
+ // entrada N, nunca misturadas:
1666
+ //
1667
+ // N- texto — uma linha só (forma original, continua igual)
1668
+ // N-{ ... } — bloco delimitado por chaves, pode ter
1669
+ // varias linhas e HTML/Markdown/CSS por
1670
+ // dentro. So fecha na "}" que corresponde a
1671
+ // "{" que abriu — chaves internas (ex: um
1672
+ // style="{color:red}" dentro do HTML) contam
1673
+ // para o aninhamento e NAO fecham a entrada
1674
+ // cedo. Rigoroso: uma "{" sem a "}"
1675
+ // correspondente antes do fim do ficheiro e'
1676
+ // erro, nunca silenciosamente ignorado.
1677
+ //
1678
+ // Resultado e' um Map indice -> valor, em cache — usado tanto por
1679
+ // REPLY (ficheiro, N) como por IMPORT {ficheiro, N}.
1680
+ _loadIndexedFile(path, basePath) {
1681
+ if (this._replyFileCache.has(path)) {
1682
+ return this._replyFileCache.get(path);
1683
+ }
1684
+ const fullPath = this.fileSystem.resolve(basePath, path);
1685
+ if (!this.fileSystem.exists(fullPath)) {
1686
+ throw new Error(`runtime error: file not found: "${fullPath}"`);
1687
+ }
1688
+ const raw = this.fileSystem.readFile(fullPath);
1689
+ const entries = this._parseIndexedEntries(raw, path);
1690
+ this._replyFileCache.set(path, entries);
1691
+ return entries;
1692
+ }
1693
+ // Parser char-by-char do formato de entradas numeradas. Percorre o
1694
+ // ficheiro procurando "N-" no inicio de uma linha (ignorando espacos);
1695
+ // o que vem a seguir decide a forma:
1696
+ // "{" logo a seguir -> bloco: conta chaves ate a correspondente
1697
+ // fechar, valor e' o conteudo entre elas (sem as chaves),
1698
+ // aparado nas pontas.
1699
+ // qualquer outra coisa -> forma de uma linha: valor e' o resto da
1700
+ // linha, aparado.
1701
+ _parseIndexedEntries(raw, path) {
1702
+ const entries = /* @__PURE__ */ new Map();
1703
+ const len = raw.length;
1704
+ let i = 0;
1705
+ while (i < len) {
1706
+ const lineStart = i;
1707
+ let j = lineStart;
1708
+ while (j < len && (raw[j] === " " || raw[j] === " ")) j++;
1709
+ const numMatch = /^\d+/.exec(raw.slice(j));
1710
+ if (!numMatch || raw[j + numMatch[0].length] !== "-") {
1711
+ const nl2 = raw.indexOf("\n", lineStart);
1712
+ i = nl2 === -1 ? len : nl2 + 1;
1713
+ continue;
1714
+ }
1715
+ const index = Number(numMatch[0]);
1716
+ let k = j + numMatch[0].length + 1;
1717
+ if (raw[k] === "{") {
1718
+ let depth = 1;
1719
+ const contentStart = k + 1;
1720
+ let p = contentStart;
1721
+ while (p < len && depth > 0) {
1722
+ if (raw[p] === "{") depth++;
1723
+ else if (raw[p] === "}") depth--;
1724
+ p++;
1725
+ }
1726
+ if (depth !== 0) {
1727
+ throw new Error(`runtime error: unclosed "{" for entry ${index} in "${path}"`);
1728
+ }
1729
+ const value2 = raw.slice(contentStart, p - 1).trim();
1730
+ entries.set(index, value2);
1731
+ i = p;
1732
+ continue;
1733
+ }
1734
+ const nl = raw.indexOf("\n", k);
1735
+ const lineEnd = nl === -1 ? len : nl;
1736
+ const value = raw.slice(k, lineEnd).trim();
1737
+ entries.set(index, value);
1738
+ i = nl === -1 ? len : nl + 1;
1739
+ }
1740
+ return entries;
1741
+ }
1742
+ // ---- Execução de um statement de ação ----
1743
+ async execStatement(stmt, ctx) {
1744
+ switch (stmt.type) {
1745
+ case "Reply": {
1746
+ const text = stmt.file ? this.resolveReplyFromFile(stmt.file, stmt.index, ctx) : String(await this.evalReplyValue(stmt.value, ctx));
1747
+ ctx.vars.lastMsg = text;
1748
+ if (this.onReply) {
1749
+ await this.onReply({ target: stmt.target, text, client: ctx.vars.client });
1750
+ }
1751
+ break;
1752
+ }
1753
+ case "Think": {
1754
+ const waiting = this.resolveWaitingConfig(stmt.waiting, ctx);
1755
+ const inicio = Date.now();
1756
+ if (this.onThinking) {
1757
+ await this.onThinking({ client: ctx.vars.client, text: waiting.text });
1758
+ }
1759
+ const fullPath = this.fileSystem.resolve(this._rootBasePath, stmt.file);
1760
+ const index = this.knowledgeCache.get(fullPath);
1761
+ const resultado = index.analyze(ctx.vars.message);
1762
+ const decorrido = Date.now() - inicio;
1763
+ const faltam = waiting.seconds * 1e3 - decorrido;
1764
+ if (faltam > 0) {
1765
+ await new Promise((resolve) => setTimeout(resolve, faltam));
1766
+ }
1767
+ if (resultado.decision === "RESPONDER") {
1768
+ const text = resultado.texto;
1769
+ ctx.vars.lastMsg = text;
1770
+ if (this.onReply) {
1771
+ await this.onReply({ target: null, text, client: ctx.vars.client });
1772
+ }
1773
+ } else if (stmt.fallback) {
1774
+ await this.execStatement(stmt.fallback, ctx);
1775
+ }
1776
+ break;
1777
+ }
1778
+ case "ForwardTo": {
1779
+ const target = String(this.evalExpr(stmt.target, ctx));
1780
+ if (this.onForward) {
1781
+ await this.onForward({ target, client: ctx.vars.client });
1782
+ }
1783
+ break;
1784
+ }
1785
+ case "ParseSignal": {
1786
+ ctx.vars.SIGNAL = await this.signalParser(ctx.rawSignal);
1787
+ break;
1788
+ }
1789
+ case "SendTo": {
1790
+ if (this.onSend) {
1791
+ await this.onSend({ target: stmt.target, signal: ctx.vars.SIGNAL });
1792
+ }
1793
+ break;
1794
+ }
1795
+ case "Insert": {
1796
+ this.execInsert(stmt, ctx);
1797
+ break;
1798
+ }
1799
+ case "Update": {
1800
+ this.execUpdate(stmt, ctx);
1801
+ break;
1802
+ }
1803
+ default:
1804
+ throw new Error(`runtime error: unsupported statement inside event: ${stmt.type}`);
1805
+ }
1806
+ }
1807
+ execInsert(stmt, ctx) {
1808
+ let columnNames;
1809
+ let values;
1810
+ if (stmt.table === "Context" && (!stmt.columns || stmt.columns.length === 0) && !stmt.values) {
1811
+ columnNames = ["client", "message", "created_at"];
1812
+ values = [ctx.vars.client, ctx.vars.message, (/* @__PURE__ */ new Date()).toISOString()];
1813
+ } else {
1814
+ columnNames = (stmt.columns || []).map((c) => c.name);
1815
+ values = (stmt.values || []).map((v) => this.evalExpr(v, ctx));
1816
+ }
1817
+ ctx.lastInsertId = this.db.insert(stmt.table, columnNames, values);
1818
+ }
1819
+ execUpdate(stmt, ctx) {
1820
+ const value = this.evalExpr(stmt.set.value, ctx);
1821
+ let whereColumn = null;
1822
+ let whereValue = null;
1823
+ if (stmt.where) {
1824
+ whereColumn = stmt.where.left.name;
1825
+ whereValue = this.evalExpr(stmt.where.right, ctx);
1826
+ }
1827
+ this.db.update(stmt.table, stmt.set.column, value, whereColumn, whereValue);
1828
+ }
1829
+ // ---- Avaliação de expressões ----
1830
+ // Ponto de entrada usado só por REPLY <expr>: trata Response()/
1831
+ // Response(alias) como caso especial assíncrono (chama this.onResponse,
1832
+ // que fala de verdade com a IA ligada) e delega qualquer outra
1833
+ // expressão ao evalExpr síncrono normal. Response() não pode ser
1834
+ // composto dentro de + (ex: REPLY "x" + Response() não é suportado) —
1835
+ // só faz sentido como o valor direto do REPLY, conforme a doc.
1836
+ async evalReplyValue(node, ctx) {
1837
+ if (node.type === "Call" && node.callee === "Response") {
1838
+ return this.execResponse(node, ctx);
1839
+ }
1840
+ return this.evalExpr(node, ctx);
1841
+ }
1842
+ // Response() / Response(alias): dispara o ciclo com a IA ligada via
1843
+ // CONNECT RESPONSE — envia a mensagem atual, espera, devolve o texto.
1844
+ // Sem argumento só é válido com exatamente uma ligação (ambíguo com
1845
+ // mais que uma); com argumento, o alias tem de corresponder a um
1846
+ // CONNECT RESPONSE já feito.
1847
+ async execResponse(node, ctx) {
1848
+ if (!this.onResponse) {
1849
+ throw new Error("runtime error: Response() failed, no AI connected (missing onResponse handler)");
1850
+ }
1851
+ let alias;
1852
+ if (node.args.length === 0) {
1853
+ if (this.responseConnections.size === 0) {
1854
+ throw new Error("runtime error: Response() failed, no CONNECT RESPONSE found");
1855
+ }
1856
+ if (this.responseConnections.size > 1) {
1857
+ throw new Error(`runtime error: Response() is ambiguous with multiple CONNECT RESPONSE (${[...this.responseConnections].join(", ")}); use Response(alias)`);
1858
+ }
1859
+ alias = [...this.responseConnections][0];
1860
+ } else {
1861
+ const argNode = node.args[0];
1862
+ if (argNode.type !== "Identifier") {
1863
+ throw new Error("runtime error: Response(alias) expects an alias name, not a string or expression");
1864
+ }
1865
+ alias = argNode.name;
1866
+ if (!this.responseConnections.has(alias)) {
1867
+ throw new Error(`runtime error: Response(${alias}) failed, no CONNECT RESPONSE ${alias} found`);
1868
+ }
1869
+ }
1870
+ const token = this.envValues.get(alias);
1871
+ const text = await this.onResponse({ alias, token, message: ctx.vars.message, client: ctx.vars.client });
1872
+ return text;
1873
+ }
1874
+ evalExpr(node, ctx) {
1875
+ switch (node.type) {
1876
+ case "Literal":
1877
+ return node.value;
1878
+ case "Identifier": {
1879
+ if (node.name === "env") {
1880
+ return Object.fromEntries(this.envValues);
1881
+ }
1882
+ if (Object.prototype.hasOwnProperty.call(ctx.vars, node.name)) {
1883
+ return ctx.vars[node.name];
1884
+ }
1885
+ if (this.envValues.has(node.name)) {
1886
+ return this.envValues.get(node.name);
1887
+ }
1888
+ throw new Error(`runtime error: unknown identifier: "${node.name}"`);
1889
+ }
1890
+ case "Member": {
1891
+ const obj = this.evalExpr(node.object, ctx);
1892
+ if (obj === null || obj === void 0) return void 0;
1893
+ return obj[node.property];
1894
+ }
1895
+ case "Call": {
1896
+ if (node.callee === "LAST_INSERT_ID") return ctx.lastInsertId;
1897
+ const fn = this.nativeFuncs.get(node.callee);
1898
+ if (!fn) throw new Error(`runtime error: unknown function: "${node.callee}"`);
1899
+ const args = node.args.map((a) => this.evalExpr(a, ctx));
1900
+ return fn(...args);
1901
+ }
1902
+ case "Binary": {
1903
+ const left = this.evalExpr(node.left, ctx);
1904
+ const right = this.evalExpr(node.right, ctx);
1905
+ if (node.op === "+") {
1906
+ if (typeof left === "number" && typeof right === "number") return left + right;
1907
+ return String(left) + String(right);
1908
+ }
1909
+ if (node.op === "=") return left === right;
1910
+ throw new Error(`runtime error: unsupported operator: "${node.op}"`);
1911
+ }
1912
+ default:
1913
+ throw new Error(`runtime error: unsupported expression: ${node.type}`);
1914
+ }
1915
+ }
1916
+ };
1917
+ module.exports = { BotQLInterpreter, MemoryDatabase };
1918
+ }
1919
+ });
1920
+ return require_botql();
1921
+ })();