effect-assistant 0.3.0 → 0.4.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/dist/Assistant.d.ts +5 -11
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantEvent.d.ts +55 -7
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +61 -7
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +4 -2
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +4 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Compaction.d.ts +1 -1
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +13 -6
- package/dist/Compaction.js.map +1 -1
- package/dist/Skill.d.ts +85 -43
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +93 -44
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +6 -9
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +40 -6
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +74 -15
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +146 -0
- package/dist/ToolApproval.d.ts.map +1 -0
- package/dist/ToolApproval.js +201 -0
- package/dist/ToolApproval.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +6 -4
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/internal/chat.js +4 -1
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/history.js +28 -3
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +10 -5
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +54 -21
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +9 -16
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +5 -7
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +28 -18
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +18 -13
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +7 -13
- package/src/AssistantEvent.ts +60 -6
- package/src/AssistantLoop.ts +4 -2
- package/src/Compaction.ts +19 -2
- package/src/Skill.ts +134 -57
- package/src/Specialist.ts +8 -11
- package/src/Timeline.ts +104 -18
- package/src/ToolApproval.ts +249 -0
- package/src/ToolDiscovery.ts +8 -3
- package/src/index.ts +5 -0
- package/src/internal/chat.ts +11 -1
- package/src/internal/history.ts +58 -3
- package/src/internal/loop.ts +120 -34
- package/src/internal/specialists.ts +7 -13
- package/src/internal/toolkit.ts +47 -30
- package/src/internal/turns.ts +24 -7
- package/dist/internal/skills.js +0 -33
- package/dist/internal/skills.js.map +0 -1
- package/src/internal/skills.ts +0 -48
package/src/AssistantEvent.ts
CHANGED
|
@@ -140,13 +140,30 @@ export class SpecialistFinished extends Schema.TaggedClass<SpecialistFinished>()
|
|
|
140
140
|
error: Schema.optional(Schema.String)
|
|
141
141
|
}) {}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Represents the user's decision on a tool call that waits for their approval: `"approved"`, so
|
|
145
|
+
* the call runs, or `"rejected"`, with an optional short reason the model reads.
|
|
146
|
+
*
|
|
147
|
+
* Its id is fixed per call (see `ToolApproval.approve`), so a decision is stored once. A decision on
|
|
148
|
+
* a call that isn't waiting any more (decided, or expired) isn't stored.
|
|
149
|
+
*
|
|
150
|
+
* @category schemas
|
|
151
|
+
* @since 0.4.0
|
|
152
|
+
*/
|
|
153
|
+
export class ApprovalDecision extends Schema.TaggedClass<ApprovalDecision>()("ApprovalDecision", {
|
|
154
|
+
id: InputId,
|
|
155
|
+
callId: Schema.String,
|
|
156
|
+
decision: Schema.Literals(["approved", "rejected"]),
|
|
157
|
+
reason: Schema.optional(Schema.String.check(Schema.isMaxLength(500)))
|
|
158
|
+
}) {}
|
|
159
|
+
|
|
143
160
|
/**
|
|
144
161
|
* Schema for every input `Assistant.submit` accepts.
|
|
145
162
|
*
|
|
146
163
|
* @category schemas
|
|
147
164
|
* @since 0.1.0
|
|
148
165
|
*/
|
|
149
|
-
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished])
|
|
166
|
+
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished, ApprovalDecision])
|
|
150
167
|
|
|
151
168
|
/**
|
|
152
169
|
* Represents an input to an assistant.
|
|
@@ -157,7 +174,8 @@ export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished]
|
|
|
157
174
|
export type Input = typeof Input.Type
|
|
158
175
|
|
|
159
176
|
/**
|
|
160
|
-
* Schema for the inputs from the chat's user, which are messages and
|
|
177
|
+
* Schema for the inputs from the chat's user, which are messages, block answers, and decisions on
|
|
178
|
+
* tool calls that wait for their approval.
|
|
161
179
|
*
|
|
162
180
|
* A turn owes each one a reply. It is also what a chat client usually may submit, since the app's
|
|
163
181
|
* events come from the server.
|
|
@@ -165,7 +183,7 @@ export type Input = typeof Input.Type
|
|
|
165
183
|
* @category schemas
|
|
166
184
|
* @since 0.1.0
|
|
167
185
|
*/
|
|
168
|
-
export const UserInput = Schema.Union([UserMessage, BlockAnswer])
|
|
186
|
+
export const UserInput = Schema.Union([UserMessage, BlockAnswer, ApprovalDecision])
|
|
169
187
|
|
|
170
188
|
/**
|
|
171
189
|
* Represents an input from the chat's user.
|
|
@@ -400,6 +418,36 @@ export class BlocksClosed extends Schema.TaggedClass<BlocksClosed>()("BlocksClos
|
|
|
400
418
|
messageIds: Schema.Array(Schema.String)
|
|
401
419
|
}) {}
|
|
402
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Represents a tool call that waits for the user's approval instead of running: its tool needs
|
|
423
|
+
* approval (Effect's `needsApproval`).
|
|
424
|
+
*
|
|
425
|
+
* **Details**
|
|
426
|
+
*
|
|
427
|
+
* It holds the call's id, the tool, and its params, in JSON, as the model wrote them. The model
|
|
428
|
+
* reads it as the call's result, saying it waits for the user; the user's
|
|
429
|
+
* {@link ApprovalDecision} decides it, and an approved call runs then, with the same id.
|
|
430
|
+
*
|
|
431
|
+
* @category schemas
|
|
432
|
+
* @since 0.4.0
|
|
433
|
+
*/
|
|
434
|
+
export class ApprovalRequested extends Schema.TaggedClass<ApprovalRequested>()("ApprovalRequested", {
|
|
435
|
+
callId: Schema.String,
|
|
436
|
+
tool: Schema.String,
|
|
437
|
+
params: Schema.Json
|
|
438
|
+
}) {}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Represents tool calls that waited for the user's approval and won't run: the user moved on, like
|
|
442
|
+
* the ones `ToolApproval.closeStale` expires. The model reads that they weren't approved.
|
|
443
|
+
*
|
|
444
|
+
* @category schemas
|
|
445
|
+
* @since 0.4.0
|
|
446
|
+
*/
|
|
447
|
+
export class ApprovalsExpired extends Schema.TaggedClass<ApprovalsExpired>()("ApprovalsExpired", {
|
|
448
|
+
callIds: Schema.Array(Schema.String)
|
|
449
|
+
}) {}
|
|
450
|
+
|
|
403
451
|
/**
|
|
404
452
|
* Schema for every event the library defines.
|
|
405
453
|
*
|
|
@@ -419,6 +467,9 @@ export const AssistantEvent = Schema.Union([
|
|
|
419
467
|
SpecialistStarted,
|
|
420
468
|
SpecialistFinished,
|
|
421
469
|
BlocksClosed,
|
|
470
|
+
ApprovalRequested,
|
|
471
|
+
ApprovalDecision,
|
|
472
|
+
ApprovalsExpired,
|
|
422
473
|
ChatDeleted
|
|
423
474
|
])
|
|
424
475
|
|
|
@@ -554,7 +605,7 @@ export type Entry = typeof Entry.Type
|
|
|
554
605
|
|
|
555
606
|
/**
|
|
556
607
|
* Returns whether an event is one of the library's inputs, which are a user message, a block
|
|
557
|
-
* answer,
|
|
608
|
+
* answer, a finished specialist run, or a decision on a tool call waiting for approval.
|
|
558
609
|
*
|
|
559
610
|
* The application's events that came in through the assistant's `submit` are inputs too (see
|
|
560
611
|
* {@link isAppInput}).
|
|
@@ -565,7 +616,8 @@ export type Entry = typeof Entry.Type
|
|
|
565
616
|
export const isInput = (event: { readonly _tag: string }): event is Input =>
|
|
566
617
|
Predicate.isTagged(event, "UserMessage") ||
|
|
567
618
|
Predicate.isTagged(event, "BlockAnswer") ||
|
|
568
|
-
Predicate.isTagged(event, "SpecialistFinished")
|
|
619
|
+
Predicate.isTagged(event, "SpecialistFinished") ||
|
|
620
|
+
Predicate.isTagged(event, "ApprovalDecision")
|
|
569
621
|
|
|
570
622
|
/**
|
|
571
623
|
* Returns whether an input starts a turn, which every input does except a run the assistant
|
|
@@ -584,7 +636,9 @@ export const isWaking = (input: Input): boolean =>
|
|
|
584
636
|
* @since 0.1.0
|
|
585
637
|
*/
|
|
586
638
|
export const isUserInput = (input: Input): input is UserInput =>
|
|
587
|
-
Predicate.isTagged(input, "UserMessage") ||
|
|
639
|
+
Predicate.isTagged(input, "UserMessage") ||
|
|
640
|
+
Predicate.isTagged(input, "BlockAnswer") ||
|
|
641
|
+
Predicate.isTagged(input, "ApprovalDecision")
|
|
588
642
|
|
|
589
643
|
/**
|
|
590
644
|
* Returns whether an event ends a turn, and so says how far the assistant has read.
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -161,8 +161,10 @@ const toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>
|
|
|
161
161
|
* the step's result and decides what comes next, with `AssistantLoop.next(state)` or
|
|
162
162
|
* `AssistantLoop.stop()`.
|
|
163
163
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
164
|
+
* In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an
|
|
165
|
+
* `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A
|
|
166
|
+
* call the user approved runs at the start of the next step, before the model is called. A
|
|
167
|
+
* specialist's run can't wait for the user: such a call fails, and the run reads why.
|
|
166
168
|
*
|
|
167
169
|
* @category constructors
|
|
168
170
|
* @since 0.1.0
|
package/src/Compaction.ts
CHANGED
|
@@ -18,6 +18,8 @@ import * as AssistantActivity from "./AssistantActivity.ts"
|
|
|
18
18
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
19
19
|
import * as BlockKit from "./BlockKit.ts"
|
|
20
20
|
import { userText } from "./internal/history.ts"
|
|
21
|
+
import { toJsonText } from "./internal/schema.ts"
|
|
22
|
+
import * as ToolApproval from "./ToolApproval.ts"
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Represents the fields a summary event must have: its text and the last `seq` it covers.
|
|
@@ -54,7 +56,7 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
|
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't
|
|
57
|
-
* answered,
|
|
59
|
+
* answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
|
|
58
60
|
* the model, or `None` when nothing is open.
|
|
59
61
|
*
|
|
60
62
|
* @category utils
|
|
@@ -100,9 +102,24 @@ export const openItems = (
|
|
|
100
102
|
: []
|
|
101
103
|
)
|
|
102
104
|
|
|
105
|
+
const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>
|
|
106
|
+
approval.status === "pending" &&
|
|
107
|
+
opened(
|
|
108
|
+
seqOf(
|
|
109
|
+
(event) =>
|
|
110
|
+
AssistantEvent.isLibraryEvent(event) &&
|
|
111
|
+
Predicate.isTagged(event, "ApprovalRequested") &&
|
|
112
|
+
event.callId === approval.request.callId
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`]
|
|
116
|
+
: []
|
|
117
|
+
)
|
|
118
|
+
|
|
103
119
|
const sections = [
|
|
104
120
|
blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
|
|
105
|
-
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : ""
|
|
121
|
+
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : "",
|
|
122
|
+
approvals.length > 0 ? `Calls waiting for the user's approval:\n${approvals.join("\n")}` : ""
|
|
106
123
|
].filter((section) => section.length > 0)
|
|
107
124
|
|
|
108
125
|
return sections.length === 0 ? Option.none() : Option.some(sections.join("\n\n"))
|
package/src/Skill.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Defines skills, which are playbooks with step-by-step instructions for a kind of task.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
* task matches.
|
|
4
|
+
* A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its
|
|
5
|
+
* description is the catalog, each skill's name and when to use it, and the model calls it to read
|
|
6
|
+
* a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.
|
|
7
7
|
*
|
|
8
8
|
* @since 0.1.0
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import * as Tool from "effect/ai/Tool"
|
|
11
|
+
import * as AiToolkit from "effect/ai/Toolkit"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import type * as Layer from "effect/Layer"
|
|
14
|
+
import * as Schema from "effect/Schema"
|
|
15
|
+
import * as Instructions from "./Instructions.ts"
|
|
16
|
+
import * as ToolFailure from "./ToolFailure.ts"
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Represents a skill.
|
|
@@ -24,11 +29,6 @@ export interface Skill<R = never> {
|
|
|
24
29
|
readonly description: string
|
|
25
30
|
/** Text, or an Effect resolved each time the skill is loaded. */
|
|
26
31
|
readonly instructions: Instructions.Instructions<R>
|
|
27
|
-
/**
|
|
28
|
-
* The tools its instructions use. A loop offers the skill only when its toolkit has every one of
|
|
29
|
-
* them, matched by name. Unset, a loop always offers it.
|
|
30
|
-
*/
|
|
31
|
-
readonly tools?: ReadonlyArray<Tool.Any> | undefined
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -38,12 +38,7 @@ export interface Skill<R = never> {
|
|
|
38
38
|
*
|
|
39
39
|
* Write the description as a trigger ("Use when…"), since it's all the model sees until it loads
|
|
40
40
|
* the skill. The instructions are text, or an Effect that is resolved each time the skill is
|
|
41
|
-
* loaded.
|
|
42
|
-
*
|
|
43
|
-
* `tools` lists the tools the instructions use. A loop whose toolkit lacks any of them, matched by
|
|
44
|
-
* name, leaves the skill out of its catalog and of `load_skill`, so a loop that gets its tools per
|
|
45
|
-
* user (by role, say) never offers a playbook it can't follow. A tool that only `ToolDiscovery`
|
|
46
|
-
* finds isn't in the toolkit, so it doesn't count. Without `tools`, every loop offers the skill.
|
|
41
|
+
* loaded. Offer skills to a loop with {@link toolkit}.
|
|
47
42
|
*
|
|
48
43
|
* **Example** (Defining a playbook for ordering campaigns)
|
|
49
44
|
*
|
|
@@ -56,30 +51,6 @@ export interface Skill<R = never> {
|
|
|
56
51
|
* 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.
|
|
57
52
|
* 3. When you have both, call start_campaign.`
|
|
58
53
|
* })
|
|
59
|
-
*
|
|
60
|
-
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
61
|
-
* turn.loop({ instructions, skills: [orderingCampaigns] })
|
|
62
|
-
* )
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* **Example** (Offering a skill only with the tools it uses)
|
|
66
|
-
*
|
|
67
|
-
* ```ts
|
|
68
|
-
* import { Skill } from "effect-assistant"
|
|
69
|
-
* import * as Tool from "effect/ai/Tool"
|
|
70
|
-
* import * as Schema from "effect/Schema"
|
|
71
|
-
*
|
|
72
|
-
* const StartCampaign = Tool.make("start_campaign", {
|
|
73
|
-
* parameters: Schema.Struct({ campaignId: Schema.String, budget: Schema.Finite }),
|
|
74
|
-
* success: Schema.String
|
|
75
|
-
* })
|
|
76
|
-
*
|
|
77
|
-
* // Left out of a loop whose toolkit has no start_campaign, like a viewer's.
|
|
78
|
-
* const orderingCampaigns = Skill.make("Ordering a campaign", {
|
|
79
|
-
* description: "Use when the user wants to order or start a campaign.",
|
|
80
|
-
* instructions: "When you know who it should reach and the budget, call start_campaign.",
|
|
81
|
-
* tools: [StartCampaign]
|
|
82
|
-
* })
|
|
83
54
|
* ```
|
|
84
55
|
*
|
|
85
56
|
* @category constructors
|
|
@@ -92,10 +63,8 @@ export const make = <R = never>(
|
|
|
92
63
|
readonly description: string
|
|
93
64
|
/** Text, or an Effect resolved each time the skill is loaded. */
|
|
94
65
|
readonly instructions: Instructions.Instructions<R>
|
|
95
|
-
/** The tools its instructions use: a loop whose toolkit lacks one leaves the skill out. */
|
|
96
|
-
readonly tools?: ReadonlyArray<Tool.Any> | undefined
|
|
97
66
|
}
|
|
98
|
-
): Skill<R> => ({ name, description: options.description, instructions: options.instructions
|
|
67
|
+
): Skill<R> => ({ name, description: options.description, instructions: options.instructions })
|
|
99
68
|
|
|
100
69
|
/**
|
|
101
70
|
* Extracts the services that skills need when they're loaded.
|
|
@@ -105,20 +74,128 @@ export const make = <R = never>(
|
|
|
105
74
|
*/
|
|
106
75
|
export type Services<S> = S extends Skill<infer R> ? R : never
|
|
107
76
|
|
|
77
|
+
const loadSkill = (description: string) =>
|
|
78
|
+
Tool.make("load_skill", {
|
|
79
|
+
description,
|
|
80
|
+
parameters: Schema.Struct({ name: Schema.String }),
|
|
81
|
+
success: Schema.String,
|
|
82
|
+
failure: ToolFailure.ToolFailure
|
|
83
|
+
})
|
|
84
|
+
|
|
108
85
|
/**
|
|
109
|
-
*
|
|
110
|
-
* skills.
|
|
86
|
+
* Represents the `load_skill` tool, which reads a skill's instructions by name.
|
|
111
87
|
*
|
|
112
|
-
* @category
|
|
113
|
-
* @since 0.
|
|
88
|
+
* @category models
|
|
89
|
+
* @since 0.4.0
|
|
90
|
+
*/
|
|
91
|
+
export type LoadSkill = ReturnType<typeof loadSkill>
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.
|
|
95
|
+
*
|
|
96
|
+
* `R` is the services the skills' instructions need, which the layer requires.
|
|
97
|
+
*
|
|
98
|
+
* @category models
|
|
99
|
+
* @since 0.4.0
|
|
100
|
+
*/
|
|
101
|
+
export interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {
|
|
102
|
+
/** The handler of `load_skill`, which resolves the instructions of these skills. */
|
|
103
|
+
readonly layer: Layer.Layer<Tool.Handler<"load_skill">, never, R>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>
|
|
107
|
+
[
|
|
108
|
+
"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.",
|
|
109
|
+
"",
|
|
110
|
+
"Skills:",
|
|
111
|
+
...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
|
|
112
|
+
].join("\n")
|
|
113
|
+
|
|
114
|
+
function toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(
|
|
115
|
+
skills: Skills
|
|
116
|
+
): Toolkit<Services<Skills[number]>>
|
|
117
|
+
// The overload states what the layer requires: the services of these skills' instructions.
|
|
118
|
+
function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
|
|
119
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]))
|
|
120
|
+
const tools = AiToolkit.make(loadSkill(catalog(skills)))
|
|
121
|
+
|
|
122
|
+
const layer = tools.toLayer(
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
const context = yield* Effect.context<unknown>()
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
load_skill: Effect.fnUntraced(function* ({ name }) {
|
|
128
|
+
const skill = byName.get(name)
|
|
129
|
+
|
|
130
|
+
if (skill === undefined) {
|
|
131
|
+
return yield* ToolFailure.fail(
|
|
132
|
+
`No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(", ")}.`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))
|
|
137
|
+
|
|
138
|
+
return resolved.text
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return Object.assign(tools, { layer })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each
|
|
149
|
+
* skill's name and when to use it, and its handler's `layer`.
|
|
150
|
+
*
|
|
151
|
+
* **Details**
|
|
152
|
+
*
|
|
153
|
+
* Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires
|
|
154
|
+
* the services the skills' instructions need. The model loads a skill by name; a name that isn't
|
|
155
|
+
* in the list is a failure the model reads, with the names there are.
|
|
156
|
+
*
|
|
157
|
+
* The handler resolves only the skills of its own list. To offer each user the skills they may
|
|
158
|
+
* use, build the toolkit in the turn from that user's list, and provide its layer to the loop's
|
|
159
|
+
* stream (`Stream.provide(skills.layer)`): building one does no work.
|
|
160
|
+
*
|
|
161
|
+
* **Example** (Giving an assistant its playbooks)
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { Skill } from "effect-assistant"
|
|
165
|
+
* import * as Layer from "effect/Layer"
|
|
166
|
+
* import * as Toolkit from "effect/ai/Toolkit"
|
|
167
|
+
*
|
|
168
|
+
* const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
|
|
169
|
+
*
|
|
170
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
171
|
+
* turn.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
|
|
172
|
+
* ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* **Example** (Offering each user the skills their role allows)
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { Skill } from "effect-assistant"
|
|
179
|
+
* import * as Effect from "effect/Effect"
|
|
180
|
+
* import * as Stream from "effect/Stream"
|
|
181
|
+
* import * as Toolkit from "effect/ai/Toolkit"
|
|
182
|
+
*
|
|
183
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
184
|
+
* Stream.unwrap(
|
|
185
|
+
* Effect.map(roleOf(turn.chatId), (role) => {
|
|
186
|
+
* const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
|
|
187
|
+
*
|
|
188
|
+
* return turn
|
|
189
|
+
* .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
|
|
190
|
+
* .pipe(Stream.provide(skills.layer))
|
|
191
|
+
* })
|
|
192
|
+
* )
|
|
193
|
+
* )
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @category constructors
|
|
197
|
+
* @since 0.4.0
|
|
114
198
|
*/
|
|
115
|
-
export const
|
|
116
|
-
skills
|
|
117
|
-
|
|
118
|
-
: [
|
|
119
|
-
prompt,
|
|
120
|
-
"",
|
|
121
|
-
"## Skills",
|
|
122
|
-
"Playbooks for common tasks. When one matches what you're doing, load it with load_skill in the same step as your first lookups, and follow it.",
|
|
123
|
-
...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
|
|
124
|
-
].join("\n")
|
|
199
|
+
export const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(
|
|
200
|
+
skills: Skills
|
|
201
|
+
) => Toolkit<Services<Skills[number]>> = toolkitOf
|
package/src/Specialist.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type { ChatId } from "./ChatId.ts"
|
|
|
23
23
|
import type { CurrentChat } from "./CurrentChat.ts"
|
|
24
24
|
import type * as Instructions from "./Instructions.ts"
|
|
25
25
|
import type { RunId } from "./RunId.ts"
|
|
26
|
-
import type { Skill } from "./Skill.ts"
|
|
27
26
|
|
|
28
27
|
const TypeId = "~effect-assistant/Specialist"
|
|
29
28
|
|
|
@@ -123,15 +122,13 @@ export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel
|
|
|
123
122
|
* @category models
|
|
124
123
|
* @since 0.1.0
|
|
125
124
|
*/
|
|
126
|
-
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never
|
|
125
|
+
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
|
|
127
126
|
readonly instructions: string | Instructions.Resolved
|
|
128
|
-
/** The loop's tools, as a native toolkit whose handlers come from the layer. */
|
|
129
|
-
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
130
127
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
128
|
+
* The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
|
|
129
|
+
* `Skill.toolkit(...)` for playbooks.
|
|
133
130
|
*/
|
|
134
|
-
readonly
|
|
131
|
+
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
135
132
|
/** The model calls allowed per brief. Defaults to 8. */
|
|
136
133
|
readonly maxSteps?: number | undefined
|
|
137
134
|
}
|
|
@@ -152,13 +149,13 @@ export interface Run {
|
|
|
152
149
|
* (follow-ups and steering). Read it before every model call, as the loop does.
|
|
153
150
|
*/
|
|
154
151
|
readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
|
|
155
|
-
/** Runs the agent loop over the run's history, with the given instructions
|
|
156
|
-
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never
|
|
157
|
-
options: LoopOptions<T
|
|
152
|
+
/** Runs the agent loop over the run's history, with the given instructions and tools. */
|
|
153
|
+
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
154
|
+
options: LoopOptions<T>
|
|
158
155
|
) => Stream.Stream<
|
|
159
156
|
AssistantLoop.Event,
|
|
160
157
|
AiError.AiError,
|
|
161
|
-
LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>
|
|
158
|
+
LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>
|
|
162
159
|
>
|
|
163
160
|
}
|
|
164
161
|
|