@luanpoppe/ai 1.1.0 → 1.1.2

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.
@@ -0,0 +1,117 @@
1
+ import type { StateSnapshot } from "@langchain/langgraph";
2
+
3
+ /**
4
+ * Interface mínima para um grafo compilado com checkpointer.
5
+ * Usado para acessar o histórico de conversas via getStateHistory.
6
+ *
7
+ * @see https://docs.langchain.com/oss/javascript/langgraph/persistence#get-state-history
8
+ * @see https://docs.langchain.com/oss/javascript/langgraph/add-memory#manage-checkpoints
9
+ */
10
+ export interface GraphWithStateHistory {
11
+ /**
12
+ * Retorna o histórico completo de checkpoints de uma thread.
13
+ * Ordenado cronologicamente (mais recente primeiro).
14
+ */
15
+ getStateHistory(config: {
16
+ configurable: { thread_id: string; checkpoint_id?: string };
17
+ }): AsyncIterable<StateSnapshot>;
18
+
19
+ /**
20
+ * Retorna o estado atual (último checkpoint) da thread.
21
+ */
22
+ getState?(config: {
23
+ configurable: { thread_id: string; checkpoint_id?: string };
24
+ }): Promise<StateSnapshot>;
25
+ }
26
+
27
+ /**
28
+ * Configuração para checkpointer em memória (desenvolvimento/testes).
29
+ */
30
+ export type MemoryCheckpointerConfig = {
31
+ type: "memory";
32
+ };
33
+
34
+ /**
35
+ * Configuração para checkpointer SQLite.
36
+ * Use ":memory:" para testes ou path para arquivo persistente.
37
+ */
38
+ export type SqliteCheckpointerConfig = {
39
+ type: "sqlite";
40
+ connectionString: string;
41
+ };
42
+
43
+ /**
44
+ * Configuração para checkpointer Postgres.
45
+ */
46
+ export type PostgresCheckpointerConfig = {
47
+ type: "postgres";
48
+ connectionString: string;
49
+ };
50
+
51
+ /**
52
+ * Configuração para checkpointer Redis.
53
+ * Requer Redis 8+ ou Redis Stack (RedisJSON, RediSearch).
54
+ */
55
+ export type RedisCheckpointerConfig = {
56
+ type: "redis";
57
+ url: string;
58
+ options?: {
59
+ defaultTTL?: number;
60
+ refreshOnRead?: boolean;
61
+ };
62
+ };
63
+
64
+ /**
65
+ * Configuração para checkpointer MongoDB com cliente existente.
66
+ */
67
+ export type MongoDBCheckpointerConfigWithClient = {
68
+ type: "mongodb";
69
+ client: { close?: () => Promise<void> };
70
+ };
71
+
72
+ /**
73
+ * Configuração para checkpointer MongoDB com URL.
74
+ */
75
+ export type MongoDBCheckpointerConfigWithUrl = {
76
+ type: "mongodb";
77
+ url: string;
78
+ };
79
+
80
+ export type MongoDBCheckpointerConfig =
81
+ | MongoDBCheckpointerConfigWithClient
82
+ | MongoDBCheckpointerConfigWithUrl;
83
+
84
+ /**
85
+ * Configuração para persistência de histórico de conversas.
86
+ * Permite escolher o backend de armazenamento.
87
+ */
88
+ export type MemoryConfig =
89
+ | MemoryCheckpointerConfig
90
+ | SqliteCheckpointerConfig
91
+ | PostgresCheckpointerConfig
92
+ | RedisCheckpointerConfig
93
+ | MongoDBCheckpointerConfig;
94
+
95
+ /**
96
+ * Tipo de mensagem no histórico (Human, AI ou Tool).
97
+ */
98
+ export type MessageRole = "human" | "ai" | "tool";
99
+
100
+ /**
101
+ * Item de mensagem extraído do histórico, com role, horário e conteúdo.
102
+ */
103
+ export type HistoryMessageItem = {
104
+ role: MessageRole;
105
+ createdAt: string;
106
+ content: string;
107
+ };
108
+
109
+ /**
110
+ * Retorno do método getHistory com histórico completo e lista de mensagens.
111
+ */
112
+ export type GetHistoryResult = {
113
+ /** Histórico completo de checkpoints (mais recente primeiro). */
114
+ fullHistory: StateSnapshot[];
115
+ /** Lista de mensagens em ordem cronológica, com role, horário e conteúdo. */
116
+ messages: HistoryMessageItem[];
117
+ };
package/src/ai.ts ADDED
@@ -0,0 +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
+ }
package/src/index.ts CHANGED
@@ -1,218 +1,38 @@
1
- import { AIModels, LLMModelConfig } from "./langchain/models";
2
- import { AIModelNames } from "./@types/model-names";
3
1
  import z from "zod";
4
- import { MessageInput } from "./langchain/messages";
5
- import {
6
- AgentMiddleware,
7
- BaseMessage,
8
- createAgent,
9
- modelFallbackMiddleware,
10
- modelRetryMiddleware,
11
- } from "langchain";
12
- import { ClientTool, ServerTool } from "@langchain/core/tools";
13
- import { AIMessages } from "./langchain/messages";
14
- import { AITools } from "./langchain/tools";
15
-
16
- type AIConstructor = {
17
- googleGeminiToken?: string;
18
- openAIApiKey?: string;
19
- openRouterApiKey?: string;
20
- };
21
-
22
- export type AICallParams = {
23
- agent?: {
24
- middleware?: AgentMiddleware[];
25
- tools?: (ServerTool | ClientTool)[];
26
- };
27
-
28
- modelConfig?: Omit<LLMModelConfig, "apiKey" | "model">;
29
-
30
- aiModel: AIModelNames;
31
- messages: MessageInput[];
32
- systemPrompt?: string;
33
- maxRetries?: number;
34
- };
35
-
36
- export type AICallReturn = Promise<{
37
- text: string;
38
- messages: BaseMessage[];
39
- }>;
40
-
41
- export type AICallStructuredOutputParams<T extends z.ZodSchema> =
42
- AICallParams & {
43
- outputSchema: T;
44
- };
45
-
46
- export type AICallStructuredOutputReturn<T> = Promise<{
47
- response: z.infer<T>;
48
- }>;
49
-
50
- export class AI {
51
- constructor(private tokens: AIConstructor) {}
52
-
53
- async call(params: AICallParams): AICallReturn {
54
- const { messages } = params;
55
-
56
- const agent = createAgent({
57
- ...this.standardAgent(params),
58
- });
59
-
60
- const response = await agent.invoke({ messages });
61
-
62
- const rawContent = response.messages.at(-1)?.content as string | undefined;
63
- const text =
64
- typeof rawContent === "string" && rawContent.trim()
65
- ? rawContent
66
- : "Empty response from the model";
67
- return {
68
- text,
69
- messages: response.messages,
70
- };
71
- }
72
-
73
- async callStructuredOutput<T extends z.ZodSchema>(
74
- params: AICallStructuredOutputParams<T>
75
- ): AICallStructuredOutputReturn<typeof params.outputSchema> {
76
- const { outputSchema, messages, aiModel } = params;
77
-
78
- // Normaliza o schema para compatibilidade com OpenAI/OpenRouter
79
- // OpenAI exige que todos os campos em properties estejam no array required
80
- const normalizedSchema = this.normalizeSchemaForOpenAI(
81
- outputSchema,
82
- aiModel
83
- );
84
-
85
- const agent = createAgent({
86
- ...this.standardAgent(params),
87
- responseFormat: normalizedSchema as any,
88
- });
89
-
90
- const response = await agent.invoke({
91
- messages,
92
- });
93
-
94
- const parsedResponse = outputSchema.parse(response?.structuredResponse);
95
-
96
- return { response: parsedResponse };
97
- }
98
-
99
- /**
100
- * Normaliza schemas Zod para compatibilidade com OpenAI/OpenRouter
101
- * OpenAI exige que todos os campos em properties estejam no array required
102
- * quando usa response_format: 'extract'
103
- */
104
- private normalizeSchemaForOpenAI<T extends z.ZodSchema>(
105
- schema: T,
106
- aiModel: string
107
- ): z.ZodSchema {
108
- // Apenas normaliza para modelos OpenAI/OpenRouter
109
- const isOpenAIModel =
110
- aiModel.startsWith("gpt") || aiModel.startsWith("openrouter/openai/");
111
-
112
- if (!isOpenAIModel) {
113
- return schema;
114
- }
115
-
116
- // Se o schema é um objeto Zod, precisamos normalizar campos opcionais
117
- if (schema instanceof z.ZodObject) {
118
- const shape = schema.shape;
119
- const newShape: Record<string, z.ZodTypeAny> = {};
120
-
121
- // Converte campos opcionais para nullable para compatibilidade com OpenAI
122
- // OpenAI requer que todos os campos estejam no array required
123
- for (const [key, value] of Object.entries(shape)) {
124
- if (value instanceof z.ZodOptional) {
125
- // Converte .optional() para .nullable() para compatibilidade com OpenAI
126
- const innerType = value._def.innerType as z.ZodTypeAny;
127
- // Usa z.union para criar um tipo nullable
128
- newShape[key] = z.union([innerType, z.null()]);
129
- } else {
130
- newShape[key] = value;
131
- }
132
- }
133
-
134
- return z.object(newShape);
135
- }
136
-
137
- return schema;
138
- }
139
-
140
- getRawAgent(
141
- params: AICallParams,
142
- outputSchema?: z.ZodSchema | undefined
143
- ): { agent: ReturnType<typeof createAgent> } {
144
- const agent = createAgent({
145
- ...this.standardAgent(params),
146
- responseFormat: outputSchema as any,
147
- });
148
-
149
- return { agent };
150
- }
151
-
152
- private getModel(params: AICallParams) {
153
- const { aiModel, modelConfig } = params;
154
-
155
- const config: LLMModelConfig = {
156
- model: aiModel,
157
- maxTokens: modelConfig?.maxTokens,
158
- temperature: modelConfig?.temperature,
159
- };
160
-
161
- if (aiModel.startsWith("gpt")) {
162
- config.apiKey = this.tokens.openAIApiKey;
163
-
164
- return AIModels.gpt(config);
165
- }
166
-
167
- if (aiModel.startsWith("gemini")) {
168
- config.apiKey = this.tokens.googleGeminiToken;
169
-
170
- return AIModels.gemini(config);
171
- }
172
-
173
- if (aiModel.startsWith("openrouter/")) {
174
- const modelName = aiModel.replace(/^openrouter\//, "");
175
- return AIModels.openrouter({
176
- ...config,
177
- model: modelName,
178
- apiKey: this.tokens.openRouterApiKey,
179
- });
180
- }
181
-
182
- throw new Error("Model not supported");
183
- }
184
-
185
- private standardAgent(
186
- params: AICallParams
187
- ): Parameters<typeof createAgent>[0] {
188
- const { systemPrompt, maxRetries = 3 } = params;
189
-
190
- const model = this.getModel(params);
191
- return {
192
- model,
193
- systemPrompt: systemPrompt ?? "",
194
- middleware: [
195
- ...this.standardMiddlewares(maxRetries),
196
- ...(params.agent?.middleware ?? []),
197
- ],
198
- tools: params.agent?.tools ?? [],
199
- responseFormat: undefined as any,
200
- };
201
- }
202
-
203
- private standardMiddlewares(maxRetries: number) {
204
- return [
205
- modelRetryMiddleware({
206
- maxRetries,
207
- backoffFactor: 2.0,
208
- initialDelayMs: 1000,
209
- }),
210
- modelFallbackMiddleware("gemini-2.5-flash", "gpt-4o-mini"),
211
- ];
212
- }
213
- }
214
-
215
- export { AIModels, AIMessages, AITools };
2
+ import { AI } from "./ai";
3
+ import type {
4
+ AICallParams,
5
+ AICallReturn,
6
+ AICallStructuredOutputParams,
7
+ AICallStructuredOutputReturn,
8
+ } from "./@types/ai-call";
9
+
10
+ export { AI };
11
+ export type { AIAgent } from "./@types/agent";
12
+ export type {
13
+ AICallParams,
14
+ AICallReturn,
15
+ AICallStructuredOutputParams,
16
+ AICallStructuredOutputReturn,
17
+ } from "./@types/ai-call";
18
+
19
+ export { AIModels } from "./langchain/models";
20
+ export { AIMessages } from "./langchain/messages";
21
+ export { AITools } from "./langchain/tools";
22
+ export { AIMemory } from "./langchain/checkpointers";
23
+ export type {
24
+ MemoryConfig,
25
+ BaseCheckpointSaver,
26
+ MemoryCheckpointerConfig,
27
+ SqliteCheckpointerConfig,
28
+ PostgresCheckpointerConfig,
29
+ RedisCheckpointerConfig,
30
+ MongoDBCheckpointerConfig,
31
+ GraphWithStateHistory,
32
+ MessageRole,
33
+ HistoryMessageItem,
34
+ GetHistoryResult,
35
+ } from "./langchain/checkpointers";
216
36
  export { AIAudioTranscription } from "./langchain/audio-transcription";
217
37
  export { AudioUtils } from "./utils/audio-utils";
218
38
  export type { AudioBuffer, AudioMimeType } from "./@types/audio";