Skip to content

Commit

Permalink
Added PyInterpreterTool (langchain-ai#3090)
Browse files Browse the repository at this point in the history
* Added PyInterpreterTool

* Updated PyInterpreterTool

* Moved PythonInterpreterTool to tools/experimental

* Move to experimental, fix race condition in constructor

* Fix lint + test

* Adds docs + example

---------

Co-authored-by: Mish Ushakov <[email protected]>
Co-authored-by: jacoblee93 <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2023
1 parent db7da8d commit 9c3120a
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/api_refs/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
"./langchain/src/experimental/llms/bittensor.ts",
"./langchain/src/experimental/hubs/makersuite/googlemakersuitehub.ts",
"./langchain/src/experimental/chains/violation_of_expectations/index.ts",
"./langchain/src/experimental/tools/pyinterpreter.ts",
"./langchain/src/evaluation/index.ts",
"./langchain/src/runnables/index.ts",
"./langchain/src/runnables/remote.ts"
Expand Down
20 changes: 20 additions & 0 deletions docs/core_docs/docs/integrations/tools/pyinterpreter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
hide_table_of_contents: true
---

import CodeBlock from "@theme/CodeBlock";

# Python interpreter tool

:::warning
This tool executes code and can potentially perform destructive actions. Be careful that you trust any code passed to it!
:::

LangChain offers an experimental tool for executing arbitrary Python code.
This can be useful in combination with an LLM that can generate code to perform more powerful computations.

## Usage

import ToolExample from "@examples/tools/pyinterpreter.ts";

<CodeBlock language="typescript">{ToolExample}</CodeBlock>
24 changes: 24 additions & 0 deletions examples/src/tools/pyinterpreter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ChatPromptTemplate } from "langchain/prompts";
import { OpenAI } from "langchain/llms/openai";
import { PythonInterpreterTool } from "langchain/experimental/tools/pyinterpreter";
import { StringOutputParser } from "langchain/schema/output_parser";

const prompt = ChatPromptTemplate.fromTemplate(
`Generate python code that does {input}. Do not generate anything else.`
);

const model = new OpenAI({});

const interpreter = await PythonInterpreterTool.initialize({
indexURL: "../node_modules/pyodide",
});
const chain = prompt
.pipe(model)
.pipe(new StringOutputParser())
.pipe(interpreter);

const result = await chain.invoke({
input: `prints "Hello LangChain"`,
});

console.log(JSON.parse(result).stdout);
3 changes: 3 additions & 0 deletions langchain/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,9 @@ experimental/hubs/makersuite/googlemakersuitehub.d.ts
experimental/chains/violation_of_expectations.cjs
experimental/chains/violation_of_expectations.js
experimental/chains/violation_of_expectations.d.ts
experimental/tools/pyinterpreter.cjs
experimental/tools/pyinterpreter.js
experimental/tools/pyinterpreter.d.ts
evaluation.cjs
evaluation.js
evaluation.d.ts
Expand Down
13 changes: 13 additions & 0 deletions langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,9 @@
"experimental/chains/violation_of_expectations.cjs",
"experimental/chains/violation_of_expectations.js",
"experimental/chains/violation_of_expectations.d.ts",
"experimental/tools/pyinterpreter.cjs",
"experimental/tools/pyinterpreter.js",
"experimental/tools/pyinterpreter.d.ts",
"evaluation.cjs",
"evaluation.js",
"evaluation.d.ts",
Expand Down Expand Up @@ -961,6 +964,7 @@
"portkey-ai": "^0.1.11",
"prettier": "^2.8.3",
"puppeteer": "^19.7.2",
"pyodide": "^0.24.1",
"redis": "^4.6.6",
"release-it": "^15.10.1",
"replicate": "^0.18.0",
Expand Down Expand Up @@ -1066,6 +1070,7 @@
"playwright": "^1.32.1",
"portkey-ai": "^0.1.11",
"puppeteer": "^19.7.2",
"pyodide": "^0.24.1",
"redis": "^4.6.4",
"replicate": "^0.18.0",
"sonix-speech-recognition": "^2.1.1",
Expand Down Expand Up @@ -1334,6 +1339,9 @@
"puppeteer": {
"optional": true
},
"pyodide": {
"optional": true
},
"redis": {
"optional": true
},
Expand Down Expand Up @@ -2735,6 +2743,11 @@
"import": "./experimental/chains/violation_of_expectations.js",
"require": "./experimental/chains/violation_of_expectations.cjs"
},
"./experimental/tools/pyinterpreter": {
"types": "./experimental/tools/pyinterpreter.d.ts",
"import": "./experimental/tools/pyinterpreter.js",
"require": "./experimental/tools/pyinterpreter.cjs"
},
"./evaluation": {
"types": "./evaluation.d.ts",
"import": "./evaluation.js",
Expand Down
2 changes: 2 additions & 0 deletions langchain/scripts/create-entrypoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const entrypoints = {
"experimental/hubs/makersuite/googlemakersuitehub",
"experimental/chains/violation_of_expectations":
"experimental/chains/violation_of_expectations/index",
"experimental/tools/pyinterpreter": "experimental/tools/pyinterpreter",
// evaluation
evaluation: "evaluation/index",
// runnables
Expand Down Expand Up @@ -498,6 +499,7 @@ const requiresOptionalDependency = [
"experimental/chat_models/anthropic_functions",
"experimental/llms/bittensor",
"experimental/hubs/makersuite/googlemakersuitehub",
"experimental/tools/pyinterpreter",
"util/convex",
];

Expand Down
239 changes: 239 additions & 0 deletions langchain/src/experimental/tools/pyinterpreter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { loadPyodide, type PyodideInterface } from "pyodide";
import { Tool, ToolParams } from "../../tools/base.js";

export type PythonInterpreterToolParams = Parameters<typeof loadPyodide>[0] &
ToolParams & {
instance: PyodideInterface;
};

export class PythonInterpreterTool extends Tool {
static lc_name() {
return "PythonInterpreterTool";
}

name = "python_interpreter";

description = `Evaluates python code in a sandbox environment. The environment resets on every execution. You must send the whole script every time and print your outputs. Script should be pure python code that can be evaluated. Packages available:
${this.availableDefaultPackages}`;

pyodideInstance: PyodideInterface;

stdout = "";

stderr = "";

constructor(options: PythonInterpreterToolParams) {
super(options);
this.pyodideInstance = options.instance;
this.pyodideInstance.setStderr({
batched: (text: string) => {
this.stderr += text;
},
});

this.pyodideInstance.setStdout({
batched: (text: string) => {
this.stdout += text;
},
});
}

async addPackage(packageName: string) {
await this.pyodideInstance.loadPackage(packageName);
this.description += `, ${packageName}`;
}

get availableDefaultPackages(): string {
return [
"asciitree",
"astropy",
"atomicwrites",
"attrs",
"autograd",
"awkward-cpp",
"bcrypt",
"beautifulsoup4",
"biopython",
"bitarray",
"bitstring",
"bleach",
"bokeh",
"boost-histogram",
"brotli",
"cachetools",
"Cartopy",
"cbor-diag",
"certifi",
"cffi",
"cffi_example",
"cftime",
"click",
"cligj",
"cloudpickle",
"cmyt",
"colorspacious",
"contourpy",
"coolprop",
"coverage",
"cramjam",
"cryptography",
"cssselect",
"cycler",
"cytoolz",
"decorator",
"demes",
"deprecation",
"distlib",
"docutils",
"exceptiongroup",
"fastparquet",
"fiona",
"fonttools",
"freesasa",
"fsspec",
"future",
"galpy",
"gensim",
"geopandas",
"gmpy2",
"gsw",
"h5py",
"html5lib",
"idna",
"igraph",
"imageio",
"iniconfig",
"jedi",
"Jinja2",
"joblib",
"jsonschema",
"kiwisolver",
"lazy-object-proxy",
"lazy_loader",
"lightgbm",
"logbook",
"lxml",
"MarkupSafe",
"matplotlib",
"matplotlib-pyodide",
"micropip",
"mne",
"more-itertools",
"mpmath",
"msgpack",
"msprime",
"multidict",
"munch",
"mypy",
"netcdf4",
"networkx",
"newick",
"nlopt",
"nltk",
"nose",
"numcodecs",
"numpy",
"opencv-python",
"optlang",
"orjson",
"packaging",
"pandas",
"parso",
"patsy",
"peewee",
"Pillow",
"pillow_heif",
"pkgconfig",
"pluggy",
"protobuf",
"py",
"pyb2d",
"pyclipper",
"pycparser",
"pycryptodome",
"pydantic",
"pyerfa",
"Pygments",
"pyheif",
"pyinstrument",
"pynacl",
"pyodide-http",
"pyodide-tblib",
"pyparsing",
"pyproj",
"pyrsistent",
"pyshp",
"pytest",
"pytest-benchmark",
"python-dateutil",
"python-magic",
"python-sat",
"python_solvespace",
"pytz",
"pywavelets",
"pyxel",
"pyyaml",
"rebound",
"reboundx",
"regex",
"retrying",
"RobotRaconteur",
"ruamel.yaml",
"rust-panic-test",
"scikit-image",
"scikit-learn",
"scipy",
"screed",
"setuptools",
"shapely",
"simplejson",
"six",
"smart_open",
"soupsieve",
"sourmash",
"sparseqr",
"sqlalchemy",
"statsmodels",
"svgwrite",
"swiglpk",
"sympy",
"termcolor",
"texttable",
"threadpoolctl",
"tomli",
"tomli-w",
"toolz",
"tqdm",
"traits",
"tskit",
"typing-extensions",
"uncertainties",
"unyt",
"webencodings",
"wordcloud",
"wrapt",
"xarray",
"xgboost",
"xlrd",
"xyzservices",
"yarl",
"yt",
"zarr",
].join(", ");
}

static async initialize(
options: Omit<PythonInterpreterToolParams, "instance">
) {
const instance = await loadPyodide(options);
return new this({ ...options, instance });
}

async _call(script: string) {
this.stdout = "";
this.stderr = "";

await this.pyodideInstance.runPythonAsync(script);
return JSON.stringify({ stdout: this.stdout, stderr: this.stderr });
}
}
29 changes: 29 additions & 0 deletions langchain/src/experimental/tools/tests/pyinterpreter.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from "@jest/globals";
import { StringOutputParser } from "../../../schema/output_parser.js";
import { OpenAI } from "../../../llms/openai.js";
import { PromptTemplate } from "../../../prompts/index.js";
import { PythonInterpreterTool } from "../pyinterpreter.js";

describe("Python Interpreter testsuite", () => {
test("hello langchain", async () => {
const prompt = PromptTemplate.fromTemplate(
`Can you generate python code that: {input}? Do not generate anything else.`
);

const model = new OpenAI({});

const interpreter = await PythonInterpreterTool.initialize({
indexURL: "../node_modules/pyodide",
});
const chain = prompt
.pipe(model)
.pipe(new StringOutputParser())
.pipe(interpreter);

const result = await chain.invoke({
input: `prints "Hello LangChain"`,
});

expect(JSON.parse(result).stdout).toBe("Hello LangChain");
});
});
Loading

0 comments on commit 9c3120a

Please sign in to comment.