From 1e85e43871da151b158de4af83f32bfbd61dbf4c Mon Sep 17 00:00:00 2001 From: Jari Bakken Date: Sat, 14 Dec 2024 04:53:04 +0100 Subject: [PATCH 1/9] fix(google-gauth): Remove logging of error responses (#7361) --- libs/langchain-google-gauth/src/auth.ts | 40 +++++++++---------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/libs/langchain-google-gauth/src/auth.ts b/libs/langchain-google-gauth/src/auth.ts index bb8053b9c521..fff6cc73cb2a 100644 --- a/libs/langchain-google-gauth/src/auth.ts +++ b/libs/langchain-google-gauth/src/auth.ts @@ -85,32 +85,20 @@ export class GAuthClient implements GoogleAbstractedClient { } async request(opts: GoogleAbstractedClientOps): Promise { - try { - const ret = await this.gauth.request(opts); - const [contentType] = ret?.headers?.["content-type"]?.split(/;/) ?? [""]; - if (opts.responseType !== "stream") { - return ret; - } else if (contentType === "text/event-stream") { - return { - ...ret, - data: new NodeSseJsonStream(ret.data), - }; - } else { - return { - ...ret, - data: new NodeJsonStream(ret.data), - }; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (xx: any) { - console.error("call to gauth.request", JSON.stringify(xx, null, 2)); - console.error( - "call to gauth.request opts=", - JSON.stringify(opts, null, 2) - ); - console.error("call to gauth.request message:", xx?.message); - throw xx; + const ret = await this.gauth.request(opts); + const [contentType] = ret?.headers?.["content-type"]?.split(/;/) ?? [""]; + if (opts.responseType !== "stream") { + return ret; + } else if (contentType === "text/event-stream") { + return { + ...ret, + data: new NodeSseJsonStream(ret.data), + }; + } else { + return { + ...ret, + data: new NodeJsonStream(ret.data), + }; } } } From 3dfaa37e702af82647d0ab518c192da98b08c368 Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Sat, 14 Dec 2024 04:53:16 +0100 Subject: [PATCH 2/9] fix(community): file system chat history (#7357) --- docs/core_docs/docs/integrations/memory/file.mdx | 4 ++-- examples/src/memory/file.ts | 2 +- libs/langchain-community/src/stores/message/file_system.ts | 4 ++-- .../src/stores/tests/file_chat_history.int.test.ts | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/core_docs/docs/integrations/memory/file.mdx b/docs/core_docs/docs/integrations/memory/file.mdx index d485be9d29c6..25a41fef2415 100644 --- a/docs/core_docs/docs/integrations/memory/file.mdx +++ b/docs/core_docs/docs/integrations/memory/file.mdx @@ -4,9 +4,9 @@ hide_table_of_contents: true import CodeBlock from "@theme/CodeBlock"; -# File Chat Message History +# File System Chat Message History -The `FileChatMessageHistory` uses a JSON file to store chat message history. For longer-term persistence across chat sessions, you can swap out the default in-memory `chatHistory` that backs chat memory classes like `BufferMemory`. +The `FileSystemChatMessageHistory` uses a JSON file to store chat message history. For longer-term persistence across chat sessions, you can swap out the default in-memory `chatHistory` that backs chat memory classes like `BufferMemory`. ## Setup diff --git a/examples/src/memory/file.ts b/examples/src/memory/file.ts index 5728ec3af26d..e8791f5fbcf3 100644 --- a/examples/src/memory/file.ts +++ b/examples/src/memory/file.ts @@ -66,6 +66,6 @@ const sessions = await chatHistory.getAllSessions(); console.log(sessions); /* [ - { sessionId: 'langchain-test-session', context: { title: "Introducing Jim" } } + { id: 'langchain-test-session', context: { title: "Introducing Jim" } } ] */ diff --git a/libs/langchain-community/src/stores/message/file_system.ts b/libs/langchain-community/src/stores/message/file_system.ts index f81af5f7a4ef..0e49b5ee95dd 100644 --- a/libs/langchain-community/src/stores/message/file_system.ts +++ b/libs/langchain-community/src/stores/message/file_system.ts @@ -189,8 +189,8 @@ export class FileSystemChatMessageHistory extends BaseListChatMessageHistory { async getAllSessions(): Promise { await this.init(); const userSessions = store[this.userId] - ? Object.values(store[this.userId]).map((session) => ({ - id: session.id, + ? Object.entries(store[this.userId]).map(([id, session]) => ({ + id, context: session.context, })) : []; diff --git a/libs/langchain-community/src/stores/tests/file_chat_history.int.test.ts b/libs/langchain-community/src/stores/tests/file_chat_history.int.test.ts index 1610bcbee0e1..0229caa30fe1 100644 --- a/libs/langchain-community/src/stores/tests/file_chat_history.int.test.ts +++ b/libs/langchain-community/src/stores/tests/file_chat_history.int.test.ts @@ -143,5 +143,7 @@ test("FileSystemChatMessageHistory set context and get all sessions", async () = expect(sessions.length).toBe(2); expect(sessions[0].context).toEqual(context1); + expect(sessions[0].id).toBeDefined(); expect(sessions[1].context).toEqual(context2); + expect(sessions[1].id).toBeDefined(); }); From f8175ef56f8a67f5c658b1199815058d3e7cb29a Mon Sep 17 00:00:00 2001 From: A-Jatana <114192121+A-Jatana@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:53:34 -0500 Subject: [PATCH 3/9] feat(community): Added Google Trends Integration (#7248) Co-authored-by: Roger Zhao Co-authored-by: nishus24 <128431351+nishus24@users.noreply.github.com> --- docs/api_refs/blacklisted-entrypoints.json | 1 + .../docs/integrations/tools/google_trends.mdx | 42 +++++ examples/src/tools/google_trends.ts | 9 + libs/langchain-community/.gitignore | 4 + libs/langchain-community/langchain.config.js | 1 + libs/langchain-community/package.json | 13 ++ .../src/tools/google_trends.ts | 161 ++++++++++++++++++ .../src/tools/tests/google_trends.int.test.ts | 34 ++++ 8 files changed, 265 insertions(+) create mode 100644 docs/core_docs/docs/integrations/tools/google_trends.mdx create mode 100644 examples/src/tools/google_trends.ts create mode 100644 libs/langchain-community/src/tools/google_trends.ts create mode 100644 libs/langchain-community/src/tools/tests/google_trends.int.test.ts diff --git a/docs/api_refs/blacklisted-entrypoints.json b/docs/api_refs/blacklisted-entrypoints.json index ea3491ec2294..419d8800827d 100644 --- a/docs/api_refs/blacklisted-entrypoints.json +++ b/docs/api_refs/blacklisted-entrypoints.json @@ -7,6 +7,7 @@ "../../langchain/src/tools/connery.ts", "../../langchain/src/tools/gmail.ts", "../../langchain/src/tools/google_places.ts", + "../../langchain/src/tools/google_trends.ts", "../../langchain/src/embeddings/bedrock.ts", "../../langchain/src/embeddings/cloudflare_workersai.ts", "../../langchain/src/embeddings/ollama.ts", diff --git a/docs/core_docs/docs/integrations/tools/google_trends.mdx b/docs/core_docs/docs/integrations/tools/google_trends.mdx new file mode 100644 index 000000000000..bbbdec5d70c0 --- /dev/null +++ b/docs/core_docs/docs/integrations/tools/google_trends.mdx @@ -0,0 +1,42 @@ +--- +hide_table_of_contents: true +--- + +import CodeBlock from "@theme/CodeBlock"; + +# Google Trends Tool + +The Google Trends Tool allows your agent to utilize the Google Trends API from SerpApi to retrieve and analyze search interest data. +This can be useful for understanding trending topics, regional search interest, and historical popularity of search terms. + +For API details see [here](https://serpapi.com/google-trends-api) + +SerpApi caches queries, so the first query will be slower while subsequent identical queries will be fast. +Occasionally, related queries will not work while interest over time will be fine. You can check your query [here](https://serpapi.com/playground?engine=google_trends&q=monster&data_type=RELATED_QUERIES). + +## Setup + +To use this tool, you will need to configure access to the Google Trends API from SerpApi. + +Get an API key from [SerpApi](https://serpapi.com/users/sign_in) + +Then, set your API key as `process.env.SERPAPI_API_KEY` or pass it in as an `apiKey` constructor argument. + +## Usage + +import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; + + + +```bash npm2yarn +npm install @langchain/openai @langchain/community @langchain/core +``` + +import ToolExample from "@examples/tools/google_trends.ts"; + +{ToolExample} + +## Related + +- Tool [conceptual guide](/docs/concepts/tools) +- Tool [how-to guides](/docs/how_to/#tools) diff --git a/examples/src/tools/google_trends.ts b/examples/src/tools/google_trends.ts new file mode 100644 index 000000000000..25cf5f174ba3 --- /dev/null +++ b/examples/src/tools/google_trends.ts @@ -0,0 +1,9 @@ +import { SERPGoogleTrendsTool } from "@langchain/community/tools/google_trends"; + +export async function run() { + const tool = new SERPGoogleTrendsTool(); + + const res = await tool.invoke("Monster"); + + console.log(res); +} diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index 5064c1f14c79..8dc708cbe23e 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -70,6 +70,10 @@ tools/google_places.cjs tools/google_places.js tools/google_places.d.ts tools/google_places.d.cts +tools/google_trends.cjs +tools/google_trends.js +tools/google_trends.d.ts +tools/google_trends.d.cts tools/google_routes.cjs tools/google_routes.js tools/google_routes.d.ts diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index b0207b8612ab..547960384372 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -51,6 +51,7 @@ export const config = { "tools/google_calendar": "tools/google_calendar/index", "tools/google_custom_search": "tools/google_custom_search", "tools/google_places": "tools/google_places", + "tools/google_trends": "tools/google_trends", "tools/google_routes": "tools/google_routes", "tools/ifttt": "tools/ifttt", "tools/searchapi": "tools/searchapi", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 6cdbe97e2664..108a51c174ce 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -874,6 +874,15 @@ "import": "./tools/google_places.js", "require": "./tools/google_places.cjs" }, + "./tools/google_trends": { + "types": { + "import": "./tools/google_trends.d.ts", + "require": "./tools/google_trends.d.cts", + "default": "./tools/google_trends.d.ts" + }, + "import": "./tools/google_trends.js", + "require": "./tools/google_trends.cjs" + }, "./tools/google_routes": { "types": { "import": "./tools/google_routes.d.ts", @@ -3191,6 +3200,10 @@ "tools/google_places.js", "tools/google_places.d.ts", "tools/google_places.d.cts", + "tools/google_trends.cjs", + "tools/google_trends.js", + "tools/google_trends.d.ts", + "tools/google_trends.d.cts", "tools/google_routes.cjs", "tools/google_routes.js", "tools/google_routes.d.ts", diff --git a/libs/langchain-community/src/tools/google_trends.ts b/libs/langchain-community/src/tools/google_trends.ts new file mode 100644 index 000000000000..18f063e791e8 --- /dev/null +++ b/libs/langchain-community/src/tools/google_trends.ts @@ -0,0 +1,161 @@ +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { Tool } from "@langchain/core/tools"; + +/** + * Interfaces for the response from the SerpApi Google Trends API. + */ +interface TimelineValue { + query: string; + value: string; + extracted_value: number; +} + +interface TimelineData { + date: string; + timestamp: string; + values: TimelineValue[]; +} + +/** + * Interface for parameters required by SERPGoogleTrendsTool class. + */ +export interface SERPGoogleTrendsToolParams { + apiKey?: string; +} + +/** + * Tool that queries the Google Trends API. Uses default interest over time. + */ +export class SERPGoogleTrendsTool extends Tool { + static lc_name() { + return "SERPGoogleTrendsTool"; + } + + get lc_secrets(): { [key: string]: string } | undefined { + return { + apiKey: "SERPAPI_API_KEY", + }; + } + + name = "google_trends"; + + protected apiKey: string; + + description = `A wrapper around Google Trends API. Useful for analyzing and retrieving trending search data based on keywords, + categories, or regions. Input should be a search query or specific parameters for trends analysis.`; + + constructor(fields?: SERPGoogleTrendsToolParams) { + super(...arguments); + const apiKey = fields?.apiKey ?? getEnvironmentVariable("SERPAPI_API_KEY"); + if (apiKey === undefined) { + throw new Error( + `Google Trends API key not set. You can set it as "SERPAPI_API_KEY" in your environment variables.` + ); + } + this.apiKey = apiKey; + } + + async _call(query: string): Promise { + /** + * Related queries only accepts one at a time, and multiple + * queries at once on interest over time (default) is effectively the same as + * each query one by one. + * + * SerpApi caches queries, so the first time will be slower + * and subsequent identical queries will be very fast. + */ + if (query.split(",").length > 1) { + throw new Error("Please do one query at a time"); + } + const serpapiApiKey = this.apiKey; + const params = new URLSearchParams({ + engine: "google_trends", + api_key: serpapiApiKey, + q: query, + }); + + const res = await fetch( + `https://serpapi.com/search.json?${params.toString()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!res.ok) { + throw new Error(`Error fetching data from SerpAPI: ${res.statusText}`); + } + + const clientDict = await res.json(); + const totalResults = clientDict.interest_over_time?.timeline_data ?? []; + + if (totalResults.length === 0) { + return "No good Trend Result was found"; + } + + const startDate = totalResults[0].date.split(" "); + const endDate = totalResults[totalResults.length - 1].date.split(" "); + const values = totalResults.map( + (result: TimelineData) => result.values[0].extracted_value + ); + const minValue = Math.min(...values); + const maxValue = Math.max(...values); + const avgValue = + values.reduce((a: number, b: number) => a + b, 0) / values.length; + const percentageChange = + ((values[values.length - 1] - values[0]) / (values[0] || 1)) * 100; + + const relatedParams = new URLSearchParams({ + engine: "google_trends", + api_key: serpapiApiKey, + data_type: "RELATED_QUERIES", + q: query, + }); + + const relatedRes = await fetch( + `https://serpapi.com/search.json?${relatedParams.toString()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + let rising = []; + let top = []; + if (!relatedRes.ok) { + // Sometimes related queries from SerpAPI will fail, but interest over time will be fine + console.error( + `Error fetching related queries from SerpAPI: ${relatedRes.statusText}` + ); + } else { + const relatedDict = await relatedRes.json(); + rising = + relatedDict.related_queries?.rising?.map( + (result: { query: string }) => result.query + ) ?? []; + top = + relatedDict.related_queries?.top?.map( + (result: { query: string }) => result.query + ) ?? []; + } + + const doc = [ + `Query: ${query}`, + `Date From: ${startDate[0]} ${startDate[1]}, ${startDate[2]}`, + `Date To: ${endDate[0]} ${endDate[1]} ${endDate[2]}`, + `Min Value: ${minValue}`, + `Max Value: ${maxValue}`, + `Average Value: ${avgValue}`, + `Percent Change: ${percentageChange.toFixed(2)}%`, + `Trend values: ${values.join(", ")}`, + `Rising Related Queries: ${rising.join(", ")}`, + `Top Related Queries: ${top.join(", ")}`, + ]; + + return doc.join("\n\n"); + } +} diff --git a/libs/langchain-community/src/tools/tests/google_trends.int.test.ts b/libs/langchain-community/src/tools/tests/google_trends.int.test.ts new file mode 100644 index 000000000000..49be189bd7ad --- /dev/null +++ b/libs/langchain-community/src/tools/tests/google_trends.int.test.ts @@ -0,0 +1,34 @@ +import { expect, describe } from "@jest/globals"; +import { SERPGoogleTrendsTool } from "../google_trends.js"; + +describe("SERPGoogleTrendsTool", () => { + test("should be setup with correct parameters", async () => { + const instance = new SERPGoogleTrendsTool(); + expect(instance.name).toBe("google_trends"); + }); + + test("SERPGoogleTrendsTool returns expected result for valid query", async () => { + const tool = new SERPGoogleTrendsTool(); + + const result = await tool._call("Coffee"); + + expect(result).toContain("Query: Coffee"); + expect(result).toContain("Date From:"); + expect(result).toContain("Date To:"); + expect(result).toContain("Min Value:"); + expect(result).toContain("Max Value:"); + expect(result).toContain("Average Value:"); + expect(result).toContain("Percent Change:"); + expect(result).toContain("Trend values:"); + expect(result).toContain("Rising Related Queries:"); + expect(result).toContain("Top Related Queries:"); + }); + + test("SERPGoogleTrendsTool returns 'No good Trend Result was found' for a non-existent query", async () => { + const tool = new SERPGoogleTrendsTool(); + + const result = await tool._call("earghajgpajrpgjaprgag"); + + expect(result).toBe("No good Trend Result was found"); + }); +}); From a84856e57129c4025a7bc7e179f89150f484ca40 Mon Sep 17 00:00:00 2001 From: ccurme Date: Fri, 13 Dec 2024 23:00:50 -0500 Subject: [PATCH 4/9] docs: add embeddings and vector store tabs (#7347) --- .../docs/integrations/chat/index.mdx | 8 ++ .../integrations/text_embedding/index.mdx | 8 ++ .../docs/integrations/vectorstores/index.mdx | 8 ++ docs/core_docs/src/theme/EmbeddingTabs.js | 127 ++++++++++++++++++ docs/core_docs/src/theme/VectorStoreTabs.js | 105 +++++++++++++++ 5 files changed, 256 insertions(+) create mode 100644 docs/core_docs/src/theme/EmbeddingTabs.js create mode 100644 docs/core_docs/src/theme/VectorStoreTabs.js diff --git a/docs/core_docs/docs/integrations/chat/index.mdx b/docs/core_docs/docs/integrations/chat/index.mdx index 3432ab9aa4bf..c366d6d70585 100644 --- a/docs/core_docs/docs/integrations/chat/index.mdx +++ b/docs/core_docs/docs/integrations/chat/index.mdx @@ -12,6 +12,14 @@ hide_table_of_contents: true If you'd like to write your own chat model, see [this how-to](/docs/how_to/custom_chat). If you'd like to contribute an integration, see [Contributing integrations](/docs/contributing). ::: +import ChatModelTabs from "@theme/ChatModelTabs"; + + + +```python +await model.invoke("Hello, world!") +``` + ## Featured providers | Model | Stream | JSON mode | [Tool Calling](/docs/how_to/tool_calling/) | [`withStructuredOutput()`](/docs/how_to/structured_output/#the-.withstructuredoutput-method) | [Multimodal](/docs/how_to/multimodal_inputs/) | diff --git a/docs/core_docs/docs/integrations/text_embedding/index.mdx b/docs/core_docs/docs/integrations/text_embedding/index.mdx index 3dab48bc7e3d..e8b4938e2e95 100644 --- a/docs/core_docs/docs/integrations/text_embedding/index.mdx +++ b/docs/core_docs/docs/integrations/text_embedding/index.mdx @@ -9,6 +9,14 @@ sidebar_class_name: hidden This page documents integrations with various model providers that allow you to use embeddings in LangChain. +import EmbeddingTabs from "@theme/EmbeddingTabs"; + + + +```javascript +await embeddings.embedQuery("Hello, world!"); +``` + import { CategoryTable, IndexTable } from "@theme/FeatureTables"; diff --git a/docs/core_docs/docs/integrations/vectorstores/index.mdx b/docs/core_docs/docs/integrations/vectorstores/index.mdx index f6c9a6f5a194..025d5a375687 100644 --- a/docs/core_docs/docs/integrations/vectorstores/index.mdx +++ b/docs/core_docs/docs/integrations/vectorstores/index.mdx @@ -7,6 +7,14 @@ sidebar_class_name: hidden A [vector store](/docs/concepts/#vectorstores) stores [embedded](/docs/concepts/embedding_models) data and performs similarity search. +import EmbeddingTabs from "@theme/EmbeddingTabs"; + + + +import VectorStoreTabs from "@theme/VectorStoreTabs"; + + + LangChain.js integrates with a variety of vector stores. You can check out a full list below: import { CategoryTable, IndexTable } from "@theme/FeatureTables"; diff --git a/docs/core_docs/src/theme/EmbeddingTabs.js b/docs/core_docs/src/theme/EmbeddingTabs.js new file mode 100644 index 000000000000..cf85cdb2cc14 --- /dev/null +++ b/docs/core_docs/src/theme/EmbeddingTabs.js @@ -0,0 +1,127 @@ +/* eslint-disable react/jsx-props-no-spreading, react/destructuring-assignment */ +import React from "react"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import CodeBlock from "@theme-original/CodeBlock"; +import Npm2Yarn from "@theme/Npm2Yarn"; + +const DEFAULTS = { + openaiParams: `{\n model: "text-embedding-3-large"\n}`, + azureParams: `{\n azureOpenAIApiEmbeddingsDeploymentName: "text-embedding-ada-002"\n}`, + awsParams: `{\n model: "amazon.titan-embed-text-v1"\n}`, + vertexParams: `{\n model: "text-embedding-004"\n}`, + mistralParams: `{\n model: "mistral-embed"\n}`, + cohereParams: `{\n model: "embed-english-v3.0"\n}`, +}; + +/** + * @typedef {Object} EmbeddingTabsProps - Component props. + * @property {string} [openaiParams] + * @property {string} [azureParams] + * @property {string} [awsParams] + * @property {string} [vertexParams] + * @property {string} [mistralParams] + * @property {string} [cohereParams] + * + * @property {boolean} [hideOpenai] + * @property {boolean} [hideAzure] + * @property {boolean} [hideAws] + * @property {boolean} [hideVertex] + * @property {boolean} [hideMistral] + * @property {boolean} [hideCohere] + * + * @property {string} [customVarName] - Custom variable name for the model. Defaults to `"embeddings"`. + */ + +/** + * @param {EmbeddingTabsProps} props - Component props. + */ +export default function EmbeddingTabs(props) { + const { customVarName } = props; + + const embeddingsVarName = customVarName ?? "embeddings"; + + const openaiParams = props.openaiParams ?? DEFAULTS.openaiParams; + const azureParams = props.azureParams ?? DEFAULTS.azureParams; + const awsParams = props.awsParams ?? DEFAULTS.awsParams; + const vertexParams = props.vertexParams ?? DEFAULTS.vertexParams; + const mistralParams = props.mistralParams ?? DEFAULTS.mistralParams; + const cohereParams = props.cohereParams ?? DEFAULTS.cohereParams; + const providers = props.providers ?? [ + "openai", + "azure", + "aws", + "vertex", + "mistral", + "cohere", + ]; + + const tabs = { + openai: { + value: "openai", + label: "OpenAI", + default: true, + text: `import { OpenAIEmbeddings } from "@langchain/openai";\n\nconst ${embeddingsVarName} = new OpenAIEmbeddings(${openaiParams});`, + envs: `OPENAI_API_KEY=your-api-key`, + dependencies: "@langchain/openai", + }, + azure: { + value: "azure", + label: "Azure", + default: false, + text: `import { AzureOpenAIEmbeddings } from "@langchain/openai";\n\nconst ${embeddingsVarName} = new AzureOpenAIEmbeddings(${azureParams});`, + envs: `AZURE_OPENAI_API_INSTANCE_NAME=\nAZURE_OPENAI_API_KEY=\nAZURE_OPENAI_API_VERSION="2024-02-01"`, + dependencies: "@langchain/openai", + }, + aws: { + value: "aws", + label: "AWS", + default: false, + text: `import { BedrockEmbeddings } from "@langchain/aws";\n\nconst ${embeddingsVarName} = new BedrockEmbeddings(${awsParams});`, + envs: `BEDROCK_AWS_REGION=your-region`, + dependencies: "@langchain/aws", + }, + vertex: { + value: "vertex", + label: "VertexAI", + default: false, + text: `import { VertexAIEmbeddings } from "@langchain/google-vertexai";\n\nconst ${embeddingsVarName} = new VertexAIEmbeddings(${vertexParams});`, + envs: `GOOGLE_APPLICATION_CREDENTIALS=credentials.json`, + dependencies: "@langchain/google-vertexai", + }, + mistral: { + value: "mistral", + label: "MistralAI", + default: false, + text: `import { MistralAIEmbeddings } from "@langchain/mistralai";\n\nconst ${embeddingsVarName} = new MistralAIEmbeddings(${mistralParams});`, + envs: `MISTRAL_API_KEY=your-api-key`, + dependencies: "@langchain/mistralai", + }, + cohere: { + value: "cohereParams", + label: "Cohere", + default: false, + text: `import { CohereEmbeddings } from "@langchain/cohere";\n\nconst ${embeddingsVarName} = new CohereEmbeddings(${cohereParams});`, + envs: `COHERE_API_KEY=your-api-key`, + dependencies: "@langchain/cohere", + }, + }; + + const displayedTabs = providers.map((provider) => tabs[provider]); + + return ( +
+

Pick your embedding model:

+ + {displayedTabs.map((tab) => ( + +

Install dependencies

+ {tab.dependencies} + {tab.envs} + {tab.text} +
+ ))} +
+
+ ); +} diff --git a/docs/core_docs/src/theme/VectorStoreTabs.js b/docs/core_docs/src/theme/VectorStoreTabs.js new file mode 100644 index 000000000000..a8ff549e70db --- /dev/null +++ b/docs/core_docs/src/theme/VectorStoreTabs.js @@ -0,0 +1,105 @@ +import React from "react"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import CodeBlock from "@theme-original/CodeBlock"; +import Npm2Yarn from "@theme/Npm2Yarn"; + +export default function VectorStoreTabs(props) { + const { customVarName } = props; + + const vectorStoreVarName = customVarName ?? "vectorStore"; + + const tabItems = [ + { + value: "Memory", + label: "Memory", + text: `import { MemoryVectorStore } from "langchain/vectorstores/memory";\n\nconst ${vectorStoreVarName} = new MemoryVectorStore(embeddings);`, + dependencies: "langchain", + default: true, + }, + { + value: "Chroma", + label: "Chroma", + text: `import { Chroma } from "@langchain/community/vectorstores/chroma";\n\nconst ${vectorStoreVarName} = new Chroma(embeddings, {\n collectionName: "a-test-collection",\n});`, + dependencies: "@langchain/community", + default: true, + }, + { + value: "FAISS", + label: "FAISS", + text: `import { FaissStore } from "@langchain/community/vectorstores/faiss";\n\nconst ${vectorStoreVarName} = new FaissStore(embeddings, {});`, + dependencies: "@langchain/community", + default: false, + }, + { + value: "MongoDB", + label: "MongoDB", + text: `import { MongoDBAtlasVectorSearch } from "@langchain/mongodb" +import { MongoClient } from "mongodb"; + +const client = new MongoClient(process.env.MONGODB_ATLAS_URI || ""); +const collection = client + .db(process.env.MONGODB_ATLAS_DB_NAME) + .collection(process.env.MONGODB_ATLAS_COLLECTION_NAME); + +const ${vectorStoreVarName} = new MongoDBAtlasVectorSearch(embeddings, { + collection: collection, + indexName: "vector_index", + textKey: "text", + embeddingKey: "embedding", +});`, + dependencies: "@langchain/mongodb", + default: false, + }, + { + value: "PGVector", + label: "PGVector", + text: `import PGVectorStore from "@langchain/community/vectorstores/pgvector"; + +const ${vectorStoreVarName} = await PGVectorStore.initialize(embeddings, {})`, + dependencies: "@langchain/community", + default: false, + }, + { + value: "Pinecone", + label: "Pinecone", + text: `import { PineconeStore } from "@langchain/pinecone"; +import { Pinecone as PineconeClient } from "@pinecone-database/pinecone"; + +const pinecone = new PineconeClient(); +const ${vectorStoreVarName} = new PineconeStore(embeddings, { + pineconeIndex, + maxConcurrency: 5, +});`, + dependencies: "@langchain/pinecone", + default: false, + }, + { + value: "Qdrant", + label: "Qdrant", + text: `import { QdrantVectorStore } from "@langchain/qdrant"; + +const ${vectorStoreVarName} = await QdrantVectorStore.fromExistingCollection(embeddings, { + url: process.env.QDRANT_URL, + collectionName: "langchainjs-testing", +});`, + dependencies: "@langchain/qdrant", + default: false, + }, + ]; + + return ( +
+

Pick your vector store:

+ + {tabItems.map((tab) => ( + +

Install dependencies

+ {tab.dependencies} + {tab.text} +
+ ))} +
+
+ ); +} From b2afdf160f538d10edb42de34094e172df1cf7c0 Mon Sep 17 00:00:00 2001 From: Bernard F Faucher Date: Fri, 13 Dec 2024 23:24:31 -0500 Subject: [PATCH 5/9] feat(community): Neo4j chat message history (#7331) Co-authored-by: jacoblee93 --- libs/langchain-community/.gitignore | 4 + libs/langchain-community/langchain.config.js | 2 + libs/langchain-community/package.json | 13 ++ .../src/load/import_constants.ts | 1 + .../src/stores/message/neo4j.ts | 160 ++++++++++++++++++ .../src/stores/tests/neo4j.int.test.ts | 138 +++++++++++++++ test-int-deps-docker-compose.yml | 15 +- 7 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 libs/langchain-community/src/stores/message/neo4j.ts create mode 100644 libs/langchain-community/src/stores/tests/neo4j.int.test.ts diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index 8dc708cbe23e..b0ce628119ae 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -802,6 +802,10 @@ stores/message/mongodb.cjs stores/message/mongodb.js stores/message/mongodb.d.ts stores/message/mongodb.d.cts +stores/message/neo4j.cjs +stores/message/neo4j.js +stores/message/neo4j.d.ts +stores/message/neo4j.d.cts stores/message/planetscale.cjs stores/message/planetscale.js stores/message/planetscale.d.ts diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index 547960384372..cbabc1c68841 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -250,6 +250,7 @@ export const config = { "stores/message/ioredis": "stores/message/ioredis", "stores/message/momento": "stores/message/momento", "stores/message/mongodb": "stores/message/mongodb", + "stores/message/neo4j": "stores/message/neo4j", "stores/message/planetscale": "stores/message/planetscale", "stores/message/postgres": "stores/message/postgres", "stores/message/redis": "stores/message/redis", @@ -473,6 +474,7 @@ export const config = { "stores/message/ipfs_datastore", "stores/message/momento", "stores/message/mongodb", + "stores/message/neo4j", "stores/message/planetscale", "stores/message/postgres", "stores/message/redis", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 108a51c174ce..31da4591d64c 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -2521,6 +2521,15 @@ "import": "./stores/message/mongodb.js", "require": "./stores/message/mongodb.cjs" }, + "./stores/message/neo4j": { + "types": { + "import": "./stores/message/neo4j.d.ts", + "require": "./stores/message/neo4j.d.cts", + "default": "./stores/message/neo4j.d.ts" + }, + "import": "./stores/message/neo4j.js", + "require": "./stores/message/neo4j.cjs" + }, "./stores/message/planetscale": { "types": { "import": "./stores/message/planetscale.d.ts", @@ -3932,6 +3941,10 @@ "stores/message/mongodb.js", "stores/message/mongodb.d.ts", "stores/message/mongodb.d.cts", + "stores/message/neo4j.cjs", + "stores/message/neo4j.js", + "stores/message/neo4j.d.ts", + "stores/message/neo4j.d.cts", "stores/message/planetscale.cjs", "stores/message/planetscale.js", "stores/message/planetscale.d.ts", diff --git a/libs/langchain-community/src/load/import_constants.ts b/libs/langchain-community/src/load/import_constants.ts index 722dd82e678b..5930f82690db 100644 --- a/libs/langchain-community/src/load/import_constants.ts +++ b/libs/langchain-community/src/load/import_constants.ts @@ -130,6 +130,7 @@ export const optionalImportEntrypoints: string[] = [ "langchain_community/stores/message/ioredis", "langchain_community/stores/message/momento", "langchain_community/stores/message/mongodb", + "langchain_community/stores/message/neo4j", "langchain_community/stores/message/planetscale", "langchain_community/stores/message/postgres", "langchain_community/stores/message/redis", diff --git a/libs/langchain-community/src/stores/message/neo4j.ts b/libs/langchain-community/src/stores/message/neo4j.ts new file mode 100644 index 000000000000..a5f132900470 --- /dev/null +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -0,0 +1,160 @@ +import neo4j, { Driver, Record, auth } from "neo4j-driver"; +import { v4 as uuidv4 } from "uuid"; +import { BaseListChatMessageHistory } from "@langchain/core/chat_history"; +import { + BaseMessage, + mapStoredMessagesToChatMessages, +} from "@langchain/core/messages"; + +export type Neo4jChatMessageHistoryConfigInput = { + sessionId?: string | number; + sessionNodeLabel?: string; + messageNodeLabel?: string; + url: string; + username: string; + password: string; + windowSize?: number; +}; + +const defaultConfig = { + sessionNodeLabel: "ChatSession", + messageNodeLabel: "ChatMessage", + windowSize: 3, +}; + +export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { + lc_namespace: string[] = ["langchain", "stores", "message", "neo4j"]; + + sessionId: string | number; + + sessionNodeLabel: string; + + messageNodeLabel: string; + + windowSize: number; + + private driver: Driver; + + constructor({ + sessionId = uuidv4(), + sessionNodeLabel = defaultConfig.sessionNodeLabel, + messageNodeLabel = defaultConfig.messageNodeLabel, + url, + username, + password, + windowSize = defaultConfig.windowSize, + }: Neo4jChatMessageHistoryConfigInput) { + super(); + + this.sessionId = sessionId; + this.sessionNodeLabel = sessionNodeLabel; + this.messageNodeLabel = messageNodeLabel; + this.windowSize = windowSize; + + if (url && username && password) { + try { + this.driver = neo4j.driver(url, auth.basic(username, password)); + } catch (e: any) { + throw new Error( + `Could not create a Neo4j driver instance. Please check the connection details.\nCause: ${e.message}` + ); + } + } else { + throw new Error("Neo4j connection details not provided."); + } + } + + static async initialize( + props: Neo4jChatMessageHistoryConfigInput + ): Promise { + const instance = new Neo4jChatMessageHistory(props); + + try { + await instance.verifyConnectivity(); + } catch (e: any) { + throw new Error( + `Could not verify connection to the Neo4j database.\nCause: ${e.message}` + ); + } + + return instance; + } + + async verifyConnectivity() { + const connectivity = await this.driver.getServerInfo(); + return connectivity; + } + + async getMessages(): Promise { + const getMessagesCypherQuery = ` + MERGE (chatSession:${this.sessionNodeLabel} {id: $sessionId}) + WITH chatSession + MATCH (chatSession)-[:LAST_MESSAGE]->(lastMessage) + MATCH p=(lastMessage)<-[:NEXT*0..${this.windowSize * 2 - 1}]-() + WITH p, length(p) AS length + ORDER BY length DESC LIMIT 1 + UNWIND reverse(nodes(p)) AS node + RETURN {data:{content: node.content}, type:node.type} AS result + `; + + try { + const { records } = await this.driver.executeQuery( + getMessagesCypherQuery, + { + sessionId: this.sessionId, + } + ); + const results = records.map((record: Record) => record.get("result")); + + return mapStoredMessagesToChatMessages(results); + } catch (e: any) { + throw new Error(`Ohno! Couldn't get messages.\nCause: ${e.message}`); + } + } + + async addMessage(message: BaseMessage): Promise { + const addMessageCypherQuery = ` + MERGE (chatSession:${this.sessionNodeLabel} {id: $sessionId}) + WITH chatSession + OPTIONAL MATCH (chatSession)-[lastMessageRel:LAST_MESSAGE]->(lastMessage) + CREATE (chatSession)-[:LAST_MESSAGE]->(newLastMessage:${this.messageNodeLabel}) + SET newLastMessage += {type:$type, content:$content} + WITH newLastMessage, lastMessageRel, lastMessage + WHERE lastMessage IS NOT NULL + CREATE (lastMessage)-[:NEXT]->(newLastMessage) + DELETE lastMessageRel + `; + + try { + await this.driver.executeQuery(addMessageCypherQuery, { + sessionId: this.sessionId, + type: message.getType(), + content: message.content, + }); + } catch (e: any) { + throw new Error(`Ohno! Couldn't add message.\nCause: ${e.message}`); + } + } + + async clear() { + const clearMessagesCypherQuery = ` + MATCH p=(chatSession:${this.sessionNodeLabel} {id: $sessionId})-[:LAST_MESSAGE]->(lastMessage)<-[:NEXT*0..]-() + UNWIND nodes(p) as node + DETACH DELETE node + `; + + try { + await this.driver.executeQuery(clearMessagesCypherQuery, { + sessionId: this.sessionId, + }); + } catch (e: any) { + throw new Error( + `Ohno! Couldn't clear chat history.\nCause: ${e.message}` + ); + } + } + + async close() { + await this.driver.close(); + } +} diff --git a/libs/langchain-community/src/stores/tests/neo4j.int.test.ts b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts new file mode 100644 index 000000000000..2f6c17d01ed6 --- /dev/null +++ b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; +import { HumanMessage, AIMessage } from "@langchain/core/messages"; +import neo4j from "neo4j-driver"; +import { Neo4jChatMessageHistory } from "../message/neo4j.js"; + +const goodConfig = { + url: "bolt://host.docker.internal:7687", + username: "neo4j", + password: "langchain", +}; + +describe("The Neo4jChatMessageHistory class", () => { + describe("Test suite", () => { + it("Runs at all", () => { + expect(true).toEqual(true); + }); + }); + + describe("Class instantiation", () => { + it("Requires a url, username and password, throwing an error if not provided", async () => { + const badConfig = {}; + await expect( + // @ts-expect-error Bad config + Neo4jChatMessageHistory.initialize(badConfig) + ).rejects.toThrow(neo4j.Neo4jError); + }); + + it("Creates a class instance from - at minimum - a url, username and password", async () => { + const instance = await Neo4jChatMessageHistory.initialize(goodConfig); + expect(instance).toBeInstanceOf(Neo4jChatMessageHistory); + await instance.close(); + }); + + it("Class instances have expected, configurable fields, and sensible defaults", async () => { + const instance = await Neo4jChatMessageHistory.initialize(goodConfig); + + expect(instance.sessionId).toBeDefined(); + expect(instance.sessionNodeLabel).toEqual("ChatSession"); + expect(instance.windowSize).toEqual(3); + expect(instance.messageNodeLabel).toEqual("ChatMessage"); + + const secondInstance = await Neo4jChatMessageHistory.initialize({ + ...goodConfig, + sessionId: "Shibboleet", + sessionNodeLabel: "Conversation", + messageNodeLabel: "Communication", + windowSize: 4, + }); + + expect(secondInstance.sessionId).toBeDefined(); + expect(secondInstance.sessionId).toEqual("Shibboleet"); + expect(instance.sessionId).not.toEqual(secondInstance.sessionId); + expect(secondInstance.sessionNodeLabel).toEqual("Conversation"); + expect(secondInstance.messageNodeLabel).toEqual("Communication"); + expect(secondInstance.windowSize).toEqual(4); + + await instance.close(); + await secondInstance.close(); + }); + }); + + describe("Core functionality", () => { + let instance: undefined | Neo4jChatMessageHistory; + + beforeEach(async () => { + instance = await Neo4jChatMessageHistory.initialize(goodConfig); + }); + + afterEach(async () => { + await instance?.clear(); + await instance?.close(); + }); + + it("Connects verifiably to the underlying Neo4j database", async () => { + const connected = await instance?.verifyConnectivity(); + expect(connected).toBeDefined(); + }); + + it("getMessages()", async () => { + let results = await instance?.getMessages(); + expect(results).toEqual([]); + const messages = [ + new HumanMessage( + "My first name is a random set of numbers and letters" + ), + new AIMessage("And other alphanumerics that changes hourly forever"), + new HumanMessage( + "My last name, a thousand vowels fading down a sinkhole to a susurrus" + ), + new AIMessage("It couldn't just be John Doe or Bingo"), + new HumanMessage( + "My address, a made-up language written out in living glyphs" + ), + new AIMessage("Lifted from demonic literature and religious text"), + new HumanMessage("Telephone: uncovered by purveyors of the ouija"), + new AIMessage("When checked against the CBGB women's room graffiti"), + new HumanMessage("My social: a sudoku"), + new AIMessage("My age is obscure"), + ]; + await instance?.addMessages(messages); + results = (await instance?.getMessages()) || []; + const windowSize = instance?.windowSize || 0; + expect(results.length).toEqual(windowSize * 2); + expect(results).toEqual(messages.slice(windowSize * -2)); + }); + + it("addMessage()", async () => { + const messages = [ + new HumanMessage("99 Bottles of beer on the wall, 99 bottles of beer!"), + new AIMessage( + "Take one down, pass it around, 98 bottles of beer on the wall." + ), + new HumanMessage("How many bottles of beer are currently on the wall?"), + new AIMessage("There are currently 98 bottles of beer on the wall."), + ]; + for (const message of messages) { + await instance?.addMessage(message); + } + const results = await instance?.getMessages(); + expect(results).toEqual(messages); + }); + + it("clear()", async () => { + const messages = [ + new AIMessage("I'm not your enemy."), + new HumanMessage("That sounds like something that my enemy would say."), + new AIMessage("You're being difficult."), + new HumanMessage("I'm being guarded."), + ]; + await instance?.addMessages(messages); + let results = await instance?.getMessages(); + expect(results).toEqual(messages); + await instance?.clear(); + results = await instance?.getMessages(); + expect(results).toEqual([]); + }); + }); +}); diff --git a/test-int-deps-docker-compose.yml b/test-int-deps-docker-compose.yml index e14c4d65779a..2c875f6f5221 100644 --- a/test-int-deps-docker-compose.yml +++ b/test-int-deps-docker-compose.yml @@ -36,4 +36,17 @@ services: qdrant: image: qdrant/qdrant:v1.9.1 ports: - - 6333:6333 \ No newline at end of file + - 6333:6333 + neo4j: + image: neo4j:latest + volumes: + - $HOME/neo4j/logs:/var/lib/neo4j/logs + - $HOME/neo4j/config:/var/lib/neo4j/config + - $HOME/neo4j/data:/var/lib/neo4j/data + - $HOME/neo4j/plugins:/var/lib/neo4j/plugins + environment: + - NEO4J_dbms_security_auth__enabled=false + ports: + - "7474:7474" + - "7687:7687" + restart: always \ No newline at end of file From 84da383c747ca345d91a4b70a301510a7c6df79d Mon Sep 17 00:00:00 2001 From: renathossain <114009484+renathossain@users.noreply.github.com> Date: Sat, 14 Dec 2024 00:50:06 -0500 Subject: [PATCH 6/9] feat(google-common): Grounding with Google Search and Vertex AI Search (#7280) Co-authored-by: William <73299741+williamc99@users.noreply.github.com> Co-authored-by: jacoblee93 --- .../integrations/chat/google_vertex_ai.ipynb | 141 +++++++++++++++++- libs/langchain-google-common/src/types.ts | 16 ++ .../src/utils/common.ts | 61 ++++---- .../src/utils/gemini.ts | 37 ++--- .../src/tests/chat_models.int.test.ts | 47 ++++++ 5 files changed, 253 insertions(+), 49 deletions(-) diff --git a/docs/core_docs/docs/integrations/chat/google_vertex_ai.ipynb b/docs/core_docs/docs/integrations/chat/google_vertex_ai.ipynb index d4de68c3f5e2..8d046defbac1 100644 --- a/docs/core_docs/docs/integrations/chat/google_vertex_ai.ipynb +++ b/docs/core_docs/docs/integrations/chat/google_vertex_ai.ipynb @@ -21,8 +21,8 @@ "source": [ "# ChatVertexAI\n", "\n", - "[Google Vertex](https://cloud.google.com/vertex-ai) is a service that exposes all foundation models available in Google Cloud, like `gemini-1.5-pro`, `gemini-1.5-flash`, etc.", - "It also provides some non-Google models such as [Anthropic's Claude](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude).", + "[Google Vertex](https://cloud.google.com/vertex-ai) is a service that exposes all foundation models available in Google Cloud, like `gemini-1.5-pro`, `gemini-2.0-flash-exp`, etc.\n", + "It also provides some non-Google models such as [Anthropic's Claude](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude).\n", "\n", "\n", "This will help you getting started with `ChatVertexAI` [chat models](/docs/concepts/chat_models). For detailed documentation of all `ChatVertexAI` features and configurations head to the [API reference](https://api.js.langchain.com/classes/langchain_google_vertexai.ChatVertexAI.html).\n", @@ -116,7 +116,7 @@ "// import { ChatVertexAI } from \"@langchain/google-vertexai-web\"\n", "\n", "const llm = new ChatVertexAI({\n", - " model: \"gemini-1.5-pro\",\n", + " model: \"gemini-2.0-flash-exp\",\n", " temperature: 0,\n", " maxRetries: 2,\n", " // For web, authOptions.credentials\n", @@ -191,6 +191,141 @@ "console.log(aiMsg.content)" ] }, + { + "cell_type": "markdown", + "id": "de2480fa", + "metadata": {}, + "source": [ + "## Tool Calling with Google Search Retrieval\n", + "\n", + "It is possible to call the model with a Google search tool which you can use to [ground](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/grounding) content generation with real-world information and reduce hallucinations.\n", + "\n", + "Grounding is currently not supported by `gemini-2.0-flash-exp`.\n", + "\n", + "You can choose to either ground using Google Search or by using a custom data store. Here are examples of both: " + ] + }, + { + "cell_type": "markdown", + "id": "fd2091ba", + "metadata": {}, + "source": [ + "### Google Search Retrieval\n", + "\n", + "Grounding example that uses Google Search:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65d019ee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The Boston Celtics won the 2024 NBA Finals, defeating the Dallas Mavericks 4-1 in the series to claim their 18th NBA championship. This victory marked their first title since 2008 and established them as the team with the most NBA championships, surpassing the Los Angeles Lakers' 17 titles.\n", + "\n" + ] + } + ], + "source": [ + "import { ChatVertexAI } from \"@langchain/google-vertexai\"\n", + "\n", + "const searchRetrievalTool = {\n", + " googleSearchRetrieval: {\n", + " dynamicRetrievalConfig: {\n", + " mode: \"MODE_DYNAMIC\", // Use Dynamic Retrieval\n", + " dynamicThreshold: 0.7, // Default for Dynamic Retrieval threshold\n", + " },\n", + " },\n", + "};\n", + "\n", + "const searchRetrievalModel = new ChatVertexAI({\n", + " model: \"gemini-1.5-pro\",\n", + " temperature: 0,\n", + " maxRetries: 0,\n", + "}).bindTools([searchRetrievalTool]);\n", + "\n", + "const searchRetrievalResult = await searchRetrievalModel.invoke(\"Who won the 2024 NBA Finals?\");\n", + "\n", + "console.log(searchRetrievalResult.content);" + ] + }, + { + "cell_type": "markdown", + "id": "ac3a4a98", + "metadata": {}, + "source": [ + "### Google Search Retrieval with Data Store\n", + "\n", + "First, set up your data store (this is a schema of an example data store):\n", + "\n", + "| ID | Date | Team 1 | Score | Team 2 |\n", + "|:-------:|:------------:|:-----------:|:--------:|:----------:|\n", + "| 3001 | 2023-09-07 | Argentina | 1 - 0 | Ecuador |\n", + "| 3002 | 2023-09-12 | Venezuela | 1 - 0 | Paraguay |\n", + "| 3003 | 2023-09-12 | Chile | 0 - 0 | Colombia |\n", + "| 3004 | 2023-09-12 | Peru | 0 - 1 | Brazil |\n", + "| 3005 | 2024-10-15 | Argentina | 6 - 0 | Bolivia |\n", + "\n", + "Then, use this data store in the example provided below:\n", + "\n", + "(Note that you have to use your own variables for `projectId` and `datastoreId`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a539d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Argentina won against Bolivia with a score of 6-0 on October 15, 2024.\n", + "\n" + ] + } + ], + "source": [ + "import { ChatVertexAI } from \"@langchain/google-vertexai\";\n", + "\n", + "const projectId = \"YOUR_PROJECT_ID\";\n", + "const datastoreId = \"YOUR_DATASTORE_ID\";\n", + "\n", + "const searchRetrievalToolWithDataset = {\n", + " retrieval: {\n", + " vertexAiSearch: {\n", + " datastore: `projects/${projectId}/locations/global/collections/default_collection/dataStores/${datastoreId}`,\n", + " },\n", + " disableAttribution: false,\n", + " },\n", + "};\n", + "\n", + "const searchRetrievalModelWithDataset = new ChatVertexAI({\n", + " model: \"gemini-1.5-pro\",\n", + " temperature: 0,\n", + " maxRetries: 0,\n", + "}).bindTools([searchRetrievalToolWithDataset]);\n", + "\n", + "const searchRetrievalModelResult = await searchRetrievalModelWithDataset.invoke(\n", + " \"What is the score of Argentina vs Bolivia football game?\"\n", + ");\n", + "\n", + "console.log(searchRetrievalModelResult.content);" + ] + }, + { + "cell_type": "markdown", + "id": "8d11f2be", + "metadata": {}, + "source": [ + "You should now get results that are grounded in the data from your provided data store." + ] + }, { "cell_type": "markdown", "id": "18e2bfc0-7e78-4528-a73f-499ac150dca8", diff --git a/libs/langchain-google-common/src/types.ts b/libs/langchain-google-common/src/types.ts index bb49cf2edd4f..b88b3e01d090 100644 --- a/libs/langchain-google-common/src/types.ts +++ b/libs/langchain-google-common/src/types.ts @@ -309,6 +309,22 @@ export interface GeminiContent { export interface GeminiTool { functionDeclarations?: GeminiFunctionDeclaration[]; + googleSearchRetrieval?: GoogleSearchRetrieval; + retrieval?: VertexAIRetrieval; +} + +export interface GoogleSearchRetrieval { + dynamicRetrievalConfig?: { + mode?: string; + dynamicThreshold?: number; + }; +} + +export interface VertexAIRetrieval { + vertexAiSearch: { + datastore: string; + }; + disableAttribution?: boolean; } export interface GeminiFunctionDeclaration { diff --git a/libs/langchain-google-common/src/utils/common.ts b/libs/langchain-google-common/src/utils/common.ts index bf8ddb228382..b40ce25fe3fc 100644 --- a/libs/langchain-google-common/src/utils/common.ts +++ b/libs/langchain-google-common/src/utils/common.ts @@ -62,32 +62,43 @@ function processToolChoice( } export function convertToGeminiTools(tools: GoogleAIToolType[]): GeminiTool[] { - const geminiTools: GeminiTool[] = [ - { - functionDeclarations: [], - }, - ]; + const geminiTools: GeminiTool[] = []; + let functionDeclarationsIndex = -1; tools.forEach((tool) => { - if ( - "functionDeclarations" in tool && - Array.isArray(tool.functionDeclarations) - ) { - const funcs: GeminiFunctionDeclaration[] = tool.functionDeclarations; - geminiTools[0].functionDeclarations?.push(...funcs); - } else if (isLangChainTool(tool)) { - const jsonSchema = zodToGeminiParameters(tool.schema); - geminiTools[0].functionDeclarations?.push({ - name: tool.name, - description: tool.description ?? `A function available to call.`, - parameters: jsonSchema as GeminiFunctionSchema, - }); - } else if (isOpenAITool(tool)) { - geminiTools[0].functionDeclarations?.push({ - name: tool.function.name, - description: - tool.function.description ?? `A function available to call.`, - parameters: jsonSchemaToGeminiParameters(tool.function.parameters), - }); + if ("googleSearchRetrieval" in tool || "retrieval" in tool) { + geminiTools.push(tool); + } else { + if (functionDeclarationsIndex === -1) { + geminiTools.push({ + functionDeclarations: [], + }); + functionDeclarationsIndex = geminiTools.length - 1; + } + if ( + "functionDeclarations" in tool && + Array.isArray(tool.functionDeclarations) + ) { + const funcs: GeminiFunctionDeclaration[] = tool.functionDeclarations; + geminiTools[functionDeclarationsIndex].functionDeclarations!.push( + ...funcs + ); + } else if (isLangChainTool(tool)) { + const jsonSchema = zodToGeminiParameters(tool.schema); + geminiTools[functionDeclarationsIndex].functionDeclarations!.push({ + name: tool.name, + description: tool.description ?? `A function available to call.`, + parameters: jsonSchema as GeminiFunctionSchema, + }); + } else if (isOpenAITool(tool)) { + geminiTools[functionDeclarationsIndex].functionDeclarations!.push({ + name: tool.function.name, + description: + tool.function.description ?? `A function available to call.`, + parameters: jsonSchemaToGeminiParameters(tool.function.parameters), + }); + } else { + throw new Error(`Received invalid tool: ${JSON.stringify(tool)}`); + } } }); return geminiTools; diff --git a/libs/langchain-google-common/src/utils/gemini.ts b/libs/langchain-google-common/src/utils/gemini.ts index e6d0f6e96001..48bd41fb5c2f 100644 --- a/libs/langchain-google-common/src/utils/gemini.ts +++ b/libs/langchain-google-common/src/utils/gemini.ts @@ -1015,34 +1015,29 @@ export function getGeminiAPI(config?: GeminiAPIConfig): GoogleAIAPI { }; } - function structuredToolsToGeminiTools( - tools: StructuredToolParams[] - ): GeminiTool[] { - return [ - { - functionDeclarations: tools.map(structuredToolToFunctionDeclaration), - }, - ]; - } - function formatTools(parameters: GoogleAIModelRequestParams): GeminiTool[] { const tools: GoogleAIToolType[] | undefined = parameters?.tools; if (!tools || tools.length === 0) { return []; } - if (tools.every(isLangChainTool)) { - return structuredToolsToGeminiTools(tools); - } else { - if ( - tools.length === 1 && - (!("functionDeclarations" in tools[0]) || - !tools[0].functionDeclarations?.length) - ) { - return []; - } - return tools as GeminiTool[]; + // Group all LangChain tools into a single functionDeclarations array + const langChainTools = tools.filter(isLangChainTool); + const otherTools = tools.filter( + (tool) => !isLangChainTool(tool) + ) as GeminiTool[]; + + const result: GeminiTool[] = [...otherTools]; + + if (langChainTools.length > 0) { + result.push({ + functionDeclarations: langChainTools.map( + structuredToolToFunctionDeclaration + ), + }); } + + return result; } function formatToolConfig( diff --git a/libs/langchain-google-vertexai/src/tests/chat_models.int.test.ts b/libs/langchain-google-vertexai/src/tests/chat_models.int.test.ts index a3b8bbe4b2d8..ddcdf579a394 100644 --- a/libs/langchain-google-vertexai/src/tests/chat_models.int.test.ts +++ b/libs/langchain-google-vertexai/src/tests/chat_models.int.test.ts @@ -617,3 +617,50 @@ describe("GAuth Anthropic Chat", () => { expect(toolCalls?.[0].args).toHaveProperty("location"); }); }); + +describe("GoogleSearchRetrievalTool", () => { + test("Supports GoogleSearchRetrievalTool", async () => { + const searchRetrievalTool = { + googleSearchRetrieval: { + dynamicRetrievalConfig: { + mode: "MODE_DYNAMIC", + dynamicThreshold: 0.7, // default is 0.7 + }, + }, + }; + const model = new ChatVertexAI({ + model: "gemini-1.5-pro", + temperature: 0, + maxRetries: 0, + }).bindTools([searchRetrievalTool]); + + const result = await model.invoke("Who won the 2024 MLB World Series?"); + expect(result.content as string).toContain("Dodgers"); + }); + + test("Can stream GoogleSearchRetrievalTool", async () => { + const searchRetrievalTool = { + googleSearchRetrieval: { + dynamicRetrievalConfig: { + mode: "MODE_DYNAMIC", + dynamicThreshold: 0.7, // default is 0.7 + }, + }, + }; + const model = new ChatVertexAI({ + model: "gemini-1.5-pro", + temperature: 0, + maxRetries: 0, + }).bindTools([searchRetrievalTool]); + + const stream = await model.stream("Who won the 2024 MLB World Series?"); + let finalMsg: AIMessageChunk | undefined; + for await (const msg of stream) { + finalMsg = finalMsg ? concat(finalMsg, msg) : msg; + } + if (!finalMsg) { + throw new Error("finalMsg is undefined"); + } + expect(finalMsg.content as string).toContain("Dodgers"); + }); +}); From 475288c9ecfb40b281fe5d439a540a99543c07cb Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Fri, 13 Dec 2024 21:59:11 -0800 Subject: [PATCH 7/9] release(google): 0.1.4 (#7362) --- libs/langchain-google-common/package.json | 2 +- libs/langchain-google-gauth/package.json | 4 ++-- libs/langchain-google-vertexai-web/package.json | 4 ++-- libs/langchain-google-vertexai/package.json | 4 ++-- libs/langchain-google-webauth/package.json | 4 ++-- yarn.lock | 14 +++++++------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/langchain-google-common/package.json b/libs/langchain-google-common/package.json index fe3fa5a001ec..3408ef93e4ef 100644 --- a/libs/langchain-google-common/package.json +++ b/libs/langchain-google-common/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-common", - "version": "0.1.3", + "version": "0.1.4", "description": "Core types and classes for Google services.", "type": "module", "engines": { diff --git a/libs/langchain-google-gauth/package.json b/libs/langchain-google-gauth/package.json index f40f0a0be029..6a971b220ef6 100644 --- a/libs/langchain-google-gauth/package.json +++ b/libs/langchain-google-gauth/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-gauth", - "version": "0.1.3", + "version": "0.1.4", "description": "Google auth based authentication support for Google services", "type": "module", "engines": { @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-common": "~0.1.3", + "@langchain/google-common": "~0.1.4", "google-auth-library": "^8.9.0" }, "peerDependencies": { diff --git a/libs/langchain-google-vertexai-web/package.json b/libs/langchain-google-vertexai-web/package.json index 737438881e94..8761e8b75975 100644 --- a/libs/langchain-google-vertexai-web/package.json +++ b/libs/langchain-google-vertexai-web/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-vertexai-web", - "version": "0.1.3", + "version": "0.1.4", "description": "LangChain.js support for Google Vertex AI Web", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-webauth": "~0.1.3" + "@langchain/google-webauth": "~0.1.4" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" diff --git a/libs/langchain-google-vertexai/package.json b/libs/langchain-google-vertexai/package.json index 58c6b3a1503f..0c238e99bc12 100644 --- a/libs/langchain-google-vertexai/package.json +++ b/libs/langchain-google-vertexai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-vertexai", - "version": "0.1.3", + "version": "0.1.4", "description": "LangChain.js support for Google Vertex AI", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-gauth": "~0.1.3" + "@langchain/google-gauth": "~0.1.4" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" diff --git a/libs/langchain-google-webauth/package.json b/libs/langchain-google-webauth/package.json index 953b925c8a49..f3bee58fb805 100644 --- a/libs/langchain-google-webauth/package.json +++ b/libs/langchain-google-webauth/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-webauth", - "version": "0.1.3", + "version": "0.1.4", "description": "Web-based authentication support for Google services", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-common": "~0.1.3", + "@langchain/google-common": "~0.1.4", "web-auth-library": "^1.0.3" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 466e5b0d4763..8ca08ed7c45d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12398,7 +12398,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-common@^0.1.0, @langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.1.3": +"@langchain/google-common@^0.1.0, @langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.1.4": version: 0.0.0-use.local resolution: "@langchain/google-common@workspace:libs/langchain-google-common" dependencies: @@ -12433,13 +12433,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.1.3": +"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.1.4": version: 0.0.0-use.local resolution: "@langchain/google-gauth@workspace:libs/langchain-google-gauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ~0.1.3 + "@langchain/google-common": ~0.1.4 "@langchain/scripts": ">=0.1.0 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -12512,7 +12512,7 @@ __metadata: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" "@langchain/google-common": ^0.1.0 - "@langchain/google-webauth": ~0.1.3 + "@langchain/google-webauth": ~0.1.4 "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -12548,7 +12548,7 @@ __metadata: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" "@langchain/google-common": ^0.1.0 - "@langchain/google-gauth": ~0.1.3 + "@langchain/google-gauth": ~0.1.4 "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -12577,13 +12577,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.1.3": +"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.1.4": version: 0.0.0-use.local resolution: "@langchain/google-webauth@workspace:libs/langchain-google-webauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ~0.1.3 + "@langchain/google-common": ~0.1.4 "@langchain/scripts": ">=0.1.0 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 From 8832af387d07e5c68247abdabef5a0e1db2d5610 Mon Sep 17 00:00:00 2001 From: xnnxnyx <79350442+xnnxnyx@users.noreply.github.com> Date: Sat, 14 Dec 2024 01:03:13 -0500 Subject: [PATCH 8/9] feat(community): Added Google Scholar Integration (#7278) Co-authored-by: jacoblee93 --- .../integrations/tools/google_scholar.ipynb | 185 ++++++++++++++++++ libs/langchain-community/.gitignore | 4 + libs/langchain-community/langchain.config.js | 1 + libs/langchain-community/package.json | 13 ++ .../src/load/import_map.ts | 2 + .../src/load/import_type.ts | 1 + .../src/tools/google_scholar.ts | 122 ++++++++++++ .../tools/tests/google_scholar.int.test.ts | 38 ++++ 8 files changed, 366 insertions(+) create mode 100644 docs/core_docs/docs/integrations/tools/google_scholar.ipynb create mode 100644 libs/langchain-community/src/tools/google_scholar.ts create mode 100644 libs/langchain-community/src/tools/tests/google_scholar.int.test.ts diff --git a/docs/core_docs/docs/integrations/tools/google_scholar.ipynb b/docs/core_docs/docs/integrations/tools/google_scholar.ipynb new file mode 100644 index 000000000000..ed6539459e1d --- /dev/null +++ b/docs/core_docs/docs/integrations/tools/google_scholar.ipynb @@ -0,0 +1,185 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "10238e62-3465-4973-9279-606cbb7ccf16", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "---\n", + "sidebar_label: Google Scholar\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "a6f91f20", + "metadata": {}, + "source": [ + "# Google Scholar Tool\n", + "\n", + "This notebook provides a quick overview for getting started with [`SERPGoogleScholarTool`](https://api.js.langchain.com/classes/_langchain_community.tools_google_scholar.SERPGoogleScholarAPITool.html). For detailed documentation of all `SERPGoogleScholarAPITool` features and configurations, head to the [API reference](https://api.js.langchain.com/classes/_langchain_community.tools_google_scholar.SERPGoogleScholarAPITool.html).\n", + "\n", + "## Overview\n", + "\n", + "### Integration details\n", + "\n", + "| Class | Package | [PY support](https://python.langchain.com/docs/integrations/tools/google_scholar/) | Package latest |\n", + "| :--- | :--- | :---: | :---: |\n", + "| [GoogleScholarTool](https://api.js.langchain.com/classes/_langchain_community.tools_google_scholar.SERPGoogleScholarAPITool.html) | [@langchain/community](https://www.npmjs.com/package/@langchain/community) | ✅ | ![NPM - Version](https://img.shields.io/npm/v/@langchain/community?style=flat-square&label=%20&) |\n", + "\n", + "### Tool features\n", + "\n", + "- Retrieve academic publications by topic, author, or query.\n", + "- Fetch metadata such as title, author, and publication year.\n", + "- Advanced search filters, including citation count and journal name.\n", + "\n", + "## Setup\n", + "\n", + "The integration lives in the `@langchain/community` package.\n", + "\n", + "```bash\n", + "npm install @langchain/community\n", + "```\n", + "\n", + "### Credentials\n", + "\n", + "Ensure you have the appropriate API key to access Google Scholar. Set it in your environment variables:\n", + "\n", + "```typescript\n", + "process.env.GOOGLE_SCHOLAR_API_KEY=\"your-serp-api-key\"\n", + "```\n", + "\n", + "It's also helpful to set up [LangSmith](https://smith.langchain.com/) for best-in-class observability:\n", + "\n", + "```typescript\n", + "process.env.LANGCHAIN_TRACING_V2=\"true\"\n", + "process.env.LANGCHAIN_API_KEY=\"your-langchain-api-key\"\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1c97218f-f366-479d-8bf7-fe9f2f6df73f", + "metadata": {}, + "source": [ + "## Instantiation\n", + "\n", + "You can import and instantiate an instance of the `SERPGoogleScholarAPITool` tool like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8b3ddfe9-ca79-494c-a7ab-1f56d9407a64", + "metadata": { + "vscode": { + "languageId": "typescript" + } + }, + "outputs": [], + "source": [ + "import { SERPGoogleScholarAPITool } from \"@langchain/community/tools/google_scholar\";\n", + "\n", + "const tool = new SERPGoogleScholarAPITool({\n", + " apiKey: process.env.SERPAPI_API_KEY,\n", + "});" + ] + }, + { + "cell_type": "markdown", + "id": "74147a1a", + "metadata": {}, + "source": [ + "## Invocation\n", + "\n", + "### Invoke directly with args\n", + "\n", + "You can invoke the tool directly with query arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65310a8b-eb0c-4d9e-a618-4f4abe2414fc", + "metadata": { + "vscode": { + "languageId": "typescript" + } + }, + "outputs": [], + "source": [ + "const results = await tool.invoke({\n", + " query: \"neural networks\",\n", + " maxResults: 5,\n", + "});\n", + "\n", + "console.log(results);" + ] + }, + { + "cell_type": "markdown", + "id": "d6e73897", + "metadata": {}, + "source": [ + "### Invoke with ToolCall\n", + "\n", + "We can also invoke the tool with a model-generated `ToolCall`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f90e33a7", + "metadata": { + "vscode": { + "languageId": "typescript" + } + }, + "outputs": [], + "source": [ + "const modelGeneratedToolCall = {\n", + " args: { query: \"machine learning\" },\n", + " id: \"1\",\n", + " name: tool.name,\n", + " type: \"tool_call\",\n", + "};\n", + "await tool.invoke(modelGeneratedToolCall);" + ] + }, + { + "cell_type": "markdown", + "id": "93848b02", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `SERPGoogleScholarAPITool` features and configurations, head to the [API reference](https://api.js.langchain.com/classes/_langchain_community.tools_google_scholar.SERPGoogleScholarAPITool.html)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "poetry-venv-311", + "language": "python", + "name": "poetry-venv-311" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index b0ce628119ae..4fde6ded00ff 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -78,6 +78,10 @@ tools/google_routes.cjs tools/google_routes.js tools/google_routes.d.ts tools/google_routes.d.cts +tools/google_scholar.cjs +tools/google_scholar.js +tools/google_scholar.d.ts +tools/google_scholar.d.cts tools/ifttt.cjs tools/ifttt.js tools/ifttt.d.ts diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index cbabc1c68841..62abeef26886 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -53,6 +53,7 @@ export const config = { "tools/google_places": "tools/google_places", "tools/google_trends": "tools/google_trends", "tools/google_routes": "tools/google_routes", + "tools/google_scholar": "tools/google_scholar", "tools/ifttt": "tools/ifttt", "tools/searchapi": "tools/searchapi", "tools/searxng_search": "tools/searxng_search", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 31da4591d64c..0326ba0be336 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -892,6 +892,15 @@ "import": "./tools/google_routes.js", "require": "./tools/google_routes.cjs" }, + "./tools/google_scholar": { + "types": { + "import": "./tools/google_scholar.d.ts", + "require": "./tools/google_scholar.d.cts", + "default": "./tools/google_scholar.d.ts" + }, + "import": "./tools/google_scholar.js", + "require": "./tools/google_scholar.cjs" + }, "./tools/ifttt": { "types": { "import": "./tools/ifttt.d.ts", @@ -3217,6 +3226,10 @@ "tools/google_routes.js", "tools/google_routes.d.ts", "tools/google_routes.d.cts", + "tools/google_scholar.cjs", + "tools/google_scholar.js", + "tools/google_scholar.d.ts", + "tools/google_scholar.d.cts", "tools/ifttt.cjs", "tools/ifttt.js", "tools/ifttt.d.ts", diff --git a/libs/langchain-community/src/load/import_map.ts b/libs/langchain-community/src/load/import_map.ts index cfc0af93456c..2ec7b20bc542 100644 --- a/libs/langchain-community/src/load/import_map.ts +++ b/libs/langchain-community/src/load/import_map.ts @@ -11,7 +11,9 @@ export * as tools__dynamic from "../tools/dynamic.js"; export * as tools__dataforseo_api_search from "../tools/dataforseo_api_search.js"; export * as tools__google_custom_search from "../tools/google_custom_search.js"; export * as tools__google_places from "../tools/google_places.js"; +export * as tools__google_trends from "../tools/google_trends.js"; export * as tools__google_routes from "../tools/google_routes.js"; +export * as tools__google_scholar from "../tools/google_scholar.js"; export * as tools__ifttt from "../tools/ifttt.js"; export * as tools__searchapi from "../tools/searchapi.js"; export * as tools__searxng_search from "../tools/searxng_search.js"; diff --git a/libs/langchain-community/src/load/import_type.ts b/libs/langchain-community/src/load/import_type.ts index 097584aef493..61efffe903c7 100644 --- a/libs/langchain-community/src/load/import_type.ts +++ b/libs/langchain-community/src/load/import_type.ts @@ -53,6 +53,7 @@ export interface SecretMap { REMOTE_RETRIEVER_AUTH_BEARER?: string; REPLICATE_API_TOKEN?: string; SEARXNG_API_BASE?: string; + SERPAPI_API_KEY?: string; TENCENT_SECRET_ID?: string; TENCENT_SECRET_KEY?: string; TOGETHER_AI_API_KEY?: string; diff --git a/libs/langchain-community/src/tools/google_scholar.ts b/libs/langchain-community/src/tools/google_scholar.ts new file mode 100644 index 000000000000..7d874910126b --- /dev/null +++ b/libs/langchain-community/src/tools/google_scholar.ts @@ -0,0 +1,122 @@ +import { Tool } from "@langchain/core/tools"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; + +/** + * Interface for parameters required by the SERPGoogleScholarAPITool class. + */ +export interface GoogleScholarAPIParams { + /** + * Optional API key for accessing the SerpApi service. + */ + apiKey?: string; +} + +/** + * Tool for querying Google Scholar using the SerpApi service. + */ +export class SERPGoogleScholarAPITool extends Tool { + /** + * Specifies the name of the tool, used internally by LangChain. + */ + static lc_name() { + return "SERPGoogleScholarAPITool"; + } + + /** + * Returns a mapping of secret environment variable names to their usage in the tool. + * @returns {object} Mapping of secret names to their environment variable counterparts. + */ + get lc_secrets(): { [key: string]: string } | undefined { + return { + apiKey: "SERPAPI_API_KEY", + }; + } + + // Name of the tool, used for logging or identification within LangChain. + name = "serp_google_scholar"; + + // The API key used for making requests to SerpApi. + protected apiKey: string; + + /** + * Description of the tool for usage documentation. + */ + description = `A wrapper around Google Scholar API via SerpApi. Useful for querying academic + articles and papers by keywords or authors. Input should be a search query string.`; + + /** + * Constructs a new instance of SERPGoogleScholarAPITool. + * @param fields - Optional parameters including an API key. + */ + constructor(fields?: GoogleScholarAPIParams) { + super(...arguments); + + // Retrieve API key from fields or environment variables. + const apiKey = fields?.apiKey ?? getEnvironmentVariable("SERPAPI_API_KEY"); + + // Throw an error if no API key is found. + if (!apiKey) { + throw new Error( + `SerpApi key not set. You can set it as "SERPAPI_API_KEY" in your environment variables.` + ); + } + this.apiKey = apiKey; + } + + /** + * Makes a request to SerpApi for Google Scholar results. + * @param input - Search query string. + * @returns A JSON string containing the search results. + * @throws Error if the API request fails or returns an error. + */ + async _call(input: string): Promise { + // Construct the URL for the API request. + const url = `https://serpapi.com/search.json?q=${encodeURIComponent( + input + )}&engine=google_scholar&api_key=${this.apiKey}`; + + // Make an HTTP GET request to the SerpApi service. + const response = await fetch(url); + + // Handle non-OK responses by extracting the error message. + if (!response.ok) { + let message; + try { + const json = await response.json(); // Attempt to parse the error response. + message = json.error; // Extract the error message from the response. + } catch (error) { + // Handle cases where the response isn't valid JSON. + message = + "Unable to parse error message: SerpApi did not return a JSON response."; + } + // Throw an error with detailed information about the failure. + throw new Error( + `Got ${response.status}: ${response.statusText} error from SerpApi: ${message}` + ); + } + + // Parse the JSON response from SerpApi. + const json = await response.json(); + + // Transform the raw response into a structured format. + const results = + json.organic_results?.map((item: any) => ({ + title: item.title, // Title of the article or paper. + link: item.link, // Direct link to the article or paper. + snippet: item.snippet, // Brief snippet or description. + publication_info: + item.publication_info?.summary + ?.split(" - ") // Split the summary at hyphens. + .slice(1) // Remove the authors from the start of the string. + .join(" - ") ?? "", // Rejoin remaining parts as publication info. + authors: + item.publication_info?.authors + ?.map((author: any) => author.name) // Extract the list of author names. + .join(", ") ?? "", // Join author names with a comma. + total_citations: item.inline_links?.cited_by?.total ?? "", // Total number of citations. + })) ?? `No results found for ${input} on Google Scholar.`; + + // Return the results as a formatted JSON string. + return JSON.stringify(results, null, 2); + } +} diff --git a/libs/langchain-community/src/tools/tests/google_scholar.int.test.ts b/libs/langchain-community/src/tools/tests/google_scholar.int.test.ts new file mode 100644 index 000000000000..fe243ae42aa9 --- /dev/null +++ b/libs/langchain-community/src/tools/tests/google_scholar.int.test.ts @@ -0,0 +1,38 @@ +import { test, expect, describe } from "@jest/globals"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; +import { SERPGoogleScholarAPITool } from "../google_scholar.js"; + +describe("SERPGoogleScholarAPITool", () => { + test("should be setup with correct parameters", async () => { + const instance = new SERPGoogleScholarAPITool({ + apiKey: getEnvironmentVariable("SERPAPI_API_KEY"), + }); + expect(instance.name).toBe("serp_google_scholar"); + }); + + test("SERPGoogleScholarAPITool returns a string for valid query", async () => { + const tool = new SERPGoogleScholarAPITool({ + apiKey: getEnvironmentVariable("SERPAPI_API_KEY"), + }); + const result = await tool.invoke("Artificial Intelligence"); + expect(typeof result).toBe("string"); + }); + + test("SERPGoogleScholarAPITool returns non-empty string for valid query", async () => { + const tool = new SERPGoogleScholarAPITool({ + apiKey: getEnvironmentVariable("SERPAPI_API_KEY"), + }); + const result = await tool.invoke("Artificial Intelligence"); + expect(result.length).toBeGreaterThan(0); + }); + + test("SERPGoogleScholarAPITool returns 'No results found' for bad query", async () => { + const tool = new SERPGoogleScholarAPITool({ + apiKey: getEnvironmentVariable("SERPAPI_API_KEY"), + }); + const result = await tool.invoke("dsalkfjsdlfjasdflasdl"); + expect(result).toBe( + '"No results found for dsalkfjsdlfjasdflasdl on Google Scholar."' + ); + }); +}); From 2cdf57c159f862dcadd3e4d0041f638a1b469fe7 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Fri, 13 Dec 2024 22:10:47 -0800 Subject: [PATCH 9/9] release(community): 0.3.19 (#7363) --- libs/langchain-community/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 0326ba0be336..b759b36153af 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/community", - "version": "0.3.18", + "version": "0.3.19", "description": "Third-party integrations for LangChain.js", "type": "module", "engines": {