@runtypelabs/persona-proxy 1.43.2 → 2.3.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/dist/index.cjs +95 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -6
- package/dist/index.d.ts +2 -6
- package/dist/index.js +95 -81
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/flows/bakery-assistant.ts +77 -65
- package/src/flows/components.ts +1 -1
- package/src/flows/conversational.ts +1 -1
- package/src/flows/scheduling.ts +1 -1
- package/src/flows/shopping-assistant.ts +2 -2
- package/src/index.test.ts +72 -0
- package/src/index.ts +12 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RuntypeFlowConfig } from "../index.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Bakery assistant flow configuration for "Flour & Stone" bakery demo
|
|
@@ -10,7 +10,7 @@ import type { TravrseFlowConfig } from "../index.js";
|
|
|
10
10
|
*
|
|
11
11
|
* Designed to guide users toward the gift card when asking for gift recommendations.
|
|
12
12
|
*/
|
|
13
|
-
export const BAKERY_ASSISTANT_FLOW:
|
|
13
|
+
export const BAKERY_ASSISTANT_FLOW: RuntypeFlowConfig = {
|
|
14
14
|
name: "Bakery Assistant Flow",
|
|
15
15
|
description: "Flour & Stone bakery shopping assistant with gift recommendations",
|
|
16
16
|
steps: [
|
|
@@ -20,96 +20,108 @@ export const BAKERY_ASSISTANT_FLOW: TravrseFlowConfig = {
|
|
|
20
20
|
type: "prompt",
|
|
21
21
|
enabled: true,
|
|
22
22
|
config: {
|
|
23
|
-
model: "
|
|
23
|
+
model: "mercury-2",
|
|
24
24
|
reasoning: false,
|
|
25
25
|
responseFormat: "JSON",
|
|
26
26
|
outputVariable: "prompt_result",
|
|
27
27
|
userPrompt: "{{user_message}}",
|
|
28
28
|
systemPrompt: `You are a helpful shopping assistant for Flour & Stone, a premium artisan bakery known for traditional bread-making and exceptional pastries.
|
|
29
29
|
|
|
30
|
-
Brand voice: Warm, knowledgeable, passionate about craft baking. Use phrases like "fresh from the oven", "handcrafted with care", "artisan tradition".
|
|
30
|
+
Brand voice: Warm, knowledgeable, passionate about craft baking. Use phrases like "fresh from the oven", "handcrafted with care", "artisan tradition". Do not explain selectors, JSON, or templating to the user.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
- current_page: The path of the current page (e.g., "/bakery-goods.html")
|
|
34
|
-
- page_elements: Array of elements on the page - USE THIS to see what products are visible
|
|
35
|
-
- cart: Current cart contents (if any items)
|
|
36
|
-
- recent_order: Recent order info (if completed)
|
|
32
|
+
## Live context (request inputs — substituted each turn)
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
The widget sends **only** these keys as dispatch **inputs** (nothing extra on the record for this demo).
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
- "Add to Cart" buttons
|
|
36
|
+
**Orientation**
|
|
37
|
+
- Path: {{current_page}} (compare before nav_then_click; e.g. /bakery-goods.html)
|
|
38
|
+
- Full URL: {{page_url}}
|
|
39
|
+
- Title: {{page_title}}
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
**Page DOM**
|
|
42
|
+
- page_elements: JSON array of enriched nodes (selector, tagName, text, role, interactivity, attributes including data-*). Prefer **selector** for message_and_click when you click a specific control.
|
|
43
|
+
- page_context: Same slice formatted for the LLM (structured card summaries when matched, then groups by interactivity).
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
{"action": "message", "text": "Your response text here"}
|
|
45
|
+
{{page_elements}}
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
{"action": "nav_then_click", "page": "/bakery-goods.html", "on_load_text": "Message after navigation"}
|
|
47
|
+
{{page_context}}
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
{
|
|
49
|
+
**Cart (for checkout — mirror cart.items when user pays)**
|
|
50
|
+
{{cart}}
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
{
|
|
52
|
+
**Recent order (if any)**
|
|
53
|
+
{{recent_order}}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
{"action": "scroll_then_add", "text": "Your message", "item": {"id": "product-id", "name": "Product Name", "price": 1200}}
|
|
55
|
+
If {{current_page}} already equals the page you would navigate to, use {"action":"message",...} instead of nav_then_click.
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
- ALWAYS check current_page in the metadata before responding
|
|
65
|
-
- Prices are in cents (1200 = $12.00)
|
|
66
|
-
- Always respond with valid JSON only
|
|
57
|
+
## Discovering products
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
1. If user wants to see/buy products AND current_page is NOT "/bakery-goods.html" → nav_then_click
|
|
70
|
-
2. If user wants to add to cart AND current_page IS "/bakery-goods.html" → scroll_then_add (scrolls to product and adds)
|
|
71
|
-
3. If user asks about gifts AND current_page is NOT "/bakery-goods.html" → nav_then_click to /bakery-goods.html
|
|
72
|
-
4. If user asks about gifts AND current_page IS "/bakery-goods.html" → scroll_then_add with gift card
|
|
73
|
-
5. If user says "yes" to adding something AND current_page IS "/bakery-goods.html" → scroll_then_add
|
|
74
|
-
6. If user confirms checkout (says "yes", "checkout", "pay", etc. after being asked about checkout) AND cart has items → checkout action
|
|
59
|
+
Use {{page_context}} for a quick scan and {{page_elements}} for exact selectors and attributes. Product rows often include data-product in **attributes**; prices appear in **text**; add-to-cart controls are usually **clickable** with stable **selector** values.
|
|
75
60
|
|
|
76
|
-
|
|
77
|
-
- Sourdough Loaf: id="sourdough-loaf", price=1200
|
|
78
|
-
- Croissant Box (6): id="croissant-box", price=2400
|
|
79
|
-
- Cinnamon Rolls (4): id="cinnamon-rolls", price=1800
|
|
80
|
-
- Baguette Trio: id="baguette-trio", price=900
|
|
81
|
-
- Almond Tart: id="almond-tart", price=800
|
|
82
|
-
- Fruit Danish: id="fruit-danish", price=600
|
|
83
|
-
- $50 Gift Card: id="gift-card-50", price=5000
|
|
84
|
-
- $25 Gift Card: id="gift-card-25", price=2500
|
|
61
|
+
## Output: one JSON object only
|
|
85
62
|
|
|
86
|
-
|
|
63
|
+
No markdown fences, no commentary before/after. Valid JSON only.
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
{"action": "
|
|
65
|
+
### 1. message
|
|
66
|
+
{"action": "message", "text": "..."}
|
|
67
|
+
Use for chat, clarifying questions, "we're already on that page", or when you need the user to choose (e.g. $25 vs $50 gift card).
|
|
90
68
|
|
|
91
|
-
|
|
92
|
-
{"action": "
|
|
69
|
+
### 2. nav_then_click
|
|
70
|
+
{"action": "nav_then_click", "page": "/bakery-goods.html", "on_load_text": "..."}
|
|
71
|
+
Use root-relative paths starting with /. Only when current_page is different from the target. This **only** changes pages — it does **not** open Stripe or payment.
|
|
93
72
|
|
|
94
|
-
|
|
95
|
-
{"action": "
|
|
73
|
+
### 3. add_to_cart
|
|
74
|
+
{"action": "add_to_cart", "text": "...", "item": {"id": "product-id", "name": "Product Name", "price": 1200}}
|
|
75
|
+
Use when adding from context without scrolling (optional; on goods page prefer scroll_then_add).
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
{"action": "scroll_then_add", "text": "
|
|
77
|
+
### 4. scroll_then_add (preferred on /bakery-goods.html)
|
|
78
|
+
{"action": "scroll_then_add", "text": "...", "item": {"id": "...", "name": "...", "price": 1200}}
|
|
79
|
+
Scrolls the product into view then adds one unit (cart merges duplicate ids into quantity).
|
|
99
80
|
|
|
100
|
-
|
|
101
|
-
{"action": "
|
|
81
|
+
### 5. checkout → Stripe (this demo)
|
|
82
|
+
{"action": "checkout", "text": "Brief message", "items": [{"name": "...", "price": 1200, "quantity": 2}, ...]}
|
|
83
|
+
**Only** this action starts hosted checkout (Stripe). **Never** use nav_then_click to a "/checkout" URL for payment here.
|
|
84
|
+
Requirements: cart in context must have items; **items array must list every cart line** with the same name, cent prices, and quantities as cart.items. If cart is null or empty, use message — do not checkout.
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
### 6. message_and_click (rare)
|
|
87
|
+
If page_elements show a specific button selector and scroll_then_add is wrong, you may use message_and_click with a CSS selector — prefer scroll_then_add on bakery-goods.html.
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
{"action": "checkout", "text": "Great! Taking you to checkout now...", "items": [{"name": "Sourdough Loaf", "price": 1200, "quantity": 1}]}
|
|
89
|
+
## Rules
|
|
108
90
|
|
|
109
|
-
|
|
110
|
-
- After
|
|
111
|
-
-
|
|
112
|
-
|
|
91
|
+
- Prices in JSON are always **integer cents** (1200 = $12.00).
|
|
92
|
+
- After adding to cart, invite checkout or more shopping.
|
|
93
|
+
- On checkout confirmation ("yes", "checkout", "pay", "proceed", etc.), build **items** from **cart.items** (all rows, correct quantity). Do not drop lines or invent prices.
|
|
94
|
+
|
|
95
|
+
## Product catalog (ids and cent prices)
|
|
96
|
+
|
|
97
|
+
- Sourdough Loaf: sourdough-loaf, 1200
|
|
98
|
+
- Croissant Box (6): croissant-box, 2400
|
|
99
|
+
- Cinnamon Rolls (4): cinnamon-rolls, 1800
|
|
100
|
+
- Baguette Trio: baguette-trio, 900
|
|
101
|
+
- Almond Tart: almond-tart, 800
|
|
102
|
+
- Fruit Danish: fruit-danish, 600
|
|
103
|
+
- $50 Gift Card: gift-card-50, 5000
|
|
104
|
+
- $25 Gift Card: gift-card-25, 2500
|
|
105
|
+
|
|
106
|
+
## Examples
|
|
107
|
+
|
|
108
|
+
Gift seeker on /bakery-locations.html:
|
|
109
|
+
{"action": "nav_then_click", "page": "/bakery-goods.html", "on_load_text": "Here are our goods! You'll find our gift cards below — $50 is our most popular. Want me to add one?"}
|
|
110
|
+
|
|
111
|
+
On /bakery-goods.html, user wants $50 gift card:
|
|
112
|
+
{"action": "scroll_then_add", "text": "Added the $50 gift card. Ready to check out?", "item": {"id": "gift-card-50", "name": "$50 Gift Card", "price": 5000}}
|
|
113
|
+
|
|
114
|
+
User on /bakery.html agrees to see products:
|
|
115
|
+
{"action": "nav_then_click", "page": "/bakery-goods.html", "on_load_text": "Here are our handcrafted goods — what sounds good today?"}
|
|
116
|
+
|
|
117
|
+
On /bakery-goods.html, add sourdough:
|
|
118
|
+
{"action": "scroll_then_add", "text": "Sourdough is in your cart. Anything else, or shall we check out?", "item": {"id": "sourdough-loaf", "name": "Sourdough Loaf", "price": 1200}}
|
|
119
|
+
|
|
120
|
+
Cart has one $50 gift card; user says yes to checkout:
|
|
121
|
+
{"action": "checkout", "text": "Opening secure checkout...", "items": [{"name": "$50 Gift Card", "price": 5000, "quantity": 1}]}
|
|
122
|
+
|
|
123
|
+
Cart has sourdough (qty 1) and croissant box (qty 1); user says "pay":
|
|
124
|
+
{"action": "checkout", "text": "Taking you to checkout...", "items": [{"name": "Sourdough Loaf", "price": 1200, "quantity": 1}, {"name": "Croissant Box (6)", "price": 2400, "quantity": 1}]}`,
|
|
113
125
|
previousMessages: "{{messages}}"
|
|
114
126
|
}
|
|
115
127
|
}
|
package/src/flows/components.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const CONVERSATIONAL_FLOW: RuntypeFlowConfig = {
|
|
|
14
14
|
type: "prompt",
|
|
15
15
|
enabled: true,
|
|
16
16
|
config: {
|
|
17
|
-
model: "
|
|
17
|
+
model: "mercury-2",
|
|
18
18
|
responseFormat: "markdown",
|
|
19
19
|
outputVariable: "prompt_result",
|
|
20
20
|
userPrompt: "{{user_message}}",
|
package/src/flows/scheduling.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const SHOPPING_ASSISTANT_FLOW: RuntypeFlowConfig = {
|
|
|
18
18
|
type: "prompt",
|
|
19
19
|
enabled: true,
|
|
20
20
|
config: {
|
|
21
|
-
model: "
|
|
21
|
+
model: "mercury-2",
|
|
22
22
|
reasoning: false,
|
|
23
23
|
responseFormat: "JSON",
|
|
24
24
|
outputVariable: "prompt_result",
|
|
@@ -97,7 +97,7 @@ export const SHOPPING_ASSISTANT_METADATA_FLOW: RuntypeFlowConfig = {
|
|
|
97
97
|
type: "prompt",
|
|
98
98
|
enabled: true,
|
|
99
99
|
config: {
|
|
100
|
-
model: "
|
|
100
|
+
model: "mercury-2",
|
|
101
101
|
reasoning: false,
|
|
102
102
|
responseFormat: "JSON",
|
|
103
103
|
outputVariable: "prompt_result",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { createChatProxyApp } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("CORS middleware", () => {
|
|
5
|
+
const savedEnv = process.env.NODE_ENV;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.env.NODE_ENV = savedEnv;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const preflight = (app: ReturnType<typeof createChatProxyApp>, origin: string, headers?: Record<string, string>) =>
|
|
12
|
+
app.request("/api/chat/dispatch", {
|
|
13
|
+
method: "OPTIONS",
|
|
14
|
+
headers: { Origin: origin, ...headers },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("allows any origin when allowedOrigins is not configured", async () => {
|
|
18
|
+
const app = createChatProxyApp();
|
|
19
|
+
const res = await preflight(app, "https://evil.com");
|
|
20
|
+
expect(res.status).toBe(204);
|
|
21
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("https://evil.com");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("allows matching origin from allowlist", async () => {
|
|
25
|
+
const app = createChatProxyApp({ allowedOrigins: ["https://good.com"] });
|
|
26
|
+
const res = await preflight(app, "https://good.com");
|
|
27
|
+
expect(res.status).toBe(204);
|
|
28
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("https://good.com");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("rejects non-matching origin in production with 403", async () => {
|
|
32
|
+
process.env.NODE_ENV = "production";
|
|
33
|
+
const app = createChatProxyApp({ allowedOrigins: ["https://good.com"] });
|
|
34
|
+
const res = await preflight(app, "https://evil.com");
|
|
35
|
+
expect(res.status).toBe(403);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects non-matching origin when NODE_ENV is unset", async () => {
|
|
39
|
+
delete process.env.NODE_ENV;
|
|
40
|
+
const app = createChatProxyApp({ allowedOrigins: ["https://good.com"] });
|
|
41
|
+
const res = await preflight(app, "https://evil.com");
|
|
42
|
+
expect(res.status).toBe(403);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("allows non-matching origin in explicit development mode", async () => {
|
|
46
|
+
process.env.NODE_ENV = "development";
|
|
47
|
+
const app = createChatProxyApp({ allowedOrigins: ["https://good.com"] });
|
|
48
|
+
const res = await preflight(app, "https://localhost:3000");
|
|
49
|
+
expect(res.status).toBe(204);
|
|
50
|
+
expect(res.headers.get("Access-Control-Allow-Origin")).toBe("https://localhost:3000");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("uses static Access-Control-Allow-Headers (not reflected)", async () => {
|
|
54
|
+
const app = createChatProxyApp();
|
|
55
|
+
const res = await preflight(app, "https://example.com", {
|
|
56
|
+
"Access-Control-Request-Headers": "X-Evil-Header, X-Custom",
|
|
57
|
+
});
|
|
58
|
+
expect(res.headers.get("Access-Control-Allow-Headers")).toBe("Content-Type, Authorization");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns 204 for OPTIONS preflight", async () => {
|
|
62
|
+
const app = createChatProxyApp();
|
|
63
|
+
const res = await preflight(app, "https://example.com");
|
|
64
|
+
expect(res.status).toBe(204);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("includes Vary: Origin header", async () => {
|
|
68
|
+
const app = createChatProxyApp();
|
|
69
|
+
const res = await preflight(app, "https://example.com");
|
|
70
|
+
expect(res.headers.get("Vary")).toBe("Origin");
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -16,10 +16,7 @@ export type RuntypeFlowConfig = {
|
|
|
16
16
|
steps: RuntypeFlowStep[];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
export type TravrseFlowStep = RuntypeFlowStep;
|
|
21
|
-
/** @deprecated Use RuntypeFlowConfig instead */
|
|
22
|
-
export type TravrseFlowConfig = RuntypeFlowConfig;
|
|
19
|
+
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Payload for message feedback (upvote/downvote)
|
|
@@ -76,8 +73,7 @@ const DEFAULT_FLOW: RuntypeFlowConfig = {
|
|
|
76
73
|
type: "prompt",
|
|
77
74
|
enabled: true,
|
|
78
75
|
config: {
|
|
79
|
-
model: "
|
|
80
|
-
// model: "gpt-4o",
|
|
76
|
+
model: "mercury-2",
|
|
81
77
|
responseFormat: "markdown",
|
|
82
78
|
outputVariable: "prompt_result",
|
|
83
79
|
userPrompt: "{{user_message}}",
|
|
@@ -97,7 +93,7 @@ const withCors =
|
|
|
97
93
|
(allowedOrigins: string[] | undefined) =>
|
|
98
94
|
async (c: Context, next: () => Promise<void>) => {
|
|
99
95
|
const origin = c.req.header("origin");
|
|
100
|
-
const isDevelopment = process.env.NODE_ENV === "development"
|
|
96
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
101
97
|
|
|
102
98
|
// Determine the CORS origin to allow
|
|
103
99
|
let corsOrigin: string;
|
|
@@ -124,9 +120,7 @@ const withCors =
|
|
|
124
120
|
|
|
125
121
|
const headers: Record<string, string> = {
|
|
126
122
|
"Access-Control-Allow-Origin": corsOrigin,
|
|
127
|
-
"Access-Control-Allow-Headers":
|
|
128
|
-
c.req.header("access-control-request-headers") ??
|
|
129
|
-
"Content-Type, Authorization",
|
|
123
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
130
124
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
131
125
|
Vary: "Origin"
|
|
132
126
|
};
|
|
@@ -173,7 +167,7 @@ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
|
|
|
173
167
|
payload.timestamp = payload.timestamp ?? new Date().toISOString();
|
|
174
168
|
|
|
175
169
|
const isDevelopment =
|
|
176
|
-
process.env.NODE_ENV === "development"
|
|
170
|
+
process.env.NODE_ENV === "development";
|
|
177
171
|
|
|
178
172
|
if (isDevelopment) {
|
|
179
173
|
console.log("\n=== Feedback Received ===");
|
|
@@ -210,7 +204,7 @@ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
|
|
|
210
204
|
|
|
211
205
|
// Chat dispatch endpoint
|
|
212
206
|
app.post(path, async (c) => {
|
|
213
|
-
const apiKey = options.apiKey ?? process.env.RUNTYPE_API_KEY
|
|
207
|
+
const apiKey = options.apiKey ?? process.env.RUNTYPE_API_KEY;
|
|
214
208
|
if (!apiKey) {
|
|
215
209
|
return c.json(
|
|
216
210
|
{ error: "Missing API key. Set RUNTYPE_API_KEY." },
|
|
@@ -228,7 +222,7 @@ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
|
|
|
228
222
|
);
|
|
229
223
|
}
|
|
230
224
|
|
|
231
|
-
const isDevelopment = process.env.NODE_ENV === "development"
|
|
225
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
232
226
|
|
|
233
227
|
// Detect agent mode: if the payload contains an `agent` field, forward it directly
|
|
234
228
|
const isAgentMode = !!clientPayload.agent;
|
|
@@ -269,6 +263,11 @@ export const createChatProxyApp = (options: ChatProxyOptions = {}) => {
|
|
|
269
263
|
}
|
|
270
264
|
};
|
|
271
265
|
|
|
266
|
+
const clientInputs = clientPayload.inputs;
|
|
267
|
+
if (clientInputs && typeof clientInputs === "object" && !Array.isArray(clientInputs)) {
|
|
268
|
+
runtypePayload.inputs = clientInputs;
|
|
269
|
+
}
|
|
270
|
+
|
|
272
271
|
if (flowId) {
|
|
273
272
|
runtypePayload.flow = { id: flowId };
|
|
274
273
|
} else {
|