@librechat/agents 2.1.3 → 2.1.4

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,1069 @@
1
+ /* eslint-disable no-process-env */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+
4
+ import { expect, test } from "@jest/globals";
5
+ import * as fs from "fs/promises";
6
+ import {
7
+ AIMessageChunk,
8
+ BaseMessage,
9
+ HumanMessage,
10
+ SystemMessage,
11
+ } from "@langchain/core/messages";
12
+ import { ChatPromptValue } from "@langchain/core/prompt_values";
13
+ import {
14
+ PromptTemplate,
15
+ ChatPromptTemplate,
16
+ AIMessagePromptTemplate,
17
+ HumanMessagePromptTemplate,
18
+ SystemMessagePromptTemplate,
19
+ } from "@langchain/core/prompts";
20
+ import { CallbackManager } from "@langchain/core/callbacks/manager";
21
+ import { concat } from "@langchain/core/utils/stream";
22
+ import { CustomAnthropic as ChatAnthropic } from "./llm";
23
+ import { AnthropicMessageResponse } from "./types";
24
+ jest.setTimeout(120000);
25
+
26
+ test("Test ChatAnthropic", async () => {
27
+ const chat = new ChatAnthropic({
28
+ modelName: "claude-3-sonnet-20240229",
29
+ maxRetries: 0,
30
+ });
31
+ const message = new HumanMessage("Hello!");
32
+ const res = await chat.invoke([message]);
33
+ expect(res.response_metadata.usage).toBeDefined();
34
+ });
35
+
36
+ test("Test ChatAnthropic with a bad API key throws appropriate error", async () => {
37
+ const chat = new ChatAnthropic({
38
+ modelName: "claude-3-sonnet-20240229",
39
+ maxRetries: 0,
40
+ apiKey: "bad",
41
+ });
42
+ let error;
43
+ try {
44
+ const message = new HumanMessage("Hello!");
45
+ await chat.invoke([message]);
46
+ } catch (e) {
47
+ error = e;
48
+ }
49
+ expect(error).toBeDefined();
50
+ expect((error as any).lc_error_code).toEqual("MODEL_AUTHENTICATION");
51
+ });
52
+
53
+ test("Test ChatAnthropic with unknown model throws appropriate error", async () => {
54
+ const chat = new ChatAnthropic({
55
+ modelName: "badbad",
56
+ maxRetries: 0,
57
+ });
58
+ let error;
59
+ try {
60
+ const message = new HumanMessage("Hello!");
61
+ await chat.invoke([message]);
62
+ } catch (e) {
63
+ error = e;
64
+ }
65
+ expect(error).toBeDefined();
66
+ expect((error as any).lc_error_code).toEqual("MODEL_NOT_FOUND");
67
+ });
68
+
69
+ test("Test ChatAnthropic Generate", async () => {
70
+ const chat = new ChatAnthropic({
71
+ modelName: "claude-3-sonnet-20240229",
72
+ maxRetries: 0,
73
+ });
74
+ const message = new HumanMessage("Hello!");
75
+ const res = await chat.generate([[message], [message]]);
76
+ expect(res.generations.length).toBe(2);
77
+ for (const generation of res.generations) {
78
+ expect(generation.length).toBe(1);
79
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
80
+ for (const message of generation) {
81
+ // console.log(message.text);
82
+ }
83
+ }
84
+ // console.log({ res });
85
+ });
86
+
87
+ test.skip("Test ChatAnthropic Generate w/ ClientOptions", async () => {
88
+ const chat = new ChatAnthropic({
89
+ modelName: "claude-3-sonnet-20240229",
90
+ maxRetries: 0,
91
+ clientOptions: {
92
+ defaultHeaders: {
93
+ "Helicone-Auth": "HELICONE_API_KEY",
94
+ },
95
+ },
96
+ });
97
+ const message = new HumanMessage("Hello!");
98
+ const res = await chat.generate([[message], [message]]);
99
+ expect(res.generations.length).toBe(2);
100
+ for (const generation of res.generations) {
101
+ expect(generation.length).toBe(1);
102
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
103
+ for (const message of generation) {
104
+ // console.log(message.text);
105
+ }
106
+ }
107
+ // console.log({ res });
108
+ });
109
+
110
+ test("Test ChatAnthropic Generate with a signal in call options", async () => {
111
+ const chat = new ChatAnthropic({
112
+ modelName: "claude-3-sonnet-20240229",
113
+ maxRetries: 0,
114
+ });
115
+ const controller = new AbortController();
116
+ const message = new HumanMessage(
117
+ "How is your day going? Be extremely verbose!"
118
+ );
119
+ await expect(() => {
120
+ const res = chat.generate([[message], [message]], {
121
+ signal: controller.signal,
122
+ });
123
+ setTimeout(() => {
124
+ controller.abort();
125
+ }, 1000);
126
+ return res;
127
+ }).rejects.toThrow();
128
+ }, 10000);
129
+
130
+ test("Test ChatAnthropic tokenUsage with a batch", async () => {
131
+ const model = new ChatAnthropic({
132
+ temperature: 0,
133
+ maxRetries: 0,
134
+ modelName: "claude-3-sonnet-20240229",
135
+ });
136
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
137
+ const res = await model.generate([
138
+ [new HumanMessage(`Hello!`)],
139
+ [new HumanMessage(`Hi!`)],
140
+ ]);
141
+ // console.log({ res });
142
+ });
143
+
144
+ test("Test ChatAnthropic in streaming mode", async () => {
145
+ let nrNewTokens = 0;
146
+ let streamedCompletion = "";
147
+
148
+ const model = new ChatAnthropic({
149
+ modelName: "claude-3-sonnet-20240229",
150
+ maxRetries: 0,
151
+ streaming: true,
152
+ callbacks: CallbackManager.fromHandlers({
153
+ async handleLLMNewToken(token: string) {
154
+ nrNewTokens += 1;
155
+ streamedCompletion += token;
156
+ },
157
+ }),
158
+ });
159
+ const message = new HumanMessage("Hello!");
160
+ const res = await model.invoke([message]);
161
+ // console.log({ res });
162
+
163
+ expect(nrNewTokens > 0).toBe(true);
164
+ expect(res.content).toBe(streamedCompletion);
165
+ });
166
+
167
+ test("Test ChatAnthropic in streaming mode with a signal", async () => {
168
+ let nrNewTokens = 0;
169
+ let streamedCompletion = "";
170
+
171
+ const model = new ChatAnthropic({
172
+ modelName: "claude-3-sonnet-20240229",
173
+ maxRetries: 0,
174
+ streaming: true,
175
+ callbacks: CallbackManager.fromHandlers({
176
+ async handleLLMNewToken(token: string) {
177
+ nrNewTokens += 1;
178
+ streamedCompletion += token;
179
+ },
180
+ }),
181
+ });
182
+ const controller = new AbortController();
183
+ const message = new HumanMessage(
184
+ "Hello! Give me an extremely verbose response"
185
+ );
186
+ await expect(() => {
187
+ const res = model.invoke([message], {
188
+ signal: controller.signal,
189
+ });
190
+ setTimeout(() => {
191
+ controller.abort();
192
+ }, 500);
193
+ return res;
194
+ }).rejects.toThrow();
195
+
196
+ // console.log({ nrNewTokens, streamedCompletion });
197
+ }, 5000);
198
+
199
+ test.skip("Test ChatAnthropic prompt value", async () => {
200
+ const chat = new ChatAnthropic({
201
+ modelName: "claude-3-sonnet-20240229",
202
+ maxRetries: 0,
203
+ });
204
+ const message = new HumanMessage("Hello!");
205
+ const res = await chat.generatePrompt([new ChatPromptValue([message])]);
206
+ expect(res.generations.length).toBe(1);
207
+ for (const generation of res.generations) {
208
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
209
+ for (const g of generation) {
210
+ // console.log(g.text);
211
+ }
212
+ }
213
+ // console.log({ res });
214
+ });
215
+
216
+ test.skip("ChatAnthropic, docs, prompt templates", async () => {
217
+ const chat = new ChatAnthropic({
218
+ modelName: "claude-3-sonnet-20240229",
219
+ maxRetries: 0,
220
+ temperature: 0,
221
+ });
222
+
223
+ const systemPrompt = PromptTemplate.fromTemplate(
224
+ "You are a helpful assistant that translates {input_language} to {output_language}."
225
+ );
226
+
227
+ const chatPrompt = ChatPromptTemplate.fromMessages([
228
+ new SystemMessagePromptTemplate(systemPrompt),
229
+ HumanMessagePromptTemplate.fromTemplate("{text}"),
230
+ ]);
231
+
232
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
233
+ const responseA = await chat.generatePrompt([
234
+ await chatPrompt.formatPromptValue({
235
+ input_language: "English",
236
+ output_language: "French",
237
+ text: "I love programming.",
238
+ }),
239
+ ]);
240
+
241
+ // console.log(responseA.generations);
242
+ });
243
+
244
+ test.skip("ChatAnthropic, longer chain of messages", async () => {
245
+ const chat = new ChatAnthropic({
246
+ modelName: "claude-3-sonnet-20240229",
247
+ maxRetries: 0,
248
+ temperature: 0,
249
+ });
250
+
251
+ const chatPrompt = ChatPromptTemplate.fromMessages([
252
+ HumanMessagePromptTemplate.fromTemplate(`Hi, my name is Joe!`),
253
+ AIMessagePromptTemplate.fromTemplate(`Nice to meet you, Joe!`),
254
+ HumanMessagePromptTemplate.fromTemplate("{text}"),
255
+ ]);
256
+
257
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
258
+ const responseA = await chat.generatePrompt([
259
+ await chatPrompt.formatPromptValue({
260
+ text: "What did I just say my name was?",
261
+ }),
262
+ ]);
263
+
264
+ // console.log(responseA.generations);
265
+ });
266
+
267
+ test.skip("ChatAnthropic, Anthropic apiUrl set manually via constructor", async () => {
268
+ // Pass the default URL through (should use this, and work as normal)
269
+ const anthropicApiUrl = "https://api.anthropic.com";
270
+ const chat = new ChatAnthropic({
271
+ modelName: "claude-3-sonnet-20240229",
272
+ maxRetries: 0,
273
+ anthropicApiUrl,
274
+ });
275
+ const message = new HumanMessage("Hello!");
276
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
277
+ const res = await chat.call([message]);
278
+ // console.log({ res });
279
+ });
280
+
281
+ test("Test ChatAnthropic stream method", async () => {
282
+ const model = new ChatAnthropic({
283
+ maxTokens: 50,
284
+ maxRetries: 0,
285
+ modelName: "claude-3-sonnet-20240229",
286
+ });
287
+ const stream = await model.stream("Print hello world.");
288
+ const chunks: any[] = [];
289
+ for await (const chunk of stream) {
290
+ chunks.push(chunk);
291
+ }
292
+ expect(chunks.length).toBeGreaterThan(1);
293
+ });
294
+
295
+ test("Test ChatAnthropic stream method with abort", async () => {
296
+ await expect(async () => {
297
+ const model = new ChatAnthropic({
298
+ maxTokens: 500,
299
+ maxRetries: 0,
300
+ modelName: "claude-3-sonnet-20240229",
301
+ });
302
+ const stream = await model.stream(
303
+ "How is your day going? Be extremely verbose.",
304
+ {
305
+ signal: AbortSignal.timeout(1000),
306
+ }
307
+ );
308
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
309
+ for await (const chunk of stream) {
310
+ // console.log(chunk);
311
+ }
312
+ }).rejects.toThrow();
313
+ });
314
+
315
+ test("Test ChatAnthropic stream method with early break", async () => {
316
+ const model = new ChatAnthropic({
317
+ maxTokens: 50,
318
+ maxRetries: 0,
319
+ modelName: "claude-3-sonnet-20240229",
320
+ });
321
+ const stream = await model.stream(
322
+ "How is your day going? Be extremely verbose."
323
+ );
324
+ let i = 0;
325
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
326
+ for await (const chunk of stream) {
327
+ // console.log(chunk);
328
+ i += 1;
329
+ if (i > 10) {
330
+ break;
331
+ }
332
+ }
333
+ });
334
+
335
+ test("Test ChatAnthropic headers passed through", async () => {
336
+ const chat = new ChatAnthropic({
337
+ modelName: "claude-3-sonnet-20240229",
338
+ maxRetries: 0,
339
+ apiKey: "NOT_REAL",
340
+ clientOptions: {
341
+ defaultHeaders: {
342
+ "X-Api-Key": process.env.ANTHROPIC_API_KEY,
343
+ },
344
+ },
345
+ });
346
+ const message = new HumanMessage("Hello!");
347
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
348
+ const res = await chat.invoke([message]);
349
+ // console.log({ res });
350
+ });
351
+
352
+ test("Test ChatAnthropic multimodal", async () => {
353
+ const chat = new ChatAnthropic({
354
+ modelName: "claude-3-sonnet-20240229",
355
+ maxRetries: 0,
356
+ });
357
+ // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment
358
+ const res = await chat.invoke([
359
+ new HumanMessage({
360
+ content: [
361
+ {
362
+ type: "image_url",
363
+ image_url: {
364
+ url: "data:image/jpeg;base64,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",
365
+ },
366
+ },
367
+ { type: "text", text: "What is this a logo for?" },
368
+ ],
369
+ }),
370
+ ]);
371
+ // console.log(res);
372
+ });
373
+
374
+ test("Stream tokens", async () => {
375
+ const model = new ChatAnthropic({
376
+ model: "claude-3-haiku-20240307",
377
+ temperature: 0,
378
+ maxTokens: 10,
379
+ });
380
+ let res: AIMessageChunk | null = null;
381
+ for await (const chunk of await model.stream(
382
+ "Why is the sky blue? Be concise."
383
+ )) {
384
+ if (!res) {
385
+ res = chunk;
386
+ } else {
387
+ res = res.concat(chunk);
388
+ }
389
+ }
390
+ // console.log(res);
391
+ expect(res?.usage_metadata).toBeDefined();
392
+ if (!res?.usage_metadata) {
393
+ return;
394
+ }
395
+ expect(res.usage_metadata.input_tokens).toBeGreaterThan(1);
396
+ expect(res.usage_metadata.output_tokens).toBeGreaterThan(1);
397
+ expect(res.usage_metadata.total_tokens).toBe(
398
+ res.usage_metadata.input_tokens + res.usage_metadata.output_tokens
399
+ );
400
+ });
401
+
402
+ test("id is supplied when invoking", async () => {
403
+ const model = new ChatAnthropic();
404
+ const result = await model.invoke("Hello");
405
+ expect(result.id).toBeDefined();
406
+ expect(result.id).not.toEqual("");
407
+ });
408
+
409
+ test("id is supplied when streaming", async () => {
410
+ const model = new ChatAnthropic();
411
+ let finalChunk: AIMessageChunk | undefined;
412
+ for await (const chunk of await model.stream("Hello")) {
413
+ finalChunk = !finalChunk ? chunk : concat(finalChunk, chunk);
414
+ }
415
+ expect(finalChunk).toBeDefined();
416
+ if (!finalChunk) return;
417
+ expect(finalChunk.id).toBeDefined();
418
+ expect(finalChunk.id).not.toEqual("");
419
+ });
420
+
421
+ const CACHED_TEXT = `## Components
422
+
423
+ LangChain provides standard, extendable interfaces and external integrations for various components useful for building with LLMs.
424
+ Some components LangChain implements, some components we rely on third-party integrations for, and others are a mix.
425
+
426
+ ### Chat models
427
+
428
+ <span data-heading-keywords="chat model,chat models"></span>
429
+
430
+ Language models that use a sequence of messages as inputs and return chat messages as outputs (as opposed to using plain text).
431
+ These are generally newer models (older models are generally \`LLMs\`, see below).
432
+ Chat models support the assignment of distinct roles to conversation messages, helping to distinguish messages from the AI, users, and instructions such as system messages.
433
+
434
+ Although the underlying models are messages in, message out, the LangChain wrappers also allow these models to take a string as input.
435
+ This gives them the same interface as LLMs (and simpler to use).
436
+ When a string is passed in as input, it will be converted to a \`HumanMessage\` under the hood before being passed to the underlying model.
437
+
438
+ LangChain does not host any Chat Models, rather we rely on third party integrations.
439
+
440
+ We have some standardized parameters when constructing ChatModels:
441
+
442
+ - \`model\`: the name of the model
443
+
444
+ Chat Models also accept other parameters that are specific to that integration.
445
+
446
+ :::important
447
+ Some chat models have been fine-tuned for **tool calling** and provide a dedicated API for it.
448
+ Generally, such models are better at tool calling than non-fine-tuned models, and are recommended for use cases that require tool calling.
449
+ Please see the [tool calling section](/docs/concepts/#functiontool-calling) for more information.
450
+ :::
451
+
452
+ For specifics on how to use chat models, see the [relevant how-to guides here](/docs/how_to/#chat-models).
453
+
454
+ #### Multimodality
455
+
456
+ Some chat models are multimodal, accepting images, audio and even video as inputs.
457
+ These are still less common, meaning model providers haven't standardized on the "best" way to define the API.
458
+ Multimodal outputs are even less common. As such, we've kept our multimodal abstractions fairly light weight
459
+ and plan to further solidify the multimodal APIs and interaction patterns as the field matures.
460
+
461
+ In LangChain, most chat models that support multimodal inputs also accept those values in OpenAI's content blocks format.
462
+ So far this is restricted to image inputs. For models like Gemini which support video and other bytes input, the APIs also support the native, model-specific representations.
463
+
464
+ For specifics on how to use multimodal models, see the [relevant how-to guides here](/docs/how_to/#multimodal).
465
+
466
+ ### LLMs
467
+
468
+ <span data-heading-keywords="llm,llms"></span>
469
+
470
+ :::caution
471
+ Pure text-in/text-out LLMs tend to be older or lower-level. Many popular models are best used as [chat completion models](/docs/concepts/#chat-models),
472
+ even for non-chat use cases.
473
+
474
+ You are probably looking for [the section above instead](/docs/concepts/#chat-models).
475
+ :::
476
+
477
+ Language models that takes a string as input and returns a string.
478
+ These are traditionally older models (newer models generally are [Chat Models](/docs/concepts/#chat-models), see above).
479
+
480
+ Although the underlying models are string in, string out, the LangChain wrappers also allow these models to take messages as input.
481
+ This gives them the same interface as [Chat Models](/docs/concepts/#chat-models).
482
+ When messages are passed in as input, they will be formatted into a string under the hood before being passed to the underlying model.
483
+
484
+ LangChain does not host any LLMs, rather we rely on third party integrations.
485
+
486
+ For specifics on how to use LLMs, see the [relevant how-to guides here](/docs/how_to/#llms).
487
+
488
+ ### Message types
489
+
490
+ Some language models take an array of messages as input and return a message.
491
+ There are a few different types of messages.
492
+ All messages have a \`role\`, \`content\`, and \`response_metadata\` property.
493
+
494
+ The \`role\` describes WHO is saying the message.
495
+ LangChain has different message classes for different roles.
496
+
497
+ The \`content\` property describes the content of the message.
498
+ This can be a few different things:
499
+
500
+ - A string (most models deal this type of content)
501
+ - A List of objects (this is used for multi-modal input, where the object contains information about that input type and that input location)
502
+
503
+ #### HumanMessage
504
+
505
+ This represents a message from the user.
506
+
507
+ #### AIMessage
508
+
509
+ This represents a message from the model. In addition to the \`content\` property, these messages also have:
510
+
511
+ **\`response_metadata\`**
512
+
513
+ The \`response_metadata\` property contains additional metadata about the response. The data here is often specific to each model provider.
514
+ This is where information like log-probs and token usage may be stored.
515
+
516
+ **\`tool_calls\`**
517
+
518
+ These represent a decision from an language model to call a tool. They are included as part of an \`AIMessage\` output.
519
+ They can be accessed from there with the \`.tool_calls\` property.
520
+
521
+ This property returns a list of \`ToolCall\`s. A \`ToolCall\` is an object with the following arguments:
522
+
523
+ - \`name\`: The name of the tool that should be called.
524
+ - \`args\`: The arguments to that tool.
525
+ - \`id\`: The id of that tool call.
526
+
527
+ #### SystemMessage
528
+
529
+ This represents a system message, which tells the model how to behave. Not every model provider supports this.
530
+
531
+ #### ToolMessage
532
+
533
+ This represents the result of a tool call. In addition to \`role\` and \`content\`, this message has:
534
+
535
+ - a \`tool_call_id\` field which conveys the id of the call to the tool that was called to produce this result.
536
+ - an \`artifact\` field which can be used to pass along arbitrary artifacts of the tool execution which are useful to track but which should not be sent to the model.
537
+
538
+ #### (Legacy) FunctionMessage
539
+
540
+ This is a legacy message type, corresponding to OpenAI's legacy function-calling API. \`ToolMessage\` should be used instead to correspond to the updated tool-calling API.
541
+
542
+ This represents the result of a function call. In addition to \`role\` and \`content\`, this message has a \`name\` parameter which conveys the name of the function that was called to produce this result.
543
+
544
+ ### Prompt templates
545
+
546
+ <span data-heading-keywords="prompt,prompttemplate,chatprompttemplate"></span>
547
+
548
+ Prompt templates help to translate user input and parameters into instructions for a language model.
549
+ This can be used to guide a model's response, helping it understand the context and generate relevant and coherent language-based output.
550
+
551
+ Prompt Templates take as input an object, where each key represents a variable in the prompt template to fill in.
552
+
553
+ Prompt Templates output a PromptValue. This PromptValue can be passed to an LLM or a ChatModel, and can also be cast to a string or an array of messages.
554
+ The reason this PromptValue exists is to make it easy to switch between strings and messages.
555
+
556
+ There are a few different types of prompt templates:
557
+
558
+ #### String PromptTemplates
559
+
560
+ These prompt templates are used to format a single string, and generally are used for simpler inputs.
561
+ For example, a common way to construct and use a PromptTemplate is as follows:
562
+
563
+ \`\`\`typescript
564
+ import { PromptTemplate } from "@langchain/core/prompts";
565
+
566
+ const promptTemplate = PromptTemplate.fromTemplate(
567
+ "Tell me a joke about {topic}"
568
+ );
569
+
570
+ await promptTemplate.invoke({ topic: "cats" });
571
+ \`\`\`
572
+
573
+ #### ChatPromptTemplates
574
+
575
+ These prompt templates are used to format an array of messages. These "templates" consist of an array of templates themselves.
576
+ For example, a common way to construct and use a ChatPromptTemplate is as follows:
577
+
578
+ \`\`\`typescript
579
+ import { ChatPromptTemplate } from "@langchain/core/prompts";
580
+
581
+ const promptTemplate = ChatPromptTemplate.fromMessages([
582
+ ["system", "You are a helpful assistant"],
583
+ ["user", "Tell me a joke about {topic}"],
584
+ ]);
585
+
586
+ await promptTemplate.invoke({ topic: "cats" });
587
+ \`\`\`
588
+
589
+ In the above example, this ChatPromptTemplate will construct two messages when called.
590
+ The first is a system message, that has no variables to format.
591
+ The second is a HumanMessage, and will be formatted by the \`topic\` variable the user passes in.
592
+
593
+ #### MessagesPlaceholder
594
+
595
+ <span data-heading-keywords="messagesplaceholder"></span>
596
+
597
+ This prompt template is responsible for adding an array of messages in a particular place.
598
+ In the above ChatPromptTemplate, we saw how we could format two messages, each one a string.
599
+ But what if we wanted the user to pass in an array of messages that we would slot into a particular spot?
600
+ This is how you use MessagesPlaceholder.
601
+
602
+ \`\`\`typescript
603
+ import {
604
+ ChatPromptTemplate,
605
+ MessagesPlaceholder,
606
+ } from "@langchain/core/prompts";
607
+ import { HumanMessage } from "@langchain/core/messages";
608
+
609
+ const promptTemplate = ChatPromptTemplate.fromMessages([
610
+ ["system", "You are a helpful assistant"],
611
+ new MessagesPlaceholder("msgs"),
612
+ ]);
613
+
614
+ promptTemplate.invoke({ msgs: [new HumanMessage({ content: "hi!" })] });
615
+ \`\`\`
616
+
617
+ This will produce an array of two messages, the first one being a system message, and the second one being the HumanMessage we passed in.
618
+ If we had passed in 5 messages, then it would have produced 6 messages in total (the system message plus the 5 passed in).
619
+ This is useful for letting an array of messages be slotted into a particular spot.
620
+
621
+ An alternative way to accomplish the same thing without using the \`MessagesPlaceholder\` class explicitly is:
622
+
623
+ \`\`\`typescript
624
+ const promptTemplate = ChatPromptTemplate.fromMessages([
625
+ ["system", "You are a helpful assistant"],
626
+ ["placeholder", "{msgs}"], // <-- This is the changed part
627
+ ]);
628
+ \`\`\`
629
+
630
+ For specifics on how to use prompt templates, see the [relevant how-to guides here](/docs/how_to/#prompt-templates).
631
+
632
+ ### Example Selectors
633
+
634
+ One common prompting technique for achieving better performance is to include examples as part of the prompt.
635
+ This gives the language model concrete examples of how it should behave.
636
+ Sometimes these examples are hardcoded into the prompt, but for more advanced situations it may be nice to dynamically select them.
637
+ Example Selectors are classes responsible for selecting and then formatting examples into prompts.
638
+
639
+ For specifics on how to use example selectors, see the [relevant how-to guides here](/docs/how_to/#example-selectors).
640
+
641
+ ### Output parsers
642
+
643
+ <span data-heading-keywords="output parser"></span>
644
+
645
+ :::note
646
+
647
+ The information here refers to parsers that take a text output from a model try to parse it into a more structured representation.
648
+ More and more models are supporting function (or tool) calling, which handles this automatically.
649
+ It is recommended to use function/tool calling rather than output parsing.
650
+ See documentation for that [here](/docs/concepts/#function-tool-calling).
651
+
652
+ :::
653
+
654
+ Responsible for taking the output of a model and transforming it to a more suitable format for downstream tasks.
655
+ Useful when you are using LLMs to generate structured data, or to normalize output from chat models and LLMs.
656
+
657
+ There are two main methods an output parser must implement:
658
+
659
+ - "Get format instructions": A method which returns a string containing instructions for how the output of a language model should be formatted.
660
+ - "Parse": A method which takes in a string (assumed to be the response from a language model) and parses it into some structure.
661
+
662
+ And then one optional one:
663
+
664
+ - "Parse with prompt": A method which takes in a string (assumed to be the response from a language model) and a prompt (assumed to be the prompt that generated such a response) and parses it into some structure. The prompt is largely provided in the event the OutputParser wants to retry or fix the output in some way, and needs information from the prompt to do so.
665
+
666
+ Output parsers accept a string or \`BaseMessage\` as input and can return an arbitrary type.
667
+
668
+ LangChain has many different types of output parsers. This is a list of output parsers LangChain supports. The table below has various pieces of information:
669
+
670
+ **Name**: The name of the output parser
671
+
672
+ **Supports Streaming**: Whether the output parser supports streaming.
673
+
674
+ **Input Type**: Expected input type. Most output parsers work on both strings and messages, but some (like OpenAI Functions) need a message with specific arguments.
675
+
676
+ **Output Type**: The output type of the object returned by the parser.
677
+
678
+ **Description**: Our commentary on this output parser and when to use it.
679
+
680
+ The current date is ${new Date().toISOString()}`;
681
+
682
+ test("system prompt caching", async () => {
683
+ const model = new ChatAnthropic({
684
+ model: "claude-3-haiku-20240307",
685
+ clientOptions: {
686
+ defaultHeaders: {
687
+ "anthropic-beta": "prompt-caching-2024-07-31",
688
+ },
689
+ },
690
+ });
691
+ const messages = [
692
+ new SystemMessage({
693
+ content: [
694
+ {
695
+ type: "text",
696
+ text: `You are a pirate. Always respond in pirate dialect.\nUse the following as context when answering questions: ${CACHED_TEXT}`,
697
+ cache_control: { type: "ephemeral" },
698
+ },
699
+ ],
700
+ }),
701
+ new HumanMessage({
702
+ content: "What types of messages are supported in LangChain?",
703
+ }),
704
+ ];
705
+ const res = await model.invoke(messages);
706
+ expect(
707
+ res.usage_metadata?.input_token_details?.cache_creation
708
+ ).toBeGreaterThan(0);
709
+ expect(res.usage_metadata?.input_token_details?.cache_read).toBe(0);
710
+ const res2 = await model.invoke(messages);
711
+ expect(res2.usage_metadata?.input_token_details?.cache_creation).toBe(0);
712
+ expect(res2.usage_metadata?.input_token_details?.cache_read).toBeGreaterThan(
713
+ 0
714
+ );
715
+ const stream = await model.stream(messages);
716
+ let agg;
717
+ for await (const chunk of stream) {
718
+ agg = agg === undefined ? chunk : concat(agg, chunk);
719
+ }
720
+ expect(agg).toBeDefined();
721
+ expect(agg!.usage_metadata?.input_token_details?.cache_creation).toBe(0);
722
+ expect(agg!.usage_metadata?.input_token_details?.cache_read).toBeGreaterThan(
723
+ 0
724
+ );
725
+ });
726
+
727
+ // TODO: Add proper test with long tool content
728
+ test.skip("tool caching", async () => {
729
+ const model = new ChatAnthropic({
730
+ model: "claude-3-haiku-20240307",
731
+ clientOptions: {
732
+ defaultHeaders: {
733
+ "anthropic-beta": "prompt-caching-2024-07-31",
734
+ },
735
+ },
736
+ }).bindTools([
737
+ {
738
+ name: "get_weather",
739
+ description: "Get the weather for a specific location",
740
+ input_schema: {
741
+ type: "object",
742
+ properties: {
743
+ location: {
744
+ type: "string",
745
+ description: "Location to get the weather for",
746
+ },
747
+ unit: {
748
+ type: "string",
749
+ description: "Temperature unit to return",
750
+ },
751
+ },
752
+ required: ["location"],
753
+ },
754
+ cache_control: { type: "ephemeral" },
755
+ },
756
+ ]);
757
+ const messages = [
758
+ new HumanMessage({
759
+ content: "What is the weather in Regensburg?",
760
+ }),
761
+ ];
762
+ const res = await model.invoke(messages);
763
+ console.log(res);
764
+ expect(
765
+ res.usage_metadata?.input_token_details?.cache_creation
766
+ ).toBeGreaterThan(0);
767
+ expect(res.usage_metadata?.input_token_details?.cache_read).toBe(0);
768
+ const res2 = await model.invoke(messages);
769
+ expect(res2.usage_metadata?.input_token_details?.cache_creation).toBe(0);
770
+ expect(res2.usage_metadata?.input_token_details?.cache_read).toBeGreaterThan(
771
+ 0
772
+ );
773
+ });
774
+
775
+ test("human message caching", async () => {
776
+ const model = new ChatAnthropic({
777
+ model: "claude-3-haiku-20240307",
778
+ clientOptions: {
779
+ defaultHeaders: {
780
+ "anthropic-beta": "prompt-caching-2024-07-31",
781
+ },
782
+ },
783
+ });
784
+
785
+ const messages = [
786
+ new SystemMessage({
787
+ content: [
788
+ {
789
+ type: "text",
790
+ text: `You are a pirate. Always respond in pirate dialect.\nUse the following as context when answering questions: ${CACHED_TEXT}`,
791
+ },
792
+ ],
793
+ }),
794
+ new HumanMessage({
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: "What types of messages are supported in LangChain?",
799
+ cache_control: { type: "ephemeral" },
800
+ },
801
+ ],
802
+ }),
803
+ ];
804
+
805
+ const res = await model.invoke(messages);
806
+ expect(
807
+ res.usage_metadata?.input_token_details?.cache_creation
808
+ ).toBeGreaterThan(0);
809
+ expect(res.usage_metadata?.input_token_details?.cache_read).toBe(0);
810
+ const res2 = await model.invoke(messages);
811
+ expect(res2.usage_metadata?.input_token_details?.cache_creation).toBe(0);
812
+ expect(res2.usage_metadata?.input_token_details?.cache_read).toBeGreaterThan(
813
+ 0
814
+ );
815
+ });
816
+
817
+ test("Can accept PDF documents", async () => {
818
+ const model = new ChatAnthropic({
819
+ model: "claude-3-5-sonnet-latest",
820
+ });
821
+
822
+ const pdfPath =
823
+ "../langchain-community/src/document_loaders/tests/example_data/Jacob_Lee_Resume_2023.pdf";
824
+ const pdfBase64 = await fs.readFile(pdfPath, "base64");
825
+
826
+ const response = await model.invoke([
827
+ ["system", "Use the provided documents to answer the question"],
828
+ [
829
+ "user",
830
+ [
831
+ {
832
+ type: "document",
833
+ source: {
834
+ type: "base64",
835
+ media_type: "application/pdf",
836
+ data: pdfBase64,
837
+ },
838
+ },
839
+ {
840
+ type: "text",
841
+ text: "Summarize the contents of this PDF",
842
+ },
843
+ ],
844
+ ],
845
+ ]);
846
+
847
+ expect(response.content.length).toBeGreaterThan(10);
848
+ });
849
+
850
+ test("Citations", async () => {
851
+ const citationsModel = new ChatAnthropic({
852
+ model: "claude-3-5-sonnet-latest",
853
+ });
854
+
855
+ const messages = [
856
+ {
857
+ role: "user",
858
+ content: [
859
+ {
860
+ type: "document",
861
+ source: {
862
+ type: "text",
863
+ media_type: "text/plain",
864
+ data: "The grass the user is asking about is bluegrass. The sky is orange because it's night.",
865
+ },
866
+ title: "My Document",
867
+ context: "This is a trustworthy document.",
868
+ citations: {
869
+ enabled: true,
870
+ },
871
+ },
872
+ {
873
+ type: "text",
874
+ text: "What color is the grass and sky?",
875
+ },
876
+ ],
877
+ },
878
+ ];
879
+
880
+ const response = await citationsModel.invoke(messages);
881
+
882
+ expect(response.content.length).toBeGreaterThan(2);
883
+ expect(Array.isArray(response.content)).toBe(true);
884
+ const blocksWithCitations = (response.content as any[]).filter(
885
+ (block) => block.citations !== undefined
886
+ );
887
+ expect(blocksWithCitations.length).toEqual(2);
888
+ expect(typeof blocksWithCitations[0].citations[0]).toEqual("object");
889
+
890
+ const stream = await citationsModel.stream(messages);
891
+ let aggregated;
892
+ let chunkHasCitation = false;
893
+ for await (const chunk of stream) {
894
+ aggregated = aggregated === undefined ? chunk : concat(aggregated, chunk);
895
+ if (
896
+ !chunkHasCitation &&
897
+ Array.isArray(chunk.content) &&
898
+ chunk.content.some((c: any) => c.citations !== undefined)
899
+ ) {
900
+ chunkHasCitation = true;
901
+ }
902
+ }
903
+ expect(chunkHasCitation).toBe(true);
904
+ expect(Array.isArray(aggregated?.content)).toBe(true);
905
+ expect(aggregated?.content.length).toBeGreaterThan(2);
906
+ expect(
907
+ (aggregated?.content as any[]).some((c) => c.citations !== undefined)
908
+ ).toBe(true);
909
+ });
910
+
911
+ test("Test thinking blocks multiturn invoke", async () => {
912
+ const model = new ChatAnthropic({
913
+ model: "claude-3-7-sonnet-latest",
914
+ maxTokens: 5000,
915
+ thinking: { type: "enabled", budget_tokens: 2000 },
916
+ });
917
+
918
+ async function doInvoke(messages: BaseMessage[]) {
919
+ const response = await model.invoke(messages);
920
+
921
+ expect(Array.isArray(response.content)).toBe(true);
922
+ const content = response.content as AnthropicMessageResponse[];
923
+ expect(content.some((block) => "thinking" in (block as any))).toBe(true);
924
+
925
+ for (const block of response.content) {
926
+ expect(typeof block).toBe("object");
927
+ if ((block as any).type === "thinking") {
928
+ expect(Object.keys(block).sort()).toEqual(
929
+ ["type", "thinking", "signature"].sort()
930
+ );
931
+ expect((block as any).thinking).toBeTruthy();
932
+ expect(typeof (block as any).thinking).toBe("string");
933
+ expect((block as any).signature).toBeTruthy();
934
+ expect(typeof (block as any).signature).toBe("string");
935
+ }
936
+ }
937
+ return response;
938
+ }
939
+
940
+ const invokeMessages = [new HumanMessage("Hello")];
941
+
942
+ invokeMessages.push(await doInvoke(invokeMessages));
943
+ invokeMessages.push(new HumanMessage("What is 42+7?"));
944
+
945
+ // test a second time to make sure that we've got input translation working correctly
946
+ await model.invoke(invokeMessages);
947
+ });
948
+
949
+ test("Test thinking blocks multiturn streaming", async () => {
950
+ const model = new ChatAnthropic({
951
+ model: "claude-3-7-sonnet-latest",
952
+ maxTokens: 5000,
953
+ thinking: { type: "enabled", budget_tokens: 2000 },
954
+ });
955
+
956
+ async function doStreaming(messages: BaseMessage[]) {
957
+ let full: AIMessageChunk | null = null;
958
+ for await (const chunk of await model.stream(messages)) {
959
+ full = full ? concat(full, chunk) : chunk;
960
+ }
961
+ expect(full).toBeInstanceOf(AIMessageChunk);
962
+ expect(Array.isArray(full?.content)).toBe(true);
963
+ const content3 = full?.content as AnthropicMessageResponse[];
964
+ expect(content3.some((block) => "thinking" in (block as any))).toBe(true);
965
+
966
+ for (const block of full?.content || []) {
967
+ expect(typeof block).toBe("object");
968
+ if ((block as any).type === "thinking") {
969
+ expect(Object.keys(block).sort()).toEqual(
970
+ ["type", "thinking", "signature", "index"].sort()
971
+ );
972
+ expect((block as any).thinking).toBeTruthy();
973
+ expect(typeof (block as any).thinking).toBe("string");
974
+ expect((block as any).signature).toBeTruthy();
975
+ expect(typeof (block as any).signature).toBe("string");
976
+ }
977
+ }
978
+ return full as AIMessageChunk;
979
+ }
980
+
981
+ const streamingMessages = [new HumanMessage("Hello")];
982
+
983
+ streamingMessages.push(await doStreaming(streamingMessages));
984
+ streamingMessages.push(new HumanMessage("What is 42+7?"));
985
+
986
+ // test a second time to make sure that we've got input translation working correctly
987
+ await doStreaming(streamingMessages);
988
+ });
989
+
990
+ test("Test redacted thinking blocks multiturn invoke", async () => {
991
+ const model = new ChatAnthropic({
992
+ model: "claude-3-7-sonnet-latest",
993
+ maxTokens: 5000,
994
+ thinking: { type: "enabled", budget_tokens: 2000 },
995
+ });
996
+
997
+ async function doInvoke(messages: BaseMessage[]) {
998
+ const response = await model.invoke(messages);
999
+ let hasReasoning = false;
1000
+
1001
+ for (const block of response.content) {
1002
+ expect(typeof block).toBe("object");
1003
+ if ((block as any).type === "redacted_thinking") {
1004
+ hasReasoning = true;
1005
+ expect(Object.keys(block).sort()).toEqual(["type", "data"].sort());
1006
+ expect((block as any).data).toBeTruthy();
1007
+ expect(typeof (block as any).data).toBe("string");
1008
+ }
1009
+ }
1010
+ expect(hasReasoning).toBe(true);
1011
+ return response;
1012
+ }
1013
+
1014
+ const invokeMessages = [
1015
+ new HumanMessage(
1016
+ "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB"
1017
+ ),
1018
+ ];
1019
+
1020
+ invokeMessages.push(await doInvoke(invokeMessages));
1021
+ invokeMessages.push(new HumanMessage("What is 42+7?"));
1022
+
1023
+ // test a second time to make sure that we've got input translation working correctly
1024
+ await doInvoke(invokeMessages);
1025
+ });
1026
+
1027
+ test("Test redacted thinking blocks multiturn streaming", async () => {
1028
+ const model = new ChatAnthropic({
1029
+ model: "claude-3-7-sonnet-latest",
1030
+ maxTokens: 5000,
1031
+ thinking: { type: "enabled", budget_tokens: 2000 },
1032
+ });
1033
+
1034
+ async function doStreaming(messages: BaseMessage[]) {
1035
+ let full: AIMessageChunk | null = null;
1036
+ for await (const chunk of await model.stream(messages)) {
1037
+ full = full ? concat(full, chunk) : chunk;
1038
+ }
1039
+ expect(full).toBeInstanceOf(AIMessageChunk);
1040
+ expect(Array.isArray(full?.content)).toBe(true);
1041
+ let streamHasReasoning = false;
1042
+
1043
+ for (const block of full?.content || []) {
1044
+ expect(typeof block).toBe("object");
1045
+ if ((block as any).type === "redacted_thinking") {
1046
+ streamHasReasoning = true;
1047
+ expect(Object.keys(block).sort()).toEqual(
1048
+ ["type", "data", "index"].sort()
1049
+ );
1050
+ expect((block as any).data).toBeTruthy();
1051
+ expect(typeof (block as any).data).toBe("string");
1052
+ }
1053
+ }
1054
+ expect(streamHasReasoning).toBe(true);
1055
+ return full as AIMessageChunk;
1056
+ }
1057
+
1058
+ const streamingMessages = [
1059
+ new HumanMessage(
1060
+ "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB"
1061
+ ),
1062
+ ];
1063
+
1064
+ streamingMessages.push(await doStreaming(streamingMessages));
1065
+ streamingMessages.push(new HumanMessage("What is 42+7?"));
1066
+
1067
+ // test a second time to make sure that we've got input translation working correctly
1068
+ await doStreaming(streamingMessages);
1069
+ });