@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/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-rc.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",
|
|
@@ -57,13 +57,21 @@
|
|
|
57
57
|
],
|
|
58
58
|
"author": "Patrick Ogilvie",
|
|
59
59
|
"license": "MIT",
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/PatrickOgilvie/popcomputer-structured-chat.git"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/PatrickOgilvie/popcomputer-structured-chat#readme",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/PatrickOgilvie/popcomputer-structured-chat/issues"
|
|
67
|
+
},
|
|
60
68
|
"publishConfig": {
|
|
61
69
|
"access": "public"
|
|
62
70
|
},
|
|
63
71
|
"peerDependencies": {
|
|
64
72
|
"@assistant-ui/core": "^0.3.1",
|
|
65
73
|
"react": "^18.0.0 || ^19.0.0",
|
|
66
|
-
"effect": "^
|
|
74
|
+
"effect": "^4.0.0-rc.109"
|
|
67
75
|
},
|
|
68
76
|
"peerDependenciesMeta": {
|
|
69
77
|
"@assistant-ui/core": {
|
|
@@ -80,7 +88,7 @@
|
|
|
80
88
|
"@types/bun": "^1.3.14",
|
|
81
89
|
"@types/react": "^19.2.17",
|
|
82
90
|
"@types/react-dom": "^19.2.3",
|
|
83
|
-
"effect": "
|
|
91
|
+
"effect": "4.0.0-rc.109",
|
|
84
92
|
"oxlint": "1.78.0",
|
|
85
93
|
"react": "^19.2.4",
|
|
86
94
|
"react-dom": "^19.2.4",
|
|
@@ -1,101 +0,0 @@
|
|
|
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 AgencyMatch {
|
|
16
|
-
readonly id: string
|
|
17
|
-
readonly name: string
|
|
18
|
-
readonly reason: string
|
|
19
|
-
readonly internalScore: number
|
|
20
|
-
readonly retrievedText: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class AgencyCatalog extends Context.Tag("AgencyCatalog")<
|
|
24
|
-
AgencyCatalog,
|
|
25
|
-
{
|
|
26
|
-
readonly search: (
|
|
27
|
-
query: string,
|
|
28
|
-
) => Effect.Effect<ReadonlyArray<AgencyMatch>>
|
|
29
|
-
}
|
|
30
|
-
>() {}
|
|
31
|
-
|
|
32
|
-
const AgencyCards = defineView({
|
|
33
|
-
name: "agency_cards",
|
|
34
|
-
version: 1,
|
|
35
|
-
schema: Schema.Struct({
|
|
36
|
-
agencies: Schema.Array(
|
|
37
|
-
Schema.Struct({
|
|
38
|
-
id: Schema.String,
|
|
39
|
-
name: Schema.String,
|
|
40
|
-
reason: Schema.String,
|
|
41
|
-
}),
|
|
42
|
-
),
|
|
43
|
-
}),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const AgencyEvidence = Schema.Struct({
|
|
47
|
-
matches: Schema.Array(
|
|
48
|
-
Schema.Struct({
|
|
49
|
-
id: Schema.String,
|
|
50
|
-
reason: Schema.String,
|
|
51
|
-
}),
|
|
52
|
-
),
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
export const SearchAgencies = defineTool({
|
|
56
|
-
name: "search_agencies",
|
|
57
|
-
description: "Find agencies relevant to the project.",
|
|
58
|
-
input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
|
|
59
|
-
execute: ({ query }) =>
|
|
60
|
-
AgencyCatalog.pipe(
|
|
61
|
-
Effect.flatMap((catalog) => catalog.search(query)),
|
|
62
|
-
),
|
|
63
|
-
}).pipe(
|
|
64
|
-
Tool.modelResult(AgencyEvidence, (matches) => ({
|
|
65
|
-
matches: matches.map(({ id, reason }) => ({ id, reason })),
|
|
66
|
-
})),
|
|
67
|
-
Tool.present(AgencyCards, (matches) => ({
|
|
68
|
-
agencies: matches.map(({ id, name, reason }) => ({
|
|
69
|
-
id,
|
|
70
|
-
name,
|
|
71
|
-
reason,
|
|
72
|
-
})),
|
|
73
|
-
})),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
export const Matching = Stage.tools({
|
|
77
|
-
name: "matching",
|
|
78
|
-
instructions: ["Route the project brief to one agency search."],
|
|
79
|
-
tools: [SearchAgencies],
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const AgencyCatalogLive = Layer.succeed(AgencyCatalog, {
|
|
83
|
-
search: () =>
|
|
84
|
-
Effect.succeed([
|
|
85
|
-
{
|
|
86
|
-
id: "agency:1",
|
|
87
|
-
name: "Northbank",
|
|
88
|
-
reason: "Relevant public-sector transformation work.",
|
|
89
|
-
internalScore: 0.93,
|
|
90
|
-
retrievedText: "Untrusted source text remains server-side.",
|
|
91
|
-
},
|
|
92
|
-
]),
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
export const example = SearchAgencies.execute({
|
|
96
|
-
query: "A public-sector digital service",
|
|
97
|
-
}).pipe(Effect.provide(AgencyCatalogLive))
|
|
98
|
-
|
|
99
|
-
export const stageExample = Matching.run([
|
|
100
|
-
Message.user("We need a public-sector digital service."),
|
|
101
|
-
])
|