@popcomputer/structured-chat 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +780 -0
  3. package/dist/adapters/openai-compatible-model.d.ts +89 -0
  4. package/dist/adapters/openai-compatible-model.js +231 -0
  5. package/dist/core/answer.d.ts +73 -0
  6. package/dist/core/answer.js +60 -0
  7. package/dist/core/chat.d.ts +124 -0
  8. package/dist/core/chat.js +490 -0
  9. package/dist/core/collect-stage.d.ts +203 -0
  10. package/dist/core/collect-stage.js +645 -0
  11. package/dist/core/command.d.ts +16 -0
  12. package/dist/core/command.js +17 -0
  13. package/dist/core/definition.d.ts +10 -0
  14. package/dist/core/definition.js +14 -0
  15. package/dist/core/json-value.d.ts +11 -0
  16. package/dist/core/json-value.js +3 -0
  17. package/dist/core/model-guard.d.ts +52 -0
  18. package/dist/core/model-guard.js +37 -0
  19. package/dist/core/model.d.ts +99 -0
  20. package/dist/core/model.js +109 -0
  21. package/dist/core/protocol.d.ts +257 -0
  22. package/dist/core/protocol.js +153 -0
  23. package/dist/core/question.d.ts +52 -0
  24. package/dist/core/question.js +62 -0
  25. package/dist/core/repair.d.ts +14 -0
  26. package/dist/core/repair.js +9 -0
  27. package/dist/core/session.d.ts +78 -0
  28. package/dist/core/session.js +34 -0
  29. package/dist/core/stage-name.d.ts +3 -0
  30. package/dist/core/stage-name.js +3 -0
  31. package/dist/core/stage.d.ts +88 -0
  32. package/dist/core/stage.js +104 -0
  33. package/dist/core/tool-set.d.ts +49 -0
  34. package/dist/core/tool-set.js +66 -0
  35. package/dist/core/tool.d.ts +149 -0
  36. package/dist/core/tool.js +215 -0
  37. package/dist/core/view.d.ts +95 -0
  38. package/dist/core/view.js +69 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.js +16 -0
  41. package/dist/integrations/assistant-ui.d.ts +95 -0
  42. package/dist/integrations/assistant-ui.js +114 -0
  43. package/dist/testing/in-memory-session-store.d.ts +4 -0
  44. package/dist/testing/in-memory-session-store.js +38 -0
  45. package/dist/testing/scenario.d.ts +59 -0
  46. package/dist/testing/scenario.js +147 -0
  47. package/dist/testing.d.ts +4 -0
  48. package/dist/testing.js +2 -0
  49. package/examples/agency-search.ts +101 -0
  50. package/examples/answer-modes.ts +92 -0
  51. package/examples/prompt-injection-policy.ts +66 -0
  52. package/package.json +89 -0
@@ -0,0 +1,92 @@
1
+ import { Effect, Schema } from "effect"
2
+ import {
3
+ Answer,
4
+ defineChat,
5
+ defineTool,
6
+ Question,
7
+ Stage,
8
+ } from "@popcomputer/structured-chat"
9
+
10
+ const SearchCatalog = defineTool({
11
+ name: "search_catalog",
12
+ description: "Search the catalog for relevant agency matches.",
13
+ input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
14
+ execute: ({ query }) => Effect.succeed({ query }),
15
+ })
16
+
17
+ const Search = Stage.tools({
18
+ name: "search",
19
+ instructions: ["Search the catalog using the completed project brief."],
20
+ tools: [SearchCatalog],
21
+ })
22
+
23
+ const ProjectBrief = Stage.collect({
24
+ name: "project_brief",
25
+ questions: {
26
+ guidance: "Ask one concise, conversational question at a time.",
27
+ escape: "Not sure yet",
28
+ },
29
+ fields: {
30
+ priority: Answer.confirmed(Schema.NonEmptyTrimmedString, {
31
+ description: "Where the client most needs outside help.",
32
+ ask: Question.adaptiveChoice(
33
+ "Where could an agency make the biggest difference?",
34
+ {
35
+ minimumOptions: 2,
36
+ maximumOptions: 3,
37
+ fallbackOptions: [
38
+ "Launch or grow a product",
39
+ "Build the brand long term",
40
+ "Fix a performance problem",
41
+ ],
42
+ },
43
+ ),
44
+ }),
45
+ location: Answer.confirmed(Schema.NonEmptyTrimmedString, {
46
+ description: "Where the client is based.",
47
+ ask: Question.adaptive(
48
+ "Ask where the client is based and whether location matters.",
49
+ {
50
+ fallback: "Where are you based, and does location matter?",
51
+ },
52
+ ),
53
+ }),
54
+ },
55
+ })
56
+
57
+ /** A required ask-then-answer workflow before catalog search. */
58
+ export const Matchmaker = defineChat({
59
+ name: "matchmaker",
60
+ version: 1,
61
+ stages: [ProjectBrief, Search],
62
+ })
63
+
64
+ const ProjectUnderstanding = Stage.collect({
65
+ name: "project_understanding",
66
+ fields: {
67
+ intent: Answer.semantic(Schema.NonEmptyTrimmedString, {
68
+ description:
69
+ "The client's searchable need, desired outcome, sector, or relevant experience.",
70
+ ask: Question.adaptive(
71
+ "Ask one useful follow-up only when the request is not searchable yet.",
72
+ {
73
+ fallback: "What outcome or experience should I search for?",
74
+ },
75
+ ),
76
+ }),
77
+ },
78
+ })
79
+
80
+ /** A chat that can infer a searchable intent from the opening request. */
81
+ export const FreeformMatchmaker = defineChat({
82
+ name: "freeform_matchmaker",
83
+ version: 1,
84
+ stages: [ProjectUnderstanding, Search],
85
+ })
86
+
87
+ /** A chat whose catalog search tool is available on the opening turn. */
88
+ export const OpenSearchChat = defineChat({
89
+ name: "open_search_chat",
90
+ version: 1,
91
+ stages: [Search],
92
+ })
@@ -0,0 +1,66 @@
1
+ import {
2
+ Context,
3
+ Effect,
4
+ Schema,
5
+ } from "effect"
6
+ import {
7
+ defineModelGuard,
8
+ defineTool,
9
+ Stage,
10
+ type ModelGuardCallContext,
11
+ type ModelGuardContext,
12
+ } from "@popcomputer/structured-chat"
13
+
14
+ /** Application policy rejection retained in the Effect error channel. */
15
+ export class UnsafeConversation extends Schema.TaggedError<UnsafeConversation>()(
16
+ "UnsafeConversation",
17
+ { reason: Schema.Literal("prompt_injection") },
18
+ ) {}
19
+
20
+ /**
21
+ * Application-owned classifier; production may use any local or remote
22
+ * policy.
23
+ */
24
+ export class ConversationSafety extends Context.Tag(
25
+ "ConversationSafety",
26
+ )<
27
+ ConversationSafety,
28
+ {
29
+ readonly checkConversation: (
30
+ context: ModelGuardContext,
31
+ ) => Effect.Effect<void, UnsafeConversation>
32
+ readonly checkCall: (
33
+ context: ModelGuardCallContext,
34
+ ) => Effect.Effect<void, UnsafeConversation>
35
+ }
36
+ >() {}
37
+
38
+ /** Effect-native guard composed into any model-backed stage. */
39
+ export const PromptInjectionPolicy = defineModelGuard({
40
+ name: "prompt_injection_policy",
41
+ check: (context) =>
42
+ ConversationSafety.pipe(
43
+ Effect.flatMap((safety) =>
44
+ safety.checkConversation(context),
45
+ ),
46
+ ),
47
+ checkCall: (context) =>
48
+ ConversationSafety.pipe(
49
+ Effect.flatMap((safety) => safety.checkCall(context)),
50
+ ),
51
+ })
52
+
53
+ const SearchAgencies = defineTool({
54
+ name: "search_agencies",
55
+ description: "Find agencies relevant to the project brief.",
56
+ input: Schema.Struct({ query: Schema.NonEmptyTrimmedString }),
57
+ execute: ({ query }) => Effect.succeed({ query }),
58
+ })
59
+
60
+ /** Closed search stage protected by the application policy. */
61
+ export const Matching = Stage.tools({
62
+ name: "matching",
63
+ instructions: ["Route the completed brief to one search."],
64
+ tools: [SearchAgencies],
65
+ guards: [PromptInjectionPolicy],
66
+ })
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@popcomputer/structured-chat",
3
+ "version": "0.1.0",
4
+ "description": "Schema-defined structured chats with typed tools, stages, and UI views",
5
+ "type": "module",
6
+ "packageManager": "bun@1.3.1",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "sideEffects": false,
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./testing": {
20
+ "types": "./dist/testing.d.ts",
21
+ "import": "./dist/testing.js"
22
+ },
23
+ "./assistant-ui": {
24
+ "types": "./dist/integrations/assistant-ui.d.ts",
25
+ "import": "./dist/integrations/assistant-ui.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "examples",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "clean": "node --input-type=module --eval \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
36
+ "build": "bun run clean && tsc",
37
+ "check": "tsc --noEmit && tsc -p tsconfig.type-tests.json && tsc -p tsconfig.examples.json",
38
+ "lint": "oxlint .",
39
+ "test": "bun test",
40
+ "test:node": "bun run build && node tests/node-esm-smoke.mjs",
41
+ "test:package:types": "tsc -p tsconfig.package-tests.json",
42
+ "test:package": "bun run build && bun run test:package:types && node tests/node-esm-smoke.mjs && npm pack --dry-run --ignore-scripts --offline --cache node_modules/.cache/npm",
43
+ "test:types": "tsc -p tsconfig.type-tests.json",
44
+ "verify": "bun run lint && bun run check && bun test && bun run test:package",
45
+ "prepack": "bun run build",
46
+ "prepublishOnly": "bun run verify"
47
+ },
48
+ "keywords": [
49
+ "chat",
50
+ "assistant-ui",
51
+ "effect",
52
+ "effect-ts",
53
+ "popcomputer",
54
+ "structured-output",
55
+ "tool-calling",
56
+ "workflow"
57
+ ],
58
+ "author": "Patrick Ogilvie",
59
+ "license": "MIT",
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "peerDependencies": {
64
+ "@assistant-ui/core": "^0.3.1",
65
+ "react": "^18.0.0 || ^19.0.0",
66
+ "effect": "^3.21.2"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@assistant-ui/core": {
70
+ "optional": true
71
+ },
72
+ "react": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "devDependencies": {
77
+ "@assistant-ui/core": "0.3.13",
78
+ "@assistant-ui/react": "^0.15.1",
79
+ "@oxlint/plugins": "1.78.0",
80
+ "@types/bun": "^1.3.14",
81
+ "@types/react": "^19.2.17",
82
+ "@types/react-dom": "^19.2.3",
83
+ "effect": "3.21.2",
84
+ "oxlint": "1.78.0",
85
+ "react": "^19.2.4",
86
+ "react-dom": "^19.2.4",
87
+ "typescript": "^5.9.3"
88
+ }
89
+ }