botql 1.0.2 → 1.2.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.
- package/Database.js +1 -146
- package/LICENSE +30 -10
- package/Parser.js +2 -759
- package/RAG.js +1 -572
- package/README.md +183 -12
- package/Terminal.js +96 -0
- package/botql.browser.js +686 -1550
- package/botql.js +4 -786
- package/createBot.js +88 -0
- package/package.json +3 -2
- package/Bootstrap.js +0 -74
package/botql.browser.js
CHANGED
|
@@ -19,648 +19,357 @@ var BotQL = (() => {
|
|
|
19
19
|
var require_Parser = __commonJS({
|
|
20
20
|
"Parser.js"(exports, module) {
|
|
21
21
|
"use strict";
|
|
22
|
-
var TokenType = {
|
|
23
|
-
|
|
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
|
-
]);
|
|
22
|
+
var TokenType = { KEYWORD: "KEYWORD", STRING: "STRING", NUMBER: "NUMBER", IDENT: "IDENT", SYMBOL: "SYMBOL", EOF: "EOF" };
|
|
23
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["CREATE", "BOT", "PLATFORM", "CONNECT", "TABLE", "PREVENT", "DEFAULT", "ON", "START", "MESSAGE", "FROM", "WHEN", "CONTAINS", "OR", "OTHERWISE", "KEYWORDS", "THINK", "WAITING", "REPLY", "TO", "FORWARD", "PARSE", "SEND", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "WHERE", "RUN", "IMPORT", "AS", "RESPONSE"]);
|
|
65
24
|
var Token = class {
|
|
66
|
-
constructor(
|
|
67
|
-
this.type =
|
|
68
|
-
|
|
69
|
-
|
|
25
|
+
constructor(e, t, s, r, i) {
|
|
26
|
+
this.type = e, this.value = t, this.line = s, this.start = r, this.end = i;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var BotQLSyntaxError = class extends Error {
|
|
30
|
+
constructor(e, { line: t = null, start: s = 0, end: r = null, near: i = null } = {}) {
|
|
31
|
+
super(e), this.name = "BotQLSyntaxError", this.line = t, this.start = s, this.end = null === r ? s + 1 : r, this.near = i;
|
|
70
32
|
}
|
|
71
33
|
};
|
|
72
34
|
var Tokenizer = class {
|
|
73
|
-
constructor(
|
|
74
|
-
this.source =
|
|
75
|
-
this.pos = 0;
|
|
76
|
-
this.line = 1;
|
|
77
|
-
this.tokens = [];
|
|
35
|
+
constructor(e) {
|
|
36
|
+
this.source = e, this.pos = 0, this.line = 1, this.tokens = [];
|
|
78
37
|
}
|
|
79
|
-
error(
|
|
80
|
-
|
|
38
|
+
error(e, t, s) {
|
|
39
|
+
let r = void 0 !== t ? t : this.pos;
|
|
40
|
+
throw new BotQLSyntaxError(`syntax error: ${e}`, { line: this.line, start: r, end: s });
|
|
81
41
|
}
|
|
82
|
-
peekChar(
|
|
83
|
-
return this.source[this.pos +
|
|
42
|
+
peekChar(e = 0) {
|
|
43
|
+
return this.source[this.pos + e];
|
|
84
44
|
}
|
|
85
45
|
tokenize() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
this.line++;
|
|
90
|
-
this.pos++;
|
|
46
|
+
for (; this.pos < this.source.length; ) {
|
|
47
|
+
let e = this.peekChar(), t = this.pos;
|
|
48
|
+
if ("\n" === e) {
|
|
49
|
+
this.line++, this.pos++;
|
|
91
50
|
continue;
|
|
92
51
|
}
|
|
93
|
-
if (/\s/.test(
|
|
52
|
+
if (/\s/.test(e)) {
|
|
94
53
|
this.pos++;
|
|
95
54
|
continue;
|
|
96
55
|
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
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++;
|
|
56
|
+
if ("-" === e && "-" === this.peekChar(1)) {
|
|
57
|
+
for (; this.pos < this.source.length && "\n" !== this.peekChar(); ) this.pos++;
|
|
112
58
|
continue;
|
|
113
59
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.tokens.push(word);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
this.error(`unexpected character: "${c}"`);
|
|
60
|
+
let s;
|
|
61
|
+
'"' === e || "'" === e ? s = this.readString(e) : /[0-9]/.test(e) ? s = this.readNumber() : "{}(),.=+*;".includes(e) ? (s = new Token(TokenType.SYMBOL, e, this.line), this.pos++) : /[A-Za-z_]/.test(e) ? s = this.readWord() : this.error(`unexpected character: "${e}"`, t, t + 1), s.start = t, s.end = this.pos, this.tokens.push(s);
|
|
120
62
|
}
|
|
121
|
-
this.
|
|
122
|
-
return this.tokens;
|
|
63
|
+
let r = this.source.length, i = new Token(TokenType.EOF, null, this.line, r, r);
|
|
64
|
+
return this.tokens.push(i), this.tokens;
|
|
123
65
|
}
|
|
124
|
-
readString(
|
|
125
|
-
|
|
66
|
+
readString(e) {
|
|
67
|
+
let t = this.line, s = this.pos;
|
|
126
68
|
this.pos++;
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
if (this.peekChar()
|
|
130
|
-
this.pos++;
|
|
131
|
-
value += this.peekChar();
|
|
132
|
-
this.pos++;
|
|
69
|
+
let r = "";
|
|
70
|
+
for (; this.pos < this.source.length && this.peekChar() !== e; ) {
|
|
71
|
+
if ("\\" === this.peekChar()) {
|
|
72
|
+
this.pos++, r += this.peekChar(), this.pos++;
|
|
133
73
|
continue;
|
|
134
74
|
}
|
|
135
|
-
|
|
136
|
-
value += this.peekChar();
|
|
137
|
-
this.pos++;
|
|
138
|
-
}
|
|
139
|
-
if (this.peekChar() !== quote) {
|
|
140
|
-
this.error("unterminated string");
|
|
75
|
+
"\n" === this.peekChar() && this.line++, r += this.peekChar(), this.pos++;
|
|
141
76
|
}
|
|
142
|
-
this.pos
|
|
143
|
-
return new Token(TokenType.STRING, value, startLine);
|
|
77
|
+
return this.peekChar() !== e && this.error("unterminated string", s, this.pos), this.pos++, new Token(TokenType.STRING, r, t);
|
|
144
78
|
}
|
|
145
79
|
readNumber() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
value += this.peekChar();
|
|
150
|
-
this.pos++;
|
|
151
|
-
}
|
|
152
|
-
return new Token(TokenType.NUMBER, Number(value), startLine);
|
|
80
|
+
let e = this.line, t = "";
|
|
81
|
+
for (; this.pos < this.source.length && /[0-9.]/.test(this.peekChar()); ) t += this.peekChar(), this.pos++;
|
|
82
|
+
return new Token(TokenType.NUMBER, Number(t), e);
|
|
153
83
|
}
|
|
154
84
|
readWord() {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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);
|
|
85
|
+
let e = this.line, t = "";
|
|
86
|
+
for (; this.pos < this.source.length && /[A-Za-z0-9_]/.test(this.peekChar()); ) t += this.peekChar(), this.pos++;
|
|
87
|
+
let s = t.toUpperCase();
|
|
88
|
+
return KEYWORDS.has(s) && t === s ? new Token(TokenType.KEYWORD, s, e) : new Token(TokenType.IDENT, t, e);
|
|
166
89
|
}
|
|
167
90
|
};
|
|
168
91
|
var Parser = class {
|
|
169
|
-
constructor(
|
|
170
|
-
this.tokens = new Tokenizer(
|
|
171
|
-
this.pos = 0;
|
|
92
|
+
constructor(e) {
|
|
93
|
+
this.source = e, this.tokens = new Tokenizer(e).tokenize(), this.pos = 0;
|
|
172
94
|
}
|
|
173
|
-
error(
|
|
174
|
-
|
|
175
|
-
throw new
|
|
95
|
+
error(e) {
|
|
96
|
+
let t = this.current(), s = t && t.type !== TokenType.EOF ? t.value : "EOF", r = t ? t.start : this.source.length, i = t ? t.end : this.source.length;
|
|
97
|
+
throw new BotQLSyntaxError(`syntax error: ${e}, near "${s}"`, { line: t ? t.line : null, start: r, end: i, near: s });
|
|
176
98
|
}
|
|
177
99
|
current() {
|
|
178
100
|
return this.tokens[this.pos];
|
|
179
101
|
}
|
|
180
|
-
at(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (value !== void 0 && tok.value !== value) return false;
|
|
184
|
-
return true;
|
|
102
|
+
at(e, t) {
|
|
103
|
+
let s = this.current();
|
|
104
|
+
return s.type === e && (void 0 === t || s.value === t);
|
|
185
105
|
}
|
|
186
|
-
atKeyword(...
|
|
187
|
-
return this.at(TokenType.KEYWORD) &&
|
|
106
|
+
atKeyword(...e) {
|
|
107
|
+
return this.at(TokenType.KEYWORD) && e.includes(this.current().value);
|
|
188
108
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
atWord(...values) {
|
|
193
|
-
const tok = this.current();
|
|
194
|
-
return (tok.type === TokenType.KEYWORD || tok.type === TokenType.IDENT) && values.includes(tok.value);
|
|
109
|
+
atWord(...e) {
|
|
110
|
+
let t = this.current();
|
|
111
|
+
return (t.type === TokenType.KEYWORD || t.type === TokenType.IDENT) && e.includes(t.value);
|
|
195
112
|
}
|
|
196
113
|
advance() {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return tok;
|
|
114
|
+
let e = this.current();
|
|
115
|
+
return e.type !== TokenType.EOF && this.pos++, e;
|
|
200
116
|
}
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
this.error(`expected ${value || type}`);
|
|
204
|
-
}
|
|
205
|
-
return this.advance();
|
|
117
|
+
expect(e, t) {
|
|
118
|
+
return this.at(e, t) || this.error(`expected ${t || e}`), this.advance();
|
|
206
119
|
}
|
|
207
|
-
expectKeyword(
|
|
208
|
-
return this.expect(TokenType.KEYWORD,
|
|
120
|
+
expectKeyword(e) {
|
|
121
|
+
return this.expect(TokenType.KEYWORD, e);
|
|
209
122
|
}
|
|
210
123
|
isAtEnd() {
|
|
211
124
|
return this.at(TokenType.EOF);
|
|
212
125
|
}
|
|
213
|
-
// ---- Programa ----
|
|
214
126
|
parseProgram() {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
return { type: "Program", body: statements };
|
|
127
|
+
let e = [];
|
|
128
|
+
for (; !this.isAtEnd(); ) e.push(this.parseStatement());
|
|
129
|
+
return { type: "Program", body: e };
|
|
220
130
|
}
|
|
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
131
|
parseBody() {
|
|
225
132
|
if (this.at(TokenType.SYMBOL, "{")) {
|
|
226
133
|
this.advance();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
statements.push(this.parseStatement());
|
|
231
|
-
}
|
|
232
|
-
this.advance();
|
|
233
|
-
return statements;
|
|
134
|
+
let e = [];
|
|
135
|
+
for (; !this.at(TokenType.SYMBOL, "}"); ) this.isAtEnd() && this.error('unclosed block "{"'), e.push(this.parseStatement());
|
|
136
|
+
return this.advance(), e;
|
|
234
137
|
}
|
|
235
138
|
return [this.parseStatement()];
|
|
236
139
|
}
|
|
237
|
-
// ---- Dispatch de statement ----
|
|
238
140
|
parseStatement() {
|
|
239
|
-
|
|
240
|
-
|
|
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 ----
|
|
141
|
+
return this.atKeyword("CREATE") ? this.parseCreate() : this.atKeyword("PLATFORM") ? this.parsePlatform() : this.atKeyword("CONNECT") ? this.parseConnect() : this.atKeyword("ON") ? this.parseOn() : this.atKeyword("WHEN") ? this.parseWhen() : this.atKeyword("OTHERWISE") ? this.parseOtherwise() : this.atKeyword("REPLY") ? this.parseReply() : this.atKeyword("THINK") ? this.parseThink() : this.atKeyword("WAITING") ? this.parseWaitingStatement() : this.atKeyword("FORWARD") ? this.parseForward() : this.atKeyword("PARSE") ? this.parseParseSignal() : this.atKeyword("SEND") ? this.parseSend() : this.atKeyword("INSERT") ? this.parseInsert() : this.atKeyword("UPDATE") ? this.parseUpdate() : this.atKeyword("RUN") ? this.parseRunBot() : this.atKeyword("IMPORT") ? this.parseImport() : void this.error("unexpected statement");
|
|
142
|
+
}
|
|
257
143
|
parseCreate() {
|
|
258
|
-
this.expectKeyword("CREATE")
|
|
259
|
-
if (this.atKeyword("BOT")) {
|
|
144
|
+
if (this.expectKeyword("CREATE"), this.atKeyword("BOT")) {
|
|
260
145
|
this.advance();
|
|
261
|
-
|
|
262
|
-
return { type: "CreateBot", name };
|
|
146
|
+
let e = this.expect(TokenType.STRING).value;
|
|
147
|
+
return { type: "CreateBot", name: e };
|
|
263
148
|
}
|
|
264
149
|
if (this.atKeyword("TABLE")) {
|
|
265
150
|
this.advance();
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const preventDefault = this.tryParsePreventDefault();
|
|
269
|
-
const defaultMessage = this.tryParseDefaultMessage();
|
|
270
|
-
return { type: "CreateTable", name, columns, preventDefault, defaultMessage };
|
|
151
|
+
let t = this.expect(TokenType.IDENT).value, s = this.parseColumnList(), r = this.tryParsePreventDefault(), i = this.tryParseDefaultMessage();
|
|
152
|
+
return { type: "CreateTable", name: t, columns: s, preventDefault: r, defaultMessage: i };
|
|
271
153
|
}
|
|
272
154
|
this.error("expected BOT or TABLE after CREATE");
|
|
273
155
|
}
|
|
274
156
|
parseColumnList() {
|
|
275
157
|
this.expect(TokenType.SYMBOL, "(");
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const word = this.advance().value;
|
|
283
|
-
if (!colType) colType = word;
|
|
284
|
-
else constraints.push(word);
|
|
158
|
+
let e = [];
|
|
159
|
+
for (; !this.at(TokenType.SYMBOL, ")"); ) {
|
|
160
|
+
let t = this.expect(TokenType.IDENT).value, s = [], r = null;
|
|
161
|
+
for (; this.at(TokenType.IDENT) && !this.at(TokenType.SYMBOL, ",") && !this.at(TokenType.SYMBOL, ")"); ) {
|
|
162
|
+
let i = this.advance().value;
|
|
163
|
+
r ? s.push(i) : r = i;
|
|
285
164
|
}
|
|
286
|
-
|
|
287
|
-
if (this.at(TokenType.SYMBOL, ",")) this.advance();
|
|
165
|
+
e.push({ name: t, columnType: r, constraints: s }), this.at(TokenType.SYMBOL, ",") && this.advance();
|
|
288
166
|
}
|
|
289
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
290
|
-
return columns;
|
|
167
|
+
return this.expect(TokenType.SYMBOL, ")"), e;
|
|
291
168
|
}
|
|
292
169
|
tryParsePreventDefault() {
|
|
293
|
-
|
|
294
|
-
this.advance();
|
|
295
|
-
this.expectKeyword("DEFAULT");
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
return false;
|
|
170
|
+
return !!this.atKeyword("PREVENT") && (this.advance(), this.expectKeyword("DEFAULT"), true);
|
|
299
171
|
}
|
|
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
172
|
tryParseDefaultMessage() {
|
|
312
173
|
if (!this.atKeyword("DEFAULT")) return null;
|
|
313
|
-
|
|
314
|
-
if (!
|
|
315
|
-
this.advance()
|
|
316
|
-
this.advance();
|
|
317
|
-
if (this.isReplyFileForm()) {
|
|
174
|
+
let e = this.tokens[this.pos + 1];
|
|
175
|
+
if (!e || e.type !== TokenType.KEYWORD || "MESSAGE" !== e.value) return null;
|
|
176
|
+
if (this.advance(), this.advance(), this.isReplyFileForm()) {
|
|
318
177
|
this.expect(TokenType.SYMBOL, "(");
|
|
319
|
-
|
|
178
|
+
let t = this.parseFileName();
|
|
320
179
|
this.expect(TokenType.SYMBOL, ",");
|
|
321
|
-
|
|
322
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
323
|
-
return { file, index };
|
|
180
|
+
let s = this.parseExpression();
|
|
181
|
+
return this.expect(TokenType.SYMBOL, ")"), { file: t, index: s };
|
|
324
182
|
}
|
|
325
|
-
|
|
326
|
-
return { value };
|
|
183
|
+
let r = this.parseExpression();
|
|
184
|
+
return { value: r };
|
|
327
185
|
}
|
|
328
|
-
// ---- PLATFORM ----
|
|
329
186
|
parsePlatform() {
|
|
330
187
|
this.expectKeyword("PLATFORM");
|
|
331
|
-
|
|
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.
|
|
188
|
+
let e = this.advance().value;
|
|
189
|
+
return { type: "Platform", value: e };
|
|
190
|
+
}
|
|
343
191
|
parseConnect() {
|
|
344
|
-
this.expectKeyword("CONNECT")
|
|
345
|
-
if (this.atKeyword("RESPONSE")) {
|
|
192
|
+
if (this.expectKeyword("CONNECT"), this.atKeyword("RESPONSE")) {
|
|
346
193
|
this.advance();
|
|
347
|
-
|
|
348
|
-
return { type: "ConnectResponse", alias };
|
|
194
|
+
let e = this.expect(TokenType.IDENT).value;
|
|
195
|
+
return { type: "ConnectResponse", alias: e };
|
|
349
196
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const credential = this.expect(TokenType.STRING).value;
|
|
353
|
-
return { type: "Connect", service, kind, credential };
|
|
197
|
+
let t = this.advance().value, s = this.advance().value, r = this.expect(TokenType.STRING).value;
|
|
198
|
+
return { type: "Connect", service: t, kind: s, credential: r };
|
|
354
199
|
}
|
|
355
|
-
// ---- ON START | ON MESSAGE | ON SIGNAL FROM "..." ----
|
|
356
200
|
parseOn() {
|
|
357
|
-
this.expectKeyword("ON")
|
|
358
|
-
if (this.atKeyword("START")) {
|
|
201
|
+
if (this.expectKeyword("ON"), this.atKeyword("START")) {
|
|
359
202
|
this.advance();
|
|
360
|
-
|
|
361
|
-
return { type: "On", event: "START", body };
|
|
203
|
+
let e = this.parseBody();
|
|
204
|
+
return { type: "On", event: "START", body: e };
|
|
362
205
|
}
|
|
363
206
|
if (this.atKeyword("MESSAGE")) {
|
|
364
207
|
this.advance();
|
|
365
|
-
|
|
366
|
-
return { type: "On", event: "MESSAGE", body };
|
|
208
|
+
let t = this.parseBody();
|
|
209
|
+
return { type: "On", event: "MESSAGE", body: t };
|
|
367
210
|
}
|
|
368
211
|
if (this.atWord("SIGNAL")) {
|
|
369
|
-
this.advance();
|
|
370
|
-
this.
|
|
371
|
-
|
|
372
|
-
const body = this.parseBody();
|
|
373
|
-
return { type: "On", event: "SIGNAL", source, body };
|
|
212
|
+
this.advance(), this.expectKeyword("FROM");
|
|
213
|
+
let s = this.expect(TokenType.STRING).value, r = this.parseBody();
|
|
214
|
+
return { type: "On", event: "SIGNAL", source: s, body: r };
|
|
374
215
|
}
|
|
375
216
|
this.error("unexpected event after ON");
|
|
376
217
|
}
|
|
377
|
-
// ---- WHEN CONTAINS "..." [OR CONTAINS "..."]* ----
|
|
378
218
|
parseWhen() {
|
|
379
219
|
this.expectKeyword("WHEN");
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
const body = this.parseBody();
|
|
386
|
-
return { type: "When", conditions, body };
|
|
220
|
+
let e = [this.parseCondition()];
|
|
221
|
+
for (; this.atKeyword("OR"); ) this.advance(), e.push(this.parseCondition());
|
|
222
|
+
let t = this.parseBody();
|
|
223
|
+
return { type: "When", conditions: e, body: t };
|
|
387
224
|
}
|
|
388
225
|
parseCondition() {
|
|
389
|
-
this.expectKeyword("CONTAINS")
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return { op: "CONTAINS_ANY", values };
|
|
226
|
+
if (this.expectKeyword("CONTAINS"), this.at(TokenType.SYMBOL, "(")) {
|
|
227
|
+
let e = this.parseStringList();
|
|
228
|
+
return { op: "CONTAINS_ANY", values: e };
|
|
393
229
|
}
|
|
394
230
|
if (this.atKeyword("KEYWORDS")) {
|
|
395
|
-
this.advance();
|
|
396
|
-
this.
|
|
397
|
-
|
|
398
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
399
|
-
return { op: "CONTAINS_KEYWORDS_FILE", path };
|
|
231
|
+
this.advance(), this.expect(TokenType.SYMBOL, "(");
|
|
232
|
+
let t = this.parseFileName();
|
|
233
|
+
return this.expect(TokenType.SYMBOL, ")"), { op: "CONTAINS_KEYWORDS_FILE", path: t };
|
|
400
234
|
}
|
|
401
|
-
|
|
402
|
-
return { op: "CONTAINS", value };
|
|
235
|
+
let s = this.expect(TokenType.STRING).value;
|
|
236
|
+
return { op: "CONTAINS", value: s };
|
|
403
237
|
}
|
|
404
|
-
// Lista de strings entre parênteses: ("a", "b", "c")
|
|
405
238
|
parseStringList() {
|
|
406
239
|
this.expect(TokenType.SYMBOL, "(");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (this.at(TokenType.SYMBOL, ",")) this.advance();
|
|
411
|
-
}
|
|
412
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
413
|
-
return values;
|
|
240
|
+
let e = [];
|
|
241
|
+
for (; !this.at(TokenType.SYMBOL, ")"); ) e.push(this.expect(TokenType.STRING).value), this.at(TokenType.SYMBOL, ",") && this.advance();
|
|
242
|
+
return this.expect(TokenType.SYMBOL, ")"), e;
|
|
414
243
|
}
|
|
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
244
|
parseFileName() {
|
|
419
|
-
let
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
name += "." + this.expect(TokenType.IDENT).value;
|
|
423
|
-
}
|
|
424
|
-
return name;
|
|
245
|
+
let e = this.expect(TokenType.IDENT).value;
|
|
246
|
+
for (; this.at(TokenType.SYMBOL, "."); ) this.advance(), e += "." + this.expect(TokenType.IDENT).value;
|
|
247
|
+
return e;
|
|
425
248
|
}
|
|
426
|
-
// ---- OTHERWISE ----
|
|
427
249
|
parseOtherwise() {
|
|
428
250
|
this.expectKeyword("OTHERWISE");
|
|
429
|
-
|
|
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.
|
|
251
|
+
let e = this.parseBody();
|
|
252
|
+
return { type: "Otherwise", body: e };
|
|
253
|
+
}
|
|
455
254
|
parseThink() {
|
|
456
|
-
this.expectKeyword("THINK");
|
|
457
|
-
this.
|
|
458
|
-
const file = this.parseFileName();
|
|
255
|
+
this.expectKeyword("THINK"), this.expect(TokenType.SYMBOL, "(");
|
|
256
|
+
let e = this.parseFileName();
|
|
459
257
|
this.expect(TokenType.SYMBOL, ")");
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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.
|
|
258
|
+
let t = this.tryParseWaiting(), s = null;
|
|
259
|
+
return this.atKeyword("OR") && (this.advance(), s = this.parseReply()), { type: "Think", file: e, waiting: t, fallback: s };
|
|
260
|
+
}
|
|
474
261
|
tryParseWaiting() {
|
|
475
262
|
if (!this.atKeyword("WAITING")) return null;
|
|
476
|
-
this.advance();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
this.advance();
|
|
484
|
-
seconds = this.parseExpression();
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
488
|
-
return { file, seconds };
|
|
263
|
+
this.advance(), this.expect(TokenType.SYMBOL, "(");
|
|
264
|
+
let e = null, t = null, s = null;
|
|
265
|
+
return !this.at(TokenType.SYMBOL, ")") && (this.at(TokenType.STRING) ? t = this.advance().value : e = this.parseFileName(), this.at(TokenType.SYMBOL, ",") && (this.advance(), s = this.parseExpression())), this.expect(TokenType.SYMBOL, ")"), { file: e, text: t, seconds: s };
|
|
266
|
+
}
|
|
267
|
+
parseWaitingStatement() {
|
|
268
|
+
let e = this.tryParseWaiting();
|
|
269
|
+
return { type: "Waiting", file: e.file, text: e.text, seconds: e.seconds };
|
|
489
270
|
}
|
|
490
271
|
parseReply() {
|
|
491
272
|
this.expectKeyword("REPLY");
|
|
492
|
-
let
|
|
493
|
-
if (this.atKeyword("TO")) {
|
|
494
|
-
this.advance();
|
|
495
|
-
target = this.advance().value;
|
|
496
|
-
}
|
|
497
|
-
if (this.isReplyFileForm()) {
|
|
273
|
+
let e = null;
|
|
274
|
+
if (this.atKeyword("TO") && (this.advance(), e = this.advance().value), this.isReplyFileForm()) {
|
|
498
275
|
this.expect(TokenType.SYMBOL, "(");
|
|
499
|
-
|
|
276
|
+
let t = this.parseFileName();
|
|
500
277
|
this.expect(TokenType.SYMBOL, ",");
|
|
501
|
-
|
|
502
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
503
|
-
return { type: "Reply", target, file, index };
|
|
278
|
+
let s = this.parseExpression();
|
|
279
|
+
return this.expect(TokenType.SYMBOL, ")"), { type: "Reply", target: e, file: t, index: s };
|
|
504
280
|
}
|
|
505
|
-
|
|
506
|
-
return { type: "Reply", target, value };
|
|
281
|
+
let r = this.parseExpression();
|
|
282
|
+
return { type: "Reply", target: e, value: r };
|
|
507
283
|
}
|
|
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
284
|
isReplyFileForm() {
|
|
513
285
|
if (!this.at(TokenType.SYMBOL, "(")) return false;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return !!next && next.type === TokenType.IDENT && !!afterNext && afterNext.type === TokenType.SYMBOL && afterNext.value === ".";
|
|
286
|
+
let e = this.tokens[this.pos + 1], t = this.tokens[this.pos + 2];
|
|
287
|
+
return !!e && e.type === TokenType.IDENT && !!t && t.type === TokenType.SYMBOL && "." === t.value;
|
|
517
288
|
}
|
|
518
|
-
// ---- FORWARD TO "contacto" ----
|
|
519
289
|
parseForward() {
|
|
520
|
-
this.expectKeyword("FORWARD");
|
|
521
|
-
this.
|
|
522
|
-
|
|
523
|
-
return { type: "ForwardTo", target };
|
|
290
|
+
this.expectKeyword("FORWARD"), this.expectKeyword("TO");
|
|
291
|
+
let e = this.parseExpression();
|
|
292
|
+
return { type: "ForwardTo", target: e };
|
|
524
293
|
}
|
|
525
|
-
// ---- PARSE SIGNAL ----
|
|
526
294
|
parseParseSignal() {
|
|
527
|
-
this.expectKeyword("PARSE");
|
|
528
|
-
if (!this.atWord("SIGNAL")) this.error("expected SIGNAL after PARSE");
|
|
529
|
-
this.advance();
|
|
530
|
-
return { type: "ParseSignal" };
|
|
295
|
+
return this.expectKeyword("PARSE"), this.atWord("SIGNAL") || this.error("expected SIGNAL after PARSE"), this.advance(), { type: "ParseSignal" };
|
|
531
296
|
}
|
|
532
|
-
// ---- SEND TO destino ----
|
|
533
297
|
parseSend() {
|
|
534
|
-
this.expectKeyword("SEND");
|
|
535
|
-
this.
|
|
536
|
-
|
|
537
|
-
return { type: "SendTo", target };
|
|
298
|
+
this.expectKeyword("SEND"), this.expectKeyword("TO");
|
|
299
|
+
let e = this.advance().value;
|
|
300
|
+
return { type: "SendTo", target: e };
|
|
538
301
|
}
|
|
539
|
-
// ---- INSERT INTO tabela [(cols)] [VALUES (vals)] ----
|
|
540
302
|
parseInsert() {
|
|
541
|
-
this.expectKeyword("INSERT");
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
let
|
|
545
|
-
|
|
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 };
|
|
303
|
+
this.expectKeyword("INSERT"), this.expectKeyword("INTO");
|
|
304
|
+
let e = this.expect(TokenType.IDENT).value, t = null;
|
|
305
|
+
this.at(TokenType.SYMBOL, "(") && (t = this.parseExpressionList());
|
|
306
|
+
let s = null;
|
|
307
|
+
return this.atKeyword("VALUES") && (this.advance(), s = this.parseExpressionList()), { type: "Insert", table: e, columns: t, values: s };
|
|
554
308
|
}
|
|
555
309
|
parseExpressionList() {
|
|
556
310
|
this.expect(TokenType.SYMBOL, "(");
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (this.at(TokenType.SYMBOL, ",")) this.advance();
|
|
561
|
-
}
|
|
562
|
-
this.expect(TokenType.SYMBOL, ")");
|
|
563
|
-
return items;
|
|
311
|
+
let e = [];
|
|
312
|
+
for (; !this.at(TokenType.SYMBOL, ")"); ) e.push(this.parseExpression()), this.at(TokenType.SYMBOL, ",") && this.advance();
|
|
313
|
+
return this.expect(TokenType.SYMBOL, ")"), e;
|
|
564
314
|
}
|
|
565
|
-
// ---- UPDATE tabela SET col = expr WHERE expr ----
|
|
566
315
|
parseUpdate() {
|
|
567
316
|
this.expectKeyword("UPDATE");
|
|
568
|
-
|
|
317
|
+
let e = this.expect(TokenType.IDENT).value;
|
|
569
318
|
this.expectKeyword("SET");
|
|
570
|
-
|
|
319
|
+
let t = this.expect(TokenType.IDENT).value;
|
|
571
320
|
this.expect(TokenType.SYMBOL, "=");
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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.
|
|
321
|
+
let s = this.parseExpression(), r = null;
|
|
322
|
+
return this.atKeyword("WHERE") && (this.advance(), r = this.parseComparison()), { type: "Update", table: e, set: { column: t, value: s }, where: r };
|
|
323
|
+
}
|
|
592
324
|
parseImport() {
|
|
593
|
-
this.expectKeyword("IMPORT");
|
|
594
|
-
this.
|
|
595
|
-
|
|
596
|
-
let
|
|
597
|
-
|
|
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 };
|
|
325
|
+
this.expectKeyword("IMPORT"), this.expect(TokenType.SYMBOL, "{");
|
|
326
|
+
let e = this.parseFileName(), t = null;
|
|
327
|
+
this.at(TokenType.SYMBOL, ",") && (this.advance(), t = this.parseExpression()), this.expect(TokenType.SYMBOL, "}");
|
|
328
|
+
let s = null;
|
|
329
|
+
return this.atKeyword("AS") && (this.advance(), s = this.expect(TokenType.IDENT).value), { type: "Import", path: e, index: t, alias: s };
|
|
608
330
|
}
|
|
609
|
-
// ---- RUN BOT ----
|
|
610
331
|
parseRunBot() {
|
|
611
|
-
this.expectKeyword("RUN");
|
|
612
|
-
this.expectKeyword("BOT");
|
|
613
|
-
return { type: "RunBot" };
|
|
332
|
+
return this.expectKeyword("RUN"), this.expectKeyword("BOT"), { type: "RunBot" };
|
|
614
333
|
}
|
|
615
|
-
// ---- Expressões: literais, identificadores, chamadas, acesso a
|
|
616
|
-
// propriedade (obj.prop) e concatenação com "+" ----
|
|
617
334
|
parseExpression() {
|
|
618
|
-
let
|
|
619
|
-
|
|
335
|
+
let e = this.parsePrimary();
|
|
336
|
+
for (; this.at(TokenType.SYMBOL, "+"); ) {
|
|
620
337
|
this.advance();
|
|
621
|
-
|
|
622
|
-
|
|
338
|
+
let t = this.parsePrimary();
|
|
339
|
+
e = { type: "Binary", op: "+", left: e, right: t };
|
|
623
340
|
}
|
|
624
|
-
return
|
|
341
|
+
return e;
|
|
625
342
|
}
|
|
626
|
-
// Igualdade, usada em WHERE (ex: id = LAST_INSERT_ID())
|
|
627
343
|
parseComparison() {
|
|
628
|
-
|
|
344
|
+
let e = this.parseExpression();
|
|
629
345
|
if (this.at(TokenType.SYMBOL, "=")) {
|
|
630
346
|
this.advance();
|
|
631
|
-
|
|
632
|
-
return { type: "Binary", op: "=", left, right };
|
|
347
|
+
let t = this.parseExpression();
|
|
348
|
+
return { type: "Binary", op: "=", left: e, right: t };
|
|
633
349
|
}
|
|
634
|
-
return
|
|
350
|
+
return e;
|
|
635
351
|
}
|
|
636
352
|
parsePrimary() {
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
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) {
|
|
353
|
+
let e = this.current();
|
|
354
|
+
if (e.type === TokenType.STRING || e.type === TokenType.NUMBER) return this.advance(), { type: "Literal", value: e.value };
|
|
355
|
+
if (e.type === TokenType.IDENT) {
|
|
647
356
|
this.advance();
|
|
648
|
-
let
|
|
357
|
+
let t = { type: "Identifier", name: e.value };
|
|
649
358
|
if (this.at(TokenType.SYMBOL, "(")) {
|
|
650
|
-
|
|
651
|
-
|
|
359
|
+
let s = this.parseExpressionList();
|
|
360
|
+
t = { type: "Call", callee: t.name, args: s };
|
|
652
361
|
}
|
|
653
|
-
|
|
362
|
+
for (; this.at(TokenType.SYMBOL, "."); ) {
|
|
654
363
|
this.advance();
|
|
655
|
-
|
|
656
|
-
|
|
364
|
+
let r = this.expect(TokenType.IDENT).value;
|
|
365
|
+
t = { type: "Member", object: t, property: r };
|
|
657
366
|
}
|
|
658
|
-
return
|
|
367
|
+
return t;
|
|
659
368
|
}
|
|
660
369
|
this.error("invalid expression");
|
|
661
370
|
}
|
|
662
371
|
};
|
|
663
|
-
module.exports = { Parser, Tokenizer, TokenType, KEYWORDS };
|
|
372
|
+
module.exports = { Parser, Tokenizer, TokenType, KEYWORDS, BotQLSyntaxError };
|
|
664
373
|
}
|
|
665
374
|
});
|
|
666
375
|
|
|
@@ -668,97 +377,72 @@ var BotQL = (() => {
|
|
|
668
377
|
var require_Database = __commonJS({
|
|
669
378
|
"Database.js"(exports, module) {
|
|
670
379
|
"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
|
-
];
|
|
380
|
+
var CONTEXT_SCHEMA = [{ name: "id", columnType: "INT", constraints: ["PRIMARY", "KEY", "AUTO_INCREMENT"] }, { name: "client", columnType: "TEXT", constraints: [] }, { name: "message", columnType: "TEXT", constraints: [] }, { name: "reply", columnType: "TEXT", constraints: [] }, { name: "created_at", columnType: "DATETIME", constraints: [] }];
|
|
678
381
|
var MemoryDatabase = class {
|
|
679
382
|
constructor() {
|
|
680
|
-
this.tables = /* @__PURE__ */ new Map();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (row) row[column] = value;
|
|
709
|
-
return row || null;
|
|
710
|
-
}
|
|
711
|
-
getRows(table) {
|
|
712
|
-
const t = this.tables.get(table);
|
|
383
|
+
this.tables = /* @__PURE__ */ new Map(), this.autoIncrement = /* @__PURE__ */ new Map();
|
|
384
|
+
}
|
|
385
|
+
createTable(e, t, r) {
|
|
386
|
+
let s = "Context" === e && 0 === t.length ? CONTEXT_SCHEMA : t;
|
|
387
|
+
if (this.tables.has(e)) {
|
|
388
|
+
if (r) return;
|
|
389
|
+
throw Error(`runtime error: table "${e}" already exists`);
|
|
390
|
+
}
|
|
391
|
+
this.tables.set(e, { columns: s, rows: [] }), this.autoIncrement.set(e, 0);
|
|
392
|
+
}
|
|
393
|
+
insert(e, t, r) {
|
|
394
|
+
let s = this.tables.get(e);
|
|
395
|
+
if (!s) throw Error(`runtime error: table "${e}" does not exist`);
|
|
396
|
+
let n = this.autoIncrement.get(e) + 1;
|
|
397
|
+
this.autoIncrement.set(e, n);
|
|
398
|
+
let i = { id: n };
|
|
399
|
+
return t.forEach((e2, t2) => {
|
|
400
|
+
i[e2] = r[t2];
|
|
401
|
+
}), s.rows.push(i), n;
|
|
402
|
+
}
|
|
403
|
+
update(e, t, r, s, n) {
|
|
404
|
+
let i = this.tables.get(e);
|
|
405
|
+
if (!i) throw Error(`runtime error: table "${e}" does not exist`);
|
|
406
|
+
let a = s ? i.rows.find((e2) => e2[s] === n) : i.rows[i.rows.length - 1];
|
|
407
|
+
return a && (a[t] = r), a || null;
|
|
408
|
+
}
|
|
409
|
+
getRows(e) {
|
|
410
|
+
let t = this.tables.get(e);
|
|
713
411
|
return t ? t.rows.slice() : [];
|
|
714
412
|
}
|
|
715
413
|
};
|
|
716
|
-
function mapColumnType(
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (type === "DATETIME" || type === "DATE") return "TEXT";
|
|
720
|
-
return "TEXT";
|
|
414
|
+
function mapColumnType(e) {
|
|
415
|
+
let t = (e || "").toUpperCase();
|
|
416
|
+
return "INT" === t || "INTEGER" === t ? "INTEGER" : "TEXT";
|
|
721
417
|
}
|
|
722
|
-
function buildColumnDef(
|
|
723
|
-
|
|
724
|
-
|
|
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;
|
|
418
|
+
function buildColumnDef(e) {
|
|
419
|
+
let t = mapColumnType(e.columnType), r = e.constraints.includes("PRIMARY") && e.constraints.includes("KEY"), s = e.constraints.includes("AUTO_INCREMENT"), n = `${e.name} ${t}`;
|
|
420
|
+
return r && (n += " PRIMARY KEY"), r && s && (n += " AUTOINCREMENT"), n;
|
|
730
421
|
}
|
|
731
422
|
var SQLiteDatabase = class {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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);
|
|
423
|
+
constructor(e = ":memory:") {
|
|
424
|
+
let { DatabaseSync: t } = __require("node:sqlite");
|
|
425
|
+
this.driver = new t(e);
|
|
426
|
+
}
|
|
427
|
+
createTable(e, t, r) {
|
|
428
|
+
let s = "Context" === e && 0 === t.length ? CONTEXT_SCHEMA : t, n = s.map(buildColumnDef).join(", ");
|
|
429
|
+
this.driver.exec(`CREATE TABLE ${r ? "IF NOT EXISTS " : ""}${e} (${n})`);
|
|
430
|
+
}
|
|
431
|
+
insert(e, t, r) {
|
|
432
|
+
let s = t.map(() => "?").join(", "), n = `INSERT INTO ${e} (${t.join(", ")}) VALUES (${s})`, i = this.driver.prepare(n).run(...r);
|
|
433
|
+
return Number(i.lastInsertRowid);
|
|
434
|
+
}
|
|
435
|
+
update(e, t, r, s, n) {
|
|
436
|
+
if (s) {
|
|
437
|
+
let i = `UPDATE ${e} SET ${t} = ? WHERE ${s} = ?`;
|
|
438
|
+
this.driver.prepare(i).run(r, n);
|
|
755
439
|
} else {
|
|
756
|
-
|
|
757
|
-
this.driver.prepare(
|
|
440
|
+
let a = `UPDATE ${e} SET ${t} = ? WHERE id = (SELECT MAX(id) FROM ${e})`;
|
|
441
|
+
this.driver.prepare(a).run(r);
|
|
758
442
|
}
|
|
759
443
|
}
|
|
760
|
-
getRows(
|
|
761
|
-
return this.driver.prepare(`SELECT * FROM ${
|
|
444
|
+
getRows(e) {
|
|
445
|
+
return this.driver.prepare(`SELECT * FROM ${e}`).all();
|
|
762
446
|
}
|
|
763
447
|
close() {
|
|
764
448
|
this.driver.close();
|
|
@@ -841,505 +525,221 @@ var BotQL = (() => {
|
|
|
841
525
|
var require_RAG = __commonJS({
|
|
842
526
|
"RAG.js"(exports, module) {
|
|
843
527
|
"use strict";
|
|
844
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
845
|
-
|
|
846
|
-
"
|
|
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, "");
|
|
528
|
+
var STOPWORDS = /* @__PURE__ */ new Set(["a", "o", "as", "os", "de", "da", "do", "das", "dos", "e", "ou", "que", "um", "uma", "uns", "umas", "em", "no", "na", "nos", "nas", "por", "para", "com", "sem", "se", "foi", "ser", "sao", "esta", "estao", "ao", "aos", "mas", "como", "tem", "ter", "nao", "sim", "meu", "minha", "seu", "sua", "eu", "tu", "ele", "ela", "nos", "vos", "eles", "elas", "isso", "isto", "aquilo", "quando", "onde", "porque", "qual", "quais", "muito", "muita", "ja", "so"]);
|
|
529
|
+
function normalizar(e) {
|
|
530
|
+
return e.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
911
531
|
}
|
|
912
532
|
var SUFIXOS_ADJETIVO_ADVERBIO = ["issimamente", "issimo", "issima", "mente"];
|
|
913
533
|
var SUFIXOS_NOMINALIZACAO = ["acoes", "acao", "imentos", "imento", "idades", "idade"];
|
|
914
|
-
function stem(
|
|
915
|
-
let
|
|
916
|
-
for (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
534
|
+
function stem(e) {
|
|
535
|
+
let t = e;
|
|
536
|
+
for (let i of SUFIXOS_ADJETIVO_ADVERBIO) if (t.length > i.length + 3 && t.endsWith(i)) {
|
|
537
|
+
t = t.slice(0, -i.length);
|
|
538
|
+
break;
|
|
921
539
|
}
|
|
922
|
-
for (
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
540
|
+
for (let o of SUFIXOS_NOMINALIZACAO) if (t.length > o.length + 3 && t.endsWith(o)) {
|
|
541
|
+
t = t.slice(0, -o.length);
|
|
542
|
+
break;
|
|
927
543
|
}
|
|
928
|
-
|
|
929
|
-
p = p.slice(0, -1);
|
|
930
|
-
}
|
|
931
|
-
return p;
|
|
544
|
+
return t.length > 4 && t.endsWith("s") && !t.endsWith("ns") && (t = t.slice(0, -1)), t;
|
|
932
545
|
}
|
|
933
|
-
function tokenizar(
|
|
934
|
-
return normalizar(
|
|
546
|
+
function tokenizar(e) {
|
|
547
|
+
return normalizar(e).toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((e2) => e2.length > 1 && !STOPWORDS.has(e2)).map(stem);
|
|
935
548
|
}
|
|
936
|
-
function distanciaEdicao(
|
|
937
|
-
if (
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (
|
|
941
|
-
|
|
942
|
-
let
|
|
943
|
-
for (let
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
linhaAnterior = linhaAtual;
|
|
955
|
-
}
|
|
956
|
-
return linhaAnterior[lb];
|
|
549
|
+
function distanciaEdicao(e, t) {
|
|
550
|
+
if (e === t) return 0;
|
|
551
|
+
let i = e.length, o = t.length;
|
|
552
|
+
if (0 === i) return o;
|
|
553
|
+
if (0 === o) return i;
|
|
554
|
+
let s = Array(o + 1);
|
|
555
|
+
for (let r = 0; r <= o; r++) s[r] = r;
|
|
556
|
+
for (let n = 1; n <= i; n++) {
|
|
557
|
+
let a = [n];
|
|
558
|
+
for (let l = 1; l <= o; l++) {
|
|
559
|
+
let c = e[n - 1] === t[l - 1] ? 0 : 1;
|
|
560
|
+
a[l] = Math.min(a[l - 1] + 1, s[l] + 1, s[l - 1] + c);
|
|
561
|
+
}
|
|
562
|
+
s = a;
|
|
563
|
+
}
|
|
564
|
+
return s[o];
|
|
957
565
|
}
|
|
958
|
-
function distanciaMaximaTolerada(
|
|
959
|
-
|
|
960
|
-
if (tamanho <= 7) return 1;
|
|
961
|
-
return 2;
|
|
566
|
+
function distanciaMaximaTolerada(e) {
|
|
567
|
+
return e <= 4 ? 0 : e <= 7 ? 1 : 2;
|
|
962
568
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
}
|
|
569
|
+
function extrairNomesProprios(e) {
|
|
570
|
+
let t = e.split(/\s+/), i = [];
|
|
571
|
+
for (let o = 1; o < t.length; o++) {
|
|
572
|
+
let s = t[o].replace(/^[(["']+|[.,;:!?)\]"']+$/g, "");
|
|
573
|
+
/^[A-ZÀ-Ý][a-zà-ÿ]+$/.test(s) && i.push(s);
|
|
980
574
|
}
|
|
981
|
-
return
|
|
575
|
+
return i;
|
|
982
576
|
}
|
|
983
577
|
var KnowledgeIndex = class _KnowledgeIndex {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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();
|
|
578
|
+
constructor(e, t = {}) {
|
|
579
|
+
for (let [i, o] of (this.synonyms = {}, Object.entries(t.synonyms || {}))) this.synonyms[stem(normalizar(i.toLowerCase()))] = stem(normalizar(o.toLowerCase()));
|
|
580
|
+
this.blocks = _KnowledgeIndex.parseBlocks(e), this._buildIndex();
|
|
998
581
|
}
|
|
999
|
-
static parseBlocks(
|
|
1000
|
-
return
|
|
582
|
+
static parseBlocks(e) {
|
|
583
|
+
return e.split(/\n\s*\n/).map((e2) => e2.trim()).filter((e2) => e2.length > 0);
|
|
1001
584
|
}
|
|
1002
|
-
_aplicarSinonimos(
|
|
1003
|
-
return
|
|
585
|
+
_aplicarSinonimos(e) {
|
|
586
|
+
return e.map((e2) => this.synonyms[e2] || e2);
|
|
1004
587
|
}
|
|
1005
588
|
_buildIndex() {
|
|
1006
|
-
this.docs = this.blocks.map((
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
let
|
|
1031
|
-
for (
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
589
|
+
for (let e of (this.docs = this.blocks.map((e2) => tokenizar(e2)), this.docTextNormalizado = this.blocks.map((e2) => normalizar(e2).toLowerCase()), this.docPalavrasSignificativas = this.blocks.map((e2) => this._palavrasSignificativas(e2)), this.docFreq = /* @__PURE__ */ new Map(), this.vocabulario = /* @__PURE__ */ new Set(), this.docs)) {
|
|
590
|
+
let t = new Set(e);
|
|
591
|
+
for (let i of t) this.docFreq.set(i, (this.docFreq.get(i) || 0) + 1), this.vocabulario.add(i);
|
|
592
|
+
}
|
|
593
|
+
this.listaVocabulario = Array.from(this.vocabulario), this.totalDocs = this.docs.length, this.avgDocLen = 0 === this.totalDocs ? 0 : this.docs.reduce((e, t) => e + t.length, 0) / this.totalDocs;
|
|
594
|
+
}
|
|
595
|
+
_idf(e) {
|
|
596
|
+
let t = this.docFreq.get(e) || 0;
|
|
597
|
+
return 0 === t ? 0 : Math.log(1 + (this.totalDocs - t + 0.5) / (t + 0.5));
|
|
598
|
+
}
|
|
599
|
+
_termoMaisProximo(e) {
|
|
600
|
+
let t = distanciaMaximaTolerada(e.length);
|
|
601
|
+
if (0 === t) return null;
|
|
602
|
+
let i = null, o = t + 1;
|
|
603
|
+
for (let s of this.listaVocabulario) {
|
|
604
|
+
if (Math.abs(s.length - e.length) > t) continue;
|
|
605
|
+
let r = distanciaEdicao(e, s);
|
|
606
|
+
r < o && (o = r, i = s);
|
|
607
|
+
}
|
|
608
|
+
return o <= t ? i : null;
|
|
609
|
+
}
|
|
610
|
+
_scoreBM25(e, t) {
|
|
611
|
+
let i = this.docs[t];
|
|
612
|
+
if (0 === i.length) return 0;
|
|
613
|
+
let o = /* @__PURE__ */ new Map();
|
|
614
|
+
for (let s of i) o.set(s, (o.get(s) || 0) + 1);
|
|
615
|
+
let r = 0;
|
|
616
|
+
for (let n of e) {
|
|
617
|
+
let a = o.get(n) || 0, l = this._idf(n), c = 1;
|
|
618
|
+
if (0 === a) {
|
|
619
|
+
let d = this._termoMaisProximo(n);
|
|
620
|
+
if (null === d || 0 === (a = o.get(d) || 0)) continue;
|
|
621
|
+
l = this._idf(d), c = 0.4;
|
|
1037
622
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
1055
|
-
|
|
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;
|
|
623
|
+
let h = 2.5 * a, u = a + 1.5 * (0.7 + 0.3 * (i.length / this.avgDocLen));
|
|
624
|
+
r += c * l * (h / u);
|
|
625
|
+
}
|
|
626
|
+
return r;
|
|
627
|
+
}
|
|
628
|
+
_palavrasSignificativas(e) {
|
|
629
|
+
return normalizar(e).toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((e2) => e2.length > 1 && !STOPWORDS.has(e2));
|
|
630
|
+
}
|
|
631
|
+
_bonusFrase(e, t) {
|
|
632
|
+
let i = this._palavrasSignificativas(e);
|
|
633
|
+
if (i.length < 2) return 0;
|
|
634
|
+
let o = this.docPalavrasSignificativas[t], s = 0;
|
|
635
|
+
for (let r = Math.min(6, i.length); r >= 2; r--) {
|
|
636
|
+
for (let n = 0; n + r <= i.length; n++) {
|
|
637
|
+
let a = i.slice(n, n + r);
|
|
638
|
+
if (this._contemSequencia(o, a)) {
|
|
639
|
+
s = r;
|
|
640
|
+
break;
|
|
1076
641
|
}
|
|
1077
642
|
}
|
|
643
|
+
if (s > 0) break;
|
|
1078
644
|
}
|
|
1079
|
-
return
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
};
|
|
645
|
+
return 0 === s ? 0 : s / i.length;
|
|
646
|
+
}
|
|
647
|
+
_contemSequencia(e, t) {
|
|
648
|
+
outer: for (let i = 0; i + t.length <= e.length; i++) {
|
|
649
|
+
for (let o = 0; o < t.length; o++) if (e[i + o] !== t[o]) continue outer;
|
|
650
|
+
return true;
|
|
1254
651
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
_bonusTitulo(e, t) {
|
|
655
|
+
if (0 === e.length) return 0;
|
|
656
|
+
let i = new Set(this.docPalavrasSignificativas[t].slice(0, 8)), o = new Set(e), s = 0;
|
|
657
|
+
for (let r of o) i.has(r) && s++;
|
|
658
|
+
return s / o.size;
|
|
659
|
+
}
|
|
660
|
+
_scoreDoc(e, t, i) {
|
|
661
|
+
let o = this._scoreBM25(t, i), s = this._bonusFrase(e, i), r = this._bonusTitulo(this._palavrasSignificativas(e), i);
|
|
662
|
+
return 1 * o + 4 * s + 3 * r;
|
|
663
|
+
}
|
|
664
|
+
_rankTodos(e) {
|
|
665
|
+
let t = this._aplicarSinonimos(tokenizar(e));
|
|
666
|
+
if (0 === t.length) return [];
|
|
667
|
+
let i = [];
|
|
668
|
+
for (let o = 0; o < this.totalDocs; o++) i.push({ index: o, score: this._scoreDoc(e, t, o) });
|
|
669
|
+
return i.sort((e2, t2) => t2.score - e2.score), i;
|
|
670
|
+
}
|
|
671
|
+
search(e) {
|
|
672
|
+
if (0 === this.totalDocs) return null;
|
|
673
|
+
let t = this._rankTodos(e);
|
|
674
|
+
if (0 === t.length) return null;
|
|
675
|
+
let i = t[0];
|
|
676
|
+
return i.score <= 0 ? null : { text: this.blocks[i.index], score: i.score, confidence: i.score / (i.score + 3), index: i.index };
|
|
677
|
+
}
|
|
678
|
+
_similaridadeBlocos(e, t) {
|
|
679
|
+
let i = new Set(this.docs[e]), o = new Set(this.docs[t]);
|
|
680
|
+
if (0 === i.size || 0 === o.size) return 0;
|
|
681
|
+
let s = 0;
|
|
682
|
+
for (let r of i) o.has(r) && s++;
|
|
683
|
+
let n = i.size + o.size - s;
|
|
684
|
+
return 0 === n ? 0 : s / n;
|
|
685
|
+
}
|
|
686
|
+
_termosPartilhados(e, t) {
|
|
687
|
+
let i = new Set(this.docs[e]), o = this.docs[t], s = 0;
|
|
688
|
+
for (let r of o) i.has(r) && s++;
|
|
689
|
+
return s;
|
|
690
|
+
}
|
|
691
|
+
_tentarUnir(e, t) {
|
|
692
|
+
let i = extrairNomesProprios(e), o = extrairNomesProprios(t);
|
|
693
|
+
if (1 !== i.length || 1 !== o.length) return null;
|
|
694
|
+
let s = i[0], r = o[0];
|
|
695
|
+
if (normalizar(s).toLowerCase() === normalizar(r).toLowerCase()) return { tipo: "duplicado" };
|
|
696
|
+
let n = e.trim().split(/\s+/)[0].replace(/[.,;:!?]+$/, "");
|
|
697
|
+
return { tipo: "unido", texto: `${n} em v\xE1rios lugares, como ${s} e ${r}.` };
|
|
698
|
+
}
|
|
699
|
+
_reanalisarFocado(e) {
|
|
700
|
+
let t = this._aplicarSinonimos(tokenizar(e));
|
|
701
|
+
if (t.length <= 2) return null;
|
|
702
|
+
let i = t.map((e2) => ({ termo: e2, idf: this._idf(e2) }));
|
|
703
|
+
i.sort((e2, t2) => t2.idf - e2.idf);
|
|
704
|
+
let o = i.slice(0, Math.max(1, Math.ceil(t.length / 2))).map((e2) => e2.termo), s = [];
|
|
705
|
+
for (let r = 0; r < this.totalDocs; r++) s.push({ index: r, score: this._scoreDoc(e, o, r) });
|
|
706
|
+
return s.sort((e2, t2) => t2.score - e2.score), s;
|
|
707
|
+
}
|
|
708
|
+
analyze(e) {
|
|
709
|
+
let t = { decision: "UNKNOWN", confidence: 0, margem: 0, texto: null, unificado: false, reanalisado: false, melhor: null, segundo: null };
|
|
710
|
+
if (0 === this.totalDocs) return t;
|
|
711
|
+
let i = this._rankTodos(e);
|
|
712
|
+
if (0 === i.length || i[0].score <= 0) return t;
|
|
713
|
+
let o = i[0], s = i[1] || { index: -1, score: 0 }, r = (o.score - s.score) / o.score, n = o.score / (o.score + 3), a = { text: this.blocks[o.index], score: o.score, index: o.index }, l = s.index >= 0 ? { text: this.blocks[s.index], score: s.score, index: s.index } : null;
|
|
714
|
+
if (n < 0.15) return t;
|
|
715
|
+
if (n >= 0.55 && r >= 0.12) return { decision: "RESPONDER", confidence: n, margem: r, texto: a.text, unificado: false, reanalisado: false, melhor: a, segundo: l };
|
|
716
|
+
if (l) {
|
|
717
|
+
let c = this._similaridadeBlocos(o.index, s.index);
|
|
718
|
+
if (c >= 0.75) return { decision: "RESPONDER", confidence: n, margem: r, texto: a.text, unificado: false, reanalisado: false, melhor: a, segundo: l };
|
|
719
|
+
let d = this._termosPartilhados(o.index, s.index);
|
|
720
|
+
if (d >= 1) {
|
|
721
|
+
let h = this._tentarUnir(a.text, l.text);
|
|
722
|
+
if (h && "duplicado" === h.tipo) return { decision: "RESPONDER", confidence: n, margem: r, texto: a.text, unificado: false, reanalisado: false, melhor: a, segundo: l };
|
|
723
|
+
if (h && "unido" === h.tipo) return { decision: "RESPONDER", confidence: n, margem: r, texto: h.texto, unificado: true, reanalisado: false, melhor: a, segundo: l };
|
|
1296
724
|
}
|
|
1297
725
|
}
|
|
1298
|
-
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
}
|
|
726
|
+
let u = this._reanalisarFocado(e);
|
|
727
|
+
if (u && u.length > 0 && u[0].score > 0) {
|
|
728
|
+
let f = u[0], $ = u[1] || { index: -1, score: 0 }, m = (f.score - $.score) / f.score, g = f.score / (f.score + 3);
|
|
729
|
+
if (g >= 0.55 && m >= 0.12) return { decision: "RESPONDER", confidence: g, margem: m, texto: this.blocks[f.index], unificado: false, reanalisado: true, melhor: { text: this.blocks[f.index], score: f.score, index: f.index }, segundo: $.index >= 0 ? { text: this.blocks[$.index], score: $.score, index: $.index } : null };
|
|
1316
730
|
}
|
|
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
|
-
};
|
|
731
|
+
return { decision: "REANALISAR", confidence: n, margem: r, texto: null, unificado: false, reanalisado: false, melhor: a, segundo: l };
|
|
1327
732
|
}
|
|
1328
733
|
};
|
|
1329
734
|
var KnowledgeCache = class {
|
|
1330
|
-
constructor(
|
|
1331
|
-
this.fileSystem =
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (this.
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
const texto = this.fileSystem.readFile(resolvedPath);
|
|
1340
|
-
const index = new KnowledgeIndex(texto, options);
|
|
1341
|
-
this.cache.set(resolvedPath, index);
|
|
1342
|
-
return index;
|
|
735
|
+
constructor(e) {
|
|
736
|
+
this.fileSystem = e, this.cache = /* @__PURE__ */ new Map();
|
|
737
|
+
}
|
|
738
|
+
get(e, t) {
|
|
739
|
+
if (this.cache.has(e)) return this.cache.get(e);
|
|
740
|
+
if (!this.fileSystem.exists(e)) throw Error(`BotQL/THINK: ficheiro de conhecimento n\xE3o encontrado: "${e}"`);
|
|
741
|
+
let i = this.fileSystem.readFile(e), o = new KnowledgeIndex(i, t);
|
|
742
|
+
return this.cache.set(e, o), o;
|
|
1343
743
|
}
|
|
1344
744
|
};
|
|
1345
745
|
module.exports = { KnowledgeIndex, KnowledgeCache, tokenizar, normalizar, stem, distanciaEdicao };
|
|
@@ -1349,572 +749,308 @@ var BotQL = (() => {
|
|
|
1349
749
|
// botql.js
|
|
1350
750
|
var require_botql = __commonJS({
|
|
1351
751
|
"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;
|
|
752
|
+
var { Parser: e } = require_Parser();
|
|
753
|
+
var { MemoryDatabase: t } = require_Database();
|
|
754
|
+
var { createDefaultFileSystem: s } = require_FileSystem();
|
|
755
|
+
var { KnowledgeCache: i } = require_RAG();
|
|
1358
756
|
var BotQLInterpreter = class _BotQLInterpreter {
|
|
1359
|
-
constructor(
|
|
1360
|
-
this.db =
|
|
1361
|
-
|
|
1362
|
-
this.
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
this.
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
}
|
|
757
|
+
constructor(e2 = {}) {
|
|
758
|
+
this.db = e2.db || new t(), this.onReply = e2.onReply || null, this.onForward = e2.onForward || null, this.onSend = e2.onSend || null, this.signalParser = e2.signalParser || ((e3) => e3), this.onThinking = e2.onThinking || null, this.botName = null, this.platform = null, this.connections = [], this.handlers = { START: [], MESSAGE: [], SIGNAL: [] }, this.running = false, this.responseConnections = /* @__PURE__ */ new Set(), this._clientesOcupados = /* @__PURE__ */ new Set(), this.onBusy = e2.onBusy || null, this.onResponse = e2.onResponse || null, this.defaultMessageConfig = null, this.nativeFuncs = /* @__PURE__ */ new Map(), this.registerFunction("NOW", () => (/* @__PURE__ */ new Date()).toISOString()), this.fileSystem = e2.fileSystem || s();
|
|
759
|
+
let n = "undefined" != typeof process && process.cwd ? process.cwd() : "";
|
|
760
|
+
this._basePathStack = [e2.basePath || n], this._importedFiles = /* @__PURE__ */ new Set(), this._rootBasePath = e2.basePath || n, this._keywordsFileCache = /* @__PURE__ */ new Map(), this._replyFileCache = /* @__PURE__ */ new Map(), this.envValues = /* @__PURE__ */ new Map(), this.knowledgeCache = new i(this.fileSystem);
|
|
761
|
+
}
|
|
762
|
+
static fromSource(t2, s2 = {}) {
|
|
763
|
+
let i2 = new e(t2).parseProgram(), n = new _BotQLInterpreter(s2);
|
|
764
|
+
return n.load(i2), n;
|
|
765
|
+
}
|
|
766
|
+
static fromFile(e2, t2 = {}) {
|
|
767
|
+
let i2 = t2.fileSystem || s(), n = i2.resolve("", e2), r = i2.readFile(n);
|
|
768
|
+
return _BotQLInterpreter.fromSource(r, { ...t2, fileSystem: i2, basePath: i2.dirname(n) });
|
|
769
|
+
}
|
|
770
|
+
registerFunction(e2, t2) {
|
|
771
|
+
this.nativeFuncs.set(e2, t2);
|
|
772
|
+
}
|
|
773
|
+
load(e2, t2 = this._basePathStack[this._basePathStack.length - 1]) {
|
|
774
|
+
let s2 = this._flattenImports(e2.body, t2);
|
|
775
|
+
for (let i2 of s2) switch (i2.type) {
|
|
776
|
+
case "CreateBot":
|
|
777
|
+
this.botName = i2.name;
|
|
778
|
+
break;
|
|
779
|
+
case "Platform":
|
|
780
|
+
this.platform = i2.value;
|
|
781
|
+
break;
|
|
782
|
+
case "Connect":
|
|
783
|
+
this.connections.push({ service: i2.service, kind: i2.kind, credential: i2.credential });
|
|
784
|
+
break;
|
|
785
|
+
case "ConnectResponse":
|
|
786
|
+
if (!this.envValues.has(i2.alias)) throw Error(`runtime error: CONNECT RESPONSE failed, alias "${i2.alias}" was not imported (use IMPORT {..., N} AS ${i2.alias})`);
|
|
787
|
+
this.responseConnections.add(i2.alias);
|
|
788
|
+
break;
|
|
789
|
+
case "CreateTable":
|
|
790
|
+
if (this.db.createTable(i2.name, i2.columns, i2.preventDefault), i2.defaultMessage) {
|
|
791
|
+
let n = "Context" === i2.name ? "client" : (i2.columns.find((e3) => "client" === e3.name || "sender" === e3.name) || {}).name;
|
|
792
|
+
if (!n) throw Error(`runtime error: DEFAULT MESSAGE on table "${i2.name}" requires a "client" or "sender" column`);
|
|
793
|
+
this.defaultMessageConfig = { table: i2.name, senderColumn: n, ...i2.defaultMessage };
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
case "On":
|
|
797
|
+
this.handlers[i2.event].push(i2);
|
|
798
|
+
break;
|
|
799
|
+
case "RunBot":
|
|
800
|
+
this.running = true;
|
|
801
|
+
break;
|
|
802
|
+
default:
|
|
803
|
+
throw Error(`runtime error: unsupported top-level statement: ${i2.type}`);
|
|
1456
804
|
}
|
|
1457
805
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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);
|
|
806
|
+
_flattenImports(t2, s2) {
|
|
807
|
+
let i2 = [], n = [];
|
|
808
|
+
for (let r of t2) {
|
|
809
|
+
if ("Import" !== r.type) {
|
|
810
|
+
n.push(r);
|
|
1483
811
|
continue;
|
|
1484
812
|
}
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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);
|
|
813
|
+
if (null !== r.index && void 0 !== r.index) {
|
|
814
|
+
let a = this.evalExpr(r.index, this.createContext()), l = this._loadIndexedFile(r.path, s2), o = l.get(Number(a));
|
|
815
|
+
if (void 0 === o) throw Error(`runtime error: IMPORT failed, entry ${a} not found in "${r.path}"`);
|
|
816
|
+
let h = r.alias || r.path.replace(/\.[^.]+$/, "");
|
|
817
|
+
this.envValues.set(h, o);
|
|
1494
818
|
continue;
|
|
1495
819
|
}
|
|
1496
|
-
|
|
1497
|
-
if (this._importedFiles.has(
|
|
1498
|
-
if (!this.fileSystem.exists(
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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);
|
|
820
|
+
let u = this.fileSystem.resolve(s2, r.path);
|
|
821
|
+
if (this._importedFiles.has(u)) continue;
|
|
822
|
+
if (!this.fileSystem.exists(u)) throw Error(`runtime error: IMPORT failed, file not found: "${u}"`);
|
|
823
|
+
this._importedFiles.add(u);
|
|
824
|
+
let c = this.fileSystem.readFile(u), d = new e(c).parseProgram(), f = this._flattenImports(d.body, this.fileSystem.dirname(u));
|
|
825
|
+
i2.push(...f);
|
|
1527
826
|
}
|
|
827
|
+
return [...i2, ...n];
|
|
1528
828
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
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;
|
|
829
|
+
createContext(e2 = {}) {
|
|
830
|
+
return { vars: { client: e2.client || null, message: e2.message || null, lastMsg: null, SIGNAL: null }, rawSignal: e2.rawSignal || null, lastInsertId: null };
|
|
1559
831
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
return this.resolveReplyFromFile(config.file, config.index, ctx);
|
|
832
|
+
async start() {
|
|
833
|
+
for (let e2 of this.handlers.START) {
|
|
834
|
+
let t2 = this.createContext();
|
|
835
|
+
await this.run(e2.body, t2);
|
|
1565
836
|
}
|
|
1566
|
-
return String(this.evalExpr(config.value, ctx));
|
|
1567
837
|
}
|
|
1568
|
-
async
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
838
|
+
async receiveMessage(e2, t2) {
|
|
839
|
+
if (this._clientesOcupados.has(e2)) return this.onBusy && await this.onBusy({ client: e2, message: t2 }), null;
|
|
840
|
+
this._clientesOcupados.add(e2);
|
|
841
|
+
try {
|
|
842
|
+
let s2 = this.createContext({ client: e2, message: t2 }), i2 = await this.greetIfNew(e2);
|
|
843
|
+
if (i2) return s2;
|
|
844
|
+
for (let n of this.handlers.MESSAGE) await this.run(n.body, s2);
|
|
845
|
+
return s2;
|
|
846
|
+
} finally {
|
|
847
|
+
this._clientesOcupados.delete(e2);
|
|
1573
848
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
849
|
+
}
|
|
850
|
+
async greetIfNew(e2) {
|
|
851
|
+
if (!this.defaultMessageConfig) return false;
|
|
852
|
+
let { table: t2, senderColumn: s2 } = this.defaultMessageConfig, i2 = this.db.getRows(t2).some((t3) => t3[s2] === e2);
|
|
853
|
+
if (i2) return false;
|
|
854
|
+
let n = this.createContext({ client: e2 }), r = this.resolveDefaultMessageText(this.defaultMessageConfig, n);
|
|
855
|
+
return n.vars.lastMsg = r, this.db.insert(t2, [s2], [e2]), this.onReply && await this.onReply({ target: null, text: r, client: e2 }), true;
|
|
856
|
+
}
|
|
857
|
+
resolveDefaultMessageText(e2, t2) {
|
|
858
|
+
return e2.file ? this.resolveReplyFromFile(e2.file, e2.index, t2) : String(this.evalExpr(e2.value, t2));
|
|
859
|
+
}
|
|
860
|
+
async receiveSignal(e2, t2) {
|
|
861
|
+
let s2 = this.createContext({ rawSignal: t2 });
|
|
862
|
+
for (let i2 of this.handlers.SIGNAL) i2.source === e2 && await this.run(i2.body, s2);
|
|
863
|
+
return s2;
|
|
864
|
+
}
|
|
865
|
+
async run(e2, t2) {
|
|
866
|
+
let s2 = false;
|
|
867
|
+
for (let i2 = 0; i2 < e2.length; i2++) {
|
|
868
|
+
let n = e2[i2];
|
|
869
|
+
if ("When" === n.type) {
|
|
870
|
+
(0 === i2 || "When" !== e2[i2 - 1].type) && (s2 = false);
|
|
871
|
+
let r = n.conditions.some((e3) => this.evalCondition(e3, t2));
|
|
872
|
+
r && (await this.run(n.body, t2), s2 = true);
|
|
1588
873
|
continue;
|
|
1589
874
|
}
|
|
1590
|
-
if (
|
|
1591
|
-
|
|
1592
|
-
groupMatched = false;
|
|
875
|
+
if ("Otherwise" === n.type) {
|
|
876
|
+
s2 || await this.run(n.body, t2), s2 = false;
|
|
1593
877
|
continue;
|
|
1594
878
|
}
|
|
1595
|
-
await this.execStatement(
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
evalCondition(
|
|
1599
|
-
if (typeof
|
|
1600
|
-
|
|
1601
|
-
if (
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
return
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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;
|
|
879
|
+
await this.execStatement(n, t2);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
evalCondition(e2, t2) {
|
|
883
|
+
if ("string" != typeof t2.vars.message) return false;
|
|
884
|
+
let s2 = t2.vars.message.toLowerCase();
|
|
885
|
+
if ("CONTAINS" === e2.op) return s2.includes(e2.value.toLowerCase());
|
|
886
|
+
if ("CONTAINS_ANY" === e2.op) return e2.values.some((e3) => s2.includes(e3.toLowerCase()));
|
|
887
|
+
if ("CONTAINS_KEYWORDS_FILE" === e2.op) {
|
|
888
|
+
let i2 = this._loadKeywordsFile(e2.path);
|
|
889
|
+
return i2.some((e3) => s2.includes(e3.toLowerCase()));
|
|
890
|
+
}
|
|
891
|
+
throw Error(`runtime error: unsupported condition: ${e2.op}`);
|
|
892
|
+
}
|
|
893
|
+
_loadKeywordsFile(e2) {
|
|
894
|
+
if (this._keywordsFileCache.has(e2)) return this._keywordsFileCache.get(e2);
|
|
895
|
+
let t2 = this.fileSystem.resolve(this._rootBasePath, e2);
|
|
896
|
+
if (!this.fileSystem.exists(t2)) throw Error(`runtime error: KEYWORDS failed, file not found: "${t2}"`);
|
|
897
|
+
let s2 = this.fileSystem.readFile(t2), i2 = s2.split("\n").map((e3) => e3.trim()).filter((e3) => e3.length > 0);
|
|
898
|
+
return this._keywordsFileCache.set(e2, i2), i2;
|
|
899
|
+
}
|
|
900
|
+
resolveWaitingConfig(e2, t2) {
|
|
901
|
+
if (!e2) return { text: null, seconds: 3 };
|
|
902
|
+
let s2 = null;
|
|
903
|
+
if (null !== e2.text && void 0 !== e2.text) s2 = e2.text;
|
|
904
|
+
else if (e2.file) {
|
|
905
|
+
let i2 = this.fileSystem.resolve(this._rootBasePath, e2.file);
|
|
906
|
+
if (!this.fileSystem.exists(i2)) throw Error(`runtime error: WAITING failed, file not found: "${i2}"`);
|
|
907
|
+
s2 = this.fileSystem.readFile(i2).trim();
|
|
908
|
+
}
|
|
909
|
+
let n = 3;
|
|
910
|
+
return e2.seconds && (n = Number(this.evalExpr(e2.seconds, t2))), { text: s2, seconds: n };
|
|
911
|
+
}
|
|
912
|
+
resolveReplyFromFile(e2, t2, s2) {
|
|
913
|
+
let i2 = this.evalExpr(t2, s2), n = this._loadIndexedFile(e2, this._rootBasePath), r = n.get(Number(i2));
|
|
914
|
+
if (void 0 === r) throw Error(`runtime error: REPLY failed, entry ${i2} not found in "${e2}"`);
|
|
915
|
+
return r;
|
|
916
|
+
}
|
|
917
|
+
_loadIndexedFile(e2, t2) {
|
|
918
|
+
if (this._replyFileCache.has(e2)) return this._replyFileCache.get(e2);
|
|
919
|
+
let s2 = this.fileSystem.resolve(t2, e2);
|
|
920
|
+
if (!this.fileSystem.exists(s2)) throw Error(`runtime error: file not found: "${s2}"`);
|
|
921
|
+
let i2 = this.fileSystem.readFile(s2), n = this._parseIndexedEntries(i2, e2);
|
|
922
|
+
return this._replyFileCache.set(e2, n), n;
|
|
923
|
+
}
|
|
924
|
+
_parseIndexedEntries(e2, t2) {
|
|
925
|
+
let s2 = /* @__PURE__ */ new Map(), i2 = e2.length, n = 0;
|
|
926
|
+
for (; n < i2; ) {
|
|
927
|
+
let r = n, a = r;
|
|
928
|
+
for (; a < i2 && (" " === e2[a] || " " === e2[a]); ) a++;
|
|
929
|
+
let l = /^\d+/.exec(e2.slice(a));
|
|
930
|
+
if (!l || "-" !== e2[a + l[0].length]) {
|
|
931
|
+
let o = e2.indexOf("\n", r);
|
|
932
|
+
n = -1 === o ? i2 : o + 1;
|
|
1713
933
|
continue;
|
|
1714
934
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
let p =
|
|
1721
|
-
|
|
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;
|
|
935
|
+
let h = Number(l[0]), u = a + l[0].length + 1;
|
|
936
|
+
if ("{" === e2[u]) {
|
|
937
|
+
let c = 1, d = u + 1, f = d;
|
|
938
|
+
for (; f < i2 && c > 0; ) "{" === e2[f] ? c++ : "}" === e2[f] && c--, f++;
|
|
939
|
+
if (0 !== c) throw Error(`runtime error: unclosed "{" for entry ${h} in "${t2}"`);
|
|
940
|
+
let p = e2.slice(d, f - 1).trim();
|
|
941
|
+
s2.set(h, p), n = f;
|
|
1732
942
|
continue;
|
|
1733
943
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
const value = raw.slice(k, lineEnd).trim();
|
|
1737
|
-
entries.set(index, value);
|
|
1738
|
-
i = nl === -1 ? len : nl + 1;
|
|
944
|
+
let m = e2.indexOf("\n", u), w = -1 === m ? i2 : m, g = e2.slice(u, w).trim();
|
|
945
|
+
s2.set(h, g), n = -1 === m ? i2 : m + 1;
|
|
1739
946
|
}
|
|
1740
|
-
return
|
|
947
|
+
return s2;
|
|
1741
948
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
switch (stmt.type) {
|
|
949
|
+
async execStatement(e2, t2) {
|
|
950
|
+
switch (e2.type) {
|
|
1745
951
|
case "Reply": {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
952
|
+
let s2 = e2.file ? this.resolveReplyFromFile(e2.file, e2.index, t2) : String(await this.evalReplyValue(e2.value, t2));
|
|
953
|
+
t2.vars.lastMsg = s2, this.onReply && await this.onReply({ target: e2.target, text: s2, client: t2.vars.client });
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
case "Waiting": {
|
|
957
|
+
let i2 = this.resolveWaitingConfig(e2, t2);
|
|
958
|
+
this.onThinking && await this.onThinking({ client: t2.vars.client, text: i2.text }), await new Promise((e3) => setTimeout(e3, 1e3 * i2.seconds));
|
|
1751
959
|
break;
|
|
1752
960
|
}
|
|
1753
961
|
case "Think": {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
}
|
|
962
|
+
let n = this.resolveWaitingConfig(e2.waiting, t2), r = Date.now();
|
|
963
|
+
this.onThinking && await this.onThinking({ client: t2.vars.client, text: n.text });
|
|
964
|
+
let a = this.fileSystem.resolve(this._rootBasePath, e2.file), l = this.knowledgeCache.get(a), o = l.analyze(t2.vars.message), h = Date.now() - r, u = 1e3 * n.seconds - h;
|
|
965
|
+
if (u > 0 && await new Promise((e3) => setTimeout(e3, u)), "RESPONDER" === o.decision) {
|
|
966
|
+
let c = o.texto;
|
|
967
|
+
t2.vars.lastMsg = c, this.onReply && await this.onReply({ target: null, text: c, client: t2.vars.client });
|
|
968
|
+
} else e2.fallback && await this.execStatement(e2.fallback, t2);
|
|
1776
969
|
break;
|
|
1777
970
|
}
|
|
1778
971
|
case "ForwardTo": {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
await this.onForward({ target, client: ctx.vars.client });
|
|
1782
|
-
}
|
|
972
|
+
let d = String(this.evalExpr(e2.target, t2));
|
|
973
|
+
this.onForward && await this.onForward({ target: d, client: t2.vars.client });
|
|
1783
974
|
break;
|
|
1784
975
|
}
|
|
1785
|
-
case "ParseSignal":
|
|
1786
|
-
|
|
976
|
+
case "ParseSignal":
|
|
977
|
+
t2.vars.SIGNAL = await this.signalParser(t2.rawSignal);
|
|
1787
978
|
break;
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
if (this.onSend) {
|
|
1791
|
-
await this.onSend({ target: stmt.target, signal: ctx.vars.SIGNAL });
|
|
1792
|
-
}
|
|
979
|
+
case "SendTo":
|
|
980
|
+
this.onSend && await this.onSend({ target: e2.target, signal: t2.vars.SIGNAL });
|
|
1793
981
|
break;
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
this.execInsert(stmt, ctx);
|
|
982
|
+
case "Insert":
|
|
983
|
+
this.execInsert(e2, t2);
|
|
1797
984
|
break;
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
this.execUpdate(stmt, ctx);
|
|
985
|
+
case "Update":
|
|
986
|
+
this.execUpdate(e2, t2);
|
|
1801
987
|
break;
|
|
1802
|
-
}
|
|
1803
988
|
default:
|
|
1804
|
-
throw
|
|
989
|
+
throw Error(`runtime error: unsupported statement inside event: ${e2.type}`);
|
|
1805
990
|
}
|
|
1806
991
|
}
|
|
1807
|
-
execInsert(
|
|
1808
|
-
let
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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];
|
|
992
|
+
execInsert(e2, t2) {
|
|
993
|
+
let s2, i2;
|
|
994
|
+
"Context" !== e2.table || e2.columns && 0 !== e2.columns.length || e2.values ? (s2 = (e2.columns || []).map((e3) => e3.name), i2 = (e2.values || []).map((e3) => this.evalExpr(e3, t2))) : (s2 = ["client", "message", "created_at"], i2 = [t2.vars.client, t2.vars.message, (/* @__PURE__ */ new Date()).toISOString()]), t2.lastInsertId = this.db.insert(e2.table, s2, i2);
|
|
995
|
+
}
|
|
996
|
+
execUpdate(e2, t2) {
|
|
997
|
+
let s2 = this.evalExpr(e2.set.value, t2), i2 = null, n = null;
|
|
998
|
+
e2.where && (i2 = e2.where.left.name, n = this.evalExpr(e2.where.right, t2)), this.db.update(e2.table, e2.set.column, s2, i2, n);
|
|
999
|
+
}
|
|
1000
|
+
async evalReplyValue(e2, t2) {
|
|
1001
|
+
return "Call" === e2.type && "Response" === e2.callee ? this.execResponse(e2, t2) : this.evalExpr(e2, t2);
|
|
1002
|
+
}
|
|
1003
|
+
async execResponse(e2, t2) {
|
|
1004
|
+
if (!this.onResponse) throw Error("runtime error: Response() failed, no AI connected (missing onResponse handler)");
|
|
1005
|
+
let s2;
|
|
1006
|
+
if (0 === e2.args.length) {
|
|
1007
|
+
if (0 === this.responseConnections.size) throw Error("runtime error: Response() failed, no CONNECT RESPONSE found");
|
|
1008
|
+
if (this.responseConnections.size > 1) throw Error(`runtime error: Response() is ambiguous with multiple CONNECT RESPONSE (${[...this.responseConnections].join(", ")}); use Response(alias)`);
|
|
1009
|
+
s2 = [...this.responseConnections][0];
|
|
1860
1010
|
} else {
|
|
1861
|
-
|
|
1862
|
-
if (
|
|
1863
|
-
|
|
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
|
-
}
|
|
1011
|
+
let i2 = e2.args[0];
|
|
1012
|
+
if ("Identifier" !== i2.type) throw Error("runtime error: Response(alias) expects an alias name, not a string or expression");
|
|
1013
|
+
if (s2 = i2.name, !this.responseConnections.has(s2)) throw Error(`runtime error: Response(${s2}) failed, no CONNECT RESPONSE ${s2} found`);
|
|
1869
1014
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
return text;
|
|
1015
|
+
let n = this.envValues.get(s2), r = await this.onResponse({ alias: s2, token: n, message: t2.vars.message, client: t2.vars.client });
|
|
1016
|
+
return r;
|
|
1873
1017
|
}
|
|
1874
|
-
evalExpr(
|
|
1875
|
-
switch (
|
|
1018
|
+
evalExpr(e2, t2) {
|
|
1019
|
+
switch (e2.type) {
|
|
1876
1020
|
case "Literal":
|
|
1877
|
-
return
|
|
1878
|
-
case "Identifier":
|
|
1879
|
-
if (
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
}
|
|
1021
|
+
return e2.value;
|
|
1022
|
+
case "Identifier":
|
|
1023
|
+
if ("env" === e2.name) return Object.fromEntries(this.envValues);
|
|
1024
|
+
if (Object.prototype.hasOwnProperty.call(t2.vars, e2.name)) return t2.vars[e2.name];
|
|
1025
|
+
if (this.envValues.has(e2.name)) return this.envValues.get(e2.name);
|
|
1026
|
+
throw Error(`runtime error: unknown identifier: "${e2.name}"`);
|
|
1890
1027
|
case "Member": {
|
|
1891
|
-
|
|
1892
|
-
if (
|
|
1893
|
-
return
|
|
1028
|
+
let s2 = this.evalExpr(e2.object, t2);
|
|
1029
|
+
if (null == s2) return;
|
|
1030
|
+
return s2[e2.property];
|
|
1894
1031
|
}
|
|
1895
1032
|
case "Call": {
|
|
1896
|
-
if (
|
|
1897
|
-
|
|
1898
|
-
if (!
|
|
1899
|
-
|
|
1900
|
-
return
|
|
1033
|
+
if ("LAST_INSERT_ID" === e2.callee) return t2.lastInsertId;
|
|
1034
|
+
let i2 = this.nativeFuncs.get(e2.callee);
|
|
1035
|
+
if (!i2) throw Error(`runtime error: unknown function: "${e2.callee}"`);
|
|
1036
|
+
let n = e2.args.map((e3) => this.evalExpr(e3, t2));
|
|
1037
|
+
return i2(...n);
|
|
1901
1038
|
}
|
|
1902
1039
|
case "Binary": {
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
return String(left) + String(right);
|
|
1040
|
+
let r = this.evalExpr(e2.left, t2), a = this.evalExpr(e2.right, t2);
|
|
1041
|
+
if ("+" === e2.op) {
|
|
1042
|
+
if ("number" == typeof r && "number" == typeof a) return r + a;
|
|
1043
|
+
return String(r) + String(a);
|
|
1908
1044
|
}
|
|
1909
|
-
if (
|
|
1910
|
-
throw
|
|
1045
|
+
if ("=" === e2.op) return r === a;
|
|
1046
|
+
throw Error(`runtime error: unsupported operator: "${e2.op}"`);
|
|
1911
1047
|
}
|
|
1912
1048
|
default:
|
|
1913
|
-
throw
|
|
1049
|
+
throw Error(`runtime error: unsupported expression: ${e2.type}`);
|
|
1914
1050
|
}
|
|
1915
1051
|
}
|
|
1916
1052
|
};
|
|
1917
|
-
module.exports = { BotQLInterpreter, MemoryDatabase };
|
|
1053
|
+
module.exports = { BotQLInterpreter, MemoryDatabase: t };
|
|
1918
1054
|
}
|
|
1919
1055
|
});
|
|
1920
1056
|
return require_botql();
|