@popcomputer/structured-chat 0.1.0 → 0.2.0-rc.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 +0 -1
- package/README.md +122 -108
- package/dist/adapters/openai-compatible-model.d.ts +4 -4
- package/dist/adapters/openai-compatible-model.js +33 -31
- package/dist/core/answer.d.ts +13 -13
- package/dist/core/answer.js +21 -12
- package/dist/core/chat.d.ts +12 -15
- package/dist/core/chat.js +36 -27
- package/dist/core/collect-stage.d.ts +11 -14
- package/dist/core/collect-stage.js +40 -37
- package/dist/core/command.d.ts +1 -1
- package/dist/core/command.js +1 -1
- package/dist/core/json-value.d.ts +1 -1
- package/dist/core/json-value.js +8 -1
- package/dist/core/model-guard.d.ts +2 -3
- package/dist/core/model-guard.js +4 -4
- package/dist/core/model.d.ts +15 -19
- package/dist/core/model.js +22 -10
- package/dist/core/protocol.d.ts +84 -79
- package/dist/core/protocol.js +18 -12
- package/dist/core/question.js +17 -15
- package/dist/core/repair.js +1 -1
- package/dist/core/session.d.ts +22 -28
- package/dist/core/session.js +16 -7
- package/dist/core/stage-name.d.ts +1 -1
- package/dist/core/stage-name.js +1 -1
- package/dist/core/stage.d.ts +4 -4
- package/dist/core/stage.js +11 -8
- package/dist/core/tool-set.js +6 -6
- package/dist/core/tool.d.ts +36 -39
- package/dist/core/tool.js +58 -31
- package/dist/core/view.d.ts +64 -39
- package/dist/core/view.js +12 -15
- package/dist/integrations/assistant-ui.js +21 -14
- package/dist/testing/in-memory-session-store.js +1 -1
- package/dist/testing/scenario.js +6 -6
- package/examples/answer-modes.ts +60 -49
- package/examples/prompt-injection-policy.ts +20 -20
- package/examples/resource-search.ts +104 -0
- package/package.json +11 -3
- package/examples/agency-search.ts +0 -101
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The framework derives the model tool schemas, runtime validation, workflow
|
|
|
14
14
|
state, browser protocol, and Effect requirements from those definitions.
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
bun add @popcomputer/structured-chat effect
|
|
17
|
+
bun add @popcomputer/structured-chat@next effect@^4.0.0-rc.109
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The published entry points are ESM-only and support Node.js 22 or newer.
|
|
@@ -28,40 +28,42 @@ import {
|
|
|
28
28
|
Stage,
|
|
29
29
|
Tool,
|
|
30
30
|
} from "@popcomputer/structured-chat"
|
|
31
|
-
import {
|
|
31
|
+
import { Schema } from "effect"
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
name: "
|
|
33
|
+
const ResultCards = defineView({
|
|
34
|
+
name: "result_cards",
|
|
35
35
|
version: 1,
|
|
36
36
|
schema: Schema.Struct({
|
|
37
|
-
|
|
37
|
+
results: Schema.Array(
|
|
38
38
|
Schema.Struct({
|
|
39
39
|
id: Schema.String,
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
title: Schema.String,
|
|
41
|
+
summary: Schema.String,
|
|
42
42
|
}),
|
|
43
43
|
),
|
|
44
44
|
}),
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
const
|
|
48
|
-
name: "
|
|
49
|
-
description: "Find
|
|
50
|
-
input: Schema.Struct({
|
|
51
|
-
|
|
47
|
+
const FindResources = defineTool({
|
|
48
|
+
name: "find_resources",
|
|
49
|
+
description: "Find resources relevant to the completed request.",
|
|
50
|
+
input: Schema.Struct({
|
|
51
|
+
query: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
52
|
+
}),
|
|
53
|
+
execute: ({ query }) => ResourceCatalog.search(query),
|
|
52
54
|
}).pipe(
|
|
53
|
-
Tool.modelResult(
|
|
54
|
-
Tool.present(
|
|
55
|
+
Tool.modelResult(ResourceEvidenceSchema, ({ evidence }) => evidence),
|
|
56
|
+
Tool.present(ResultCards, ({ results }) => ({ results })),
|
|
55
57
|
)
|
|
56
58
|
|
|
57
|
-
const
|
|
58
|
-
name: "
|
|
59
|
-
instructions: ["Route the completed
|
|
60
|
-
tools: [
|
|
59
|
+
const Lookup = Stage.tools({
|
|
60
|
+
name: "lookup",
|
|
61
|
+
instructions: ["Route the completed request to one resource lookup."],
|
|
62
|
+
tools: [FindResources],
|
|
61
63
|
})
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
`
|
|
66
|
+
`FindResources` is the single source of truth for one capability. Its input
|
|
65
67
|
schema becomes both the model-facing JSON Schema and the authoritative runtime
|
|
66
68
|
parser. Its Effect error and service requirements remain typed.
|
|
67
69
|
|
|
@@ -76,6 +78,10 @@ The three result surfaces are deliberately separate:
|
|
|
76
78
|
Internal data does not reach the model or browser merely because a tool loaded
|
|
77
79
|
it.
|
|
78
80
|
|
|
81
|
+
See the compile-checked
|
|
82
|
+
[`resource-search.ts`](./examples/resource-search.ts) example for a complete
|
|
83
|
+
tool, service, model projection, browser projection, and stage definition.
|
|
84
|
+
|
|
79
85
|
## Compose directly with document-graph
|
|
80
86
|
|
|
81
87
|
Structured chat does not wrap or replace retrieval. An application-defined
|
|
@@ -83,24 +89,26 @@ Structured chat does not wrap or replace retrieval. An application-defined
|
|
|
83
89
|
execution directly:
|
|
84
90
|
|
|
85
91
|
```ts
|
|
86
|
-
import {
|
|
92
|
+
import { SearchKnowledgeBase } from "./knowledge-graph.js"
|
|
87
93
|
|
|
88
|
-
const
|
|
89
|
-
name: "
|
|
90
|
-
description: "Find
|
|
91
|
-
input: Schema.Struct({
|
|
92
|
-
|
|
94
|
+
const FindResources = defineTool({
|
|
95
|
+
name: "find_resources",
|
|
96
|
+
description: "Find resources supported by relevant source evidence.",
|
|
97
|
+
input: Schema.Struct({
|
|
98
|
+
query: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
99
|
+
}),
|
|
100
|
+
execute: ({ query }) => SearchKnowledgeBase.search(query, { limit: 6 }),
|
|
93
101
|
}).pipe(
|
|
94
|
-
Tool.modelResult(
|
|
95
|
-
Tool.present(
|
|
102
|
+
Tool.modelResult(ResourceEvidenceSchema, toModelEvidence),
|
|
103
|
+
Tool.present(ResultCards, toResultCards),
|
|
96
104
|
)
|
|
97
105
|
```
|
|
98
106
|
|
|
99
|
-
`
|
|
100
|
-
combine direct
|
|
101
|
-
typed
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
`SearchKnowledgeBase` remains the application-owned graph retrieval policy: it
|
|
108
|
+
can combine direct resource matches with supporting documents reached through
|
|
109
|
+
typed relationships. Structured chat contributes the closed tool schema, stage
|
|
110
|
+
policy, result projections, and UI protocol. The graph's typed failures and
|
|
111
|
+
Effect requirements flow through the tool without another adapter layer.
|
|
104
112
|
|
|
105
113
|
## Put a free-form conversation on rails
|
|
106
114
|
|
|
@@ -115,41 +123,40 @@ import {
|
|
|
115
123
|
} from "@popcomputer/structured-chat"
|
|
116
124
|
import { Schema } from "effect"
|
|
117
125
|
|
|
118
|
-
const
|
|
119
|
-
name: "
|
|
126
|
+
const RequestDetails = Stage.collect({
|
|
127
|
+
name: "request_details",
|
|
120
128
|
questions: {
|
|
121
129
|
guidance:
|
|
122
|
-
"Ask one conversational question at a time and briefly explain why the answer improves the
|
|
130
|
+
"Ask one conversational question at a time and briefly explain why the answer improves the result.",
|
|
123
131
|
escape: "Not sure yet",
|
|
124
132
|
},
|
|
125
133
|
fields: {
|
|
126
|
-
|
|
127
|
-
description:
|
|
128
|
-
"The unmet need the client most wants an agency to solve.",
|
|
134
|
+
goal: Answer.semantic(Schema.Trimmed.check(Schema.isNonEmpty()), {
|
|
135
|
+
description: "The outcome the user wants to achieve.",
|
|
129
136
|
ask: Question.adaptiveChoice(
|
|
130
|
-
"
|
|
137
|
+
"What would you like help accomplishing?",
|
|
131
138
|
{
|
|
132
139
|
minimumOptions: 3,
|
|
133
140
|
maximumOptions: 5,
|
|
134
141
|
fallbackOptions: [
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
142
|
+
"Understand a topic",
|
|
143
|
+
"Compare available options",
|
|
144
|
+
"Plan the next steps",
|
|
138
145
|
],
|
|
139
146
|
},
|
|
140
147
|
),
|
|
141
148
|
}),
|
|
142
|
-
|
|
143
|
-
description: "
|
|
144
|
-
ask: Question.fixed("
|
|
149
|
+
audience: Answer.explicit(Schema.Trimmed.check(Schema.isNonEmpty()), {
|
|
150
|
+
description: "Who the requested result is for.",
|
|
151
|
+
ask: Question.fixed("Who is this for?"),
|
|
145
152
|
}),
|
|
146
153
|
},
|
|
147
154
|
})
|
|
148
155
|
|
|
149
|
-
const
|
|
150
|
-
name: "
|
|
156
|
+
const ResourceFinder = defineChat({
|
|
157
|
+
name: "resource_finder",
|
|
151
158
|
version: 1,
|
|
152
|
-
stages: [
|
|
159
|
+
stages: [RequestDetails, Lookup],
|
|
153
160
|
})
|
|
154
161
|
```
|
|
155
162
|
|
|
@@ -170,7 +177,7 @@ while the field is pending. The value is validated against the answer schema
|
|
|
170
177
|
at definition time and requires `questions.escape` to be configured; a
|
|
171
178
|
confirmed field still requires its question to have been issued first.
|
|
172
179
|
Fields without an escape value stay unresolved and are re-asked from another
|
|
173
|
-
angle, so a
|
|
180
|
+
angle, so a user who genuinely cannot answer never blocks the workflow
|
|
174
181
|
unless the application wants it to.
|
|
175
182
|
|
|
176
183
|
For an adaptive choice, valid contextual model suggestions take precedence.
|
|
@@ -214,15 +221,15 @@ Applications can read the typed value and its provenance without reaching into
|
|
|
214
221
|
the persisted state shape:
|
|
215
222
|
|
|
216
223
|
```ts
|
|
217
|
-
const
|
|
224
|
+
const goal = ResourceFinder.getAcceptedAnswer(
|
|
218
225
|
reply.turn.state,
|
|
219
|
-
|
|
220
|
-
"
|
|
226
|
+
RequestDetails,
|
|
227
|
+
"goal",
|
|
221
228
|
)
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
goal?.value
|
|
231
|
+
goal?.evidence.messageIndex
|
|
232
|
+
goal?.evidence.quote
|
|
226
233
|
```
|
|
227
234
|
|
|
228
235
|
For semantic answers, the quote supports the model's inference; it does not
|
|
@@ -234,23 +241,23 @@ Keep parsing and business acceptance separate when a structurally valid value
|
|
|
234
241
|
may still be unsuitable for the workflow:
|
|
235
242
|
|
|
236
243
|
```ts
|
|
237
|
-
class
|
|
238
|
-
"
|
|
239
|
-
{ minimum: Schema.Number },
|
|
244
|
+
class ResultLimitOutOfRange extends Schema.TaggedError<ResultLimitOutOfRange>()(
|
|
245
|
+
"ResultLimitOutOfRange",
|
|
246
|
+
{ minimum: Schema.Number, maximum: Schema.Number },
|
|
240
247
|
) {}
|
|
241
248
|
|
|
242
|
-
const
|
|
249
|
+
const ResultLimit = Answer.explicit(Schema.Number, {
|
|
243
250
|
// The description remains model-visible, so repeat acceptance constraints.
|
|
244
|
-
description: "
|
|
245
|
-
ask: Question.fixed("
|
|
246
|
-
validate: (
|
|
247
|
-
|
|
251
|
+
description: "Number of results; must be a whole number from 1 to 20",
|
|
252
|
+
ask: Question.fixed("How many results would you like?"),
|
|
253
|
+
validate: (limit) =>
|
|
254
|
+
Number.isInteger(limit) && limit >= 1 && limit <= 20
|
|
248
255
|
? Effect.void
|
|
249
|
-
: Effect.fail(
|
|
256
|
+
: Effect.fail(
|
|
257
|
+
new ResultLimitOutOfRange({ minimum: 1, maximum: 20 }),
|
|
258
|
+
),
|
|
250
259
|
reject: {
|
|
251
|
-
ask: Question.fixed(
|
|
252
|
-
"Our minimum engagement is £5,000. Could you revise the budget?",
|
|
253
|
-
),
|
|
260
|
+
ask: Question.fixed("Choose a whole number from 1 to 20."),
|
|
254
261
|
},
|
|
255
262
|
})
|
|
256
263
|
```
|
|
@@ -313,8 +320,8 @@ const program = Effect.gen(function* () {
|
|
|
313
320
|
const request = yield* parseRequest(StructuredChatTurnRequestSchema)
|
|
314
321
|
const publicSessionId = request.session?.id ?? crypto.randomUUID()
|
|
315
322
|
|
|
316
|
-
const reply = yield*
|
|
317
|
-
namespace:
|
|
323
|
+
const reply = yield* ResourceFinder.reply({
|
|
324
|
+
namespace: authenticatedActor.id,
|
|
318
325
|
sessionId: publicSessionId,
|
|
319
326
|
expectedRevision: request.session?.revision,
|
|
320
327
|
message: request.message,
|
|
@@ -324,7 +331,7 @@ const program = Effect.gen(function* () {
|
|
|
324
331
|
{ ...reply, sessionId: publicSessionId },
|
|
325
332
|
{
|
|
326
333
|
result: ({ result }) => [
|
|
327
|
-
Text.make("
|
|
334
|
+
Text.make("Here are the most relevant evidence-backed resources."),
|
|
328
335
|
...result.views,
|
|
329
336
|
],
|
|
330
337
|
},
|
|
@@ -357,13 +364,13 @@ meaning.
|
|
|
357
364
|
## Continue naturally after the first tool result
|
|
358
365
|
|
|
359
366
|
`Stage.tools(...)` stays active by default. A user can refine the previous
|
|
360
|
-
|
|
367
|
+
lookup without restarting the collection stage:
|
|
361
368
|
|
|
362
369
|
```txt
|
|
363
|
-
User: We need a
|
|
364
|
-
Assistant: [
|
|
365
|
-
User:
|
|
366
|
-
Assistant: [refined
|
|
370
|
+
User: We need onboarding guidance for a new team.
|
|
371
|
+
Assistant: [resource results]
|
|
372
|
+
User: Prefer concise resources with practical examples.
|
|
373
|
+
Assistant: [refined resource results]
|
|
367
374
|
```
|
|
368
375
|
|
|
369
376
|
When a tool defines `Tool.modelResult(...)`, each bounded result is retained in
|
|
@@ -389,20 +396,20 @@ Declare side effects honestly. A command receives the package-derived stable
|
|
|
389
396
|
ID that its application endpoint must use as an idempotency key:
|
|
390
397
|
|
|
391
398
|
```ts
|
|
392
|
-
const
|
|
393
|
-
name: "
|
|
394
|
-
description: "
|
|
395
|
-
input:
|
|
399
|
+
const CreateRequest = defineCommand({
|
|
400
|
+
name: "create_request",
|
|
401
|
+
description: "Create the confirmed request once.",
|
|
402
|
+
input: CreateRequestInput,
|
|
396
403
|
execute: (input, { commandId }) =>
|
|
397
|
-
|
|
404
|
+
Requests.create(input, { idempotencyKey: commandId }),
|
|
398
405
|
}).pipe(
|
|
399
|
-
Tool.present(
|
|
406
|
+
Tool.present(RequestReceipt, toRequestReceipt),
|
|
400
407
|
)
|
|
401
408
|
|
|
402
|
-
const
|
|
403
|
-
name: "
|
|
404
|
-
instructions: ["
|
|
405
|
-
command:
|
|
409
|
+
const CreateRequestStage = Stage.command({
|
|
410
|
+
name: "create_request",
|
|
411
|
+
instructions: ["Create the confirmed request."],
|
|
412
|
+
command: CreateRequest,
|
|
406
413
|
})
|
|
407
414
|
```
|
|
408
415
|
|
|
@@ -431,10 +438,10 @@ Repair is disabled by default. Enable it only on chats whose final stage is a
|
|
|
431
438
|
repeatable query:
|
|
432
439
|
|
|
433
440
|
```ts
|
|
434
|
-
const
|
|
435
|
-
name: "
|
|
441
|
+
const RepairableResourceFinder = defineChat({
|
|
442
|
+
name: "resource_finder",
|
|
436
443
|
version: 2,
|
|
437
|
-
stages: [
|
|
444
|
+
stages: [RequestDetails, Lookup],
|
|
438
445
|
repair: Repair.standard({ maximumCorrections: 5 }),
|
|
439
446
|
})
|
|
440
447
|
```
|
|
@@ -550,21 +557,21 @@ import {
|
|
|
550
557
|
} from "@popcomputer/structured-chat/assistant-ui"
|
|
551
558
|
import type { ReactNode } from "react"
|
|
552
559
|
|
|
553
|
-
const
|
|
554
|
-
render:
|
|
560
|
+
const ResultCardsUI = makeAssistantView(ResultCards, {
|
|
561
|
+
render: ResultCardsComponent,
|
|
555
562
|
fallback: InvalidCardFallback,
|
|
556
563
|
})
|
|
557
564
|
|
|
558
565
|
const QuestionUI = makeAssistantView(CollectQuestionView, {
|
|
559
|
-
render:
|
|
566
|
+
render: RequestQuestion,
|
|
560
567
|
fallback: InvalidQuestionFallback,
|
|
561
568
|
})
|
|
562
569
|
|
|
563
570
|
const model = makeAssistantChatModelAdapter({
|
|
564
|
-
endpoint: "/api/
|
|
571
|
+
endpoint: "/api/resource-finder/turn",
|
|
565
572
|
})
|
|
566
573
|
|
|
567
|
-
export function
|
|
574
|
+
export function ResourceFinderRuntime({
|
|
568
575
|
children,
|
|
569
576
|
}: Readonly<{ children: ReactNode }>) {
|
|
570
577
|
const runtime = useLocalRuntime(model)
|
|
@@ -572,7 +579,7 @@ export function MatchmakerRuntime({
|
|
|
572
579
|
return (
|
|
573
580
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
574
581
|
<QuestionUI />
|
|
575
|
-
<
|
|
582
|
+
<ResultCardsUI />
|
|
576
583
|
{children}
|
|
577
584
|
</AssistantRuntimeProvider>
|
|
578
585
|
)
|
|
@@ -598,16 +605,16 @@ history as authoritative.
|
|
|
598
605
|
The default remains one call:
|
|
599
606
|
|
|
600
607
|
```ts
|
|
601
|
-
const result = yield*
|
|
608
|
+
const result = yield* Lookup.run(messages)
|
|
602
609
|
```
|
|
603
610
|
|
|
604
611
|
Approval flows, dry runs, and evals can stop after a strictly parsed proposal:
|
|
605
612
|
|
|
606
613
|
```ts
|
|
607
|
-
const call = yield*
|
|
614
|
+
const call = yield* Lookup.plan(messages)
|
|
608
615
|
|
|
609
616
|
// Application-owned review or approval.
|
|
610
|
-
const result = yield*
|
|
617
|
+
const result = yield* Lookup.toolSet.execute(call)
|
|
611
618
|
```
|
|
612
619
|
|
|
613
620
|
`plan` uses the same trusted instructions, closed tool set, guards, provider
|
|
@@ -627,15 +634,22 @@ import {
|
|
|
627
634
|
} from "@popcomputer/structured-chat/testing"
|
|
628
635
|
|
|
629
636
|
const ModelTest = Scenario.model(
|
|
630
|
-
Scenario.answers(
|
|
631
|
-
|
|
637
|
+
Scenario.answers(RequestDetails, {
|
|
638
|
+
goal: Scenario.quoted("prepare an onboarding plan", {
|
|
639
|
+
quote: "prepare an onboarding plan",
|
|
640
|
+
}),
|
|
641
|
+
audience: Scenario.quoted("new team members", {
|
|
642
|
+
quote: "new team members",
|
|
643
|
+
}),
|
|
644
|
+
}),
|
|
645
|
+
Scenario.call(FindResources, {
|
|
646
|
+
query: "onboarding plan for new team members",
|
|
632
647
|
}),
|
|
633
|
-
Scenario.call(SearchAgencies, { query: "Leeds public services" }),
|
|
634
648
|
)
|
|
635
649
|
|
|
636
|
-
const reply = yield*
|
|
650
|
+
const reply = yield* ResourceFinder.reply({
|
|
637
651
|
sessionId: "scenario-1",
|
|
638
|
-
message: "
|
|
652
|
+
message: "Help me prepare an onboarding plan for new team members.",
|
|
639
653
|
}).pipe(
|
|
640
654
|
Effect.provide(Layer.merge(ModelTest, inMemoryChatSessionStore)),
|
|
641
655
|
)
|
|
@@ -657,8 +671,8 @@ rules:
|
|
|
657
671
|
|
|
658
672
|
```ts
|
|
659
673
|
Scenario.repairs(
|
|
660
|
-
Scenario.replace(
|
|
661
|
-
quote: "Actually,
|
|
674
|
+
Scenario.replace(RequestDetails, "audience", "engineering managers", {
|
|
675
|
+
quote: "Actually, engineering managers",
|
|
662
676
|
}),
|
|
663
677
|
)
|
|
664
678
|
```
|
|
@@ -703,10 +717,10 @@ const InjectionPolicy = defineModelGuard({
|
|
|
703
717
|
PolicyService.checkCall({ messages, call }),
|
|
704
718
|
})
|
|
705
719
|
|
|
706
|
-
const
|
|
707
|
-
name: "
|
|
708
|
-
instructions: ["Route the completed
|
|
709
|
-
tools: [
|
|
720
|
+
const Lookup = Stage.tools({
|
|
721
|
+
name: "lookup",
|
|
722
|
+
instructions: ["Route the completed request to one resource lookup."],
|
|
723
|
+
tools: [FindResources],
|
|
710
724
|
guards: [InjectionPolicy],
|
|
711
725
|
})
|
|
712
726
|
```
|
|
@@ -3,7 +3,7 @@ import { StructuredChatModel, type ChatModelUnavailableReasonSchema, type Struct
|
|
|
3
3
|
import type { ModelToolDefinition } from "../core/tool.js";
|
|
4
4
|
import { type JsonValue } from "../core/json-value.js";
|
|
5
5
|
/** Bounded timeout for one provider tool-call request. */
|
|
6
|
-
export declare const StructuredChatRequestTimeoutSchema: Schema.
|
|
6
|
+
export declare const StructuredChatRequestTimeoutSchema: Schema.Number;
|
|
7
7
|
interface OpenAICompatibleMessage {
|
|
8
8
|
readonly role: "system" | "user";
|
|
9
9
|
readonly content: string;
|
|
@@ -21,10 +21,10 @@ type OpenAICompatibleInputValue = JsonValue | ReadonlyArray<OpenAICompatibleMess
|
|
|
21
21
|
interface OpenAICompatibleInput {
|
|
22
22
|
readonly [key: string]: OpenAICompatibleInputValue;
|
|
23
23
|
}
|
|
24
|
-
declare const OpenAICompatibleToolArgumentsSchema: Schema.
|
|
24
|
+
declare const OpenAICompatibleToolArgumentsSchema: Schema.Literals<readonly ["guided", "strict"]>;
|
|
25
25
|
type OpenAICompatibleToolArguments = Schema.Schema.Type<typeof OpenAICompatibleToolArgumentsSchema>;
|
|
26
26
|
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
27
|
-
export declare const StructuredChatModelIdSchema: Schema.
|
|
27
|
+
export declare const StructuredChatModelIdSchema: Schema.Trimmed;
|
|
28
28
|
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
29
29
|
export type StructuredChatModelId = Schema.Schema.Type<typeof StructuredChatModelIdSchema>;
|
|
30
30
|
/** One provider request with its parsed model identifier. */
|
|
@@ -44,7 +44,7 @@ export interface CloudflareWorkersAIProviderConfig extends OpenAICompatibleProvi
|
|
|
44
44
|
export interface OpenAIProviderConfig extends OpenAICompatibleProviderConfig {
|
|
45
45
|
}
|
|
46
46
|
/** Stable identifier for a built-in model provider. */
|
|
47
|
-
export declare const StructuredChatProviderIdSchema: Schema.
|
|
47
|
+
export declare const StructuredChatProviderIdSchema: Schema.Literals<readonly ["cloudflare-workers-ai", "openai"]>;
|
|
48
48
|
/** Stable identifier for a built-in model provider. */
|
|
49
49
|
export type StructuredChatProviderId = Schema.Schema.Type<typeof StructuredChatProviderIdSchema>;
|
|
50
50
|
interface StructuredChatProviderRuntime {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Exit, Layer, Schema } from "effect";
|
|
2
2
|
import { ChatModelUnavailable, StructuredChatModel, UnsupportedModelToolSchema, } from "../core/model.js";
|
|
3
3
|
import { JsonValueSchema, } from "../core/json-value.js";
|
|
4
4
|
/** Bounded timeout for one provider tool-call request. */
|
|
5
|
-
export const StructuredChatRequestTimeoutSchema = Schema.Number.
|
|
6
|
-
const OpenAICompatibleToolArgumentsSchema = Schema.
|
|
5
|
+
export const StructuredChatRequestTimeoutSchema = Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 60_000 }));
|
|
6
|
+
const OpenAICompatibleToolArgumentsSchema = Schema.Literals([
|
|
7
|
+
"guided",
|
|
8
|
+
"strict",
|
|
9
|
+
]);
|
|
7
10
|
/** Bounded provider model identifier used for routing and diagnostics. */
|
|
8
|
-
export const StructuredChatModelIdSchema = Schema.
|
|
11
|
+
export const StructuredChatModelIdSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(200));
|
|
9
12
|
/** Stable identifier for a built-in model provider. */
|
|
10
|
-
export const StructuredChatProviderIdSchema = Schema.
|
|
13
|
+
export const StructuredChatProviderIdSchema = Schema.Literals([
|
|
14
|
+
"cloudflare-workers-ai",
|
|
15
|
+
"openai",
|
|
16
|
+
]);
|
|
11
17
|
const StructuredChatProviderRuntime = Symbol("@popcomputer/structured-chat/StructuredChatProviderRuntime");
|
|
12
18
|
const openAIModelSupportsStrictToolArguments = (model) => /^(?:chat-latest$|gpt-4o(?:-|$)|gpt-4\.1(?:-|$)|gpt-5(?:[.-]|$)|o3(?:-|$)|o4(?:-|$))/u.test(model);
|
|
13
19
|
const makeProvider = (id, config, toolArguments) => {
|
|
@@ -45,28 +51,24 @@ export const ModelProvider = {
|
|
|
45
51
|
},
|
|
46
52
|
};
|
|
47
53
|
const ToolCallResponseSchema = Schema.Struct({
|
|
48
|
-
choices: Schema.Tuple(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
choices: Schema.Tuple([
|
|
55
|
+
Schema.Struct({
|
|
56
|
+
message: Schema.Struct({
|
|
57
|
+
tool_calls: Schema.Tuple([
|
|
58
|
+
Schema.Struct({
|
|
59
|
+
function: Schema.Struct({
|
|
60
|
+
name: Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(100)),
|
|
61
|
+
arguments: Schema.String.check(Schema.isMaxLength(20_000)),
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
]),
|
|
65
|
+
}),
|
|
56
66
|
}),
|
|
57
|
-
|
|
67
|
+
]),
|
|
58
68
|
});
|
|
59
69
|
const unavailable = (reason) => new ChatModelUnavailable({ reason });
|
|
60
|
-
const parseJson = (input) => Effect.
|
|
61
|
-
|
|
62
|
-
// parsing succeeds; failures are mapped to the typed unavailable reason.
|
|
63
|
-
try: () => JSON.parse(input),
|
|
64
|
-
catch: () => unavailable("invalid_response"),
|
|
65
|
-
});
|
|
66
|
-
const JsonSchemaObjectSchema = Schema.Record({
|
|
67
|
-
key: Schema.String,
|
|
68
|
-
value: JsonValueSchema,
|
|
69
|
-
});
|
|
70
|
+
const parseJson = (input) => Schema.decodeEffect(Schema.fromJsonString(JsonValueSchema))(input).pipe(Effect.mapError(() => unavailable("invalid_response")));
|
|
71
|
+
const JsonSchemaObjectSchema = Schema.Record(Schema.String, JsonValueSchema);
|
|
70
72
|
const isJsonSchemaObject = (value) => Schema.is(JsonSchemaObjectSchema)(value);
|
|
71
73
|
const appendJsonPointer = (path, segment) => `${path}/${segment.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
72
74
|
const findStrictObjectIssue = (schema, path) => {
|
|
@@ -145,15 +147,15 @@ const findStrictSchemaIssue = (schema) => {
|
|
|
145
147
|
return findStrictObjectIssue(schema, "#");
|
|
146
148
|
};
|
|
147
149
|
const strictToolIssue = (tool) => {
|
|
148
|
-
const parsedSchema = Schema.
|
|
149
|
-
if (
|
|
150
|
+
const parsedSchema = Schema.decodeUnknownExit(JsonSchemaObjectSchema)(tool.inputSchema, { onExcessProperty: "error" });
|
|
151
|
+
if (Exit.isFailure(parsedSchema)) {
|
|
150
152
|
return new UnsupportedModelToolSchema({
|
|
151
153
|
tool: tool.name,
|
|
152
154
|
path: "#",
|
|
153
155
|
reason: "root_not_object",
|
|
154
156
|
});
|
|
155
157
|
}
|
|
156
|
-
const issue = findStrictSchemaIssue(parsedSchema.
|
|
158
|
+
const issue = findStrictSchemaIssue(parsedSchema.value);
|
|
157
159
|
return issue === undefined
|
|
158
160
|
? undefined
|
|
159
161
|
: new UnsupportedModelToolSchema({
|
|
@@ -215,10 +217,10 @@ export const makeStructuredChatModel = (config) => {
|
|
|
215
217
|
requestTool: (request) => toProviderInput(request, runtime.requestOptions, runtime.toolArguments).pipe(Effect.flatMap((input) => Effect.tryPromise({
|
|
216
218
|
try: (signal) => runtime.complete(input, signal),
|
|
217
219
|
catch: (cause) => unavailable(classifyError(cause)),
|
|
218
|
-
})), Effect.
|
|
220
|
+
})), Effect.timeoutOrElse({
|
|
219
221
|
duration: timeoutMilliseconds,
|
|
220
|
-
|
|
221
|
-
}), Effect.flatMap((response) => Schema.
|
|
222
|
+
orElse: () => Effect.fail(unavailable("timed_out")),
|
|
223
|
+
}), Effect.flatMap((response) => Schema.decodeUnknownEffect(ToolCallResponseSchema)(response).pipe(Effect.mapError(() => unavailable("invalid_response")))), Effect.flatMap((response) => {
|
|
222
224
|
const tool = response.choices[0].message.tool_calls[0].function;
|
|
223
225
|
return parseJson(tool.arguments).pipe(Effect.map((arguments_) => ({
|
|
224
226
|
name: tool.name,
|
|
@@ -228,4 +230,4 @@ export const makeStructuredChatModel = (config) => {
|
|
|
228
230
|
};
|
|
229
231
|
};
|
|
230
232
|
/** Build an Effect layer for one provider-backed structured chat model. */
|
|
231
|
-
export const structuredChatModelLayer = (config) => Layer.succeed(StructuredChatModel, makeStructuredChatModel(config));
|
|
233
|
+
export const structuredChatModelLayer = (config) => Layer.succeed(StructuredChatModel, StructuredChatModel.of(makeStructuredChatModel(config)));
|
package/dist/core/answer.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { ChoiceQuestion, FixedQuestion, QuestionDefinition, QuestionDefinitionContract } from "./question.js";
|
|
3
3
|
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
4
|
-
export declare const AnswerModeSchema: Schema.
|
|
4
|
+
export declare const AnswerModeSchema: Schema.Literals<readonly ["semantic", "explicit", "confirmed"]>;
|
|
5
5
|
/** How strongly a collect-stage answer must be grounded in user messages. */
|
|
6
6
|
export type AnswerMode = Schema.Schema.Type<typeof AnswerModeSchema>;
|
|
7
7
|
/** Minimum runtime shape retained for every collect-stage answer. */
|
|
8
8
|
export interface AnswerDefinitionContract {
|
|
9
9
|
readonly _tag: "AnswerDefinition";
|
|
10
10
|
readonly mode: AnswerMode;
|
|
11
|
-
readonly schema: Schema.
|
|
11
|
+
readonly schema: Schema.ConstraintCodec<unknown, unknown>;
|
|
12
12
|
readonly description: string;
|
|
13
13
|
readonly question: QuestionDefinitionContract;
|
|
14
14
|
readonly validate?: (value: never) => Effect.Effect<void, unknown, unknown>;
|
|
@@ -20,16 +20,16 @@ export interface AnswerDefinitionContract {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/** One typed fact required by a collect stage. */
|
|
23
|
-
export interface AnswerDefinition<Mode extends AnswerMode, ValueSchema extends Schema.
|
|
23
|
+
export interface AnswerDefinition<Mode extends AnswerMode, ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error = never, Requirements = never> extends AnswerDefinitionContract {
|
|
24
24
|
readonly mode: Mode;
|
|
25
25
|
readonly schema: ValueSchema;
|
|
26
|
-
readonly question: QuestionDefinition<
|
|
27
|
-
readonly validate?: (value:
|
|
26
|
+
readonly question: QuestionDefinition<ValueSchema["Type"]>;
|
|
27
|
+
readonly validate?: (value: ValueSchema["Type"]) => Effect.Effect<void, Error, Requirements>;
|
|
28
28
|
readonly reject?: {
|
|
29
|
-
readonly ask: FixedQuestion | ChoiceQuestion<
|
|
29
|
+
readonly ask: FixedQuestion | ChoiceQuestion<ValueSchema["Type"]>;
|
|
30
30
|
};
|
|
31
31
|
readonly escape?: {
|
|
32
|
-
readonly value:
|
|
32
|
+
readonly value: ValueSchema["Type"];
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
interface DefineAnswerBase<Value> {
|
|
@@ -58,12 +58,12 @@ export interface DefineValidatedAnswerInput<Value, Error, Requirements> extends
|
|
|
58
58
|
}
|
|
59
59
|
/** Configuration shared by all answer grounding modes. */
|
|
60
60
|
export type DefineAnswerInput<Value, Error = never, Requirements = never> = DefineUnvalidatedAnswerInput<Value> | DefineValidatedAnswerInput<Value, Error, Requirements>;
|
|
61
|
-
declare function semantic<ValueSchema extends Schema.
|
|
62
|
-
declare function semantic<ValueSchema extends Schema.
|
|
63
|
-
declare function explicit<ValueSchema extends Schema.
|
|
64
|
-
declare function explicit<ValueSchema extends Schema.
|
|
65
|
-
declare function confirmed<ValueSchema extends Schema.
|
|
66
|
-
declare function confirmed<ValueSchema extends Schema.
|
|
61
|
+
declare function semantic<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"semantic", ValueSchema, never, never>;
|
|
62
|
+
declare function semantic<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"semantic", ValueSchema, Error, Requirements>;
|
|
63
|
+
declare function explicit<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"explicit", ValueSchema, never, never>;
|
|
64
|
+
declare function explicit<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"explicit", ValueSchema, Error, Requirements>;
|
|
65
|
+
declare function confirmed<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>>(schema: ValueSchema, input: DefineUnvalidatedAnswerInput<ValueSchema["Type"]>): AnswerDefinition<"confirmed", ValueSchema, never, never>;
|
|
66
|
+
declare function confirmed<ValueSchema extends Schema.ConstraintCodec<unknown, unknown>, Error, Requirements>(schema: ValueSchema, input: DefineValidatedAnswerInput<ValueSchema["Type"], Error, Requirements>): AnswerDefinition<"confirmed", ValueSchema, Error, Requirements>;
|
|
67
67
|
/** Constructors for semantic, explicit, and explicitly confirmed facts. */
|
|
68
68
|
export declare const Answer: {
|
|
69
69
|
readonly semantic: typeof semantic;
|