@falai/agent 4.0.0-alpha.3 → 4.0.0-alpha.5
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/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +12 -0
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +7 -1
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +13 -7
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/utils/phrases.d.ts +25 -0
- package/dist/cjs/utils/phrases.d.ts.map +1 -0
- package/dist/cjs/utils/phrases.js +38 -0
- package/dist/cjs/utils/phrases.js.map +1 -0
- package/dist/cjs/utils/template.d.ts +5 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +28 -2
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +12 -0
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +7 -1
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +13 -7
- package/dist/core/Understand.js.map +1 -1
- package/dist/utils/phrases.d.ts +25 -0
- package/dist/utils/phrases.d.ts.map +1 -0
- package/dist/utils/phrases.js +35 -0
- package/dist/utils/phrases.js.map +1 -0
- package/dist/utils/template.d.ts +5 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +28 -2
- package/dist/utils/template.js.map +1 -1
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/migration/v3-to-v4.md +8 -4
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/reference/trigger.md +20 -0
- package/docs/start/04-add-tools.md +1 -1
- package/package.json +3 -2
- package/src/core/FlowSpec.ts +18 -0
- package/src/core/Prompt.ts +7 -1
- package/src/core/Understand.ts +9 -5
- package/src/utils/phrases.ts +40 -0
- package/src/utils/template.ts +27 -2
package/src/core/FlowSpec.ts
CHANGED
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
} from "../types/flow.js";
|
|
44
44
|
import type { StructuredSchema } from "../types/schema.js";
|
|
45
45
|
import { isDuration } from "../utils/duration.js";
|
|
46
|
+
import { splitPhrases } from "../utils/phrases.js";
|
|
46
47
|
import { toWireSchema } from "../utils/schema.js";
|
|
47
48
|
|
|
48
49
|
// ── The JSON form ───────────────────────────────────────────────────────
|
|
@@ -233,6 +234,8 @@ interface LooseInstruction {
|
|
|
233
234
|
}
|
|
234
235
|
interface LooseTrigger {
|
|
235
236
|
repeat?: Repeat;
|
|
237
|
+
message?: string[];
|
|
238
|
+
mention?: string[];
|
|
236
239
|
event?: string;
|
|
237
240
|
silence?: Duration;
|
|
238
241
|
after?: Duration;
|
|
@@ -428,6 +431,21 @@ export function validateFlow<C = unknown, D = LooseData>(
|
|
|
428
431
|
duration(trigger.after, at, "after");
|
|
429
432
|
if (typeof trigger.repeat === "object") duration(trigger.repeat.cooldown, at, "repeat.cooldown");
|
|
430
433
|
pred(trigger.if, at, "if");
|
|
434
|
+
// Phrases opening with `!` rule the trigger out; a list of nothing but
|
|
435
|
+
// those can never fire, so the flow is dead and nothing would say so.
|
|
436
|
+
// `message: []` is the deliberate catch-all and stays legal.
|
|
437
|
+
for (const key of ["message", "mention"] as const) {
|
|
438
|
+
const phrases: string[] | undefined = trigger[key];
|
|
439
|
+
if (!phrases?.length) continue;
|
|
440
|
+
if (splitPhrases(phrases).counts.length > 0) continue;
|
|
441
|
+
throw problem(
|
|
442
|
+
at,
|
|
443
|
+
`every ${key} phrase starts with "!", so nothing can ever match it`,
|
|
444
|
+
`A "!" phrase rules the trigger out. Add at least one plain phrase saying when it should fire${
|
|
445
|
+
key === "message" ? ", or use an empty list for a catch-all" : ""
|
|
446
|
+
}.`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
431
449
|
});
|
|
432
450
|
|
|
433
451
|
flow.steps.forEach((step, i) => {
|
package/src/core/Prompt.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { AgentOptions } from "../types/agent.js";
|
|
11
11
|
import type { FieldDef, Instruction } from "../types/flow.js";
|
|
12
|
+
import { splitPhrases } from "../utils/phrases.js";
|
|
12
13
|
import { isKnown } from "../utils/schema.js";
|
|
13
14
|
import { render, type TemplateScope } from "../utils/template.js";
|
|
14
15
|
|
|
@@ -106,7 +107,12 @@ export function instructionsSection<C, D>(groups: InstructionGroup<C, D>[], scop
|
|
|
106
107
|
const text = render(item.prompt, scope).trim();
|
|
107
108
|
if (!text) continue;
|
|
108
109
|
const when = item.when === undefined ? [] : Array.isArray(item.when) ? item.when : [item.when];
|
|
109
|
-
const
|
|
110
|
+
const { counts, excludes } = splitPhrases(when);
|
|
111
|
+
const clauses = [
|
|
112
|
+
...(counts.length ? [`apply only when: ${counts.join(" OR ")}`] : []),
|
|
113
|
+
...(excludes.length ? [`never when: ${excludes.join(" OR ")}`] : []),
|
|
114
|
+
];
|
|
115
|
+
const condition = clauses.length ? ` (${clauses.join("; ")})` : "";
|
|
110
116
|
lines.push(`- [${item.kind ?? "should"}] ${group.caption} ${text}${condition}`);
|
|
111
117
|
}
|
|
112
118
|
}
|
package/src/core/Understand.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { FieldDef, FieldDefs, Flow, ParamDef, ParamDefs } from "../types/fl
|
|
|
23
23
|
import type { StructuredSchema } from "../types/schema.js";
|
|
24
24
|
import { extractEmbeddedJSONObject, isRecord } from "../utils/json.js";
|
|
25
25
|
import { logger } from "../utils/logger.js";
|
|
26
|
+
import { splitPhrases } from "../utils/phrases.js";
|
|
26
27
|
import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/schema.js";
|
|
27
28
|
import { render, type TemplateScope } from "../utils/template.js";
|
|
28
29
|
import { readUsage } from "../utils/usage.js";
|
|
@@ -245,8 +246,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
|
|
|
245
246
|
];
|
|
246
247
|
candidates.forEach((flow, i) => {
|
|
247
248
|
lines.push(`${i + 1}. ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
249
|
+
const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
|
|
250
|
+
if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
|
|
251
|
+
if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
|
|
250
252
|
});
|
|
251
253
|
lines.push(
|
|
252
254
|
"",
|
|
@@ -265,12 +267,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
|
|
|
265
267
|
const lines = [
|
|
266
268
|
"## Things the customer may mention",
|
|
267
269
|
"For each item, answer true when the customer's message clearly brings it up, false otherwise. " +
|
|
268
|
-
"Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough."
|
|
270
|
+
"Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
|
|
271
|
+
"A 'Does not count when' line overrides a match: if one of those fits, answer false.",
|
|
269
272
|
];
|
|
270
273
|
for (const flow of flows) {
|
|
271
274
|
lines.push(`- ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
275
|
+
const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
|
|
276
|
+
if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
|
|
277
|
+
if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
|
|
274
278
|
const defs = extractDefs(flow);
|
|
275
279
|
if (defs) {
|
|
276
280
|
lines.push(" When true, also extract:");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting an AI-judged phrase list into what counts and what rules it out.
|
|
3
|
+
*
|
|
4
|
+
* A trigger's phrases are alternatives: one match is enough. That makes the
|
|
5
|
+
* list useless for saying "but not this" — and "but not this" is what keeps a
|
|
6
|
+
* classifier honest. A customer answering "pode sim" to an offer of a meeting
|
|
7
|
+
* is agreeing to the meeting, not asking for a human, and without a way to say
|
|
8
|
+
* so every cheerful yes reads as a handoff request.
|
|
9
|
+
*
|
|
10
|
+
* So a phrase that opens with `!` is an exclusion: it does not make the trigger
|
|
11
|
+
* fire, it stops it. Exclusions win over matches, and the model is told both
|
|
12
|
+
* lists separately.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Phrases {
|
|
16
|
+
/** Any one of these makes it true. */
|
|
17
|
+
counts: string[];
|
|
18
|
+
/** Any one of these makes it false, whatever matched. */
|
|
19
|
+
excludes: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Split a phrase list on the leading `!`. Blank entries and a bare `"!"` are
|
|
24
|
+
* dropped: they would render as an empty bullet and mean nothing to the model.
|
|
25
|
+
*/
|
|
26
|
+
export function splitPhrases(phrases: readonly string[]): Phrases {
|
|
27
|
+
const counts: string[] = [];
|
|
28
|
+
const excludes: string[] = [];
|
|
29
|
+
for (const phrase of phrases) {
|
|
30
|
+
const trimmed = phrase.trim();
|
|
31
|
+
if (!trimmed) continue;
|
|
32
|
+
if (trimmed.startsWith("!")) {
|
|
33
|
+
const body = trimmed.slice(1).trim();
|
|
34
|
+
if (body) excludes.push(body);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
counts.push(trimmed);
|
|
38
|
+
}
|
|
39
|
+
return { counts, excludes };
|
|
40
|
+
}
|
package/src/utils/template.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path that resolves to an EMPTY string is a different case: the host knows
|
|
10
|
+
* the field and knows it is blank. Then the placeholder goes and `tidy` closes
|
|
11
|
+
* the gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends
|
|
12
|
+
* "Ola, tudo bem?" and never "Ola , tudo bem?".
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
export interface TemplateScope {
|
|
@@ -16,10 +21,30 @@ export interface TemplateScope {
|
|
|
16
21
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
17
22
|
|
|
18
23
|
export function render(template: string, scope: TemplateScope): string {
|
|
19
|
-
|
|
24
|
+
let emptied = false;
|
|
25
|
+
const out = template.replace(PLACEHOLDER, (match, path: string) => {
|
|
20
26
|
const value = lookup(scope, path.split("."));
|
|
21
|
-
|
|
27
|
+
if (value === undefined || value === null) return match;
|
|
28
|
+
const text = stringify(value);
|
|
29
|
+
if (text === "") emptied = true;
|
|
30
|
+
return text;
|
|
22
31
|
});
|
|
32
|
+
return emptied ? tidy(out) : out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Close the hole an empty value leaves: a doubled space, a space before
|
|
37
|
+
* punctuation, a space at the end of a line.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately narrow. It only runs on a string where something substituted to
|
|
40
|
+
* "", and it only collapses a run of spaces that follows a visible character,
|
|
41
|
+
* so indentation in a markdown list survives.
|
|
42
|
+
*/
|
|
43
|
+
function tidy(text: string): string {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ")
|
|
46
|
+
.replace(/ +([,.;:!?\u2026])/g, "$1")
|
|
47
|
+
.replace(/[ \t]+$/gm, "");
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
/** `render` over every string inside a value, recursively. Non-strings pass through. */
|