-
-
Notifications
You must be signed in to change notification settings - Fork 711
Expand file tree
/
Copy path30_chat.ts
More file actions
158 lines (140 loc) · 6.3 KB
/
Copy path30_chat.ts
File metadata and controls
158 lines (140 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* @title Stateful chat sessions
*
* The AI `Chat` module maintains conversation history automatically. Build
* AI agents or chat assistants.
*/
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { Config, Context, DateTime, Effect, Layer, Ref, Schema } from "effect"
import { AiError, Chat, Prompt, Tool, Toolkit } from "effect/unstable/ai"
import { FetchHttpClient } from "effect/unstable/http"
// ---------------------------------------------------------------------------
// Provider setup
// ---------------------------------------------------------------------------
const OpenAiClientLayer = OpenAiClient.layerConfig({
apiKey: Config.Redacted("OPENAI_API_KEY")
}).pipe(Layer.provide(FetchHttpClient.layer))
// ---------------------------------------------------------------------------
// Tools for the agentic loop
// ---------------------------------------------------------------------------
const Tools = Toolkit.make(Tool.make("getCurrentTime", {
description: "Get the current time in ISO format",
parameters: Schema.Struct({
id: Schema.String
}),
success: Schema.String
}))
const ToolsLayer = Tools.toLayer(Effect.gen(function*() {
yield* Effect.logDebug("Initializing tools...")
return Tools.of({
getCurrentTime: Effect.fn("Tools.getCurrentTime")(function*(_) {
const now = yield* DateTime.now
return DateTime.formatIso(now)
})
})
}))
// ---------------------------------------------------------------------------
// Service that wraps Chat for a domain use-case
// ---------------------------------------------------------------------------
export class AiAssistantError extends Schema.TaggedError<AiAssistantError>()("AiAssistantError", {
reason: AiError.AiErrorReason
}) {
static fromAiError(error: AiError.AiError) {
return new AiAssistantError({ reason: error.reason })
}
}
export class AiAssistant extends Context.Service<AiAssistant, {
// Send a message while maintaining conversation history across turns.
chat(message: string): Effect.Effect<string, AiAssistantError>
// Ask a question and use an agentic loop with tool calls to answer it.
agent(question: string): Effect.Effect<string, AiAssistantError>
}>()("acme/AiAssistant") {
static readonly layer = Layer.effect(
AiAssistant,
Effect.gen(function*() {
// Choose the model you want to use for the chat sessions.
const modelLayer = yield* OpenAiLanguageModel.model("gpt-5.2").captureRequirements
// ---------------------------------------------------------------------------
// 1. Chat.empty — basic multi-turn conversation
// ---------------------------------------------------------------------------
// Create a new chat session with `Chat.empty` or `Chat.fromPrompt`. The
// session maintains conversation history automatically, so you can focus on
// the current turn without having to manage context.
const newSession = yield* Chat.fromPrompt(Prompt.empty.pipe(
Prompt.setSystem("You are a helpful assistant that answers questions.")
))
// You can also create a chat using a json export.
const json = yield* newSession.exportJson
const session = yield* Chat.fromJson(json)
const chat = Effect.fn("AiAssistant.chat")(
function*(message: string) {
// Create a new turn in the conversation by passing the user's message
// to `session.generateText`.
const response = yield* session.generateText({ prompt: message }).pipe(
// Provide the model layer to use.
// You could potentially use different models for different turns,
// or even switch models in the middle of a conversation.
Effect.provide(modelLayer)
)
// You can inspect the accumulated history at any point through the
// `history` ref on the chat instance.
const history = yield* Ref.get(session.history)
yield* Effect.logInfo(
`Conversation has ${history.content.length} messages`
)
return response.text
},
Effect.mapError((error) => AiAssistantError.fromAiError(error))
)
// ---------------------------------------------------------------------------
// 2. Create agentic loops with tools
// ---------------------------------------------------------------------------
const tools = yield* Tools
const agent = Effect.fn("AiAssistant.agent")(
function*(question: string) {
// We start the agent with a system prompt and the user question. The
// agent can then call tools in a loop until it decides to return a
// final answer.
const session = yield* Chat.fromPrompt([
{ role: "system", content: "You are an assistant that can use tools to answer questions." },
{ role: "user", content: question }
])
while (true) {
const response = yield* session.generateText({
prompt: [], // No additional prompt — the model has full access to the conversation history
toolkit: tools // Provide the tools to the model
}).pipe(
// Provide the model layer to use.
// You could potentially use different models for different turns,
// or even switch models in the middle of a conversation.
Effect.provide(modelLayer)
)
if (response.toolCalls.length > 0) {
// If the model called any tools, execute them and the Chat module
// will automatically add the tool results to the conversation
// history before the next turn.
continue
}
// If there are no tool calls, the model has returned a final answer
// and we can exit the loop.
return response.text
}
},
// Remap AI errors to our domain-specific error type, but die on
// unexpected errors.
Effect.catchTag(
"AiError",
(error) => Effect.fail(AiAssistantError.fromAiError(error)),
(e) => Effect.die(e)
)
)
return AiAssistant.of({
chat,
agent
})
})
).pipe(
// Provide the OpenAI client and tools layers to the AiAssistant service.
Layer.provide([OpenAiClientLayer, ToolsLayer])
)
}