@luanpoppe/ai 1.1.2 → 1.1.3

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/src/ai.ts CHANGED
@@ -1,306 +1,306 @@
1
- import { AIModels, LLMModelConfig } from "./langchain/models";
2
- import z from "zod";
3
- import { createAgent, modelRetryMiddleware } from "langchain";
4
- import type { AIAgent } from "./@types/agent";
5
-
6
- import {
7
- AIMemory,
8
- type BaseCheckpointSaver,
9
- type MemoryConfig,
10
- } from "./langchain/checkpointers";
11
- import type { AIModelNames } from "./@types/model-names";
12
- import type {
13
- AICallParams,
14
- AICallReturn,
15
- AICallStructuredOutputParams,
16
- AICallStructuredOutputReturn,
17
- } from "./@types/ai-call";
18
-
19
- export type {
20
- AICallParams,
21
- AICallReturn,
22
- AICallStructuredOutputParams,
23
- AICallStructuredOutputReturn,
24
- } from "./@types/ai-call";
25
-
26
- type AIConstructor = {
27
- googleGeminiToken?: string;
28
- openAIApiKey?: string;
29
- openRouterApiKey?: string;
30
- /** Lista padrão de modelos de fallback (usada em call/callStructuredOutput quando não passada no método) */
31
- aiModelsFallback?: AIModelNames[];
32
- /** Configuração de persistência de histórico (memory, sqlite, postgres, redis, mongodb) ou instância AIMemory */
33
- memory?: MemoryConfig | AIMemory;
34
- /** Instância de checkpointer para usuários avançados (alternativa a memory) */
35
- checkpointer?: BaseCheckpointSaver;
36
- };
37
-
38
- export class AI {
39
- private _memory: AIMemory | undefined;
40
-
41
- /**
42
- * Instância de AIMemory. Lança exceção se memory não estiver configurado.
43
- *
44
- * @example
45
- * const { fullHistory, messages } = await ai.memory.getHistory(threadId);
46
- */
47
- get memory(): AIMemory {
48
- if (!this._memory) {
49
- throw new Error(
50
- "memory não está configurado. Passe memory no construtor do AI (ex: memory: { type: 'memory' }).",
51
- );
52
- }
53
- return this._memory;
54
- }
55
-
56
- private checkpointer: BaseCheckpointSaver | undefined;
57
- private checkpointerPromise: Promise<BaseCheckpointSaver> | undefined;
58
-
59
- constructor(private config: AIConstructor) {
60
- if (config.checkpointer) {
61
- this.checkpointer = config.checkpointer;
62
- }
63
- if (config.memory) {
64
- this._memory =
65
- config.memory instanceof AIMemory
66
- ? config.memory
67
- : new AIMemory(config.memory);
68
- }
69
- }
70
-
71
- private async getCheckpointer(): Promise<BaseCheckpointSaver | undefined> {
72
- if (this.checkpointer) return this.checkpointer;
73
- if (this._memory) {
74
- if (!this.checkpointerPromise) {
75
- this.checkpointerPromise = this._memory.getCheckpointer();
76
- }
77
- this.checkpointer = await this.checkpointerPromise;
78
- return this.checkpointer;
79
- }
80
- return undefined;
81
- }
82
-
83
- private ensureThreadIdWhenCheckpointer(params: AICallParams): void {
84
- if (this.config.checkpointer || this.config.memory !== undefined) {
85
- if (!params.threadId) {
86
- throw new Error(
87
- "threadId é obrigatório quando memory ou checkpointer está configurado. " +
88
- "Passe threadId em AICallParams para identificar a conversa.",
89
- );
90
- }
91
- }
92
- }
93
-
94
- private async invokeWithRetryAndFallback<T>(
95
- params: AICallParams,
96
- createAgentForModel: (paramsForModel: AICallParams) => AIAgent,
97
- execute: (agent: AIAgent) => Promise<T>,
98
- ): Promise<{ result: T; agent: AIAgent }> {
99
- const fallback =
100
- params.aiModelsFallback ?? this.config.aiModelsFallback ?? [];
101
- const models: (typeof params.aiModel)[] = [params.aiModel, ...fallback];
102
- let lastError: unknown;
103
-
104
- for (const aiModel of models) {
105
- const paramsForModel = { ...params, aiModel };
106
- const agent = createAgentForModel(paramsForModel);
107
-
108
- try {
109
- const result = await execute(agent);
110
- return { result, agent };
111
- } catch (error) {
112
- lastError = error;
113
- }
114
- }
115
-
116
- throw lastError;
117
- }
118
-
119
- async call(params: AICallParams): AICallReturn {
120
- const { messages } = params;
121
-
122
- this.ensureThreadIdWhenCheckpointer(params);
123
- const checkpointer = await this.getCheckpointer();
124
-
125
- const invokeConfig =
126
- params.threadId && checkpointer
127
- ? { configurable: { thread_id: params.threadId } }
128
- : undefined;
129
-
130
- const { result: response, agent } = await this.invokeWithRetryAndFallback(
131
- params,
132
- (paramsForModel) =>
133
- createAgent({
134
- ...this.standardAgent(paramsForModel, checkpointer),
135
- }),
136
- (agent) => agent.invoke({ messages }, invokeConfig as any),
137
- );
138
-
139
- this._memory?.setAgent(agent);
140
-
141
- const rawContent = response.messages.at(-1)?.content as string | undefined;
142
- const text =
143
- typeof rawContent === "string" && rawContent.trim()
144
- ? rawContent
145
- : "Empty response from the model";
146
- return {
147
- text,
148
- messages: response.messages,
149
- };
150
- }
151
-
152
- async callStructuredOutput<T extends z.ZodSchema>(
153
- params: AICallStructuredOutputParams<T>,
154
- ): AICallStructuredOutputReturn<typeof params.outputSchema> {
155
- const { outputSchema, messages } = params;
156
-
157
- this.ensureThreadIdWhenCheckpointer(params);
158
- const checkpointer = await this.getCheckpointer();
159
-
160
- const invokeConfig =
161
- params.threadId && checkpointer
162
- ? { configurable: { thread_id: params.threadId } }
163
- : undefined;
164
-
165
- const { result: response, agent } = await this.invokeWithRetryAndFallback(
166
- params,
167
- (paramsForModel) =>
168
- createAgent({
169
- ...this.standardAgent(paramsForModel, checkpointer),
170
- responseFormat: this.normalizeSchemaForOpenAI(
171
- outputSchema,
172
- paramsForModel.aiModel,
173
- ) as any,
174
- }),
175
- (agent) => agent.invoke({ messages }, invokeConfig as any),
176
- );
177
-
178
- this._memory?.setAgent(agent);
179
-
180
- const parsedResponse = outputSchema.parse(response?.structuredResponse);
181
-
182
- return { response: parsedResponse };
183
- }
184
-
185
- /**
186
- * Normaliza schemas Zod para compatibilidade com OpenAI/OpenRouter
187
- * OpenAI exige que todos os campos em properties estejam no array required
188
- * quando usa response_format: 'extract'
189
- */
190
- private normalizeSchemaForOpenAI<T extends z.ZodSchema>(
191
- schema: T,
192
- aiModel: string,
193
- ): z.ZodSchema {
194
- // Apenas normaliza para modelos OpenAI/OpenRouter
195
- const isOpenAIModel =
196
- aiModel.startsWith("gpt") || aiModel.startsWith("openrouter/openai/");
197
-
198
- if (!isOpenAIModel) {
199
- return schema;
200
- }
201
-
202
- // Se o schema é um objeto Zod, precisamos normalizar campos opcionais
203
- if (schema instanceof z.ZodObject) {
204
- const shape = schema.shape;
205
- const newShape: Record<string, z.ZodTypeAny> = {};
206
-
207
- // Converte campos opcionais para nullable para compatibilidade com OpenAI
208
- // OpenAI requer que todos os campos estejam no array required
209
- for (const [key, value] of Object.entries(shape)) {
210
- if (value instanceof z.ZodOptional) {
211
- // Converte .optional() para .nullable() para compatibilidade com OpenAI
212
- const innerType = value._def.innerType as z.ZodTypeAny;
213
- // Usa z.union para criar um tipo nullable
214
- newShape[key] = z.union([innerType, z.null()]);
215
- } else {
216
- newShape[key] = value;
217
- }
218
- }
219
-
220
- return z.object(newShape);
221
- }
222
-
223
- return schema;
224
- }
225
-
226
- async getRawAgent(
227
- params: AICallParams,
228
- outputSchema?: z.ZodSchema | undefined,
229
- ): Promise<{ agent: AIAgent }> {
230
- this.ensureThreadIdWhenCheckpointer(params);
231
- const checkpointer = await this.getCheckpointer();
232
-
233
- const agent = createAgent({
234
- ...this.standardAgent(params, checkpointer),
235
- responseFormat: outputSchema as any,
236
- });
237
-
238
- this._memory?.setAgent(agent);
239
-
240
- return { agent };
241
- }
242
-
243
- private getModel(params: AICallParams) {
244
- const { aiModel, modelConfig } = params;
245
-
246
- const config: LLMModelConfig = {
247
- model: aiModel,
248
- maxTokens: modelConfig?.maxTokens,
249
- temperature: modelConfig?.temperature,
250
- };
251
-
252
- if (aiModel.startsWith("gpt")) {
253
- config.apiKey = this.config.openAIApiKey;
254
-
255
- return AIModels.gpt(config);
256
- }
257
-
258
- if (aiModel.startsWith("gemini")) {
259
- config.apiKey = this.config.googleGeminiToken;
260
-
261
- return AIModels.gemini(config);
262
- }
263
-
264
- if (aiModel.startsWith("openrouter/")) {
265
- const modelName = aiModel.replace(/^openrouter\//, "");
266
- return AIModels.openrouter({
267
- ...config,
268
- model: modelName,
269
- apiKey: this.config.openRouterApiKey,
270
- });
271
- }
272
-
273
- throw new Error("Model not supported");
274
- }
275
-
276
- private standardAgent(
277
- params: AICallParams,
278
- checkpointer?: BaseCheckpointSaver,
279
- ): Parameters<typeof createAgent>[0] {
280
- const { systemPrompt, maxRetries = 3 } = params;
281
-
282
- const model = this.getModel(params);
283
- return {
284
- model,
285
- systemPrompt: systemPrompt ?? "",
286
- middleware: [
287
- ...this.standardMiddlewares(maxRetries),
288
- ...(params.agent?.middleware ?? []),
289
- ],
290
- tools: params.agent?.tools ?? [],
291
- responseFormat: undefined as any,
292
- ...(checkpointer && { checkpointer }),
293
- };
294
- }
295
-
296
- private standardMiddlewares(maxRetries: number) {
297
- return [
298
- modelRetryMiddleware({
299
- maxRetries,
300
- backoffFactor: 2.0,
301
- initialDelayMs: 1000,
302
- onFailure: "error",
303
- }),
304
- ];
305
- }
306
- }
1
+ import { AIModels, LLMModelConfig } from "./langchain/models";
2
+ import z from "zod";
3
+ import { createAgent, modelRetryMiddleware } from "langchain";
4
+ import type { AIAgent } from "./@types/agent";
5
+
6
+ import {
7
+ AIMemory,
8
+ type BaseCheckpointSaver,
9
+ type MemoryConfig,
10
+ } from "./langchain/memory";
11
+ import type { AIModelNames } from "./@types/model-names";
12
+ import type {
13
+ AICallParams,
14
+ AICallReturn,
15
+ AICallStructuredOutputParams,
16
+ AICallStructuredOutputReturn,
17
+ } from "./@types/ai-call";
18
+
19
+ export type {
20
+ AICallParams,
21
+ AICallReturn,
22
+ AICallStructuredOutputParams,
23
+ AICallStructuredOutputReturn,
24
+ } from "./@types/ai-call";
25
+
26
+ type AIConstructor = {
27
+ googleGeminiToken?: string;
28
+ openAIApiKey?: string;
29
+ openRouterApiKey?: string;
30
+ /** Lista padrão de modelos de fallback (usada em call/callStructuredOutput quando não passada no método) */
31
+ aiModelsFallback?: AIModelNames[];
32
+ /** Configuração de persistência de histórico (memory, sqlite, postgres, redis, mongodb) ou instância AIMemory */
33
+ memory?: MemoryConfig | AIMemory;
34
+ /** Instância de checkpointer para usuários avançados (alternativa a memory) */
35
+ checkpointer?: BaseCheckpointSaver;
36
+ };
37
+
38
+ export class AI {
39
+ private _memory: AIMemory | undefined;
40
+
41
+ /**
42
+ * Instância de AIMemory. Lança exceção se memory não estiver configurado.
43
+ *
44
+ * @example
45
+ * const { fullHistory, messages } = await ai.memory.getHistory(threadId);
46
+ */
47
+ get memory(): AIMemory {
48
+ if (!this._memory) {
49
+ throw new Error(
50
+ "memory não está configurado. Passe memory no construtor do AI (ex: memory: { type: 'memory' }).",
51
+ );
52
+ }
53
+ return this._memory;
54
+ }
55
+
56
+ private checkpointer: BaseCheckpointSaver | undefined;
57
+ private checkpointerPromise: Promise<BaseCheckpointSaver> | undefined;
58
+
59
+ constructor(private config: AIConstructor) {
60
+ if (config.checkpointer) {
61
+ this.checkpointer = config.checkpointer;
62
+ }
63
+ if (config.memory) {
64
+ this._memory =
65
+ config.memory instanceof AIMemory
66
+ ? config.memory
67
+ : new AIMemory(config.memory);
68
+ }
69
+ }
70
+
71
+ private async getCheckpointer(): Promise<BaseCheckpointSaver | undefined> {
72
+ if (this.checkpointer) return this.checkpointer;
73
+ if (this._memory) {
74
+ if (!this.checkpointerPromise) {
75
+ this.checkpointerPromise = this._memory.getCheckpointer();
76
+ }
77
+ this.checkpointer = await this.checkpointerPromise;
78
+ return this.checkpointer;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ private ensureThreadIdWhenCheckpointer(params: AICallParams): void {
84
+ if (this.config.checkpointer || this.config.memory !== undefined) {
85
+ if (!params.threadId) {
86
+ throw new Error(
87
+ "threadId é obrigatório quando memory ou checkpointer está configurado. " +
88
+ "Passe threadId em AICallParams para identificar a conversa.",
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ private async invokeWithRetryAndFallback<T>(
95
+ params: AICallParams,
96
+ createAgentForModel: (paramsForModel: AICallParams) => AIAgent,
97
+ execute: (agent: AIAgent) => Promise<T>,
98
+ ): Promise<{ result: T; agent: AIAgent }> {
99
+ const fallback =
100
+ params.aiModelsFallback ?? this.config.aiModelsFallback ?? [];
101
+ const models: (typeof params.aiModel)[] = [params.aiModel, ...fallback];
102
+ let lastError: unknown;
103
+
104
+ for (const aiModel of models) {
105
+ const paramsForModel = { ...params, aiModel };
106
+ const agent = createAgentForModel(paramsForModel);
107
+
108
+ try {
109
+ const result = await execute(agent);
110
+ return { result, agent };
111
+ } catch (error) {
112
+ lastError = error;
113
+ }
114
+ }
115
+
116
+ throw lastError;
117
+ }
118
+
119
+ async call(params: AICallParams): AICallReturn {
120
+ const { messages } = params;
121
+
122
+ this.ensureThreadIdWhenCheckpointer(params);
123
+ const checkpointer = await this.getCheckpointer();
124
+
125
+ const invokeConfig =
126
+ params.threadId && checkpointer
127
+ ? { configurable: { thread_id: params.threadId } }
128
+ : undefined;
129
+
130
+ const { result: response, agent } = await this.invokeWithRetryAndFallback(
131
+ params,
132
+ (paramsForModel) =>
133
+ createAgent({
134
+ ...this.standardAgent(paramsForModel, checkpointer),
135
+ }),
136
+ (agent) => agent.invoke({ messages }, invokeConfig as any),
137
+ );
138
+
139
+ this._memory?.setAgent(agent);
140
+
141
+ const rawContent = response.messages.at(-1)?.content as string | undefined;
142
+ const text =
143
+ typeof rawContent === "string" && rawContent.trim()
144
+ ? rawContent
145
+ : "Empty response from the model";
146
+ return {
147
+ text,
148
+ messages: response.messages,
149
+ };
150
+ }
151
+
152
+ async callStructuredOutput<T extends z.ZodSchema>(
153
+ params: AICallStructuredOutputParams<T>,
154
+ ): AICallStructuredOutputReturn<typeof params.outputSchema> {
155
+ const { outputSchema, messages } = params;
156
+
157
+ this.ensureThreadIdWhenCheckpointer(params);
158
+ const checkpointer = await this.getCheckpointer();
159
+
160
+ const invokeConfig =
161
+ params.threadId && checkpointer
162
+ ? { configurable: { thread_id: params.threadId } }
163
+ : undefined;
164
+
165
+ const { result: response, agent } = await this.invokeWithRetryAndFallback(
166
+ params,
167
+ (paramsForModel) =>
168
+ createAgent({
169
+ ...this.standardAgent(paramsForModel, checkpointer),
170
+ responseFormat: this.normalizeSchemaForOpenAI(
171
+ outputSchema,
172
+ paramsForModel.aiModel,
173
+ ) as any,
174
+ }),
175
+ (agent) => agent.invoke({ messages }, invokeConfig as any),
176
+ );
177
+
178
+ this._memory?.setAgent(agent);
179
+
180
+ const parsedResponse = outputSchema.parse(response?.structuredResponse);
181
+
182
+ return { response: parsedResponse };
183
+ }
184
+
185
+ /**
186
+ * Normaliza schemas Zod para compatibilidade com OpenAI/OpenRouter
187
+ * OpenAI exige que todos os campos em properties estejam no array required
188
+ * quando usa response_format: 'extract'
189
+ */
190
+ private normalizeSchemaForOpenAI<T extends z.ZodSchema>(
191
+ schema: T,
192
+ aiModel: string,
193
+ ): z.ZodSchema {
194
+ // Apenas normaliza para modelos OpenAI/OpenRouter
195
+ const isOpenAIModel =
196
+ aiModel.startsWith("gpt") || aiModel.startsWith("openrouter/openai/");
197
+
198
+ if (!isOpenAIModel) {
199
+ return schema;
200
+ }
201
+
202
+ // Se o schema é um objeto Zod, precisamos normalizar campos opcionais
203
+ if (schema instanceof z.ZodObject) {
204
+ const shape = schema.shape;
205
+ const newShape: Record<string, z.ZodTypeAny> = {};
206
+
207
+ // Converte campos opcionais para nullable para compatibilidade com OpenAI
208
+ // OpenAI requer que todos os campos estejam no array required
209
+ for (const [key, value] of Object.entries(shape)) {
210
+ if (value instanceof z.ZodOptional) {
211
+ // Converte .optional() para .nullable() para compatibilidade com OpenAI
212
+ const innerType = value._def.innerType as z.ZodTypeAny;
213
+ // Usa z.union para criar um tipo nullable
214
+ newShape[key] = z.union([innerType, z.null()]);
215
+ } else {
216
+ newShape[key] = value;
217
+ }
218
+ }
219
+
220
+ return z.object(newShape);
221
+ }
222
+
223
+ return schema;
224
+ }
225
+
226
+ async getRawAgent(
227
+ params: AICallParams,
228
+ outputSchema?: z.ZodSchema | undefined,
229
+ ): Promise<{ agent: AIAgent }> {
230
+ this.ensureThreadIdWhenCheckpointer(params);
231
+ const checkpointer = await this.getCheckpointer();
232
+
233
+ const agent = createAgent({
234
+ ...this.standardAgent(params, checkpointer),
235
+ responseFormat: outputSchema as any,
236
+ });
237
+
238
+ this._memory?.setAgent(agent);
239
+
240
+ return { agent };
241
+ }
242
+
243
+ private getModel(params: AICallParams) {
244
+ const { aiModel, modelConfig } = params;
245
+
246
+ const config: LLMModelConfig = {
247
+ model: aiModel,
248
+ maxTokens: modelConfig?.maxTokens,
249
+ temperature: modelConfig?.temperature,
250
+ };
251
+
252
+ if (aiModel.startsWith("gpt")) {
253
+ config.apiKey = this.config.openAIApiKey;
254
+
255
+ return AIModels.gpt(config);
256
+ }
257
+
258
+ if (aiModel.startsWith("gemini")) {
259
+ config.apiKey = this.config.googleGeminiToken;
260
+
261
+ return AIModels.gemini(config);
262
+ }
263
+
264
+ if (aiModel.startsWith("openrouter/")) {
265
+ const modelName = aiModel.replace(/^openrouter\//, "");
266
+ return AIModels.openrouter({
267
+ ...config,
268
+ model: modelName,
269
+ apiKey: this.config.openRouterApiKey,
270
+ });
271
+ }
272
+
273
+ throw new Error("Model not supported");
274
+ }
275
+
276
+ private standardAgent(
277
+ params: AICallParams,
278
+ checkpointer?: BaseCheckpointSaver,
279
+ ): Parameters<typeof createAgent>[0] {
280
+ const { systemPrompt, maxRetries = 3 } = params;
281
+
282
+ const model = this.getModel(params);
283
+ return {
284
+ model,
285
+ systemPrompt: systemPrompt ?? "",
286
+ middleware: [
287
+ ...this.standardMiddlewares(maxRetries),
288
+ ...(params.agent?.middleware ?? []),
289
+ ],
290
+ tools: params.agent?.tools ?? [],
291
+ responseFormat: undefined as any,
292
+ ...(checkpointer && { checkpointer }),
293
+ };
294
+ }
295
+
296
+ private standardMiddlewares(maxRetries: number) {
297
+ return [
298
+ modelRetryMiddleware({
299
+ maxRetries,
300
+ backoffFactor: 2.0,
301
+ initialDelayMs: 1000,
302
+ onFailure: "error",
303
+ }),
304
+ ];
305
+ }
306
+ }
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ export type {
19
19
  export { AIModels } from "./langchain/models";
20
20
  export { AIMessages } from "./langchain/messages";
21
21
  export { AITools } from "./langchain/tools";
22
- export { AIMemory } from "./langchain/checkpointers";
22
+ export { AIMemory } from "./langchain/memory";
23
23
  export type {
24
24
  MemoryConfig,
25
25
  BaseCheckpointSaver,
@@ -32,7 +32,7 @@ export type {
32
32
  MessageRole,
33
33
  HistoryMessageItem,
34
34
  GetHistoryResult,
35
- } from "./langchain/checkpointers";
35
+ } from "./langchain/memory";
36
36
  export { AIAudioTranscription } from "./langchain/audio-transcription";
37
37
  export { AudioUtils } from "./utils/audio-utils";
38
38
  export type { AudioBuffer, AudioMimeType } from "./@types/audio";