Add E2B as an optional code sandbox provider

- Specify E2B api key and template to use via env variables
- Try load, use e2b library when E2B api key set
- Fallback to try use terrarium sandbox otherwise
- Enable more python packages in e2b sandbox like rdkit via custom e2b template

- Use Async E2B Sandbox
- Parallelize file IO with sandbox
- Add documentation on how to enable E2B as code sandbox instead of Terrarium
This commit is contained in:
Debanjum
2025-02-15 03:11:15 +05:30
parent b4183c7333
commit 45fb85f1df
7 changed files with 157 additions and 18 deletions

View File

@@ -32,6 +32,14 @@ on:
required: false required: false
default: 200 default: 200
type: number type: number
sandbox:
description: 'Code sandbox to use'
required: false
default: 'terrarium'
type: choice
options:
- terrarium
- e2b
jobs: jobs:
eval: eval:
@@ -100,6 +108,8 @@ jobs:
SERPER_DEV_API_KEY: ${{ matrix.dataset != 'math500' && secrets.SERPER_DEV_API_KEY }} SERPER_DEV_API_KEY: ${{ matrix.dataset != 'math500' && secrets.SERPER_DEV_API_KEY }}
OLOSTEP_API_KEY: ${{ matrix.dataset != 'math500' && secrets.OLOSTEP_API_KEY }} OLOSTEP_API_KEY: ${{ matrix.dataset != 'math500' && secrets.OLOSTEP_API_KEY }}
HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_TOKEN: ${{ secrets.HF_TOKEN }}
E2B_API_KEY: ${{ inputs.sandbox == 'e2b' && secrets.E2B_API_KEY }}
E2B_TEMPLATE: ${{ vars.E2B_TEMPLATE }}
KHOJ_ADMIN_EMAIL: khoj KHOJ_ADMIN_EMAIL: khoj
KHOJ_ADMIN_PASSWORD: khoj KHOJ_ADMIN_PASSWORD: khoj
POSTGRES_HOST: localhost POSTGRES_HOST: localhost
@@ -148,6 +158,7 @@ jobs:
echo "**$(head -n 1 *_evaluation_summary_*.txt)**" >> $GITHUB_STEP_SUMMARY echo "**$(head -n 1 *_evaluation_summary_*.txt)**" >> $GITHUB_STEP_SUMMARY
echo "- Khoj Version: ${{ steps.hatch.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "- Khoj Version: ${{ steps.hatch.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- Chat Model: Gemini 2.0 Flash" >> $GITHUB_STEP_SUMMARY echo "- Chat Model: Gemini 2.0 Flash" >> $GITHUB_STEP_SUMMARY
echo "- Code Sandbox: ${{ inputs.sandbox}}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
tail -n +2 *_evaluation_summary_*.txt >> $GITHUB_STEP_SUMMARY tail -n +2 *_evaluation_summary_*.txt >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY

View File

@@ -58,8 +58,10 @@ services:
- KHOJ_DEBUG=False - KHOJ_DEBUG=False
- KHOJ_ADMIN_EMAIL=username@example.com - KHOJ_ADMIN_EMAIL=username@example.com
- KHOJ_ADMIN_PASSWORD=password - KHOJ_ADMIN_PASSWORD=password
# Default URL of Terrarium, the Python sandbox used by Khoj to run code. Its container is specified above # Default URL of Terrarium, the default Python sandbox used by Khoj to run code. Its container is specified above
- KHOJ_TERRARIUM_URL=http://sandbox:8080 - KHOJ_TERRARIUM_URL=http://sandbox:8080
# Uncomment line below to have Khoj run code in remote E2B code sandbox instead of the self-hosted Terrarium sandbox above. Get your E2B API key from https://e2b.dev/.
# - E2B_API_KEY=your_e2b_api_key
# Default URL of SearxNG, the default web search engine used by Khoj. Its container is specified above # Default URL of SearxNG, the default web search engine used by Khoj. Its container is specified above
- KHOJ_SEARXNG_URL=http://search:8080 - KHOJ_SEARXNG_URL=http://search:8080
# Uncomment line below to use with Ollama running on your local machine at localhost:11434. # Uncomment line below to use with Ollama running on your local machine at localhost:11434.

View File

@@ -3,22 +3,23 @@
# Code Execution # Code Execution
Khoj can generate and run very simple Python code snippets as well. This is useful if you want to generate a plot, run a simple calculation, or do some basic data manipulation. LLMs by default aren't skilled at complex quantitative tasks. Code generation & execution can come in handy for such tasks. Khoj can generate and run simple Python code as well. This is useful if you want to have Khoj do some data analysis, generate plots and reports. LLMs by default aren't skilled at complex quantitative tasks. Code generation & execution can come in handy for such tasks.
Just use `/code` in your chat command. Khoj automatically infers when to use the code tool. You can also tell it explicitly to use the code tool or use the `/code` [slash command](https://docs.khoj.dev/features/chat/#commands) in your chat.
### Setup (Self-Hosting) ## Setup (Self-Hosting)
Run [Cohere's Terrarium](https://github.com/cohere-ai/cohere-terrarium) on your machine to enable code generation and execution. ### Terrarium Sandbox
Use [Cohere's Terrarium](https://github.com/cohere-ai/cohere-terrarium) to host the code sandbox locally on your machine for free.
Check the [instructions](https://github.com/cohere-ai/cohere-terrarium?tab=readme-ov-file#development) for running from source. To run with Docker, use our [docker-compose.yml](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml) to automatically setup the Terrarium code sandbox, or start it manually like this:
For running with Docker, you can use our [docker-compose.yml](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml), or start it manually like this:
```bash ```bash
docker pull ghcr.io/khoj-ai/terrarium:latest docker pull ghcr.io/khoj-ai/terrarium:latest
docker run -d -p 8080:8080 ghcr.io/khoj-ai/terrarium:latest docker run -d -p 8080:8080 ghcr.io/khoj-ai/terrarium:latest
``` ```
To run from source, check [these instructions](https://github.com/khoj-ai/cohere-terrarium?tab=readme-ov-file#development).
#### Verify #### Verify
Verify that it's running, by evaluating a simple Python expression: Verify that it's running, by evaluating a simple Python expression:
@@ -28,3 +29,12 @@ curl -X POST -H "Content-Type: application/json" \
--data-raw '{"code": "1 + 1"}' \ --data-raw '{"code": "1 + 1"}' \
--no-buffer --no-buffer
``` ```
### E2B Sandbox
[E2B](https://e2b.dev/) allows Khoj to run code on a remote but versatile sandbox with support for more python libraries. This is [not free](https://e2b.dev/pricing).
To have Khoj use E2B as the code sandbox:
1. Generate an API key on [their dashboard](https://e2b.dev/dashboard).
2. Set the `E2B_API_KEY` environment variable to it on the machine running your Khoj server.
- When using our [docker-compose.yml](https://github.com/khoj-ai/khoj/blob/master/docker-compose.yml), uncomment and set the `E2B_API_KEY` env var in the `docker-compose.yml` file.
3. Now restart your Khoj server to switch to using the E2B code sandbox.

View File

@@ -68,7 +68,7 @@ dependencies = [
"authlib == 1.2.1", "authlib == 1.2.1",
"llama-cpp-python == 0.2.88", "llama-cpp-python == 0.2.88",
"itsdangerous == 2.1.2", "itsdangerous == 2.1.2",
"httpx == 0.25.0", "httpx == 0.27.2",
"pgvector == 0.2.4", "pgvector == 0.2.4",
"psycopg2-binary == 2.9.9", "psycopg2-binary == 2.9.9",
"lxml == 4.9.3", "lxml == 4.9.3",
@@ -92,6 +92,7 @@ dependencies = [
"pyjson5 == 1.6.7", "pyjson5 == 1.6.7",
"resend == 1.0.1", "resend == 1.0.1",
"email-validator == 2.2.0", "email-validator == 2.2.0",
"e2b-code-interpreter ~= 1.0.0",
] ]
dynamic = ["version"] dynamic = ["version"]

View File

@@ -974,9 +974,8 @@ Khoj:
python_code_generation_prompt = PromptTemplate.from_template( python_code_generation_prompt = PromptTemplate.from_template(
""" """
You are Khoj, an advanced python programmer. You are tasked with constructing a python program to best answer the user query. You are Khoj, an advanced python programmer. You are tasked with constructing a python program to best answer the user query.
- The python program will run in a pyodide python sandbox with no network access. - The python program will run in a sandbox with no network access.
- You can write programs to run complex calculations, analyze data, create charts, generate documents to meticulously answer the query. - You can write programs to run complex calculations, analyze data, create charts, generate documents to meticulously answer the query.
- The sandbox has access to the standard library, matplotlib, panda, numpy, scipy, bs4 and sympy packages. The requests, torch, catboost, tensorflow and tkinter packages are not available.
- List known file paths to required user documents in "input_files" and known links to required documents from the web in the "input_links" field. - List known file paths to required user documents in "input_files" and known links to required documents from the web in the "input_links" field.
- The python program should be self-contained. It can only read data generated by the program itself and from provided input_files, input_links by their basename (i.e filename excluding file path). - The python program should be self-contained. It can only read data generated by the program itself and from provided input_files, input_links by their basename (i.e filename excluding file path).
- Do not try display images or plots in the code directly. The code should save the image or plot to a file instead. - Do not try display images or plots in the code directly. The code should save the image or plot to a file instead.
@@ -1030,6 +1029,13 @@ Code Execution Results:
""".strip() """.strip()
) )
e2b_sandbox_context = """
- The sandbox has access to only the standard library, matplotlib, pandas, numpy, scipy, bs4, sympy, einops, biopython, shapely, plotly and rdkit packages. The requests, torch, catboost, tensorflow and tkinter packages are not available.
""".strip()
terrarium_sandbox_context = """
The sandbox has access to the standard library, matplotlib, pandas, numpy, scipy, bs4 and sympy packages. The requests, torch, catboost, tensorflow, rdkit and tkinter packages are not available.
""".strip()
# Automations # Automations
# -- # --

View File

@@ -27,7 +27,12 @@ from khoj.processor.conversation.utils import (
load_complex_json, load_complex_json,
) )
from khoj.routers.helpers import send_message_to_model_wrapper from khoj.routers.helpers import send_message_to_model_wrapper
from khoj.utils.helpers import is_none_or_empty, timer, truncate_code_context from khoj.utils.helpers import (
is_e2b_code_sandbox_enabled,
is_none_or_empty,
timer,
truncate_code_context,
)
from khoj.utils.rawconfig import LocationData from khoj.utils.rawconfig import LocationData
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -131,6 +136,12 @@ async def generate_python_code(
prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else "" prompts.personality_context.format(personality=agent.personality) if agent and agent.personality else ""
) )
# add sandbox specific context like available packages
sandbox_context = (
prompts.e2b_sandbox_context if is_e2b_code_sandbox_enabled() else prompts.terrarium_sandbox_context
)
personality_context = f"{sandbox_context}\n{personality_context}"
code_generation_prompt = prompts.python_code_generation_prompt.format( code_generation_prompt = prompts.python_code_generation_prompt.format(
current_date=utc_date, current_date=utc_date,
query=q, query=q,
@@ -182,15 +193,104 @@ async def execute_sandboxed_python(code: str, input_data: list[dict], sandbox_ur
Reference data i/o format based on Terrarium example client code at: Reference data i/o format based on Terrarium example client code at:
https://github.com/cohere-ai/cohere-terrarium/blob/main/example-clients/python/terrarium_client.py https://github.com/cohere-ai/cohere-terrarium/blob/main/example-clients/python/terrarium_client.py
""" """
headers = {"Content-Type": "application/json"}
cleaned_code = clean_code_python(code) cleaned_code = clean_code_python(code)
data = {"code": cleaned_code, "files": input_data} if is_e2b_code_sandbox_enabled():
try:
return await execute_e2b(cleaned_code, input_data)
except ImportError:
pass
return await execute_terrarium(cleaned_code, input_data, sandbox_url)
async def execute_e2b(code: str, input_files: list[dict]) -> dict[str, Any]:
"""Execute code and handle file I/O in e2b sandbox"""
from e2b_code_interpreter import AsyncSandbox
sandbox = await AsyncSandbox.create(
api_key=os.getenv("E2B_API_KEY"),
template=os.getenv("E2B_TEMPLATE", "pmt2o0ghpang8gbiys57"),
timeout=120,
request_timeout=30,
)
try:
# Upload input files in parallel
upload_tasks = [
sandbox.files.write(path=file["filename"], data=base64.b64decode(file["b64_data"]), request_timeout=30)
for file in input_files
]
await asyncio.gather(*upload_tasks)
# Note stored files before execution
E2bFile = NamedTuple("E2bFile", [("name", str), ("path", str)])
original_files = {E2bFile(f.name, f.path) for f in await sandbox.files.list("~")}
# Execute code from main.py file
execution = await sandbox.run_code(code=code, timeout=60)
# Collect output files
output_files = []
# Identify new files created during execution
new_files = set(E2bFile(f.name, f.path) for f in await sandbox.files.list("~")) - original_files
# Read newly created files in parallel
download_tasks = [sandbox.files.read(f.path, request_timeout=30) for f in new_files]
downloaded_files = await asyncio.gather(*download_tasks)
for f, content in zip(new_files, downloaded_files):
if isinstance(content, bytes):
# Binary files like PNG - encode as base64
b64_data = base64.b64encode(content).decode("utf-8")
elif Path(f.name).suffix in [".png", ".jpeg", ".jpg", ".svg"]:
# Ignore image files as they are extracted from execution results below for inline display
continue
else:
# Text files - encode utf-8 string as base64
b64_data = base64.b64encode(content.encode("utf-8")).decode("utf-8")
output_files.append({"filename": f.name, "b64_data": b64_data})
# Collect output files from execution results
for idx, result in enumerate(execution.results):
for result_type in ["png", "jpeg", "svg", "text", "markdown", "json"]:
if b64_data := getattr(result, result_type, None):
output_files.append({"filename": f"{idx}.{result_type}", "b64_data": b64_data})
break
# collect logs
success = not execution.error and not execution.logs.stderr
stdout = "\n".join(execution.logs.stdout)
errors = "\n".join(execution.logs.stderr)
if execution.error:
errors = f"{execution.error}\n{errors}"
return {
"code": code,
"success": success,
"std_out": stdout,
"std_err": errors,
"output_files": output_files,
}
except Exception as e:
return {
"code": code,
"success": False,
"std_err": f"Sandbox failed to execute code: {str(e)}",
"output_files": [],
}
async def execute_terrarium(
code: str,
input_data: list[dict],
sandbox_url: str,
) -> dict[str, Any]:
"""Execute code using Terrarium sandbox"""
headers = {"Content-Type": "application/json"}
data = {"code": code, "files": input_data}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(sandbox_url, json=data, headers=headers, timeout=30) as response: async with session.post(sandbox_url, json=data, headers=headers, timeout=30) as response:
if response.status == 200: if response.status == 200:
result: dict[str, Any] = await response.json() result: dict[str, Any] = await response.json()
result["code"] = cleaned_code result["code"] = code
# Store decoded output files # Store decoded output files
result["output_files"] = result.get("output_files", []) result["output_files"] = result.get("output_files", [])
for output_file in result["output_files"]: for output_file in result["output_files"]:
@@ -202,7 +302,7 @@ async def execute_sandboxed_python(code: str, input_data: list[dict], sandbox_ur
return result return result
else: else:
return { return {
"code": cleaned_code, "code": code,
"success": False, "success": False,
"std_err": f"Failed to execute code with {response.status}", "std_err": f"Failed to execute code with {response.status}",
"output_files": [], "output_files": [],

View File

@@ -321,6 +321,12 @@ def get_device() -> torch.device:
return torch.device("cpu") return torch.device("cpu")
def is_e2b_code_sandbox_enabled():
"""Check if E2B code sandbox is enabled.
Set E2B_API_KEY environment variable to use it."""
return not is_none_or_empty(os.getenv("E2B_API_KEY"))
class ConversationCommand(str, Enum): class ConversationCommand(str, Enum):
Default = "default" Default = "default"
General = "general" General = "general"
@@ -362,20 +368,23 @@ command_descriptions_for_agent = {
ConversationCommand.Code: "Agent can run Python code to parse information, run complex calculations, create documents and charts.", ConversationCommand.Code: "Agent can run Python code to parse information, run complex calculations, create documents and charts.",
} }
e2b_tool_description = "To run Python code in a E2B sandbox with no network access. Helpful to parse complex information, run calculations, create text documents and create charts with quantitative data. Only matplotlib, pandas, numpy, scipy, bs4, sympy, einops, biopython, shapely and rdkit external packages are available."
terrarium_tool_description = "To run Python code in a Terrarium, Pyodide sandbox with no network access. Helpful to parse complex information, run complex calculations, create plaintext documents and create charts with quantitative data. Only matplotlib, panda, numpy, scipy, bs4 and sympy external packages are available."
tool_descriptions_for_llm = { tool_descriptions_for_llm = {
ConversationCommand.Default: "To use a mix of your internal knowledge and the user's personal knowledge, or if you don't entirely understand the query.", ConversationCommand.Default: "To use a mix of your internal knowledge and the user's personal knowledge, or if you don't entirely understand the query.",
ConversationCommand.General: "To use when you can answer the question without any outside information or personal knowledge", ConversationCommand.General: "To use when you can answer the question without any outside information or personal knowledge",
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.", ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**", ConversationCommand.Online: "To search for the latest, up-to-date information from the internet. Note: **Questions about Khoj should always use this data source**",
ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.", ConversationCommand.Webpage: "To use if the user has directly provided the webpage urls or you are certain of the webpage urls to read.",
ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse complex information, run complex calculations, create plaintext documents, and create charts with quantitative data. Only matplotlib, panda, numpy, scipy, bs4 and sympy external packages are available.", ConversationCommand.Code: e2b_tool_description if is_e2b_code_sandbox_enabled() else terrarium_tool_description,
} }
function_calling_description_for_llm = { function_calling_description_for_llm = {
ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.", ConversationCommand.Notes: "To search the user's personal knowledge base. Especially helpful if the question expects context from the user's notes or documents.",
ConversationCommand.Online: "To search the internet for information. Useful to get a quick, broad overview from the internet. Provide all relevant context to ensure new searches, not in previous iterations, are performed.", ConversationCommand.Online: "To search the internet for information. Useful to get a quick, broad overview from the internet. Provide all relevant context to ensure new searches, not in previous iterations, are performed.",
ConversationCommand.Webpage: "To extract information from webpages. Useful for more detailed research from the internet. Usually used when you know the webpage links to refer to. Share the webpage links and information to extract in your query.", ConversationCommand.Webpage: "To extract information from webpages. Useful for more detailed research from the internet. Usually used when you know the webpage links to refer to. Share the webpage links and information to extract in your query.",
ConversationCommand.Code: "To run Python code in a Pyodide sandbox with no network access. Helpful when need to parse complex information, run complex calculations, create plaintext documents, and create charts with quantitative data. Only matplotlib, panda, numpy, scipy, bs4 and sympy external packages are available.", ConversationCommand.Code: e2b_tool_description if is_e2b_code_sandbox_enabled() else terrarium_tool_description,
} }
mode_descriptions_for_llm = { mode_descriptions_for_llm = {