Skip to content

Commit 8adb7a3

Browse files
committed
Replace codeinterpreterapi with PythonREPL and update models to gpt-5.4
The interpreter tool previously relied on codeinterpreterapi (remote sandbox) which is no longer compatible with the current dependency stack. This replaces it with langchain_experimental PythonREPL (local execution) while preserving the same agent contract. Key changes: - tool_interpreter.py: rewrite using PythonREPL — injects file previews (columns, dtypes, 3-row sample) into the LLM prompt so generated code uses exact column names; truncates REPL stdout at 2000 chars to prevent context overflow; wires shared llm_instance from the agent instead of instantiating a separate model - agent.py: passes llm_instance to tool constructor via import_tools introspection - params.ini: bump default OpenAI models from gpt-4o / gpt-4o-mini to gpt-5.4 / gpt-5.4-mini - requirements.txt / environment.yml: pin langchain_experimental==0.3.4 and remove codeinterpreterapi / codeboxapi dependencies - Remove custom_sqlite_file.py (unused after langgraph checkpoint refactor) Security note: PythonREPL executes LLM-generated code in-process without sandboxing. A warning comment has been added; restrict to trusted deployment environments.
1 parent 085c973 commit 8adb7a3

7 files changed

Lines changed: 139 additions & 209 deletions

File tree

app/config/params.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
[llm_preview]
2-
id = gpt-4o
2+
id = gpt-5.4
33
temperature = 0.3
44
max_retries = 3
55

66
[llm_o]
7-
id = gpt-4o
7+
id = gpt-5.4
88
temperature = 0
99
max_retries = 3
1010

1111

1212
[llm_mini]
13-
id = gpt-4o-mini
13+
id = gpt-5.4-mini
1414
temperature = 0
1515
max_retries = 3
1616

@@ -48,7 +48,7 @@ max_retries = 3
4848
base_url = https://llama-3-1-70b-instruct.endpoints.kepler.ai.cloud.ovh.net/api/openai_compat/v1
4949

5050
[llm_litellm_openai]
51-
id = gpt-4o
51+
id = gpt-5.4
5252
temperature = 0
5353

5454

app/core/agents/enpkg/tool_taxon.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
from pydantic import BaseModel, Field
77

8-
from typing import Optional
9-
108
from langchain.callbacks.manager import (
119
CallbackManagerForToolRun,
1210
)

app/core/agents/interpreter/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def create_agent(llms, graph, openai_key, session_id) -> AgentExecutor:
1818
tool_parameters = {
1919
"openai_key": openai_key,
2020
"session_id": session_id,
21+
"llm_instance": llms.get(MODEL_CHOICE),
2122
}
2223

2324
tools = import_tools(directory, module_prefix, **tool_parameters)
Lines changed: 132 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
from __future__ import annotations
22

3+
# SECURITY WARNING: This tool uses langchain_experimental PythonREPL, which executes
4+
# LLM-generated Python code directly in the server process with no sandboxing.
5+
# Only deploy in trusted environments where the server host is not exposed to
6+
# untrusted users. Do not use in public-facing deployments without additional
7+
# isolation (e.g. Docker, restricted OS user, network policy).
38

4-
from codeinterpreterapi import CodeInterpreterSession, File, settings
5-
from pydantic import BaseModel, Field
6-
from langchain.tools import BaseTool
7-
8-
from typing import Optional
9-
10-
from langchain.callbacks.manager import (
11-
CallbackManagerForToolRun,
12-
)
139
import json
14-
from app.core.session import setup_logger, create_user_session
15-
1610
import os
1711
import re
18-
import tempfile
1912
from pathlib import Path
13+
from typing import Any, Optional
14+
15+
from langchain.callbacks.manager import CallbackManagerForToolRun
16+
from langchain.tools import BaseTool
17+
from langchain_core.messages import HumanMessage, SystemMessage
18+
from langchain_experimental.utilities import PythonREPL
19+
from langchain_openai import ChatOpenAI
20+
from pydantic import BaseModel, Field
21+
2022
from app.core.memory.database_manager import tools_database
23+
from app.core.session import create_user_session, setup_logger
2124

2225
logger = setup_logger(__name__)
2326

2427

2528
class InterpreterInput(BaseModel):
26-
input: str = Field(description="Input from Interpreter Agent containing the user's question, necessary file paths and other information.")
29+
input: str = Field(
30+
description="Input from Interpreter Agent containing the user's question, necessary file paths and other information."
31+
)
32+
2733

2834
class Interpreter(BaseTool):
2935

@@ -38,111 +44,135 @@ class Interpreter(BaseTool):
3844
None: Outputs the response after interpreting files.
3945
"""
4046
args_schema: type[BaseModel] = InterpreterInput
41-
openai_key: str = None
42-
session_id: str = None
47+
openai_key: Optional[str] = None
48+
session_id: Optional[str] = None
49+
llm_instance: Optional[Any] = None
4350

44-
def __init__(self, openai_key: str, session_id: str):
51+
def __init__(self, openai_key: Optional[str] = None, session_id: Optional[str] = None, llm_instance: Optional[Any] = None):
4552
super().__init__()
46-
self.openai_key = openai_key
53+
self.openai_key = openai_key or os.getenv("OPENAI_API_KEY")
4754
self.session_id = session_id
55+
self.llm_instance = llm_instance
4856

4957
def _run(
5058
self,
5159
input: str,
5260
run_manager: Optional[CallbackManagerForToolRun] = None,
53-
) -> None:
54-
61+
) -> str:
5562
logger.info(f"Input: {input}")
5663

5764
file_paths = self.extract_file_paths(input)
5865
logger.info(f"File paths: {file_paths}")
5966

6067
session_dir = create_user_session(self.session_id, user_session_dir=True)
61-
62-
settings.OPENAI_API_KEY = self.openai_key
63-
settings.MODEL = "gpt-3.5-turbo"
64-
65-
with CodeInterpreterSession() as session:
66-
67-
db_manager = tools_database()
68-
69-
user_request = (
70-
"You are an interpreter helping to analyze different questions, files and outputs generated from a series of LLMs."
71-
f"The details of the current request: {input}"
72-
"Please interpret the current request to generate a meaningful answer."
73-
"Here's some instructions that you have to follow for acheiving the task:"
74-
"1. For any file provided, analyse if and provide clear and brief information about it unless something else is asked."
75-
"2. Only if a specific visualization (e.g., bar chart, diagram) is requested in the question, use the provided information to generate a .json file containing the JSON code for a Plotly graph. This file should be named identically to the analyzed file."
76-
"3. After you finish your tasks, your answer should contain both the interpretation asked and the full visualization file name if visualization was requested."
77-
)
78-
79-
files = []
80-
81-
for file in file_paths:
82-
files.append(File.from_path(file))
83-
logger.info(f"File added to interpreter Agent: {file}")
84-
85-
if not files:
86-
87-
logger.info("No files provided from the Interpreter Agent. Manually scrapping the database")
88-
output_merged = db_manager.get("tool_merge_result") # Get all file paths from the database associated with the tool_interpreter
89-
if output_merged is not None:
90-
output_merged_json = json.loads(output_merged)
91-
merged_filepaths = output_merged_json['output']['paths']
92-
93-
for merged_filepath in merged_filepaths:
94-
logger.info(f"File added to interpreter Agent from the Output Merged tool: {merged_filepath}")
95-
files.append(File.from_path(merged_filepath))
96-
97-
if not files:
98-
99-
sparql_output = db_manager.get("tool_sparql") # Get all file paths from the database associated with the tool_interpreter
100-
if sparql_output is not None:
101-
sparql_output_json = json.loads(sparql_output)
102-
sparql_filepaths = sparql_output_json['output']['paths']
103-
104-
for sparql_filepath in sparql_filepaths:
105-
logger.info(f"File added to interpreter Agent from the SPARQL tool: {sparql_filepath}")
106-
files.append(File.from_path(sparql_filepath))
107-
108-
# generate the response
109-
logger.info(f"Files submitted: {files}")
110-
response = session.generate_response(user_request, files=files)
111-
logger.info(f"Interpreter Agent Response: {response}")
112-
113-
filepaths = []
114-
115-
# Handling and saving output files
116-
if response.files:
117-
for file in response.files:
118-
logger.info(f"File: {file.name}")
119-
120-
generated_file_path = session_dir / file.name
121-
with open(generated_file_path, 'wb') as f:
122-
f.write(file.content)
123-
filepaths.append(str(generated_file_path))
124-
125-
logger.info(f"File saved: {file.name}")
126-
else:
127-
logger.info("No files generated by Interpreter Tool.")
128-
129-
output_data = {
130-
"output": {
131-
"paths": filepaths
132-
}
133-
}
134-
135-
68+
db_manager = tools_database()
69+
70+
if not file_paths:
71+
output_merged = db_manager.get("tool_merge_result")
72+
if output_merged is not None:
73+
merged_json = json.loads(output_merged)
74+
file_paths = merged_json.get("output", {}).get("paths", [])
75+
logger.info(f"File paths from merge result: {file_paths}")
76+
77+
if not file_paths:
78+
sparql_output = db_manager.get("tool_sparql")
79+
if sparql_output is not None:
80+
sparql_json = json.loads(sparql_output)
81+
file_paths = sparql_json.get("output", {}).get("paths", [])
82+
logger.info(f"File paths from SPARQL tool: {file_paths}")
83+
84+
llm = self.llm_instance or ChatOpenAI(
85+
api_key=self.openai_key,
86+
model="gpt-5.4",
87+
temperature=0,
88+
)
89+
90+
system_prompt = (
91+
"You are a Python data analysis assistant.\n"
92+
"Write a self-contained Python script that reads the provided file(s), "
93+
"fulfils the user's request, and prints a concise text summary to stdout.\n\n"
94+
"Rules:\n"
95+
"- pandas, numpy, json, pathlib, and plotly are available.\n"
96+
"- Use the exact column names shown in the file previews — do not guess them.\n"
97+
"- Print a SHORT summary (counts, stats, top-N rows) — never print an entire dataframe.\n"
98+
f"- Save any output files (visualizations, processed data) to: {session_dir}\n"
99+
"- For visualizations use Plotly and write JSON with: "
100+
f'fig.write_json(str(Path("{session_dir}") / "<same-stem-as-input>.json"))\n'
101+
"- Do NOT call plt.show(), fig.show(), or any interactive display.\n"
102+
"- Do NOT install packages inside the script.\n"
103+
"- Return ONLY the Python code inside a single ```python ... ``` block."
104+
)
105+
106+
file_previews = []
107+
for fp in file_paths:
136108
try:
137-
db_manager.put(data=json.dumps(output_data), tool_name="tool_interpreter")
138-
except Exception as e:
139-
logger.error(f"Error saving to database: {e}")
140-
141-
return f"{response}.\n\n The full path of the files generated are:\n" + "\n".join(filepaths)
109+
import pandas as pd
110+
sep = "\t" if str(fp).endswith((".tsv", ".txt")) else ","
111+
preview = pd.read_csv(fp, sep=sep, nrows=3)
112+
# Truncate long cell values so wide/URL-heavy columns don't blow context
113+
preview = preview.map(
114+
lambda v: str(v)[:80] + "…" if isinstance(v, str) and len(str(v)) > 80 else v
115+
)
116+
file_previews.append(
117+
f"File: {fp}\n"
118+
f"Columns: {preview.columns.tolist()}\n"
119+
f"Dtypes: {preview.dtypes.to_dict()}\n"
120+
f"Preview (3 rows):\n{preview.to_string(index=False)}"
121+
)
122+
except Exception:
123+
file_previews.append(f"File: {fp} (could not preview)")
124+
125+
preview_block = "\n\n".join(file_previews) if file_previews else "(no files)"
126+
user_message = f"File previews:\n{preview_block}\n\nRequest: {input}"
127+
128+
logger.info("Requesting analysis code from LLM")
129+
response = llm.invoke(
130+
[SystemMessage(content=system_prompt), HumanMessage(content=user_message)]
131+
)
132+
133+
code_blocks = re.findall(r"```python\s*(.*?)\s*```", response.content, re.DOTALL)
134+
if not code_blocks:
135+
code_blocks = re.findall(r"```\s*(.*?)\s*```", response.content, re.DOTALL)
136+
137+
if not code_blocks:
138+
logger.error("LLM did not return a Python code block")
139+
return "Interpreter could not generate analysis code for this request."
140+
141+
code = code_blocks[0]
142+
logger.info(f"Generated code:\n{code}")
143+
144+
# Snapshot existing files so we can detect what was newly created
145+
existing_files = set(session_dir.iterdir()) if session_dir.exists() else set()
146+
147+
repl = PythonREPL()
148+
execution_output = repl.run(code)
149+
# Truncate to keep the agent context manageable (~2000 chars ≈ ~500 tokens)
150+
_MAX_OUTPUT = 2000
151+
if len(execution_output) > _MAX_OUTPUT:
152+
execution_output = execution_output[:_MAX_OUTPUT] + f"\n… [output truncated at {_MAX_OUTPUT} chars]"
153+
logger.info(f"Execution output: {execution_output}")
154+
155+
# Collect files written by the executed code
156+
new_files = (
157+
set(session_dir.iterdir()) - existing_files if session_dir.exists() else set()
158+
)
159+
filepaths = [str(f) for f in new_files]
160+
logger.info(f"Files generated: {filepaths}")
161+
162+
output_data = {"output": {"paths": filepaths}}
163+
try:
164+
db_manager.put(data=json.dumps(output_data), tool_name="tool_interpreter")
165+
except Exception as e:
166+
logger.error(f"Error saving to database: {e}")
167+
168+
suffix = (
169+
"\n\nThe full path of the files generated are:\n" + "\n".join(filepaths)
170+
if filepaths
171+
else ""
172+
)
173+
return f"{execution_output}{suffix}"
142174

143175
def extract_file_paths(self, text: str):
144-
# Regex to find file paths or filenames with extensions, possibly surrounded by quotes
145176
regex = r"['\"]?([a-zA-Z0-9_/\\-]+(?:\.csv|\.tsv|\.mgf|\.txt|\.xlsx|\.xls))['\"]?"
146177
matches = re.finditer(regex, text)
147-
file_paths = [match.group(1).replace("'", "").replace('"', '') for match in matches]
148-
return file_paths
178+
return [match.group(1).replace("'", "").replace('"', "") for match in matches]

0 commit comments

Comments
 (0)