Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/chromadb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: ChromaDB Test

on:
pull_request:
paths:
- "langchain-chromadb/**"
- "policies/**"
push:
tags:
- langchain-chromadb/v*
defaults:
run:
working-directory: langchain-chromadb

jobs:
test:
strategy:
matrix:
cerbos-version: ["latest"]
node-version: ["20", "22", "24", "25"]
chroma-image: ["ghcr.io/chroma-core/chroma:latest"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Cerbos
uses: cerbos/cerbos-setup-action@v1
with:
version: ${{ matrix.cerbos-version }}

- name: Start ChromaDB
run: |
docker run -d --name chromadb -p 8000:8000 ${{ matrix.chroma-image }}
for _ in {1..30}; do
if curl -sf http://127.0.0.1:8000/api/v2/heartbeat > /dev/null; then
exit 0
fi
sleep 1
done
echo "ChromaDB failed to start" >&2
exit 1

- name: Test using Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- run: npm install
- run: npm run test
env:
CHROMA_URL: http://127.0.0.1:8000
1 change: 1 addition & 0 deletions .github/workflows/mongoose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "mongoose/**"
- "policies/**"
push:
tags:
- mongoose/v*
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/prisma.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "prisma/**"
- "policies/**"
push:
tags:
- prisma/v*
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/sqlalchemy_pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
paths:
- "sqlalchemy/**"
- "policies/**"
branches:
- main

Expand All @@ -27,9 +28,9 @@ jobs:
- uses: pdm-project/setup-pdm@main
name: Setup PDM
with:
python-version: "3.10" # Version range or exact version of a Python version to use, the same as actions/setup-python
prerelease: true # Allow prerelease versions to be installed
enable-pep582: true # Enable PEP 582 package loading globally
python-version: "3.10" # Version range or exact version of a Python version to use, the same as actions/setup-python
prerelease: true # Allow prerelease versions to be installed
enable-pep582: true # Enable PEP 582 package loading globally

- run: pdm install -G testcontainers

Expand Down
5 changes: 5 additions & 0 deletions langchain-chromadb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
lib
.DS_Store
.chromadb
131 changes: 131 additions & 0 deletions langchain-chromadb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Cerbos + LangChain.js ChromaDB Adapter

An adapter library that takes a [Cerbos](https://cerbos.dev) Query Plan ([PlanResources API](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan)) response and converts it into a [ChromaDB](https://www.trychroma.com/) filter object that can be passed to the LangChain.js Chroma vector store. This is designed to work alongside a project using the [Cerbos Javascript SDK](https://github.com/cerbos/cerbos-sdk-javascript).

The following conditions are supported: `and`, `or`, `not`, `eq`, `ne`, `lt`, `gt`, `le`, `ge` and `in`.

Not Supported:

- `every`
- `contains`
- `search`
- `mode`
- `startsWith`
- `endsWith`
- `isSet`
- Scalar filters
- Atomic number operations
- Composite keys

## Requirements

- Cerbos > v0.16
- `@cerbos/http` or `@cerbos/grpc` client

## Usage

```
npm install @cerbos/lanchain-chromadb
```

This package exports a function:

```ts
import { queryPlanToChromaDB, PlanKind } from "@cerbos/lanchain-chromadb";

queryPlanToChromaDB({ queryPlan, fieldNameMapper }): {
kind: PlanKind,
filters?: any // a filter to pass as the `where` property of a ChromaDB query
}
```

where `PlanKind` is:

```ts
export enum PlanKind {
/**
* The specified action is always allowed for the principal on resources matching the input.
*/
ALWAYS_ALLOWED = "KIND_ALWAYS_ALLOWED",

/**
* The specified action is always denied for the principal on resources matching the input.
*/
ALWAYS_DENIED = "KIND_ALWAYS_DENIED",

/**
* The specified action is conditionally allowed for the principal on resources matching the input.
*/
CONDITIONAL = "KIND_CONDITIONAL",
}
```

The function requires the full query plan from Cerbos to be passed in an object along with a `fieldNameMapper`.

A basic implementation can be as simple as:

```ts
import { GRPC as Cerbos } from "@cerbos/grpc";
import { Chroma } from "@langchain/community/vectorstores/chroma";
import { OpenAIEmbeddings } from "@langchain/openai";

import { queryPlanToChromaDB, PlanKind } from "@cerbos/lanchain-chromadb";

const cerbos = new Cerbos("localhost:3592", { tls: false });

// Fetch the query plan from Cerbos passing in the principal
// resource type and action
const queryPlan = await cerbos.planResources({
principal: {....},
resource: { kind: "resourceKind" },
action: "view"
})

const filterResult = queryPlanToChromaDB({
queryPlan,
fieldNameMapper: {
"request.resource.attr.aFieldName": "metadataFieldName"
}
});

if(filterResult.kind === PlanKind.ALWAYS_DENIED) {
// return empty or throw an error depending on your app.
return [];
}

const chroma = await Chroma.fromExistingCollection(new OpenAIEmbeddings(), {
collectionName: "my_collection",
});

const matches = await chroma.similaritySearch("query", 10, {
...filterResult.filters,
});
```

The `fieldNameMapper` is used to convert the field names in the query plan response to names of fields in your ChromaDB metadata - this can be done as a map or a function:

```ts
const filters = queryPlanToChromaDB({
queryPlan,
fieldNameMapper: {
"request.resource.attr.aFieldName": "metadataFieldName",
},
});

//or

const filters = queryPlanToChromaDB({
queryPlan,
fieldNameMapper: (fieldName: string): string => {
if (fieldName.indexOf("request.resource.") > 0) {
return fieldName.replace("request.resource.attr", "metadata");
}

if (fieldName.indexOf("request.principal.") > 0) {
return fieldName.replace("request.principal.attr", "principal");
}

return fieldName;
},
});
```
13 changes: 13 additions & 0 deletions langchain-chromadb/cerbos-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
server:
httpListenAddr: ":3592"
grpcListenAddr: ":3593"

storage:
driver: "disk"
disk:
directory: ../policies
watchForChanges: true

telemetry:
disabled: true
20 changes: 20 additions & 0 deletions langchain-chromadb/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const esModules = ["uuid"].join("|");

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
extensionsToTreatAsEsm: [".ts"],
transform: {
"^.+\\.[tj]sx?$": [
"ts-jest",
{
useESM: true,
},
],
},
transformIgnorePatterns: [`node_modules/(?!(${esModules})/)`],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
};
Loading
Loading