Skip to content

Commit 6b3210e

Browse files
authored
docs[minor],langchain[minor],google-common[minor]: Add Gemini tools agent docs (#4930)
* docs[minor],langchain[minor],google-common[minor]: Add Gemini tools agent docs * chore: lint files * chore: lint files * bruh * chore: lint files * cr * fix int tests * drop test * fix rest of tests * nit
1 parent ae01a09 commit 6b3210e

File tree

9 files changed

+211
-179
lines changed

9 files changed

+211
-179
lines changed

docs/core_docs/docs/integrations/chat/google_vertex_ai.mdx

+13
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,16 @@ import ChatVertexAIWSA from "@examples/models/chat/integration_googlevertexai-ws
119119
:::tip
120120
See the LangSmith trace for the example above [here](https://smith.langchain.com/public/41bbbddb-f357-4bfa-a111-def8294a4514/r).
121121
:::
122+
123+
### VertexAI tools agent
124+
125+
The Gemini family of models not only support tool calling, but can also be used in the OpenAI Tools agent.
126+
Here's an example:
127+
128+
import AgentsExample from "@examples/models/chat/chat_mistralai_agents.ts";
129+
130+
<CodeBlock language="typescript">{AgentsExample}</CodeBlock>
131+
132+
:::tip
133+
See the LangSmith trace for the agent example above [here](https://smith.langchain.com/public/3294d553-c961-4088-acfe-62252ab17d9a/r).
134+
:::

docs/core_docs/docs/integrations/chat/mistral.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import WSAJSONExample from "@examples/models/chat/chat_mistralai_wsa_json.ts";
9696

9797
<CodeBlock language="typescript">{WSAJSONExample}</CodeBlock>
9898

99-
### OpenAI tools agent
99+
### OpenAI-style tools agent
100100

101101
The larger Mistral models not only support tool calling, but can also be used in the OpenAI Tools agent.
102102
Here's an example:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { z } from "zod";
2+
3+
import { pull } from "langchain/hub";
4+
import { DynamicStructuredTool } from "@langchain/core/tools";
5+
import { AgentExecutor, createOpenAIToolsAgent } from "langchain/agents";
6+
7+
import type { ChatPromptTemplate } from "@langchain/core/prompts";
8+
import { ChatVertexAI } from "@langchain/google-vertexai";
9+
// Uncomment this if you're running inside a web/edge environment.
10+
// import { ChatVertexAI } from "@langchain/google-vertexai-web";
11+
12+
const llm: any = new ChatVertexAI({
13+
temperature: 0,
14+
modelName: "gemini-1.0-pro",
15+
});
16+
17+
// Get the prompt to use - you can modify this!
18+
// If you want to see the prompt in full, you can at:
19+
// https://smith.langchain.com/hub/hwchase17/openai-tools-agent
20+
const prompt = await pull<ChatPromptTemplate>("hwchase17/openai-tools-agent");
21+
22+
const currentWeatherTool = new DynamicStructuredTool({
23+
name: "get_current_weather",
24+
description: "Get the current weather in a given location",
25+
schema: z.object({
26+
location: z.string().describe("The city and state, e.g. San Francisco, CA"),
27+
}),
28+
func: async () => Promise.resolve("28 °C"),
29+
});
30+
31+
const agent = await createOpenAIToolsAgent({
32+
llm,
33+
tools: [currentWeatherTool],
34+
prompt,
35+
});
36+
37+
const agentExecutor = new AgentExecutor({
38+
agent,
39+
tools: [currentWeatherTool],
40+
});
41+
42+
const input = "What's the weather like in Paris?";
43+
const { output } = await agentExecutor.invoke({ input });
44+
45+
console.log(output);
46+
47+
/*
48+
It's 28 degrees Celsius in Paris.
49+
*/

libs/langchain-google-common/src/tests/chat_models.test.ts

+5-104
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ import {
66
HumanMessage,
77
HumanMessageChunk,
88
MessageContentComplex,
9-
MessageContentText,
109
SystemMessage,
1110
ToolMessage,
1211
} from "@langchain/core/messages";
13-
import { StructuredToolInterface } from "@langchain/core/tools";
14-
import { FakeTool } from "@langchain/core/utils/testing";
15-
// eslint-disable-next-line import/no-extraneous-dependencies
16-
import { z } from "zod";
1712

1813
import { ChatGoogleBase, ChatGoogleBaseInput } from "../chat_models.js";
1914
import { authOptions, MockClient, MockClientAuthInfo, mockId } from "./mock.js";
@@ -213,13 +208,7 @@ describe("Mock ChatGoogle", () => {
213208
expect(result._getType()).toEqual("ai");
214209
const aiMessage = result as AIMessage;
215210
expect(aiMessage.content).toBeDefined();
216-
expect(aiMessage.content.length).toBeGreaterThanOrEqual(1);
217-
expect(aiMessage.content[0]).toHaveProperty("type");
218-
219-
const complexContent = aiMessage.content[0] as MessageContentComplex;
220-
expect(complexContent.type).toEqual("text");
221-
const content = complexContent as MessageContentText;
222-
expect(content.text).toEqual("T");
211+
expect(aiMessage.content).toBe("T");
223212
});
224213

225214
test("1. Invoke response format", async () => {
@@ -244,13 +233,7 @@ describe("Mock ChatGoogle", () => {
244233
expect(result._getType()).toEqual("ai");
245234
const aiMessage = result as AIMessage;
246235
expect(aiMessage.content).toBeDefined();
247-
expect(aiMessage.content.length).toBeGreaterThanOrEqual(1);
248-
expect(aiMessage.content[0]).toHaveProperty("type");
249-
250-
const complexContent = aiMessage.content[0] as MessageContentComplex;
251-
expect(complexContent.type).toEqual("text");
252-
const content = complexContent as MessageContentText;
253-
expect(content.text).toEqual("T");
236+
expect(aiMessage.content).toBe("T");
254237
});
255238

256239
// SystemMessages will be turned into the human request with the prompt
@@ -327,13 +310,7 @@ describe("Mock ChatGoogle", () => {
327310
expect(result._getType()).toEqual("ai");
328311
const aiMessage = result as AIMessage;
329312
expect(aiMessage.content).toBeDefined();
330-
expect(aiMessage.content.length).toBeGreaterThanOrEqual(1);
331-
expect(aiMessage.content[0]).toHaveProperty("type");
332-
333-
const complexContent = aiMessage.content[0] as MessageContentComplex;
334-
expect(complexContent.type).toEqual("text");
335-
const content = complexContent as MessageContentText;
336-
expect(content.text).toEqual("T");
313+
expect(aiMessage.content).toBe("T");
337314
}
338315

339316
expect(caught).toEqual(true);
@@ -386,10 +363,7 @@ describe("Mock ChatGoogle", () => {
386363
expect(parts[1].inlineData).toHaveProperty("mimeType");
387364
expect(parts[1].inlineData).toHaveProperty("data");
388365

389-
expect(result.content[0]).toHaveProperty("text");
390-
expect((result.content[0] as MessageContentText).text).toEqual(
391-
"A blue square."
392-
);
366+
expect(result.content).toBe("A blue square.");
393367
});
394368

395369
test("4. Functions Bind - Gemini format request", async () => {
@@ -546,78 +520,6 @@ describe("Mock ChatGoogle", () => {
546520
expect(parameters.required[0]).toBe("testName");
547521
});
548522

549-
test("4. Functions - zod format request", async () => {
550-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
551-
const record: Record<string, any> = {};
552-
const projectId = mockId();
553-
const authOptions: MockClientAuthInfo = {
554-
record,
555-
projectId,
556-
resultFile: "chat-4-mock.json",
557-
};
558-
559-
const zodSchema = z.object({
560-
testName: z.string().describe("The name of the test that should be run."),
561-
});
562-
const tools: StructuredToolInterface[] = [
563-
new FakeTool({
564-
name: "test",
565-
description:
566-
"Run a test with a specific name and get if it passed or failed",
567-
schema: zodSchema,
568-
}),
569-
];
570-
571-
const model = new ChatGoogle({
572-
authOptions,
573-
}).bind({
574-
tools,
575-
});
576-
577-
const result = await model.invoke("What?");
578-
579-
const toolsResult = record?.opts?.data?.tools;
580-
console.log("toolsResult", JSON.stringify(toolsResult, null, 1));
581-
expect(toolsResult).toBeDefined();
582-
expect(Array.isArray(toolsResult)).toBeTruthy();
583-
expect(toolsResult).toHaveLength(1);
584-
585-
const toolResult = toolsResult[0];
586-
expect(toolResult).toBeDefined();
587-
expect(toolResult).toHaveProperty("functionDeclarations");
588-
expect(Array.isArray(toolResult.functionDeclarations)).toBeTruthy();
589-
expect(toolResult.functionDeclarations).toHaveLength(1);
590-
591-
const functionDeclaration = toolResult.functionDeclarations[0];
592-
expect(functionDeclaration.name).toBe("test");
593-
expect(functionDeclaration.description).toBe(
594-
"Run a test with a specific name and get if it passed or failed"
595-
);
596-
expect(functionDeclaration.parameters).toBeDefined();
597-
expect(typeof functionDeclaration.parameters).toBe("object");
598-
599-
const parameters = functionDeclaration?.parameters;
600-
expect(parameters.type).toBe("object");
601-
expect(parameters).toHaveProperty("properties");
602-
expect(parameters).not.toHaveProperty("additionalProperties");
603-
expect(parameters).not.toHaveProperty("$schema");
604-
expect(typeof parameters.properties).toBe("object");
605-
606-
expect(parameters.properties.testName).toBeDefined();
607-
expect(typeof parameters.properties.testName).toBe("object");
608-
expect(parameters.properties.testName.type).toBe("string");
609-
expect(parameters.properties.testName.description).toBe(
610-
"The name of the test that should be run."
611-
);
612-
613-
expect(parameters.required).toBeDefined();
614-
expect(Array.isArray(parameters.required)).toBeTruthy();
615-
expect(parameters.required).toHaveLength(1);
616-
expect(parameters.required[0]).toBe("testName");
617-
618-
console.log(result);
619-
});
620-
621523
test("4. Functions - results", async () => {
622524
// eslint-disable-next-line @typescript-eslint/no-explicit-any
623525
const record: Record<string, any> = {};
@@ -660,8 +562,7 @@ describe("Mock ChatGoogle", () => {
660562

661563
console.log(JSON.stringify(result, null, 1));
662564
expect(result).toHaveProperty("content");
663-
expect(Array.isArray(result.content)).toBeTruthy();
664-
expect(result.content).toHaveLength(0);
565+
expect(result.content).toBe("");
665566
const args = result?.lc_kwargs?.additional_kwargs;
666567
expect(args).toBeDefined();
667568
expect(args).toHaveProperty("tool_calls");

libs/langchain-google-common/src/utils/common.ts

+60
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { StructuredToolInterface } from "@langchain/core/tools";
12
import type {
3+
GeminiTool,
24
GoogleAIBaseLanguageModelCallOptions,
35
GoogleAIModelParams,
46
GoogleAIModelRequestParams,
@@ -35,6 +37,64 @@ export function copyAIModelParamsInto(
3537
options?.safetySettings ?? params?.safetySettings ?? target.safetySettings;
3638

3739
ret.tools = options?.tools;
40+
// Ensure tools are formatted properly for Gemini
41+
const geminiTools = options?.tools
42+
?.map((tool) => {
43+
if (
44+
"function" in tool &&
45+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
46+
"parameters" in (tool.function as Record<string, any>)
47+
) {
48+
// Tool is in OpenAI format. Convert to Gemini then return.
49+
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
const castTool = tool.function as Record<string, any>;
52+
const cleanedParameters = castTool.parameters;
53+
if ("$schema" in cleanedParameters) {
54+
delete cleanedParameters.$schema;
55+
}
56+
if ("additionalProperties" in cleanedParameters) {
57+
delete cleanedParameters.additionalProperties;
58+
}
59+
const toolInGeminiFormat: GeminiTool = {
60+
functionDeclarations: [
61+
{
62+
name: castTool.name,
63+
description: castTool.description,
64+
parameters: cleanedParameters,
65+
},
66+
],
67+
};
68+
return toolInGeminiFormat;
69+
} else if ("functionDeclarations" in tool) {
70+
return tool;
71+
} else {
72+
return null;
73+
}
74+
})
75+
.filter((tool): tool is GeminiTool => tool !== null);
76+
77+
const structuredOutputTools = options?.tools
78+
?.map((tool) => {
79+
if ("lc_namespace" in tool) {
80+
return tool;
81+
} else {
82+
return null;
83+
}
84+
})
85+
.filter((tool): tool is StructuredToolInterface => tool !== null);
86+
87+
if (
88+
structuredOutputTools &&
89+
structuredOutputTools.length > 0 &&
90+
geminiTools &&
91+
geminiTools.length > 0
92+
) {
93+
throw new Error(
94+
`Cannot mix structured tools with Gemini tools.\nReceived ${structuredOutputTools.length} structured tools and ${geminiTools.length} Gemini tools.`
95+
);
96+
}
97+
ret.tools = geminiTools ?? structuredOutputTools;
3898

3999
return ret;
40100
}

libs/langchain-google-common/src/utils/gemini.ts

+43-13
Original file line numberDiff line numberDiff line change
@@ -179,20 +179,39 @@ function toolMessageToContent(message: ToolMessage): GeminiContent[] {
179179
},
180180
""
181181
);
182-
const content = JSON.parse(contentStr);
183-
return [
184-
{
185-
role: "function",
186-
parts: [
187-
{
188-
functionResponse: {
189-
name: message.tool_call_id,
190-
response: content,
182+
183+
try {
184+
const content = JSON.parse(contentStr);
185+
return [
186+
{
187+
role: "function",
188+
parts: [
189+
{
190+
functionResponse: {
191+
name: message.tool_call_id,
192+
response: content,
193+
},
191194
},
192-
},
193-
],
194-
},
195-
];
195+
],
196+
},
197+
];
198+
} catch (_) {
199+
return [
200+
{
201+
role: "function",
202+
parts: [
203+
{
204+
functionResponse: {
205+
name: message.tool_call_id,
206+
response: {
207+
response: contentStr,
208+
},
209+
},
210+
},
211+
],
212+
},
213+
];
214+
}
196215
}
197216

198217
export function baseMessageToContent(message: BaseMessage): GeminiContent[] {
@@ -445,6 +464,17 @@ export function chunkToString(chunk: BaseMessageChunk): string {
445464

446465
export function partToMessage(part: GeminiPart): BaseMessageChunk {
447466
const fields = partsToBaseMessageFields([part]);
467+
if (typeof fields.content === "string") {
468+
return new AIMessageChunk(fields);
469+
} else if (fields.content.every((item) => item.type === "text")) {
470+
const newContent = fields.content
471+
.map((item) => ("text" in item ? item.text : ""))
472+
.join("");
473+
return new AIMessageChunk({
474+
...fields,
475+
content: newContent,
476+
});
477+
}
448478
return new AIMessageChunk(fields);
449479
}
450480

0 commit comments

Comments
 (0)