Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: langchain-ai/langchainjs
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 55215e4dc22efa2a57ca773deb48976ea6bf9b73
Choose a base ref
...
head repository: langchain-ai/langchainjs
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 7e395cb00a132236d1ad4dcde51371c84dc345e5
Choose a head ref
  • 6 commits
  • 15 files changed
  • 3 contributors

Commits on Jan 13, 2025

  1. release(core): 0.3.30 (#7514)

    jacoblee93 authored Jan 13, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    dbda6d0 View commit details
  2. fix(core): Add run name for trimMessages (#7517)

    jacoblee93 authored Jan 13, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    de63626 View commit details
  3. fix(google-vertexai): fix bug when not using logprobs (#7515)

    afirstenberg authored Jan 13, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    457c8f2 View commit details
  4. fix(google-common,google-*): fix retryable errors (#7516)

    siviter-t authored Jan 13, 2025
    Copy the full SHA
    4daa673 View commit details
  5. release(google-common): 0.1.8 (#7518)

    jacoblee93 authored Jan 13, 2025
    Copy the full SHA
    ff0dc58 View commit details

Commits on Jan 18, 2025

  1. fix(core): Fix trim messages mutation bug (#7547)

    jacoblee93 authored Jan 18, 2025
    Copy the full SHA
    7e395cb View commit details
2 changes: 1 addition & 1 deletion langchain-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@langchain/core",
"version": "0.3.29",
"version": "0.3.30",
"description": "Core LangChain.js abstractions and schemas",
"type": "module",
"engines": {
89 changes: 86 additions & 3 deletions langchain-core/src/messages/tests/message_utils.test.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,11 @@ import {
mergeMessageRuns,
trimMessages,
} from "../transformers.js";
import { AIMessage } from "../ai.js";
import { AIMessage, AIMessageChunk } from "../ai.js";
import { ChatMessage } from "../chat.js";
import { HumanMessage } from "../human.js";
import { SystemMessage } from "../system.js";
import { ToolMessage } from "../tool.js";
import { BaseMessage } from "../base.js";
import {
getBufferString,
@@ -187,6 +188,7 @@ describe("trimMessages can trim", () => {
defaultMsgSuffixLen;
}
}
console.log(count);
return count;
};

@@ -196,6 +198,84 @@ describe("trimMessages can trim", () => {
};
};

it("should not mutate messages if no trimming occurs with strategy last", async () => {
const trimmer = trimMessages({
maxTokens: 128000,
strategy: "last",
startOn: [HumanMessage],
endOn: [AIMessage, ToolMessage],
tokenCounter: () => 1,
});
const messages = [
new HumanMessage({
content: "Fetch the last 5 emails from Flora Testington's inbox.",
additional_kwargs: {},
response_metadata: {},
}),
new AIMessageChunk({
id: "chatcmpl-abcdefg",
content: "",
additional_kwargs: {
tool_calls: [
{
function: {
name: "getEmails",
arguments: JSON.stringify({
inboxName: "flora@foo.org",
amount: 5,
folder: "Inbox",
searchString: null,
from: null,
subject: null,
}),
},
id: "foobarbaz",
index: 0,
type: "function",
},
],
},
response_metadata: {
usage: {},
},
tool_calls: [
{
name: "getEmails",
args: {
inboxName: "flora@foo.org",
amount: 5,
folder: "Inbox",
searchString: null,
from: null,
subject: null,
},
id: "foobarbaz",
type: "tool_call",
},
],
tool_call_chunks: [
{
name: "getEmails",
args: '{"inboxName":"flora@foo.org","amount":5,"folder":"Inbox","searchString":null,"from":null,"subject":null,"cc":[],"bcc":[]}',
id: "foobarbaz",
index: 0,
type: "tool_call_chunk",
},
],
invalid_tool_calls: [],
}),
new ToolMessage({
content: "a whole bunch of emails!",
name: "getEmails",
additional_kwargs: {},
response_metadata: {},
tool_call_id: "foobarbaz",
}),
];
const trimmedMessages = await trimmer.invoke(messages);
expect(trimmedMessages).toEqual(messages);
});

it("First 30 tokens, not allowing partial messages", async () => {
const { messages, dummyTokenCounter } = messagesAndTokenCounterFactory();
const trimmedMessages = await trimMessages(messages, {
@@ -319,6 +399,7 @@ describe("trimMessages can trim", () => {

it("Last 30 tokens, including system message, allowing partial messages, end on HumanMessage", async () => {
const { messages, dummyTokenCounter } = messagesAndTokenCounterFactory();
console.log(messages);
const trimmedMessages = await trimMessages(messages, {
maxTokens: 30,
tokenCounter: dummyTokenCounter,
@@ -431,10 +512,12 @@ describe("trimMessages can trim", () => {
"langchain_core",
"runnables",
]);
expect("func" in trimmedMessages).toBeTruthy();
expect("bound" in trimmedMessages).toBeTruthy();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect("func" in (trimmedMessages as any).bound).toBeTruthy();
// `func` is protected, so we need to cast it to any to access it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(typeof (trimmedMessages as any).func).toBe("function");
expect(typeof (trimmedMessages as any).bound.func).toBe("function");
});
});

22 changes: 14 additions & 8 deletions langchain-core/src/messages/transformers.ts
Original file line number Diff line number Diff line change
@@ -274,7 +274,7 @@ function _mergeMessageRuns(messages: BaseMessage[]): BaseMessage[] {
}
const merged: BaseMessage[] = [];
for (const msg of messages) {
const curr = msg; // Create a shallow copy of the message
const curr = msg;
const last = merged.pop();
if (!last) {
merged.push(curr);
@@ -643,7 +643,9 @@ export function trimMessages(
const trimmerOptions = messagesOrOptions;
return RunnableLambda.from((input: BaseMessage[]) =>
_trimMessagesHelper(input, trimmerOptions)
);
).withConfig({
runName: "trim_messages",
});
}
}

@@ -859,20 +861,24 @@ async function _lastMaxTokens(
...rest
} = options;

// Create a copy of messages to avoid mutation
let messagesCopy = [...messages];

if (endOn) {
const endOnArr = Array.isArray(endOn) ? endOn : [endOn];
while (
messages &&
!_isMessageType(messages[messages.length - 1], endOnArr)
messagesCopy.length > 0 &&
!_isMessageType(messagesCopy[messagesCopy.length - 1], endOnArr)
) {
messages.pop();
messagesCopy = messagesCopy.slice(0, -1);
}
}

const swappedSystem = includeSystem && messages[0]._getType() === "system";
const swappedSystem =
includeSystem && messagesCopy[0]?._getType() === "system";
let reversed_ = swappedSystem
? messages.slice(0, 1).concat(messages.slice(1).reverse())
: messages.reverse();
? messagesCopy.slice(0, 1).concat(messagesCopy.slice(1).reverse())
: messagesCopy.reverse();

reversed_ = await _firstMaxTokens(reversed_, {
...rest,
2 changes: 1 addition & 1 deletion libs/langchain-google-common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@langchain/google-common",
"version": "0.1.7",
"version": "0.1.8",
"description": "Core types and classes for Google services.",
"type": "module",
"engines": {
2 changes: 1 addition & 1 deletion libs/langchain-google-common/src/chat_models.ts
Original file line number Diff line number Diff line change
@@ -195,7 +195,7 @@ export abstract class ChatGoogleBase<AuthOptions>

stopSequences: string[] = [];

logprobs: boolean = false;
logprobs: boolean;

topLogprobs: number = 0;

118 changes: 115 additions & 3 deletions libs/langchain-google-common/src/tests/chat_models.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect, test } from "@jest/globals";
import { expect, test, jest } from "@jest/globals";
import {
AIMessage,
BaseMessage,
@@ -16,7 +16,13 @@ import { Serialized } from "@langchain/core/load/serializable";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { ChatGoogleBase, ChatGoogleBaseInput } from "../chat_models.js";
import { authOptions, MockClient, MockClientAuthInfo, mockId } from "./mock.js";
import {
authOptions,
MockClient,
MockClientAuthInfo,
MockClientError,
mockId,
} from "./mock.js";
import {
GeminiTool,
GoogleAIBaseLLMInput,
@@ -169,6 +175,39 @@ describe("Mock ChatGoogle - Gemini", () => {
expect(data.systemInstruction).not.toBeDefined();
});

test("1. Basic request format - retryable request", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
record,
projectId,
resultFile: "chat-1-mock.json",
};
const model = new ChatGoogle({
authOptions,
});
const messages: BaseMessageLike[] = [
new HumanMessage("Flip a coin and tell me H for heads and T for tails"),
new AIMessage("H"),
new HumanMessage("Flip it again"),
];

const retryableError = new MockClientError(429);
const requestSpy = jest
.spyOn(MockClient.prototype, "request")
.mockRejectedValueOnce(retryableError);

await model.invoke(messages);

expect(record.opts).toBeDefined();
expect(record.opts.data).toBeDefined();
const { data } = record.opts;
expect(data.contents).toBeDefined();
expect(data.contents.length).toEqual(3);

expect(requestSpy).toHaveBeenCalledTimes(2);
});

test("1. Invoke request format", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
@@ -1251,7 +1290,80 @@ describe("Mock ChatGoogle - Gemini", () => {
expect(record.opts.data.tools[0]).toHaveProperty("googleSearch");
});

test("7. logprobs", async () => {
test("7. logprobs request true", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
record,
projectId,
resultFile: "chat-7-mock.json",
};

const model = new ChatGoogle({
authOptions,
modelName: "gemini-1.5-flash-002",
logprobs: true,
topLogprobs: 5,
});
const result = await model.invoke(
"What are some names for a company that makes fancy socks?"
);
expect(result).toBeDefined();
const data = record?.opts?.data;
expect(data).toBeDefined();
expect(data.generationConfig.responseLogprobs).toEqual(true);
expect(data.generationConfig.logprobs).toEqual(5);
});

test("7. logprobs request false", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
record,
projectId,
resultFile: "chat-7-mock.json",
};

const model = new ChatGoogle({
authOptions,
modelName: "gemini-1.5-flash-002",
logprobs: false,
topLogprobs: 5,
});
const result = await model.invoke(
"What are some names for a company that makes fancy socks?"
);
expect(result).toBeDefined();
const data = record?.opts?.data;
expect(data).toBeDefined();
expect(data.generationConfig.responseLogprobs).toEqual(false);
expect(data.generationConfig.logprobs).not.toBeDefined();
});

test("7. logprobs request not defined", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
record,
projectId,
resultFile: "chat-7-mock.json",
};

const model = new ChatGoogle({
authOptions,
modelName: "gemini-1.5-flash-002",
});
const result = await model.invoke(
"What are some names for a company that makes fancy socks?"
);
expect(result).toBeDefined();
const data = record?.opts?.data;
expect(data).toBeDefined();
expect(data.generationConfig.responseLogprobs).not.toBeDefined();
expect(data.generationConfig.logprobs).not.toBeDefined();
});

test("7. logprobs response", async () => {
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
29 changes: 28 additions & 1 deletion libs/langchain-google-common/src/tests/llms.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from "@jest/globals";
import { expect, test, jest } from "@jest/globals";
import {
BaseMessage,
HumanMessageChunk,
@@ -10,6 +10,7 @@ import {
authOptions,
MockClient,
MockClientAuthInfo,
MockClientError,
mockFile,
mockId,
} from "./mock.js";
@@ -194,6 +195,32 @@ describe("Mock Google LLM", () => {
// console.log("record", JSON.stringify(record, null, 2));
});

test("1: generateContent - retryable request", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const record: Record<string, any> = {};
const projectId = mockId();
const authOptions: MockClientAuthInfo = {
record,
projectId,
resultFile: "llm-1-mock.json",
};
const model = new GoogleLLM({
authOptions,
});

const retryableError = new MockClientError(429);
const requestSpy = jest
.spyOn(MockClient.prototype, "request")
.mockRejectedValueOnce(retryableError);

const response = await model.invoke("Hello world");

expect(response).toEqual(
"1. Sock it to Me!\n2. Heel Yeah Socks\n3. Sole Mates\n4. Happy Soles\n5. Toe-tally Awesome Socks\n6. Sock Appeal\n7. Footsie Wootsies\n8. Thread Heads\n9. Sock Squad\n10. Sock-a-licious\n11. Darn Good Socks\n12. Sockcessories\n13. Sole Searching\n14. Sockstar\n15. Socktopia\n16. Sockology\n17. Elevated Toes\n18. The Urban Sole\n19. The Hippie Sole\n20. Sole Fuel"
);
expect(requestSpy).toHaveBeenCalledTimes(2);
});

test("1: invoke", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const record: Record<string, any> = {};
Loading