Skip to content

Commit 5068e28

Browse files
committed
Merge branch 'main' into feat/implement-csv-upload
2 parents 04cc7ce + 0ad2207 commit 5068e28

File tree

14 files changed

+222
-79
lines changed

14 files changed

+222
-79
lines changed

.changeset/itchy-ads-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-llama": patch
3+
---
4+
5+
Add support E2B code interpreter tool for FastAPI

helpers/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ export const installTemplate = async (
171171
);
172172
}
173173
}
174+
175+
// Create tool-output directory
176+
if (props.tools && props.tools.length > 0) {
177+
await fsExtra.mkdir(path.join(props.root, "tool-output"));
178+
}
174179
} else {
175180
// this is a frontend for a full-stack app, create .env file with model information
176181
await createFrontendEnvFile(props.root, {

helpers/tools.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,13 @@ export const supportedTools: Tool[] = [
9090
{
9191
display: "Code Interpreter",
9292
name: "interpreter",
93-
dependencies: [],
94-
supportedFrameworks: ["express", "nextjs"],
93+
dependencies: [
94+
{
95+
name: "e2b_code_interpreter",
96+
version: "0.0.7",
97+
},
98+
],
99+
supportedFrameworks: ["fastapi", "express", "nextjs"],
95100
type: ToolType.LOCAL,
96101
envVars: [
97102
{
@@ -154,7 +159,6 @@ export const writeToolsConfig = async (
154159
tools: Tool[] = [],
155160
type: ConfigFileType = ConfigFileType.YAML,
156161
) => {
157-
if (tools.length === 0) return; // no tools selected, no config need
158162
const configContent: {
159163
[key in ToolType]: Record<string, any>;
160164
} = {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import os
2+
import logging
3+
import base64
4+
import uuid
5+
from pydantic import BaseModel
6+
from typing import List, Tuple, Dict
7+
from llama_index.core.tools import FunctionTool
8+
from e2b_code_interpreter import CodeInterpreter
9+
from e2b_code_interpreter.models import Logs
10+
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class InterpreterExtraResult(BaseModel):
16+
type: str
17+
filename: str
18+
url: str
19+
20+
21+
class E2BToolOutput(BaseModel):
22+
is_error: bool
23+
logs: Logs
24+
results: List[InterpreterExtraResult] = []
25+
26+
27+
class E2BCodeInterpreter:
28+
29+
output_dir = "tool-output"
30+
31+
def __init__(self, api_key: str, filesever_url_prefix: str):
32+
self.api_key = api_key
33+
self.filesever_url_prefix = filesever_url_prefix
34+
35+
def get_output_path(self, filename: str) -> str:
36+
# if output directory doesn't exist, create it
37+
if not os.path.exists(self.output_dir):
38+
os.makedirs(self.output_dir, exist_ok=True)
39+
return os.path.join(self.output_dir, filename)
40+
41+
def save_to_disk(self, base64_data: str, ext: str) -> Dict:
42+
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
43+
buffer = base64.b64decode(base64_data)
44+
output_path = self.get_output_path(filename)
45+
46+
try:
47+
with open(output_path, "wb") as file:
48+
file.write(buffer)
49+
except IOError as e:
50+
logger.error(f"Failed to write to file {output_path}: {str(e)}")
51+
raise e
52+
53+
logger.info(f"Saved file to {output_path}")
54+
55+
return {
56+
"outputPath": output_path,
57+
"filename": filename,
58+
}
59+
60+
def get_file_url(self, filename: str) -> str:
61+
return f"{self.filesever_url_prefix}/{self.output_dir}/{filename}"
62+
63+
def parse_result(self, result) -> List[InterpreterExtraResult]:
64+
"""
65+
The result could include multiple formats (e.g. png, svg, etc.) but encoded in base64
66+
We save each result to disk and return saved file metadata (extension, filename, url)
67+
"""
68+
if not result:
69+
return []
70+
71+
output = []
72+
73+
try:
74+
formats = result.formats()
75+
base64_data_arr = [result[format] for format in formats]
76+
77+
for ext, base64_data in zip(formats, base64_data_arr):
78+
if ext and base64_data:
79+
result = self.save_to_disk(base64_data, ext)
80+
filename = result["filename"]
81+
output.append(
82+
InterpreterExtraResult(
83+
type=ext, filename=filename, url=self.get_file_url(filename)
84+
)
85+
)
86+
except Exception as error:
87+
logger.error("Error when saving data to disk", error)
88+
89+
return output
90+
91+
def interpret(self, code: str) -> E2BToolOutput:
92+
with CodeInterpreter(api_key=self.api_key) as interpreter:
93+
logger.info(
94+
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
95+
)
96+
exec = interpreter.notebook.exec_cell(code)
97+
98+
if exec.error:
99+
output = E2BToolOutput(is_error=True, logs=[exec.error])
100+
else:
101+
if len(exec.results) == 0:
102+
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
103+
else:
104+
results = self.parse_result(exec.results[0])
105+
output = E2BToolOutput(
106+
is_error=False, logs=exec.logs, results=results
107+
)
108+
return output
109+
110+
111+
def code_interpret(code: str) -> Dict:
112+
"""
113+
Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
114+
"""
115+
api_key = os.getenv("E2B_API_KEY")
116+
filesever_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
117+
if not api_key:
118+
raise ValueError(
119+
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key"
120+
)
121+
if not filesever_url_prefix:
122+
raise ValueError(
123+
"FILESERVER_URL_PREFIX is required to display file output from sandbox"
124+
)
125+
126+
interpreter = E2BCodeInterpreter(
127+
api_key=api_key, filesever_url_prefix=filesever_url_prefix
128+
)
129+
output = interpreter.interpret(code)
130+
return output.dict()
131+
132+
133+
# Specify as functions tools to be loaded by the ToolFactory
134+
tools = [FunctionTool.from_defaults(code_interpret)]

templates/components/engines/typescript/agent/tools/interpreter.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type InterpreterToolParams = {
1515
fileServerURLPrefix?: string;
1616
};
1717

18-
export type InterpreterToolOuput = {
18+
export type InterpreterToolOutput = {
1919
isError: boolean;
2020
logs: Logs;
2121
extraResult: InterpreterExtraResult[];
@@ -88,23 +88,23 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
8888
return this.codeInterpreter;
8989
}
9090

91-
public async codeInterpret(code: string): Promise<InterpreterToolOuput> {
91+
public async codeInterpret(code: string): Promise<InterpreterToolOutput> {
9292
console.log(
9393
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
9494
);
9595
const interpreter = await this.initInterpreter();
9696
const exec = await interpreter.notebook.execCell(code);
9797
if (exec.error) console.error("[Code Interpreter error]", exec.error);
9898
const extraResult = await this.getExtraResult(exec.results[0]);
99-
const result: InterpreterToolOuput = {
99+
const result: InterpreterToolOutput = {
100100
isError: !!exec.error,
101101
logs: exec.logs,
102102
extraResult,
103103
};
104104
return result;
105105
}
106106

107-
async call(input: InterpreterParameter): Promise<InterpreterToolOuput> {
107+
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
108108
const result = await this.codeInterpret(input.code);
109109
await this.codeInterpreter?.close();
110110
return result;

templates/types/streaming/express/src/controllers/stream-helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function appendSourceData(
4343
...node.node.toMutableJSON(),
4444
id: node.node.id_,
4545
score: node.score ?? null,
46+
url: getNodeUrl(node.node.metadata),
4647
})),
4748
},
4849
});

templates/types/streaming/fastapi/app/api/routers/chat.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import logging
13
from pydantic import BaseModel
24
from typing import List, Any, Optional, Dict, Tuple
35
from fastapi import APIRouter, Depends, HTTPException, Request, status
@@ -11,6 +13,7 @@
1113

1214
chat_router = r = APIRouter()
1315

16+
logger = logging.getLogger("uvicorn")
1417

1518
class _Message(BaseModel):
1619
role: MessageRole
@@ -38,14 +41,27 @@ class _SourceNodes(BaseModel):
3841
metadata: Dict[str, Any]
3942
score: Optional[float]
4043
text: str
44+
url: Optional[str]
4145

4246
@classmethod
4347
def from_source_node(cls, source_node: NodeWithScore):
48+
metadata = source_node.node.metadata
49+
url = metadata.get("URL")
50+
51+
if not url:
52+
file_name = metadata.get("file_name")
53+
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
54+
if not url_prefix:
55+
logger.warning("Warning: FILESERVER_URL_PREFIX not set in environment variables")
56+
if file_name and url_prefix:
57+
url = f"{url_prefix}/data/{file_name}"
58+
4459
return cls(
4560
id=source_node.node.node_id,
46-
metadata=source_node.node.metadata,
61+
metadata=metadata,
4762
score=source_node.score,
4863
text=source_node.node.text, # type: ignore
64+
url=url
4965
)
5066

5167
@classmethod
@@ -93,7 +109,13 @@ async def chat(
93109

94110
event_handler = EventCallbackHandler()
95111
chat_engine.callback_manager.handlers.append(event_handler) # type: ignore
96-
response = await chat_engine.astream_chat(last_message_content, messages)
112+
try:
113+
response = await chat_engine.astream_chat(last_message_content, messages)
114+
except Exception as e:
115+
raise HTTPException(
116+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
117+
detail=f"Error in chat engine: {e}",
118+
)
97119

98120
async def content_generator():
99121
# Yield the text response

templates/types/streaming/fastapi/app/api/routers/messaging.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import json
22
import asyncio
3+
import logging
34
from typing import AsyncGenerator, Dict, Any, List, Optional
45
from llama_index.core.callbacks.base import BaseCallbackHandler
56
from llama_index.core.callbacks.schema import CBEventType
67
from llama_index.core.tools.types import ToolOutput
78
from pydantic import BaseModel
89

910

11+
logger = logging.getLogger(__name__)
12+
13+
1014
class CallbackEvent(BaseModel):
1115
event_type: CBEventType
1216
payload: Optional[Dict[str, Any]] = None
@@ -72,15 +76,19 @@ def get_agent_tool_response(self) -> dict | None:
7276
}
7377

7478
def to_response(self):
75-
match self.event_type:
76-
case "retrieve":
77-
return self.get_retrieval_message()
78-
case "function_call":
79-
return self.get_tool_message()
80-
case "agent_step":
81-
return self.get_agent_tool_response()
82-
case _:
83-
return None
79+
try:
80+
match self.event_type:
81+
case "retrieve":
82+
return self.get_retrieval_message()
83+
case "function_call":
84+
return self.get_tool_message()
85+
case "agent_step":
86+
return self.get_agent_tool_response()
87+
case _:
88+
return None
89+
except Exception as e:
90+
logger.error(f"Error in converting event to response: {e}")
91+
return None
8492

8593

8694
class EventCallbackHandler(BaseCallbackHandler):

templates/types/streaming/fastapi/main.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@
3737
async def redirect_to_docs():
3838
return RedirectResponse(url="/docs")
3939

40-
if os.path.exists("data"):
41-
app.mount("/api/files/data", StaticFiles(directory="data"), name="data-static")
40+
41+
def mount_static_files(directory, path):
42+
if os.path.exists(directory):
43+
app.mount(path, StaticFiles(directory=directory), name=f"{directory}-static")
44+
45+
46+
# Mount the data files to serve the file viewer
47+
mount_static_files("data", "/api/files/data")
48+
# Mount the output files from tools
49+
mount_static_files("tool-output", "/api/files/tool-output")
50+
4251
app.include_router(chat_router, prefix="/api/chat")
4352

4453

templates/types/streaming/nextjs/app/api/chat/stream-helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function appendSourceData(
4343
...node.node.toMutableJSON(),
4444
id: node.node.id_,
4545
score: node.score ?? null,
46+
url: getNodeUrl(node.node.metadata),
4647
})),
4748
},
4849
});

0 commit comments

Comments
 (0)