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