@popcomputer/structured-chat 0.1.0 → 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 +0 -1
- package/README.md +175 -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 +28 -15
- package/dist/core/collect-stage.js +58 -37
- package/dist/core/command.d.ts +1 -1
- package/dist/core/command.js +1 -1
- package/dist/core/debug-protocol.d.ts +126 -0
- package/dist/core/debug-protocol.js +19 -0
- package/dist/core/debug.d.ts +103 -0
- package/dist/core/debug.js +276 -0
- 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/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/integrations/assistant-ui-debug.d.ts +25 -0
- package/dist/integrations/assistant-ui-debug.js +1162 -0
- package/dist/integrations/assistant-ui.d.ts +10 -3
- package/dist/integrations/assistant-ui.js +48 -18
- 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 +15 -3
- package/examples/agency-search.ts +0 -101
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ComponentType, type FC } from "react";
|
|
2
|
+
import type { StructuredChatDebugSnapshot } from "../core/debug.js";
|
|
2
3
|
import type { ViewData, ViewDefinitionContract } from "../core/view.js";
|
|
3
4
|
import { type StructuredChatSessionReference, type StructuredChatAssistantMessage } from "../core/protocol.js";
|
|
4
5
|
/** Runtime status supplied by assistant-ui to a data-part renderer. */
|
|
@@ -52,6 +53,12 @@ export type AssistantChatFetch = (input: string, init: RequestInit) => Promise<R
|
|
|
52
53
|
export interface AssistantChatModelAdapterOptions {
|
|
53
54
|
readonly endpoint: string;
|
|
54
55
|
readonly fetch?: AssistantChatFetch;
|
|
56
|
+
/**
|
|
57
|
+
* Select the explicit debug response contract and receive its safe state
|
|
58
|
+
* projection after every successful turn. Observer failures are ignored so
|
|
59
|
+
* they cannot change the outcome of an already-persisted chat turn.
|
|
60
|
+
*/
|
|
61
|
+
readonly onDebugSnapshot?: (snapshot: StructuredChatDebugSnapshot) => void | Promise<void>;
|
|
55
62
|
}
|
|
56
63
|
/** Minimum assistant message shape consumed by the browser adapter. */
|
|
57
64
|
export interface AssistantChatThreadMessage {
|
|
@@ -88,8 +95,8 @@ export interface AssistantChatModelAdapter {
|
|
|
88
95
|
/**
|
|
89
96
|
* Adapt assistant-ui to one server-owned structured chat endpoint.
|
|
90
97
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
98
|
+
* By default, only the latest text and opaque prior revision cross the browser
|
|
99
|
+
* boundary. Supplying `onDebugSnapshot` explicitly selects the separate debug
|
|
100
|
+
* response contract and receives its safe answer-and-stage projection.
|
|
94
101
|
*/
|
|
95
102
|
export declare const makeAssistantChatModelAdapter: (options: AssistantChatModelAdapterOptions) => AssistantChatModelAdapter;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { makeAssistantDataUI, } from "@assistant-ui/core/react";
|
|
2
|
-
import {
|
|
2
|
+
import { Exit, Result, Schema } from "effect";
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema, StructuredChatTurnResponseSchema, } from "../core/protocol.js";
|
|
5
|
+
import { JsonValueSchema } from "../core/json-value.js";
|
|
5
6
|
/**
|
|
6
7
|
* Register one defineView contract as a strictly decoded assistant-ui data UI.
|
|
7
8
|
*
|
|
@@ -10,19 +11,25 @@ import { StructuredChatSessionReferenceSchema, StructuredChatTurnRequestSchema,
|
|
|
10
11
|
*/
|
|
11
12
|
export const makeAssistantView = (view, config) => {
|
|
12
13
|
const Renderer = (props) => {
|
|
13
|
-
const
|
|
14
|
+
const data = Schema.decodeUnknownResult(JsonValueSchema)(props.data);
|
|
15
|
+
if (Result.isFailure(data)) {
|
|
16
|
+
return config.fallback === undefined
|
|
17
|
+
? null
|
|
18
|
+
: createElement(config.fallback, props);
|
|
19
|
+
}
|
|
20
|
+
const decoded = view.decodeResult({
|
|
14
21
|
type: "data",
|
|
15
22
|
name: view.name,
|
|
16
|
-
data:
|
|
17
|
-
}
|
|
18
|
-
if (
|
|
23
|
+
data: data.success,
|
|
24
|
+
});
|
|
25
|
+
if (Result.isFailure(decoded)) {
|
|
19
26
|
return config.fallback === undefined
|
|
20
27
|
? null
|
|
21
28
|
: createElement(config.fallback, props);
|
|
22
29
|
}
|
|
23
30
|
return createElement(config.render, {
|
|
24
31
|
...props,
|
|
25
|
-
data: decoded.
|
|
32
|
+
data: decoded.success.data,
|
|
26
33
|
});
|
|
27
34
|
};
|
|
28
35
|
return makeAssistantDataUI({
|
|
@@ -32,6 +39,15 @@ export const makeAssistantView = (view, config) => {
|
|
|
32
39
|
};
|
|
33
40
|
/** assistant-ui metadata key carrying the latest opaque session reference. */
|
|
34
41
|
export const assistantChatSessionMetadataKey = "popcomputerStructuredChatSession";
|
|
42
|
+
const notifyDebugSnapshot = (callback, snapshot) => {
|
|
43
|
+
try {
|
|
44
|
+
const notified = callback(snapshot);
|
|
45
|
+
void Promise.resolve(notified).catch(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Debug observers are deliberately isolated from the persisted turn.
|
|
49
|
+
}
|
|
50
|
+
};
|
|
35
51
|
const readMessageText = (message) => message.content
|
|
36
52
|
.flatMap((part) => part.type === "text" && "text" in part ? [part.text] : [])
|
|
37
53
|
.join("\n")
|
|
@@ -42,9 +58,9 @@ const readLatestSession = (messages) => {
|
|
|
42
58
|
if (message?.role !== "assistant") {
|
|
43
59
|
continue;
|
|
44
60
|
}
|
|
45
|
-
const session = Schema.
|
|
46
|
-
if (
|
|
47
|
-
return session.
|
|
61
|
+
const session = Schema.decodeUnknownExit(StructuredChatSessionReferenceSchema)(message.metadata.custom[assistantChatSessionMetadataKey]);
|
|
62
|
+
if (Exit.isSuccess(session)) {
|
|
63
|
+
return session.value;
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
66
|
return undefined;
|
|
@@ -65,13 +81,14 @@ const readTurnRequest = (messages) => {
|
|
|
65
81
|
/**
|
|
66
82
|
* Adapt assistant-ui to one server-owned structured chat endpoint.
|
|
67
83
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
84
|
+
* By default, only the latest text and opaque prior revision cross the browser
|
|
85
|
+
* boundary. Supplying `onDebugSnapshot` explicitly selects the separate debug
|
|
86
|
+
* response contract and receives its safe answer-and-stage projection.
|
|
71
87
|
*/
|
|
72
88
|
export const makeAssistantChatModelAdapter = (options) => {
|
|
73
89
|
const fetch_ = options.fetch ??
|
|
74
90
|
((input, init) => globalThis.fetch(input, init));
|
|
91
|
+
const onDebugSnapshot = options.onDebugSnapshot;
|
|
75
92
|
return {
|
|
76
93
|
run: async ({ messages, abortSignal }) => {
|
|
77
94
|
const request = readTurnRequest(messages);
|
|
@@ -95,17 +112,30 @@ export const makeAssistantChatModelAdapter = (options) => {
|
|
|
95
112
|
catch {
|
|
96
113
|
throw new Error("Structured chat returned an invalid response");
|
|
97
114
|
}
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
115
|
+
let value;
|
|
116
|
+
if (onDebugSnapshot === undefined) {
|
|
117
|
+
const decoded = Schema.decodeUnknownExit(StructuredChatTurnResponseSchema)(body, { onExcessProperty: "error" });
|
|
118
|
+
if (Exit.isFailure(decoded)) {
|
|
119
|
+
throw new Error("Structured chat returned an invalid response");
|
|
120
|
+
}
|
|
121
|
+
value = decoded.value;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const { StructuredChatDebugTurnResponseSchema } = await import("../core/debug-protocol.js");
|
|
125
|
+
const decoded = Schema.decodeUnknownExit(StructuredChatDebugTurnResponseSchema)(body, { onExcessProperty: "error" });
|
|
126
|
+
if (Exit.isFailure(decoded)) {
|
|
127
|
+
throw new Error("Structured chat returned an invalid response");
|
|
128
|
+
}
|
|
129
|
+
notifyDebugSnapshot(onDebugSnapshot, decoded.value.debug);
|
|
130
|
+
value = decoded.value;
|
|
101
131
|
}
|
|
102
132
|
return {
|
|
103
|
-
content:
|
|
133
|
+
content: value.message.content,
|
|
104
134
|
metadata: {
|
|
105
|
-
custom:
|
|
135
|
+
custom: value.session === undefined
|
|
106
136
|
? {}
|
|
107
137
|
: {
|
|
108
|
-
[assistantChatSessionMetadataKey]:
|
|
138
|
+
[assistantChatSessionMetadataKey]: value.session,
|
|
109
139
|
},
|
|
110
140
|
},
|
|
111
141
|
};
|
|
@@ -30,7 +30,7 @@ const replaceSnapshot = (sessions, input) => {
|
|
|
30
30
|
return [snapshot, next];
|
|
31
31
|
};
|
|
32
32
|
/** In-memory optimistic session store intended for tests and examples. */
|
|
33
|
-
export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ({
|
|
33
|
+
export const inMemoryChatSessionStore = Layer.effect(ChatSessionStore, Ref.make(new Map()).pipe(Effect.map((sessions) => ChatSessionStore.of({
|
|
34
34
|
load: (scope) => Ref.get(sessions).pipe(Effect.map((current) => current.get(scopeKey(scope)) ?? null)),
|
|
35
35
|
replace: (input) => Ref.modify(sessions, (current) => replaceSnapshot(current, input)).pipe(Effect.flatMap((result) => result instanceof ChatSessionConflict
|
|
36
36
|
? Effect.fail(result)
|
package/dist/testing/scenario.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, Layer, Ref, Schema } from "effect";
|
|
|
2
2
|
import { StructuredChatModel, } from "../core/model.js";
|
|
3
3
|
import { JsonValueSchema, } from "../core/json-value.js";
|
|
4
4
|
const scenarioQuote = Symbol("@popcomputer/structured-chat/testing/ScenarioQuote");
|
|
5
|
-
const ScenarioQuoteSchema = Schema.
|
|
5
|
+
const ScenarioQuoteSchema = Schema.Trimmed.check(Schema.isNonEmpty(), Schema.isMaxLength(2_000));
|
|
6
6
|
const evidenceIndex = (request, quoted) => {
|
|
7
7
|
if (quoted.messageIndex !== undefined) {
|
|
8
8
|
const message = request.untrustedMessages[quoted.messageIndex];
|
|
@@ -49,7 +49,7 @@ const answers = (stage, proposed, options = {}) => ({
|
|
|
49
49
|
if (value === undefined || answer === undefined) {
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
encodedAnswers[field] = Schema.
|
|
52
|
+
encodedAnswers[field] = Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value.value));
|
|
53
53
|
evidenceIndex(request, value);
|
|
54
54
|
evidence.push({
|
|
55
55
|
field,
|
|
@@ -76,7 +76,7 @@ const answers = (stage, proposed, options = {}) => ({
|
|
|
76
76
|
const call = (tool, input) => ({
|
|
77
77
|
respond: () => ({
|
|
78
78
|
name: tool.name,
|
|
79
|
-
arguments: Schema.
|
|
79
|
+
arguments: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(tool.inputSchema)(input)),
|
|
80
80
|
}),
|
|
81
81
|
});
|
|
82
82
|
const replace = (stage, field, value, options) => {
|
|
@@ -92,7 +92,7 @@ const replace = (stage, field, value, options) => {
|
|
|
92
92
|
_tag: "ReplaceAcceptedAnswer",
|
|
93
93
|
stage: stage.name,
|
|
94
94
|
field,
|
|
95
|
-
value: Schema.
|
|
95
|
+
value: Schema.decodeUnknownSync(JsonValueSchema)(Schema.encodeSync(answer.schema)(value)),
|
|
96
96
|
evidence: {
|
|
97
97
|
quote: support.quote,
|
|
98
98
|
},
|
|
@@ -126,14 +126,14 @@ const repairs = (first, ...remaining) => ({
|
|
|
126
126
|
});
|
|
127
127
|
const model = (first, ...remaining) => Layer.effect(StructuredChatModel, Ref.make(0).pipe(Effect.map((cursor) => {
|
|
128
128
|
const steps = [first, ...remaining];
|
|
129
|
-
return {
|
|
129
|
+
return StructuredChatModel.of({
|
|
130
130
|
requestTool: (request) => Ref.getAndUpdate(cursor, (index) => index + 1).pipe(Effect.flatMap((index) => {
|
|
131
131
|
const step = steps[index];
|
|
132
132
|
return step === undefined
|
|
133
133
|
? Effect.die(new Error(`Scenario model exhausted after ${steps.length} requests`))
|
|
134
134
|
: Effect.sync(() => step.respond(request));
|
|
135
135
|
})),
|
|
136
|
-
};
|
|
136
|
+
});
|
|
137
137
|
})));
|
|
138
138
|
/** Typed constructors for concise valid transcript scenarios. */
|
|
139
139
|
export const Scenario = {
|
package/examples/answer-modes.ts
CHANGED
|
@@ -9,84 +9,95 @@ import {
|
|
|
9
9
|
|
|
10
10
|
const SearchCatalog = defineTool({
|
|
11
11
|
name: "search_catalog",
|
|
12
|
-
description: "Search the catalog for relevant
|
|
13
|
-
input: Schema.Struct({
|
|
12
|
+
description: "Search the catalog for relevant resources.",
|
|
13
|
+
input: Schema.Struct({
|
|
14
|
+
query: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
15
|
+
}),
|
|
14
16
|
execute: ({ query }) => Effect.succeed({ query }),
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
const Search = Stage.tools({
|
|
18
20
|
name: "search",
|
|
19
|
-
instructions: ["Search the catalog using the completed
|
|
21
|
+
instructions: ["Search the catalog using the completed request details."],
|
|
20
22
|
tools: [SearchCatalog],
|
|
21
23
|
})
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
name: "
|
|
25
|
+
const RequestDetails = Stage.collect({
|
|
26
|
+
name: "request_details",
|
|
25
27
|
questions: {
|
|
26
28
|
guidance: "Ask one concise, conversational question at a time.",
|
|
27
29
|
escape: "Not sure yet",
|
|
28
30
|
},
|
|
29
31
|
fields: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
goal: Answer.confirmed(
|
|
33
|
+
Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
34
|
+
{
|
|
35
|
+
description: "The outcome the user wants to achieve.",
|
|
36
|
+
ask: Question.adaptiveChoice(
|
|
37
|
+
"What would you like help accomplishing?",
|
|
38
|
+
{
|
|
39
|
+
minimumOptions: 2,
|
|
40
|
+
maximumOptions: 3,
|
|
41
|
+
fallbackOptions: [
|
|
42
|
+
"Understand a topic",
|
|
43
|
+
"Compare available options",
|
|
44
|
+
"Plan the next steps",
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
),
|
|
50
|
+
audience: Answer.confirmed(
|
|
51
|
+
Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
52
|
+
{
|
|
53
|
+
description: "Who the requested result is for.",
|
|
54
|
+
ask: Question.adaptive(
|
|
55
|
+
"Ask who will use the result and what they already know.",
|
|
56
|
+
{
|
|
57
|
+
fallback: "Who is this for, and what do they already know?",
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
),
|
|
54
62
|
},
|
|
55
63
|
})
|
|
56
64
|
|
|
57
65
|
/** A required ask-then-answer workflow before catalog search. */
|
|
58
|
-
export const
|
|
59
|
-
name: "
|
|
66
|
+
export const GuidedResourceFinder = defineChat({
|
|
67
|
+
name: "guided_resource_finder",
|
|
60
68
|
version: 1,
|
|
61
|
-
stages: [
|
|
69
|
+
stages: [RequestDetails, Search],
|
|
62
70
|
})
|
|
63
71
|
|
|
64
|
-
const
|
|
65
|
-
name: "
|
|
72
|
+
const RequestUnderstanding = Stage.collect({
|
|
73
|
+
name: "request_understanding",
|
|
66
74
|
fields: {
|
|
67
|
-
intent: Answer.semantic(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
intent: Answer.semantic(
|
|
76
|
+
Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
77
|
+
{
|
|
78
|
+
description:
|
|
79
|
+
"The user's searchable goal, subject, constraints, or desired outcome.",
|
|
80
|
+
ask: Question.adaptive(
|
|
81
|
+
"Ask one useful follow-up only when the request is not searchable yet.",
|
|
82
|
+
{
|
|
83
|
+
fallback: "What subject or outcome should I search for?",
|
|
84
|
+
},
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
),
|
|
77
88
|
},
|
|
78
89
|
})
|
|
79
90
|
|
|
80
91
|
/** A chat that can infer a searchable intent from the opening request. */
|
|
81
|
-
export const
|
|
82
|
-
name: "
|
|
92
|
+
export const FreeformResourceFinder = defineChat({
|
|
93
|
+
name: "freeform_resource_finder",
|
|
83
94
|
version: 1,
|
|
84
|
-
stages: [
|
|
95
|
+
stages: [RequestUnderstanding, Search],
|
|
85
96
|
})
|
|
86
97
|
|
|
87
98
|
/** A chat whose catalog search tool is available on the opening turn. */
|
|
88
|
-
export const
|
|
89
|
-
name: "
|
|
99
|
+
export const OpenResourceSearch = defineChat({
|
|
100
|
+
name: "open_resource_search",
|
|
90
101
|
version: 1,
|
|
91
102
|
stages: [Search],
|
|
92
103
|
})
|
|
@@ -21,9 +21,7 @@ export class UnsafeConversation extends Schema.TaggedError<UnsafeConversation>()
|
|
|
21
21
|
* Application-owned classifier; production may use any local or remote
|
|
22
22
|
* policy.
|
|
23
23
|
*/
|
|
24
|
-
export class ConversationSafety extends Context.
|
|
25
|
-
"ConversationSafety",
|
|
26
|
-
)<
|
|
24
|
+
export class ConversationSafety extends Context.Service<
|
|
27
25
|
ConversationSafety,
|
|
28
26
|
{
|
|
29
27
|
readonly checkConversation: (
|
|
@@ -33,34 +31,36 @@ export class ConversationSafety extends Context.Tag(
|
|
|
33
31
|
context: ModelGuardCallContext,
|
|
34
32
|
) => Effect.Effect<void, UnsafeConversation>
|
|
35
33
|
}
|
|
36
|
-
>() {}
|
|
34
|
+
>()("ConversationSafety") {}
|
|
37
35
|
|
|
38
36
|
/** Effect-native guard composed into any model-backed stage. */
|
|
39
37
|
export const PromptInjectionPolicy = defineModelGuard({
|
|
40
38
|
name: "prompt_injection_policy",
|
|
41
39
|
check: (context) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
),
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const safety = yield* ConversationSafety
|
|
42
|
+
return yield* safety.checkConversation(context)
|
|
43
|
+
}),
|
|
47
44
|
checkCall: (context) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const safety = yield* ConversationSafety
|
|
47
|
+
return yield* safety.checkCall(context)
|
|
48
|
+
}),
|
|
51
49
|
})
|
|
52
50
|
|
|
53
|
-
const
|
|
54
|
-
name: "
|
|
55
|
-
description: "Find
|
|
56
|
-
input: Schema.Struct({
|
|
51
|
+
const FindResources = defineTool({
|
|
52
|
+
name: "find_resources",
|
|
53
|
+
description: "Find resources relevant to the request.",
|
|
54
|
+
input: Schema.Struct({
|
|
55
|
+
query: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
56
|
+
}),
|
|
57
57
|
execute: ({ query }) => Effect.succeed({ query }),
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
/** Closed search stage protected by the application policy. */
|
|
61
|
-
export const
|
|
62
|
-
name: "
|
|
63
|
-
instructions: ["Route the completed
|
|
64
|
-
tools: [
|
|
61
|
+
export const Lookup = Stage.tools({
|
|
62
|
+
name: "lookup",
|
|
63
|
+
instructions: ["Route the completed request to one resource lookup."],
|
|
64
|
+
tools: [FindResources],
|
|
65
65
|
guards: [PromptInjectionPolicy],
|
|
66
66
|
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Effect,
|
|
4
|
+
Layer,
|
|
5
|
+
Schema,
|
|
6
|
+
} from "effect"
|
|
7
|
+
import {
|
|
8
|
+
defineTool,
|
|
9
|
+
defineView,
|
|
10
|
+
Message,
|
|
11
|
+
Stage,
|
|
12
|
+
Tool,
|
|
13
|
+
} from "@popcomputer/structured-chat"
|
|
14
|
+
|
|
15
|
+
interface ResourceMatch {
|
|
16
|
+
readonly id: string
|
|
17
|
+
readonly title: string
|
|
18
|
+
readonly summary: string
|
|
19
|
+
readonly internalScore: number
|
|
20
|
+
readonly retrievedText: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ResourceCatalog extends Context.Service<
|
|
24
|
+
ResourceCatalog,
|
|
25
|
+
{
|
|
26
|
+
readonly search: (
|
|
27
|
+
query: string,
|
|
28
|
+
) => Effect.Effect<ReadonlyArray<ResourceMatch>>
|
|
29
|
+
}
|
|
30
|
+
>()("ResourceCatalog") {}
|
|
31
|
+
|
|
32
|
+
const ResultCards = defineView({
|
|
33
|
+
name: "result_cards",
|
|
34
|
+
version: 1,
|
|
35
|
+
schema: Schema.Struct({
|
|
36
|
+
results: Schema.Array(
|
|
37
|
+
Schema.Struct({
|
|
38
|
+
id: Schema.String,
|
|
39
|
+
title: Schema.String,
|
|
40
|
+
summary: Schema.String,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const ResourceEvidence = Schema.Struct({
|
|
47
|
+
matches: Schema.Array(
|
|
48
|
+
Schema.Struct({
|
|
49
|
+
id: Schema.String,
|
|
50
|
+
summary: Schema.String,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const FindResources = defineTool({
|
|
56
|
+
name: "find_resources",
|
|
57
|
+
description: "Find resources relevant to the request.",
|
|
58
|
+
input: Schema.Struct({
|
|
59
|
+
query: Schema.Trimmed.check(Schema.isNonEmpty()),
|
|
60
|
+
}),
|
|
61
|
+
execute: ({ query }) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const catalog = yield* ResourceCatalog
|
|
64
|
+
return yield* catalog.search(query)
|
|
65
|
+
}),
|
|
66
|
+
}).pipe(
|
|
67
|
+
Tool.modelResult(ResourceEvidence, (matches) => ({
|
|
68
|
+
matches: matches.map(({ id, summary }) => ({ id, summary })),
|
|
69
|
+
})),
|
|
70
|
+
Tool.present(ResultCards, (matches) => ({
|
|
71
|
+
results: matches.map(({ id, title, summary }) => ({
|
|
72
|
+
id,
|
|
73
|
+
title,
|
|
74
|
+
summary,
|
|
75
|
+
})),
|
|
76
|
+
})),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
export const Lookup = Stage.tools({
|
|
80
|
+
name: "lookup",
|
|
81
|
+
instructions: ["Route the completed request to one resource lookup."],
|
|
82
|
+
tools: [FindResources],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const ResourceCatalogLive = Layer.succeed(ResourceCatalog, {
|
|
86
|
+
search: () =>
|
|
87
|
+
Effect.succeed([
|
|
88
|
+
{
|
|
89
|
+
id: "resource:1",
|
|
90
|
+
title: "Onboarding checklist",
|
|
91
|
+
summary: "A concise sequence of practical onboarding steps.",
|
|
92
|
+
internalScore: 0.93,
|
|
93
|
+
retrievedText: "Untrusted source text remains server-side.",
|
|
94
|
+
},
|
|
95
|
+
]),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const example = FindResources.execute({
|
|
99
|
+
query: "A practical onboarding guide",
|
|
100
|
+
}).pipe(Effect.provide(ResourceCatalogLive))
|
|
101
|
+
|
|
102
|
+
export const stageExample = Lookup.run([
|
|
103
|
+
Message.user("We need a practical onboarding guide."),
|
|
104
|
+
])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popcomputer/structured-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Schema-defined structured chats with typed tools, stages, and UI views",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.1",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"./assistant-ui": {
|
|
24
24
|
"types": "./dist/integrations/assistant-ui.d.ts",
|
|
25
25
|
"import": "./dist/integrations/assistant-ui.js"
|
|
26
|
+
},
|
|
27
|
+
"./assistant-ui/debug": {
|
|
28
|
+
"types": "./dist/integrations/assistant-ui-debug.d.ts",
|
|
29
|
+
"import": "./dist/integrations/assistant-ui-debug.js"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
28
32
|
"files": [
|
|
@@ -57,13 +61,21 @@
|
|
|
57
61
|
],
|
|
58
62
|
"author": "Patrick Ogilvie",
|
|
59
63
|
"license": "MIT",
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/PatrickOgilvie/popcomputer-structured-chat.git"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/PatrickOgilvie/popcomputer-structured-chat#readme",
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/PatrickOgilvie/popcomputer-structured-chat/issues"
|
|
71
|
+
},
|
|
60
72
|
"publishConfig": {
|
|
61
73
|
"access": "public"
|
|
62
74
|
},
|
|
63
75
|
"peerDependencies": {
|
|
64
76
|
"@assistant-ui/core": "^0.3.1",
|
|
65
77
|
"react": "^18.0.0 || ^19.0.0",
|
|
66
|
-
"effect": "^
|
|
78
|
+
"effect": "^4.0.0-rc.109"
|
|
67
79
|
},
|
|
68
80
|
"peerDependenciesMeta": {
|
|
69
81
|
"@assistant-ui/core": {
|
|
@@ -80,7 +92,7 @@
|
|
|
80
92
|
"@types/bun": "^1.3.14",
|
|
81
93
|
"@types/react": "^19.2.17",
|
|
82
94
|
"@types/react-dom": "^19.2.3",
|
|
83
|
-
"effect": "
|
|
95
|
+
"effect": "4.0.0-rc.109",
|
|
84
96
|
"oxlint": "1.78.0",
|
|
85
97
|
"react": "^19.2.4",
|
|
86
98
|
"react-dom": "^19.2.4",
|