From 907b6a0df4c995964d11fedc930fa69fa2f5f654 Mon Sep 17 00:00:00 2001 From: BernardFaucher Date: Sun, 8 Dec 2024 06:35:48 +0000 Subject: [PATCH 1/7] add neo4j to test integration services --- test-int-deps-docker-compose.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 56812dbfdda961cb039fa9c80b425e37858a6430 Mon Sep 17 00:00:00 2001 From: BernardFaucher Date: Sun, 8 Dec 2024 06:38:13 +0000 Subject: [PATCH 2/7] add Neo4jChatMessageHistory class and integration tests to community message stores --- .../src/stores/message/neo4j.ts | 156 ++++++++++++++++++ .../src/stores/tests/neo4j.int.test.ts | 152 +++++++++++++++++ 2 files changed, 308 insertions(+) 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/src/stores/message/neo4j.ts b/libs/langchain-community/src/stores/message/neo4j.ts new file mode 100644 index 000000000000..86adacba99d8 --- /dev/null +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -0,0 +1,156 @@ +import neo4j from "neo4j-driver"; +import { Driver, Record, Neo4jError, 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; + + #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) { + throw new Neo4jError({ + message: "Could not create a Neo4j driver instance. Please check the connection details.", + cause: e + }); + } + } else { + throw new Neo4jError({ + message: "Neo4j connection details not provided." + }); + } + } + + 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 messages = await this.#driver.session().run( + getMessagesCypherQuery, + { + sessionId: this.sessionId + } + ) + const results = messages.records.map((record: Record) => record.get('result')) + + return mapStoredMessagesToChatMessages(results) + } catch (e) { + throw new Neo4jError({ + message: `Ohno! Couldn't get messages. Cause: ${e}`, + cause: e + }); + } + } + + 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.session().run( + addMessageCypherQuery, + { + sessionId: this.sessionId, + type: message.getType(), + content: message.content + }) + } catch (e) { + throw new Neo4jError({ + message: `Ohno! Couldn't add message. Cause: ${e}`, + cause: e + }) + } + } + + 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.session().run( + clearMessagesCypherQuery, + { + sessionId: this.sessionId, + }) + } catch (e) { + throw new Neo4jError({ + message: `Ohno! Couldn't clear chat history. Cause: ${e}`, + cause: e + }) + } + } + + 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..ad0427e348e8 --- /dev/null +++ b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; +import { Neo4jChatMessageHistory } from "../message/neo4j.js"; +import { HumanMessage, AIMessage } from "@langchain/core/messages"; +import neo4j from "neo4j-driver"; + +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", + () => { + const badInstantiation = () => { + const badConfig = {} + // @ts-ignore + const instance = new Neo4jChatMessageHistory(badConfig) + } + expect(badInstantiation).toThrow(neo4j.Neo4jError) + } + ) + + it( + "Creates a class instance from - at minimum - a url, username and password", + () => { + const instance = new Neo4jChatMessageHistory(goodConfig) + expect(instance).toBeInstanceOf(Neo4jChatMessageHistory) + } + ) + + it( + "Class instances have expected, configurable fields, and sensible defaults", + () => { + const instance = new Neo4jChatMessageHistory(goodConfig) + + expect(instance.sessionId).toBeDefined() + expect(instance.sessionNodeLabel).toEqual("ChatSession") + expect(instance.windowSize).toEqual(3) + expect(instance.messageNodeLabel).toEqual("ChatMessage") + + const secondInstance = new Neo4jChatMessageHistory({ + ...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) + } + ) + }) + + describe( + "Core functionality", + () => { + + let instance: undefined | Neo4jChatMessageHistory; + + beforeEach(() => { + instance = new Neo4jChatMessageHistory(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 (let 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([]) + } + ) + } + ) +}) From 9f82036bbb3d2e8cff84177be3173258418ff678 Mon Sep 17 00:00:00 2001 From: BernardFaucher Date: Wed, 11 Dec 2024 01:54:49 +0000 Subject: [PATCH 3/7] format new file with prettier; prefer private over #; give class an async factory function --- .../src/stores/message/neo4j.ts | 96 +++++++++++-------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/libs/langchain-community/src/stores/message/neo4j.ts b/libs/langchain-community/src/stores/message/neo4j.ts index 86adacba99d8..a1c65e25084c 100644 --- a/libs/langchain-community/src/stores/message/neo4j.ts +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -15,23 +15,22 @@ export type Neo4jChatMessageHistoryConfigInput = { 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; - #driver: Driver; + private driver: Driver; constructor({ sessionId = uuidv4(), @@ -42,7 +41,7 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { password, windowSize = defaultConfig.windowSize, }: Neo4jChatMessageHistoryConfigInput) { - super() + super(); this.sessionId = sessionId; this.sessionNodeLabel = sessionNodeLabel; @@ -51,23 +50,41 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { if (url && username && password) { try { - this.#driver = neo4j.driver(url, auth.basic(username, password)); + this.driver = neo4j.driver(url, auth.basic(username, password)); } catch (e) { throw new Neo4jError({ - message: "Could not create a Neo4j driver instance. Please check the connection details.", - cause: e + message: + "Could not create a Neo4j driver instance. Please check the connection details.", + cause: e, }); } } else { throw new Neo4jError({ - message: "Neo4j connection details not provided." + message: "Neo4j connection details not provided.", + }); + } + } + + static async initialize( + props: Neo4jChatMessageHistoryConfigInput + ): Promise { + const instance = new Neo4jChatMessageHistory(props); + + try { + await instance.verifyConnectivity(); + } catch (e) { + throw new Neo4jError({ + message: `Could not verify connection to the Neo4j database. Cause: ${e}`, + cause: e, }); } + + return instance; } async verifyConnectivity() { - const connectivity = await this.#driver.getServerInfo(); - return connectivity + const connectivity = await this.driver.getServerInfo(); + return connectivity; } async getMessages(): Promise { @@ -75,27 +92,26 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { MERGE (chatSession:${this.sessionNodeLabel} {id: $sessionId}) WITH chatSession MATCH (chatSession)-[:LAST_MESSAGE]->(lastMessage) - MATCH p=(lastMessage)<-[:NEXT*0..${(this.windowSize * 2) - 1}]-() + 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 messages = await this.#driver.session().run( - getMessagesCypherQuery, - { - sessionId: this.sessionId - } - ) - const results = messages.records.map((record: Record) => record.get('result')) - - return mapStoredMessagesToChatMessages(results) + const messages = await this.driver.session().run(getMessagesCypherQuery, { + sessionId: this.sessionId, + }); + const results = messages.records.map((record: Record) => + record.get("result") + ); + + return mapStoredMessagesToChatMessages(results); } catch (e) { throw new Neo4jError({ message: `Ohno! Couldn't get messages. Cause: ${e}`, - cause: e + cause: e, }); } } @@ -111,21 +127,19 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { WHERE lastMessage IS NOT NULL CREATE (lastMessage)-[:NEXT]->(newLastMessage) DELETE lastMessageRel - ` + `; try { - await this.#driver.session().run( - addMessageCypherQuery, - { - sessionId: this.sessionId, - type: message.getType(), - content: message.content - }) + await this.driver.session().run(addMessageCypherQuery, { + sessionId: this.sessionId, + type: message.getType(), + content: message.content, + }); } catch (e) { throw new Neo4jError({ message: `Ohno! Couldn't add message. Cause: ${e}`, - cause: e - }) + cause: e, + }); } } @@ -134,23 +148,21 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { MATCH p=(chatSession:${this.sessionNodeLabel} {id: $sessionId})-[:LAST_MESSAGE]->(lastMessage)<-[:NEXT*0..]-() UNWIND nodes(p) as node DETACH DELETE node - ` + `; try { - await this.#driver.session().run( - clearMessagesCypherQuery, - { - sessionId: this.sessionId, - }) + await this.driver.session().run(clearMessagesCypherQuery, { + sessionId: this.sessionId, + }); } catch (e) { throw new Neo4jError({ message: `Ohno! Couldn't clear chat history. Cause: ${e}`, - cause: e - }) + cause: e, + }); } } async close() { - await this.#driver.close() + await this.driver.close(); } } From 67b37f59f458c2499f567c1469d51cc1a2bac260 Mon Sep 17 00:00:00 2001 From: BernardFaucher Date: Wed, 11 Dec 2024 01:57:09 +0000 Subject: [PATCH 4/7] format test with prettier; update test to instantiate with factory function; prefer expect-error over ignore --- .../src/stores/tests/neo4j.int.test.ts | 262 +++++++++--------- 1 file changed, 124 insertions(+), 138 deletions(-) diff --git a/libs/langchain-community/src/stores/tests/neo4j.int.test.ts b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts index ad0427e348e8..bcd86e09619b 100644 --- a/libs/langchain-community/src/stores/tests/neo4j.int.test.ts +++ b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts @@ -4,149 +4,135 @@ import { HumanMessage, AIMessage } from "@langchain/core/messages"; import neo4j from "neo4j-driver"; const goodConfig = { - url: 'bolt://host.docker.internal:7687', - username: 'neo4j', - password: 'langchain' -} + 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) - }) - }) + expect(true).toEqual(true); + }); + }); describe("Class instantiation", () => { - it( - "Requires a url, username and password, throwing an error if not provided", - () => { - const badInstantiation = () => { - const badConfig = {} - // @ts-ignore - const instance = new Neo4jChatMessageHistory(badConfig) - } - expect(badInstantiation).toThrow(neo4j.Neo4jError) - } - ) + it("Requires a url, username and password, throwing an error if not provided", async () => { + const badConfig = {}; + await expect( + // @ts-expect-error + Neo4jChatMessageHistory.initialize(badConfig) + ).rejects.toThrow(neo4j.Neo4jError); + }); - it( - "Creates a class instance from - at minimum - a url, username and password", - () => { - const instance = new Neo4jChatMessageHistory(goodConfig) - expect(instance).toBeInstanceOf(Neo4jChatMessageHistory) - } - ) - - it( - "Class instances have expected, configurable fields, and sensible defaults", - () => { - const instance = new Neo4jChatMessageHistory(goodConfig) - - expect(instance.sessionId).toBeDefined() - expect(instance.sessionNodeLabel).toEqual("ChatSession") - expect(instance.windowSize).toEqual(3) - expect(instance.messageNodeLabel).toEqual("ChatMessage") - - const secondInstance = new Neo4jChatMessageHistory({ - ...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) + 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 (let message of messages) { + await instance?.addMessage(message); } - ) - }) - - describe( - "Core functionality", - () => { - - let instance: undefined | Neo4jChatMessageHistory; - - beforeEach(() => { - instance = new Neo4jChatMessageHistory(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 (let 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([]) - } - ) - } - ) -}) + 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([]); + }); + }); +}); From 18d4b76aaaeca4041ab3b4b106fdf0436956ef32 Mon Sep 17 00:00:00 2001 From: BernardFaucher Date: Fri, 13 Dec 2024 16:47:18 +0000 Subject: [PATCH 5/7] prefer driver.executeQuery to session.run --- libs/langchain-community/src/stores/message/neo4j.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/langchain-community/src/stores/message/neo4j.ts b/libs/langchain-community/src/stores/message/neo4j.ts index a1c65e25084c..8c8fb5d358bf 100644 --- a/libs/langchain-community/src/stores/message/neo4j.ts +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -100,10 +100,10 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { `; try { - const messages = await this.driver.session().run(getMessagesCypherQuery, { + const { records } = await this.driver.executeQuery(getMessagesCypherQuery, { sessionId: this.sessionId, }); - const results = messages.records.map((record: Record) => + const results = records.map((record: Record) => record.get("result") ); @@ -130,7 +130,7 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { `; try { - await this.driver.session().run(addMessageCypherQuery, { + await this.driver.executeQuery(addMessageCypherQuery, { sessionId: this.sessionId, type: message.getType(), content: message.content, @@ -151,7 +151,7 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { `; try { - await this.driver.session().run(clearMessagesCypherQuery, { + await this.driver.executeQuery(clearMessagesCypherQuery, { sessionId: this.sessionId, }); } catch (e) { From 7ac3b00ed50d62634e2f5b52a16041c15cd2fc44 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Fri, 13 Dec 2024 19:38:32 -0800 Subject: [PATCH 6/7] Fix lint --- .../src/stores/message/neo4j.ts | 18 +++++++++++------- .../src/stores/tests/neo4j.int.test.ts | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libs/langchain-community/src/stores/message/neo4j.ts b/libs/langchain-community/src/stores/message/neo4j.ts index 8c8fb5d358bf..4234c3889060 100644 --- a/libs/langchain-community/src/stores/message/neo4j.ts +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -1,5 +1,4 @@ -import neo4j from "neo4j-driver"; -import { Driver, Record, Neo4jError, auth } from "neo4j-driver"; +import neo4j, { Driver, Record, Neo4jError, auth } from "neo4j-driver"; import { v4 as uuidv4 } from "uuid"; import { BaseListChatMessageHistory } from "@langchain/core/chat_history"; import { @@ -25,9 +24,13 @@ const defaultConfig = { export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { lc_namespace: string[] = ["langchain", "stores", "message", "neo4j"]; + sessionId: string | number; + sessionNodeLabel: string; + messageNodeLabel: string; + windowSize: number; private driver: Driver; @@ -100,12 +103,13 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { `; try { - const { records } = await this.driver.executeQuery(getMessagesCypherQuery, { - sessionId: this.sessionId, - }); - const results = records.map((record: Record) => - record.get("result") + const { records } = await this.driver.executeQuery( + getMessagesCypherQuery, + { + sessionId: this.sessionId, + } ); + const results = records.map((record: Record) => record.get("result")); return mapStoredMessagesToChatMessages(results); } catch (e) { diff --git a/libs/langchain-community/src/stores/tests/neo4j.int.test.ts b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts index bcd86e09619b..2f6c17d01ed6 100644 --- a/libs/langchain-community/src/stores/tests/neo4j.int.test.ts +++ b/libs/langchain-community/src/stores/tests/neo4j.int.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; -import { Neo4jChatMessageHistory } from "../message/neo4j.js"; 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", @@ -20,7 +20,7 @@ describe("The Neo4jChatMessageHistory class", () => { it("Requires a url, username and password, throwing an error if not provided", async () => { const badConfig = {}; await expect( - // @ts-expect-error + // @ts-expect-error Bad config Neo4jChatMessageHistory.initialize(badConfig) ).rejects.toThrow(neo4j.Neo4jError); }); @@ -113,7 +113,7 @@ describe("The Neo4jChatMessageHistory class", () => { 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 (let message of messages) { + for (const message of messages) { await instance?.addMessage(message); } const results = await instance?.getMessages(); From 9a1a2e753002b6930b0ad3ed11987d28e5ba9e54 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Fri, 13 Dec 2024 19:51:46 -0800 Subject: [PATCH 7/7] Add entrypoint, use native error class instead of Neo4jError --- 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 | 48 +++++++------------ 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index 5064c1f14c79..3a5f83854558 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -798,6 +798,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 b0207b8612ab..badc0089e22e 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -249,6 +249,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", @@ -472,6 +473,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 7b441119bdbd..fef17ea9eba2 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -2512,6 +2512,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", @@ -3919,6 +3928,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 index 4234c3889060..a5f132900470 100644 --- a/libs/langchain-community/src/stores/message/neo4j.ts +++ b/libs/langchain-community/src/stores/message/neo4j.ts @@ -1,4 +1,4 @@ -import neo4j, { Driver, Record, Neo4jError, auth } from "neo4j-driver"; +import neo4j, { Driver, Record, auth } from "neo4j-driver"; import { v4 as uuidv4 } from "uuid"; import { BaseListChatMessageHistory } from "@langchain/core/chat_history"; import { @@ -54,17 +54,13 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { if (url && username && password) { try { this.driver = neo4j.driver(url, auth.basic(username, password)); - } catch (e) { - throw new Neo4jError({ - message: - "Could not create a Neo4j driver instance. Please check the connection details.", - cause: e, - }); + } catch (e: any) { + throw new Error( + `Could not create a Neo4j driver instance. Please check the connection details.\nCause: ${e.message}` + ); } } else { - throw new Neo4jError({ - message: "Neo4j connection details not provided.", - }); + throw new Error("Neo4j connection details not provided."); } } @@ -75,11 +71,10 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { try { await instance.verifyConnectivity(); - } catch (e) { - throw new Neo4jError({ - message: `Could not verify connection to the Neo4j database. Cause: ${e}`, - cause: e, - }); + } catch (e: any) { + throw new Error( + `Could not verify connection to the Neo4j database.\nCause: ${e.message}` + ); } return instance; @@ -112,11 +107,8 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { const results = records.map((record: Record) => record.get("result")); return mapStoredMessagesToChatMessages(results); - } catch (e) { - throw new Neo4jError({ - message: `Ohno! Couldn't get messages. Cause: ${e}`, - cause: e, - }); + } catch (e: any) { + throw new Error(`Ohno! Couldn't get messages.\nCause: ${e.message}`); } } @@ -139,11 +131,8 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { type: message.getType(), content: message.content, }); - } catch (e) { - throw new Neo4jError({ - message: `Ohno! Couldn't add message. Cause: ${e}`, - cause: e, - }); + } catch (e: any) { + throw new Error(`Ohno! Couldn't add message.\nCause: ${e.message}`); } } @@ -158,11 +147,10 @@ export class Neo4jChatMessageHistory extends BaseListChatMessageHistory { await this.driver.executeQuery(clearMessagesCypherQuery, { sessionId: this.sessionId, }); - } catch (e) { - throw new Neo4jError({ - message: `Ohno! Couldn't clear chat history. Cause: ${e}`, - cause: e, - }); + } catch (e: any) { + throw new Error( + `Ohno! Couldn't clear chat history.\nCause: ${e.message}` + ); } }