diff --git a/examples/src/chains/graph_db_falkordb.ts b/examples/src/chains/graph_db_falkordb.ts new file mode 100644 index 000000000000..3f943e404ab8 --- /dev/null +++ b/examples/src/chains/graph_db_falkordb.ts @@ -0,0 +1,30 @@ +import { FalkorDBGraph } from "@langchain/community/graphs/falkordb"; +import { OpenAI } from "@langchain/openai"; +import { GraphCypherQAChain } from "langchain/chains/graph_qa/cypher"; + +/** + * This example uses FalkorDB database, which is native graph database. + * To set it up follow the instructions on https://docs.falkordb.com/. + */ + +const url = "bolt://localhost:6379"; + +const graph = await FalkorDBGraph.initialize({ url }); +const model = new OpenAI({ temperature: 0 }); + +// Populate the database with two nodes and a relationship +await graph.query( + "CREATE (a:Actor {name:'Bruce Willis'})" + + "-[:ACTED_IN]->(:Movie {title: 'Pulp Fiction'})" +); + +await graph.refreshSchema(); + +const chain = GraphCypherQAChain.fromLLM({ + llm: model, + graph, +}); + +const res = await chain.run("Who played in Pulp Fiction?"); +console.log(res); +// Bruce Willis played in Pulp Fiction. \ No newline at end of file diff --git a/langchain/.env.example b/langchain/.env.example index 2eda74311a41..036e06e9c0fe 100644 --- a/langchain/.env.example +++ b/langchain/.env.example @@ -95,6 +95,7 @@ NEO4J_PASSWORD=ADD_YOURS_HERE MEMGRAPH_URI=ADD_YOURS_HERE MEMGRAPH_USERNAME=ADD_YOURS_HERE MEMGRAPH_PASSWORD=ADD_YOURS_HERE +FALKORDB_URI=ADD_YOURS_HERE CLOSEVECTOR_API_KEY=ADD_YOURS_HERE CLOSEVECTOR_API_SECRET=ADD_YOURS_HERE GPLACES_API_KEY=ADD_YOURS_HERE diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index dc58963eed75..d63e8d12704b 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -227,6 +227,7 @@ export const config = { // graphs "graphs/neo4j_graph": "graphs/neo4j_graph", "graphs/memgraph_graph": "graphs/memgraph_graph", + "graphs/falkordb": "graphs/falkordb", // document_compressors "document_compressors/ibm": "document_compressors/ibm", // document transformers @@ -463,6 +464,7 @@ export const config = { //graphs "graphs/neo4j_graph", "graphs/memgraph_graph", + "graphs/falkordb", // document_compressors "document_compressors/ibm", // document_transformers diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index 22104200da1d..68a0f2e6a15d 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -191,6 +191,7 @@ "mongodb": "^5.2.0", "mysql2": "^3.9.8", "neo4j-driver": "^5.17.0", + "falkordb": "^6.2.5", "node-llama-cpp": "3.1.1", "notion-to-md": "^3.1.0", "officeparser": "^4.0.4", @@ -324,6 +325,7 @@ "mongodb": ">=5.2.0", "mysql2": "^3.9.8", "neo4j-driver": "*", + "falkordb": "*", "notion-to-md": "^3.1.0", "officeparser": "^4.0.4", "openai": "*", @@ -644,6 +646,9 @@ "neo4j-driver": { "optional": true }, + "falkordb": { + "optional": true + }, "notion-to-md": { "optional": true }, @@ -2365,6 +2370,15 @@ "import": "./graphs/memgraph_graph.js", "require": "./graphs/memgraph_graph.cjs" }, + "./graphs/falkordb": { + "types": { + "import": "./graphs/falkordb.d.ts", + "require": "./graphs/falkordb.d.cts", + "default": "./graphs/falkordb.d.ts" + }, + "import": "./graphs/falkordb.js", + "require": "./graphs/falkordb.cjs" + }, "./document_compressors/ibm": { "types": { "import": "./document_compressors/ibm.d.ts", @@ -3929,6 +3943,10 @@ "graphs/memgraph_graph.js", "graphs/memgraph_graph.d.ts", "graphs/memgraph_graph.d.cts", + "graphs/falkordb.cjs", + "graphs/falkordb.js", + "graphs/falkordb.d.ts", + "graphs/falkordb.d.cts", "document_compressors/ibm.cjs", "document_compressors/ibm.js", "document_compressors/ibm.d.ts", diff --git a/libs/langchain-community/src/graphs/falkordb.ts b/libs/langchain-community/src/graphs/falkordb.ts new file mode 100644 index 000000000000..b50f1832571b --- /dev/null +++ b/libs/langchain-community/src/graphs/falkordb.ts @@ -0,0 +1,158 @@ +import { FalkorDB, Graph } from "falkordb"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any + +interface FalkorDBGraphConfig { + url: string; + graph?: string; + enhancedSchema?: boolean; +} + +interface StructuredSchema { + nodeProps: { [key: string]: string[] }; + relProps: { [key: string]: string[] }; + relationships: { start: string; type: string; end: string }[]; +} + +export class FalkorDBGraph { + private driver: FalkorDB; + + private graph: Graph; + + private schema = ""; + + private structuredSchema: StructuredSchema = { + nodeProps: {}, + relProps: {}, + relationships: [], + }; + + private enhancedSchema: boolean; + + constructor({ enhancedSchema = false }: FalkorDBGraphConfig) { + try { + this.enhancedSchema = enhancedSchema; + } catch (error) { + console.error("Error in FalkorDBGraph constructor:", error); + throw new Error("Failed to initialize FalkorDBGraph."); + } + } + + static async initialize(config: FalkorDBGraphConfig): Promise { + const graph = new FalkorDBGraph(config); + const driver = await FalkorDB.connect({ + socket: { + host: new URL(config.url).hostname, + port: parseInt(new URL(config.url).port, 10), + }, + }); + graph.driver = driver; + await graph.verifyConnectivity(); + + return graph; + } + + + getSchema(): string { + return this.schema; + } + + getStructuredSchema(): StructuredSchema { + return this.structuredSchema; + } + + async selectGraph(graphName: string): Promise { + this.graph = await this.driver.selectGraph(graphName); + } + + async query(query: string): Promise { + return await this.graph.query(query); + } + + async verifyConnectivity(): Promise { + await this.driver.info() + } + + + async refreshSchema(): Promise { + const nodePropertiesQuery = ` + MATCH (n) + WITH keys(n) as keys, labels(n) AS labels + UNWIND labels AS label + UNWIND keys AS key + WITH label, collect(DISTINCT key) AS properties + RETURN {label: label, properties: properties} AS output + `; + + const relPropertiesQuery = ` + MATCH ()-[r]->() + WITH keys(r) as keys, type(r) AS type + UNWIND keys AS key + WITH type, collect(DISTINCT key) AS properties + RETURN {type: type, properties: properties} AS output + `; + + const relQuery = ` + MATCH (n)-[r]->(m) + UNWIND labels(n) as src_label + UNWIND labels(m) as dst_label + RETURN DISTINCT {start: src_label, type: type(r), end: dst_label} AS output + `; + + const nodePropertiesResult = await this.query(nodePropertiesQuery); + const relationshipsPropertiesResult = await this.query(relPropertiesQuery); + const relationshipsResult = await this.query(relQuery); + + const nodeProperties = nodePropertiesResult.data || []; + const relationshipsProperties = relationshipsPropertiesResult.data || []; + const relationships = relationshipsResult.data || []; + + this.structuredSchema = { + nodeProps: Object.fromEntries( + nodeProperties.map((el: { output: { label: string; properties: string[] } }) => [el.output.label, el.output.properties]) + ), + relProps: Object.fromEntries( + relationshipsProperties.map((el: { output: { type: string; properties: string[] } }) => [el.output.type, el.output.properties]) + ), + relationships: relationships.map((el: { output: { start: string; type: string; end: string } }) => el.output), + }; + + if (this.enhancedSchema) { + await this.enhanceSchemaDetails(); + } + + this.schema = this.formatSchema(); +} + + private async enhanceSchemaDetails(): Promise { + console.log("Enhanced schema details not yet implemented for FalkorDB."); + } + + private formatSchema(): string { + const { nodeProps, relProps, relationships } = this.structuredSchema; + + const formattedNodeProps = Object.entries(nodeProps) + .map(([label, props]) => `${label}: {${props.join(", ")}}`) + .join("\n"); + + const formattedRelProps = Object.entries(relProps) + .map(([type, props]) => `${type}: {${props.join(", ")}}`) + .join("\n"); + + const formattedRelationships = relationships + .map((rel) => `(:${rel.start}) -[:${rel.type}]-> (:${rel.end})`) + .join("\n"); + + return [ + "Node properties are the following:", + formattedNodeProps, + "Relationship properties are the following:", + formattedRelProps, + "The relationships are the following:", + formattedRelationships, + ].join("\n"); + } + + async close(): Promise { + await this.driver.close(); + } +} \ No newline at end of file diff --git a/libs/langchain-community/src/graphs/tests/falkordb_graph.int.test.ts b/libs/langchain-community/src/graphs/tests/falkordb_graph.int.test.ts new file mode 100644 index 000000000000..290eb9025794 --- /dev/null +++ b/libs/langchain-community/src/graphs/tests/falkordb_graph.int.test.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-process-env */ + +import { test } from "@jest/globals"; +import { FalkorDBGraph } from "../falkordb.js"; + +describe("FalkorDB Graph Tests", () => { + const url = process.env.FALKORDB_URI as string; + let graph: FalkorDBGraph; + + beforeEach(async () => { + graph = await FalkorDBGraph.initialize({ url }); + await graph.selectGraph("falkordbGraph"); + await graph.refreshSchema(); + await graph.query("MATCH (n) DETACH DELETE n"); + }); + + afterEach(async () => { + await graph.close(); + }); + + test("Test that FalkorDN database is correctly instantiated and connected", async () => { + expect(url).toBeDefined(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return graph.query('RETURN "test" AS output').then((output: any) => { + const expectedOutput = [{ output: "test" }]; + expect(output.data).toEqual(expectedOutput); + }); + }); + + test("Verify refreshSchema accurately updates the schema", async () => { + await graph.query(` + CREATE (:Person {name: 'Alice', age: 30})-[:FRIENDS_WITH]->(:Person {name: 'Bob', age: 25}) + `); + await graph.refreshSchema(); + + const schema = graph.getSchema(); + expect(schema).toContain("Person"); + expect(schema).toContain("FRIENDS_WITH"); + expect(schema).toContain("name"); + expect(schema).toContain("age"); + }); +}); \ No newline at end of file diff --git a/libs/langchain-community/src/load/import_constants.ts b/libs/langchain-community/src/load/import_constants.ts index 6ac412ca9543..9d017aca294f 100644 --- a/libs/langchain-community/src/load/import_constants.ts +++ b/libs/langchain-community/src/load/import_constants.ts @@ -114,6 +114,7 @@ export const optionalImportEntrypoints: string[] = [ "langchain_community/retrievers/zep_cloud", "langchain_community/graphs/neo4j_graph", "langchain_community/graphs/memgraph_graph", + "langchain_community/graphs/falkordb", "langchain_community/document_compressors/ibm", "langchain_community/document_transformers/html_to_text", "langchain_community/document_transformers/mozilla_readability",