@flow-engineer/flow 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +594 -2
- package/coding-agents/skill.md +321 -0
- package/coding-agents/test-skill.md +223 -0
- package/coding-agents/tools-skill.md +89 -0
- package/dist/flow-messaging/src/client.d.ts +34 -0
- package/dist/flow-messaging/src/client.js +130 -0
- package/dist/flow-messaging/src/index.d.ts +37 -0
- package/dist/flow-messaging/src/index.js +409 -0
- package/dist/simulators/src/index.d.ts +2 -0
- package/dist/simulators/src/index.js +2 -0
- package/dist/simulators/src/page.html +1245 -0
- package/dist/simulators/src/person.html +100 -0
- package/dist/simulators/src/platforms.d.ts +29 -0
- package/dist/simulators/src/platforms.js +72 -0
- package/dist/simulators/src/server.d.ts +201 -0
- package/dist/simulators/src/server.js +759 -0
- package/dist/src/agent.d.ts +504 -0
- package/dist/src/agent.js +818 -0
- package/dist/src/channels.d.ts +178 -0
- package/dist/src/channels.js +243 -0
- package/dist/src/conversation.d.ts +361 -0
- package/dist/src/conversation.js +1487 -0
- package/dist/src/db.d.ts +18 -0
- package/dist/src/db.js +57 -0
- package/dist/src/events.d.ts +17 -0
- package/dist/src/events.js +20 -0
- package/dist/src/http.d.ts +45 -0
- package/dist/src/http.js +269 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +11 -0
- package/dist/src/model.d.ts +21 -0
- package/dist/src/model.js +96 -0
- package/dist/src/names.d.ts +17 -0
- package/dist/src/names.js +38 -0
- package/dist/src/nudges.d.ts +23 -0
- package/dist/src/nudges.js +117 -0
- package/dist/src/postgres.d.ts +20 -0
- package/dist/src/postgres.js +62 -0
- package/dist/src/schedules.d.ts +60 -0
- package/dist/src/schedules.js +249 -0
- package/dist/src/skills.d.ts +12 -0
- package/dist/src/skills.js +69 -0
- package/dist/src/store.d.ts +214 -0
- package/dist/src/store.js +448 -0
- package/dist/src/talk.d.ts +57 -0
- package/dist/src/talk.js +166 -0
- package/dist/src/time.d.ts +71 -0
- package/dist/src/time.js +170 -0
- package/dist/src/tools.d.ts +43 -0
- package/dist/src/tools.js +26 -0
- package/dist/src/traces.d.ts +31 -0
- package/dist/src/traces.js +303 -0
- package/dist/src/types.d.ts +260 -0
- package/dist/src/types.js +2 -0
- package/dist/testing/src/check-endpoint.d.ts +31 -0
- package/dist/testing/src/check-endpoint.js +158 -0
- package/dist/testing/src/chinwag.d.ts +2 -0
- package/dist/testing/src/chinwag.js +6 -0
- package/dist/testing/src/cli.d.ts +2 -0
- package/dist/testing/src/cli.js +563 -0
- package/dist/testing/src/client.d.ts +115 -0
- package/dist/testing/src/client.js +955 -0
- package/dist/testing/src/http-agent.d.ts +47 -0
- package/dist/testing/src/http-agent.js +155 -0
- package/dist/testing/src/index.d.ts +3 -0
- package/dist/testing/src/index.js +2 -0
- package/dist/testing/src/new.d.ts +9 -0
- package/dist/testing/src/new.js +122 -0
- package/dist/testing/src/print-run.d.ts +3 -0
- package/dist/testing/src/print-run.js +22 -0
- package/dist/testing/src/protocol.d.ts +159 -0
- package/dist/testing/src/protocol.js +28 -0
- package/dist/testing/src/report.d.ts +74 -0
- package/dist/testing/src/report.js +90 -0
- package/dist/testing/src/show.d.ts +36 -0
- package/dist/testing/src/show.js +280 -0
- package/dist/testing/src/suite.d.ts +87 -0
- package/dist/testing/src/suite.js +198 -0
- package/examples/cafe/agent/AGENTS.md +10 -0
- package/examples/cafe/agent/skills/allergens/SKILL.md +7 -0
- package/examples/cafe/src/agent.ts +39 -0
- package/examples/cafe/src/main.ts +16 -0
- package/examples/cafe/src/tools.ts +62 -0
- package/examples/cafe/tests/scenarios/latte-correction.yaml +30 -0
- package/examples/cafe/tests/scenarios/order-fails.yaml +26 -0
- package/examples/cafe/tests/tools/get_menu.yaml +14 -0
- package/examples/cafe/tests/tools/place_order.yaml +29 -0
- package/examples/http-agent/server.mjs +32 -0
- package/examples/http-agent/tests/scenarios/opening-hours.yaml +16 -0
- package/flow-messaging/README.md +152 -0
- package/package.json +65 -4
- package/spec/agent-endpoint.openapi.yaml +148 -0
- package/spec/nudges.openapi.yaml +167 -0
- package/spec/request.schema.json +121 -0
- package/spec/response.schema.json +284 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// `flow check-endpoint <url>`: checks an agent's HTTP endpoint against the contract in spec/
|
|
2
|
+
// (agent-endpoint.openapi.yaml, request.schema.json, response.schema.json) before any test run. It
|
|
3
|
+
// POSTs a fixed set of synthetic messages, as `flow test --agent-url` would, and checks each
|
|
4
|
+
// answer: the status and body (the same rules a run applies, testing/src/http-agent.ts), the
|
|
5
|
+
// response schema, what WhatsApp can show, and that every id a reply names is one of the person's
|
|
6
|
+
// messages. No cloud, no key and no model call on Flow Framework's side: whatever the agent does with the
|
|
7
|
+
// messages (its own model, its tools) runs on its side.
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { WHATSAPP, z } from "../../src/index.js";
|
|
13
|
+
import { setting } from "../../src/traces.js";
|
|
14
|
+
import { checkReply, postMessage, TurnError } from "./http-agent.js";
|
|
15
|
+
import { packageRoot } from "./new.js";
|
|
16
|
+
/** A 1×1 PNG. */
|
|
17
|
+
const PHOTO = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
18
|
+
export const CASES = [
|
|
19
|
+
{ id: "message", title: "a typed message", messages: [{ messageId: "m1", message: { text: "hi, what time are you open today?" } }] },
|
|
20
|
+
{
|
|
21
|
+
id: "double-text",
|
|
22
|
+
title: "two messages in a row, in one conversation",
|
|
23
|
+
messages: [
|
|
24
|
+
{ messageId: "m1", message: { text: "can i get a large latte" } },
|
|
25
|
+
{ messageId: "m2", message: { text: "oat milk actually" } },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ id: "file", title: "a photo with no caption", messages: [{ messageId: "m1", message: { text: "", attachments: [{ kind: "image", mime: "image/png", name: "photo.png", data: PHOTO }] } }] },
|
|
29
|
+
{
|
|
30
|
+
id: "message-ids",
|
|
31
|
+
title: "a tapped button quoting the agent, with an id to react to",
|
|
32
|
+
messages: [
|
|
33
|
+
{ messageId: "m1", message: { text: "do you have oat milk?" } },
|
|
34
|
+
{ messageId: "m2", message: { text: "Oat", choice: { title: "Oat" }, replyTo: { text: "Which milk would you like?", fromAgent: true } } },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{ id: "no-reply", title: "a message that may need no answer", messages: [{ messageId: "m1", message: { text: "ok 👍" } }] },
|
|
38
|
+
];
|
|
39
|
+
const schemaFile = (name) => JSON.parse(readFileSync(join(packageRoot(), "spec", name), "utf8"));
|
|
40
|
+
function schemas() {
|
|
41
|
+
const response = schemaFile("response.schema.json");
|
|
42
|
+
const shape = z.fromJSONSchema({ ...response, properties: { replies: { type: "array" } } });
|
|
43
|
+
// One schema per reply type, so a bad reply is reported against its own type.
|
|
44
|
+
const replies = new Map(Object.keys(response.$defs).map((type) => [type, z.fromJSONSchema({ $schema: response.$schema, $defs: response.$defs, $ref: `#/$defs/${type}` })]));
|
|
45
|
+
return { shape, replies };
|
|
46
|
+
}
|
|
47
|
+
const issues = (error, at) => error.issues.map((i) => `${[at, ...i.path].join(".")}: ${i.message}`).join("; ");
|
|
48
|
+
/** Why this answer breaks the contract, or null. `ids` are the person's messages so far. */
|
|
49
|
+
function problem(replies, ids, s) {
|
|
50
|
+
for (const [n, reply] of replies.entries()) {
|
|
51
|
+
const at = `replies[${n}]`;
|
|
52
|
+
const type = reply?.type;
|
|
53
|
+
const schema = typeof type === "string" ? s.replies.get(type) : undefined;
|
|
54
|
+
if (!schema)
|
|
55
|
+
return `${at}.type: must be one of ${[...s.replies.keys()].join(", ")}`;
|
|
56
|
+
const valid = schema.safeParse(reply);
|
|
57
|
+
if (!valid.success)
|
|
58
|
+
return issues(valid.error, at);
|
|
59
|
+
const shown = checkReply(reply, WHATSAPP);
|
|
60
|
+
if ("refused" in shown)
|
|
61
|
+
return `${at}: would not be sent on WhatsApp: ${shown.refused}`;
|
|
62
|
+
const r = reply;
|
|
63
|
+
for (const id of [r.replyTo, r.messageId, ...(r.messageIds ?? [])]) {
|
|
64
|
+
if (id !== undefined && !ids.includes(id))
|
|
65
|
+
return `${at} names ${JSON.stringify(id)}, which is not one of the person's messages (${ids.join(", ")})`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const described = (answers) => answers
|
|
71
|
+
.map((a) => {
|
|
72
|
+
const types = a.replies.map((r) => String(r.type));
|
|
73
|
+
return `${a.messageId}: ${types.length ? `${types.length} ${types.length === 1 ? "reply" : "replies"} (${types.join(", ")})` : "no reply"}`;
|
|
74
|
+
})
|
|
75
|
+
.join("; ");
|
|
76
|
+
export async function checkEndpoint(options) {
|
|
77
|
+
const s = schemas();
|
|
78
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
79
|
+
// Fresh conversations on every check, in the cloud's form, so an endpoint that keeps history never mixes runs.
|
|
80
|
+
const session = `check_${randomBytes(4).toString("hex")}`;
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const c of CASES) {
|
|
83
|
+
const conversationId = `whatsapp:${session}.${c.id}.whatsapp.1`;
|
|
84
|
+
const answers = [];
|
|
85
|
+
let failed = null;
|
|
86
|
+
for (const [n, { messageId, message }] of c.messages.entries()) {
|
|
87
|
+
let replies;
|
|
88
|
+
try {
|
|
89
|
+
replies = await postMessage(options, timeoutMs, { conversationId, messageId, channel: "whatsapp", message });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
failed = `${messageId}: ${error instanceof TurnError ? error.message : error.message}`;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
answers.push({ messageId, replies });
|
|
96
|
+
const wrong = problem(replies, c.messages.slice(0, n + 1).map((m) => m.messageId), s);
|
|
97
|
+
if (wrong) {
|
|
98
|
+
failed = `${messageId}: ${wrong}`;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
results.push({ id: c.id, title: c.title, pass: failed === null, detail: failed ?? described(answers), answers });
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
const USAGE = `Usage: flow check-endpoint <url> [--token-env NAME] [--agent-timeout <s>] [--json]
|
|
107
|
+
|
|
108
|
+
Checks an agent's HTTP endpoint against Flow Framework's contract (spec/agent-endpoint.openapi.yaml) with
|
|
109
|
+
a fixed set of synthetic messages. No cloud, no key, no model call on Flow Framework's side.
|
|
110
|
+
|
|
111
|
+
--token-env NAME Send the value of NAME as authorization: Bearer <value>
|
|
112
|
+
(default: FLOW_FRAMEWORK_AGENT_TOKEN, when it is set)
|
|
113
|
+
--agent-timeout <s> How long one message waits for its answer (default: 60)
|
|
114
|
+
--json Print the results as JSON
|
|
115
|
+
|
|
116
|
+
Exit status: 0 when every case passed, 1 otherwise.`;
|
|
117
|
+
/** The command: prints one line per case (or JSON) and returns the exit status. */
|
|
118
|
+
export async function checkEndpointCli(args, env = process.env, print = console.log, printError = console.error) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = parseArgs({ args, allowPositionals: true, options: { "token-env": { type: "string" }, "agent-timeout": { type: "string", default: "60" }, json: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false } } });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
printError(`${error.message}\n\n${USAGE}`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const { values, positionals } = parsed;
|
|
128
|
+
if (values.help) {
|
|
129
|
+
print(USAGE);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
const url = positionals.length === 1 ? positionals[0] : "";
|
|
133
|
+
if (!/^https?:\/\/[^/]/.test(url) || !URL.canParse(url)) {
|
|
134
|
+
printError(`check-endpoint takes one http:// or https:// URL, such as http://localhost:3000/flow.\n\n${USAGE}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
const timeout = Number(values["agent-timeout"]);
|
|
138
|
+
if (!(timeout > 0)) {
|
|
139
|
+
printError("--agent-timeout must be a number of seconds above 0.");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const tokenEnv = values["token-env"];
|
|
143
|
+
const token = (tokenEnv ? env[tokenEnv] : setting("AGENT_TOKEN", env)) || undefined;
|
|
144
|
+
if (tokenEnv && !token) {
|
|
145
|
+
printError(`${tokenEnv} is not set.`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
const results = await checkEndpoint({ url, token, timeoutMs: timeout * 1000 });
|
|
149
|
+
const passed = results.filter((r) => r.pass).length;
|
|
150
|
+
if (values.json)
|
|
151
|
+
print(JSON.stringify({ url, passed: passed === results.length, cases: results }, null, 2));
|
|
152
|
+
else {
|
|
153
|
+
for (const r of results)
|
|
154
|
+
print(`${r.pass ? "PASS" : "FAIL"} ${r.id} (${r.title}): ${r.detail}`);
|
|
155
|
+
print(`\n${passed} of ${results.length} cases passed.`);
|
|
156
|
+
}
|
|
157
|
+
return passed === results.length ? 0 : 1;
|
|
158
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `chinwag` command from before the rename, kept for one release: it says so on stderr (stdout
|
|
3
|
+
// stays the report's, for --json), then runs the flow command (testing/src/cli.ts) as it is.
|
|
4
|
+
console.error("chinwag is now Flow Framework (@flow-engineer/flow): run npx flow, and npm i @flow-engineer/flow.");
|
|
5
|
+
await import("./cli.js");
|
|
6
|
+
export {};
|