By API call, I can attach multiple files to Assistants’ code interpreter when creating or modifying…
The success then seen in the Playground without borking out errors:
I can then trash can a file there, removing its attachment.
That local Python utility, created for testing this symptom? Available to you here.
Requires:
- Python (newer, like 3.11+)
tkinter (standard library with typical desktop install)
openai module
- local environment OPENAI_API_KEY or others scraped by openai lib.
- save as
assistant_maker.pyw, where pyw extension suppresses a console and all the deprecation warnings.
import os
import threading
from dataclasses import dataclass, field
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import openai
SUPPORTED_EXTENSIONS: set[str] = {
".c",
".cpp",
".cs",
".css",
".csv",
".doc",
".docx",
".gif",
".go",
".html",
".java",
".jpeg",
".jpg",
".js",
".json",
".md",
".pdf",
".php",
".pkl",
".png",
".pptx",
".py",
".rb",
".tar",
".tex",
".ts",
".txt",
".webp",
".xlsx",
".xml",
".zip",
}
@dataclass
class FileRecord:
local_path: str
filename: str
openai_file_id: str | None = None
status: str = "pending" # pending, uploading, uploaded, error, unsupported, deleted
attached_current: bool = False
attached_assistant_ids: set[str] = field(default_factory=set)
last_error: str | None = None
class AssistantBuilderApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("OpenAI Assistant Builder")
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("OPENAI_API_KEY environment variable is not set.")
self.client = openai.OpenAI(api_key=api_key, timeout=60.0, max_retries=1)
self.current_assistant_id: str | None = None
self.file_records: dict[str, FileRecord] = {}
self.path_index: dict[str, str] = {}
self._build_ui()
def _build_ui(self) -> None:
top = ttk.Frame(self.root, padding=10)
top.pack(side=tk.TOP, fill=tk.X)
ttk.Label(top, text="Assistant ID:").pack(side=tk.LEFT)
self.assistant_id_var = tk.StringVar(value="(new)")
self.assistant_id_label = ttk.Label(top, textvariable=self.assistant_id_var, width=40)
self.assistant_id_label.pack(side=tk.LEFT, padx=(4, 10))
self.new_button = ttk.Button(top, text="New Assistant", command=self.new_assistant)
self.new_button.pack(side=tk.LEFT, padx=5)
self.delete_button = ttk.Button(top, text="Delete Assistant", command=self.delete_current_assistant)
self.delete_button.pack(side=tk.LEFT, padx=5)
main_pane = ttk.Panedwindow(self.root, orient=tk.HORIZONTAL)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
assistant_frame = ttk.Labelframe(main_pane, text="Assistant Parameters", padding=10)
files_frame = ttk.Labelframe(main_pane, text="Files for Code Interpreter", padding=10)
main_pane.add(assistant_frame, weight=1)
main_pane.add(files_frame, weight=2)
# Assistant parameters
row = 0
ttk.Label(assistant_frame, text="Name:").grid(row=row, column=0, sticky="w")
self.name_var = tk.StringVar()
ttk.Entry(assistant_frame, textvariable=self.name_var).grid(row=row, column=1, sticky="ew")
row += 1
ttk.Label(assistant_frame, text="Model:").grid(row=row, column=0, sticky="w")
self.model_var = tk.StringVar(value="gpt-4.1")
ttk.Entry(assistant_frame, textvariable=self.model_var).grid(row=row, column=1, sticky="ew")
row += 1
ttk.Label(assistant_frame, text="Temperature:").grid(row=row, column=0, sticky="w")
self.temperature_var = tk.StringVar(value="0.5")
ttk.Entry(assistant_frame, textvariable=self.temperature_var, width=8).grid(
row=row,
column=1,
sticky="w",
)
row += 1
ttk.Label(assistant_frame, text="Top P:").grid(row=row, column=0, sticky="w")
self.top_p_var = tk.StringVar(value="0.2")
ttk.Entry(assistant_frame, textvariable=self.top_p_var, width=8).grid(
row=row,
column=1,
sticky="w",
)
row += 1
ttk.Label(assistant_frame, text="Metadata (JSON object):").grid(row=row, column=0, sticky="nw")
self.metadata_text = tk.Text(assistant_frame, height=4, width=40, wrap="word")
self.metadata_text.grid(row=row, column=1, sticky="nsew", pady=(0, 5))
row += 1
ttk.Label(assistant_frame, text="Instructions:").grid(row=row, column=0, sticky="nw")
self.instructions_text = tk.Text(assistant_frame, height=8, width=40, wrap="word")
self.instructions_text.grid(row=row, column=1, sticky="nsew")
row += 1
ttk.Label(
assistant_frame,
text="Mapping:",
).grid(row=row, column=0, sticky="nw")
auto_frame = ttk.Frame(assistant_frame)
auto_frame.grid(row=row, column=1, sticky="nsew", pady=(2, 0))
self.auto_instructions_text = tk.Text(
auto_frame,
height=8,
width=40,
wrap="word",
state="disabled",
)
auto_scrollbar = ttk.Scrollbar(
auto_frame,
orient="vertical",
command=self.auto_instructions_text.yview,
)
self.auto_instructions_text.configure(yscrollcommand=auto_scrollbar.set)
self.auto_instructions_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
auto_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
row += 1
tools_frame = ttk.Frame(assistant_frame)
tools_frame.grid(row=row, column=1, sticky="w", pady=(5, 0))
self.code_interpreter_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
tools_frame,
text="Enable Code Interpreter",
variable=self.code_interpreter_var,
command=self._refresh_auto_instructions_preview,
).pack(side=tk.LEFT)
row += 1
self.create_update_button = ttk.Button(
assistant_frame,
text="Create Assistant",
command=self.create_or_update_assistant,
)
self.create_update_button.grid(row=row, column=1, sticky="e", pady=(10, 0))
assistant_frame.columnconfigure(1, weight=1)
# Let metadata, instructions, and auto-mapping areas grow with the window
assistant_frame.rowconfigure(4, weight=1) # metadata
assistant_frame.rowconfigure(5, weight=1) # instructions
assistant_frame.rowconfigure(6, weight=1) # auto mapping
# Files frame
btns = ttk.Frame(files_frame)
btns.pack(fill=tk.X)
ttk.Button(btns, text="Add Files…", command=self.add_files).pack(side=tk.LEFT)
ttk.Button(btns, text="Attach/Detach Selected", command=self.toggle_attach_selected).pack(
side=tk.LEFT,
padx=(5, 0),
)
ttk.Button(
btns,
text="Delete Selected From Server",
command=self.delete_selected_files_from_server,
).pack(side=tk.LEFT, padx=(5, 0))
ttk.Button(
btns,
text="Remove From List",
command=self.remove_selected_from_list,
).pack(side=tk.LEFT, padx=(5, 0))
columns = ("filename", "status", "file_id", "attached", "assistants")
self.tree = ttk.Treeview(
files_frame,
columns=columns,
show="headings",
selectmode="extended",
height=12,
)
self.tree.heading("filename", text="Filename")
self.tree.heading("status", text="Status")
self.tree.heading("file_id", text="File ID")
self.tree.heading("attached", text="Attached (current)")
self.tree.heading("assistants", text="Attached to Assistants")
self.tree.column("filename", width=220, anchor="w")
self.tree.column("status", width=80, anchor="w")
self.tree.column("file_id", width=260, anchor="w")
self.tree.column("attached", width=110, anchor="center")
self.tree.column("assistants", width=180, anchor="w")
self.tree.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
self.tree.bind("<Double-1>", self.on_tree_double_click)
# Log frame
log_frame = ttk.Labelframe(self.root, text="Log", padding=5)
log_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=(0, 10))
self.log_text = tk.Text(log_frame, height=8, wrap="word")
self.log_text.pack(fill=tk.BOTH, expand=True)
# Initial preview (blank)
self._refresh_auto_instructions_preview()
self.log("Ready. Select files and define assistant parameters.")
def log(self, message: str) -> None:
self.log_text.insert("end", message + "\n")
self.log_text.see("end")
def new_assistant(self) -> None:
self.current_assistant_id = None
self.assistant_id_var.set("(new)")
self.name_var.set("")
self.model_var.set("gpt-4.1")
self.temperature_var.set("0.1")
self.top_p_var.set("1.0")
self.metadata_text.delete("1.0", "end")
self.instructions_text.delete("1.0", "end")
self.code_interpreter_var.set(True)
for item_id, record in self.file_records.items():
record.attached_current = False
self.update_tree_row(item_id)
self.create_update_button.configure(text="Create Assistant")
self._refresh_auto_instructions_preview()
self.log("Started a new assistant configuration.")
def delete_current_assistant(self) -> None:
if not self.current_assistant_id:
messagebox.showinfo("Delete Assistant", "No assistant is currently loaded.")
return
if not messagebox.askyesno(
"Delete Assistant",
f"Delete assistant {self.current_assistant_id} from OpenAI?",
):
return
assistant_id = self.current_assistant_id
def worker() -> None:
try:
self.client.beta.assistants.delete(assistant_id)
def on_success() -> None:
self.log(f"Deleted assistant {assistant_id}.")
if self.current_assistant_id == assistant_id:
self.current_assistant_id = None
self.assistant_id_var.set("(new)")
self.create_update_button.configure(text="Create Assistant")
self.root.after(0, on_success)
except Exception as e: # noqa: BLE001
error_text = str(e)
def on_error() -> None:
self.log(f"Error deleting assistant {assistant_id}:\n{error_text}")
messagebox.showerror("Delete Assistant Error", error_text)
self.root.after(0, on_error)
threading.Thread(target=worker, daemon=True).start()
def _update_supported_extensions_from_error(self, error_text: str) -> None:
import re
idx = error_text.find("Supported formats:")
if idx == -1:
return
supported_part = error_text[idx:]
# Extract quoted extensions like "py", "json", etc.
exts = re.findall(r'"([a-zA-Z0-9]+)"', supported_part)
if not exts:
return
new_set = {f".{ext.lower()}" for ext in exts}
if not new_set:
return
global SUPPORTED_EXTENSIONS
if new_set != SUPPORTED_EXTENSIONS:
SUPPORTED_EXTENSIONS = new_set
self.log(
"Updated supported extensions from API error: "
+ ", ".join(sorted(SUPPORTED_EXTENSIONS))
)
def add_files(self) -> None:
patterns = " ".join(f"*{ext}" for ext in sorted(SUPPORTED_EXTENSIONS))
filetypes = [
("Supported assistant file types", patterns),
("All files", "*.*"),
]
paths = filedialog.askopenfilenames(
title="Select files for assistants",
filetypes=filetypes,
)
if not paths:
return
unsupported_names: list[str] = []
for path in paths:
if not path:
continue
if path in self.path_index:
self.log(f"File already in list: {path}")
continue
filename = os.path.basename(path)
ext = os.path.splitext(filename)[1].lower()
record = FileRecord(local_path=path, filename=filename)
# Pre-filter unsupported extensions so we don't even attempt upload
if not ext or ext not in SUPPORTED_EXTENSIONS:
record.status = "unsupported"
record.attached_current = False
else:
# NEW: default to attached for supported files
record.attached_current = True
attached_value = "Yes" if record.attached_current else "No"
item_id = self.tree.insert(
"",
"end",
values=(record.filename, record.status, "", attached_value, ""),
)
self.file_records[item_id] = record
self.path_index[path] = item_id
if record.status == "unsupported":
unsupported_names.append(record.filename)
self.log(f"Unsupported file type (not uploaded): {record.filename}")
else:
# Supported: begin upload; once complete, mapping text will be updated
self.upload_file(item_id)
if unsupported_names:
msg = (
"These files have extensions that are not supported for "
"'assistants' uploads and will not be uploaded:\n\n"
+ "\n".join(f"- {name}" for name in unsupported_names)
)
messagebox.showwarning("Unsupported File Types", msg)
def upload_file(self, item_id: str) -> None:
record = self.file_records.get(item_id)
if record is None:
return
if record.status in ("uploading", "uploaded", "unsupported"):
return
record.status = "uploading"
self.update_tree_row(item_id)
def worker() -> None:
try:
with open(record.local_path, "rb") as f:
uploaded = self.client.files.create(file=f, purpose="assistants")
file_id = uploaded.id
def on_success() -> None:
record.openai_file_id = file_id
record.status = "uploaded"
record.last_error = None
self.update_tree_row(item_id)
self.log(f"Uploaded {record.filename} -> {file_id}")
self._refresh_auto_instructions_preview()
self.root.after(0, on_success)
except Exception as e: # noqa: BLE001
error_text = str(e)
def on_error() -> None:
invalid_ext = (
"Invalid extension" in error_text
and "Supported formats:" in error_text
)
if invalid_ext:
self._update_supported_extensions_from_error(error_text)
record.status = "unsupported"
else:
record.status = "error"
record.last_error = error_text
self.update_tree_row(item_id)
self.log(f"Error uploading {record.filename}:\n{error_text}")
# Avoid dialog storms for extension issues; only pop for other errors
if not invalid_ext:
messagebox.showerror("File Upload Error", error_text)
self._refresh_auto_instructions_preview()
self.root.after(0, on_error)
threading.Thread(target=worker, daemon=True).start()
def update_tree_row(self, item_id: str) -> None:
record = self.file_records.get(item_id)
if record is None:
return
attached = "Yes" if record.attached_current else "No"
assistants = ", ".join(sorted(record.attached_assistant_ids)) if record.attached_assistant_ids else ""
self.tree.item(
item_id,
values=(
record.filename,
record.status,
record.openai_file_id or "",
attached,
assistants,
),
)
def on_tree_double_click(self, event: tk.Event) -> None: # type: ignore[override]
item_id = self.tree.identify_row(event.y)
if not item_id:
return
record = self.file_records.get(item_id)
if record is None:
return
if record.status != "uploaded" or not record.openai_file_id:
self.log(f"File not yet uploaded; cannot attach: {record.filename}")
return
record.attached_current = not record.attached_current
self.update_tree_row(item_id)
self._refresh_auto_instructions_preview()
def toggle_attach_selected(self) -> None:
selected = self.tree.selection()
if not selected:
return
for item_id in selected:
record = self.file_records.get(item_id)
if record is None:
continue
if record.status != "uploaded" or not record.openai_file_id:
self.log(
f"Cannot attach file until it has been uploaded successfully: {record.filename}"
)
continue
record.attached_current = not record.attached_current
self.update_tree_row(item_id)
self._refresh_auto_instructions_preview()
def delete_selected_files_from_server(self) -> None:
selected = self.tree.selection()
if not selected:
return
for item_id in list(selected):
record = self.file_records.get(item_id)
if record is None:
continue
if not record.openai_file_id:
messagebox.showinfo(
"No Remote File",
f"\"{record.filename}\" has not been uploaded to the server.\n"
"Use 'Remove From List' to remove it from this table.",
)
continue
if record.attached_assistant_ids:
ids_str = ", ".join(sorted(record.attached_assistant_ids))
msg = (
f"This file is attached to assistant(s): {ids_str}.\n"
"Deleting it will remove it for those assistants as well.\n\n"
"Are you sure you want to delete it from the server?"
)
else:
msg = (
f"Delete remote file {record.openai_file_id} for {record.filename}?\n\n"
"This cannot be undone."
)
if not messagebox.askyesno("Delete File", msg):
continue
self._delete_remote_file(item_id, record)
def _delete_remote_file(self, item_id: str, record: FileRecord) -> None:
file_id = record.openai_file_id
if not file_id:
self._remove_file_record(item_id, record)
return
def worker() -> None:
try:
self.client.files.delete(file_id)
def on_success() -> None:
self.log(f"Deleted remote file {file_id} ({record.filename}).")
record.status = "deleted"
self._remove_file_record(item_id, record)
self.root.after(0, on_success)
except Exception as e: # noqa: BLE001
error_text = str(e)
def on_error() -> None:
self.log(
f"Error deleting remote file {file_id} ({record.filename}):\n{error_text}"
)
messagebox.showerror("Delete File Error", error_text)
self.root.after(0, on_error)
threading.Thread(target=worker, daemon=True).start()
def remove_selected_from_list(self) -> None:
selected = self.tree.selection()
if not selected:
return
for item_id in list(selected):
record = self.file_records.get(item_id)
if record is None:
continue
self.log(
f"Removed from list (remote file preserved if it was uploaded): "
f"{record.filename}"
)
self._remove_file_record(item_id, record)
def _remove_file_record(self, item_id: str, record: FileRecord) -> None:
self.tree.delete(item_id)
self.file_records.pop(item_id, None)
self.path_index.pop(record.local_path, None)
self._refresh_auto_instructions_preview()
def _parse_assistant_fields(self) -> tuple[str | None, str, str, float, float, dict]:
name = self.name_var.get().strip() or None
model = self.model_var.get().strip() or "gpt-4.1"
temp_str = self.temperature_var.get().strip()
top_p_str = self.top_p_var.get().strip()
try:
temperature = float(temp_str) if temp_str else 0.1
except ValueError:
temperature = 0.1
self.temperature_var.set(str(temperature))
self.log("Invalid temperature value; reset to 0.1.")
try:
top_p = float(top_p_str) if top_p_str else 1.0
except ValueError:
top_p = 1.0
self.top_p_var.set(str(top_p))
self.log("Invalid top_p value; reset to 1.0.")
import json
metadata_text = self.metadata_text.get("1.0", "end").strip()
metadata: dict = {}
if metadata_text:
try:
parsed = json.loads(metadata_text)
if not isinstance(parsed, dict):
raise ValueError("Metadata JSON must be an object (mapping).")
metadata = parsed
except Exception as e: # noqa: BLE001
messagebox.showerror("Metadata Error", f"Could not parse metadata JSON:\n{e}")
raise
instructions = self.instructions_text.get("1.0", "end").strip()
return name, instructions, model, temperature, top_p, metadata
def _collect_attached_file_ids(self) -> set[str]:
file_ids: set[str] = set()
for record in self.file_records.values():
if record.attached_current and record.status == "uploaded" and record.openai_file_id:
file_ids.add(record.openai_file_id)
return file_ids
def _build_file_mapping_instructions(self, file_ids: set[str]) -> str:
if not file_ids:
return ""
rows: list[tuple[str, str]] = []
for record in self.file_records.values():
if record.openai_file_id and record.openai_file_id in file_ids:
rows.append((record.openai_file_id, record.filename))
if not rows:
return ""
rows.sort(key=lambda pair: pair[1].lower())
parts: list[str] = []
parts.append(
"\n\n---\n\n"
"These file names have been initially uploaded to your mount point at `/mnt/data`, "
"but have been uploaded with an ID in the format `file-xxx`. "
"Here is a map to the original file name for your understanding:\n\n"
)
parts.append("| uploaded file ID | original file name |\n")
parts.append("| --- | --- |\n")
for file_id, filename in rows:
parts.append(f"| `{file_id}` | `{filename}` |\n")
return "".join(parts)
def _refresh_auto_instructions_preview(self) -> None:
if not hasattr(self, "auto_instructions_text"):
return
if not self.code_interpreter_var.get():
mapping_text = ""
else:
file_ids = self._collect_attached_file_ids()
mapping_text = self._build_file_mapping_instructions(file_ids)
self.auto_instructions_text.configure(state="normal")
self.auto_instructions_text.delete("1.0", "end")
if mapping_text:
# Show only the appended section; strip leading blank lines for readability
self.auto_instructions_text.insert("1.0", mapping_text.lstrip())
self.auto_instructions_text.configure(state="disabled")
def create_or_update_assistant(self) -> None:
try:
name, instructions, model, temperature, top_p, metadata = self._parse_assistant_fields()
except Exception:
return
tools: list[dict] = []
tool_resources: dict = {}
if self.code_interpreter_var.get():
tools.append({"type": "code_interpreter"})
file_ids_set = self._collect_attached_file_ids()
file_ids = list(file_ids_set)
if file_ids:
tool_resources["code_interpreter"] = {"file_ids": file_ids}
else:
file_ids_set = set()
file_ids = []
mapping_suffix = ""
if file_ids_set:
mapping_suffix = self._build_file_mapping_instructions(file_ids_set)
# Keep preview in sync with whatever we'll actually send
self._refresh_auto_instructions_preview()
full_instructions = instructions + mapping_suffix if mapping_suffix else instructions
if not full_instructions:
if not messagebox.askyesno(
"No Instructions",
"No instructions text has been provided. Continue anyway?",
):
return
kwargs: dict = {
"instructions": full_instructions or None,
"model": model,
"tools": tools,
"temperature": temperature,
"top_p": top_p,
}
if name:
kwargs["name"] = name
if metadata:
kwargs["metadata"] = metadata
if tool_resources:
kwargs["tool_resources"] = tool_resources
is_create = self.current_assistant_id is None
if is_create:
action_label = "create"
else:
action_label = "update"
self.log(
f"Submitting assistant {action_label} request with model={model}, "
f"{len(file_ids)} attached file(s)."
)
def worker() -> None:
try:
if is_create:
assistant = self.client.beta.assistants.create(**kwargs)
else:
assistant = self.client.beta.assistants.update(
self.current_assistant_id,
**kwargs,
)
def on_success() -> None:
self.current_assistant_id = assistant.id
self.assistant_id_var.set(assistant.id)
self.create_update_button.configure(text="Update Assistant")
returned_file_ids: set[str] = set()
tool_resources_obj = getattr(assistant, "tool_resources", None)
if tool_resources_obj is not None:
code_interp = getattr(tool_resources_obj, "code_interpreter", None)
if code_interp is not None:
returned_ids = getattr(code_interp, "file_ids", None)
if returned_ids:
returned_file_ids = set(returned_ids)
missing = file_ids_set - returned_file_ids
if missing:
self.log(
"Warning: some file IDs were not present in the assistant's "
f"code interpreter tool resources: {', '.join(sorted(missing))}"
)
else:
self.log(
f"Assistant {assistant.id} {action_label}d successfully "
f"with {len(returned_file_ids)} attached file(s)."
)
for item_id, record in self.file_records.items():
if record.openai_file_id and record.openai_file_id in returned_file_ids:
record.attached_assistant_ids.add(assistant.id)
record.attached_current = True
elif record.attached_current:
record.attached_current = False
self.update_tree_row(item_id)
self._refresh_auto_instructions_preview()
self.root.after(0, on_success)
except Exception as e: # noqa: BLE001
error_text = str(e)
def on_error() -> None:
self.log(
f"Error during assistant {action_label}:\n{error_text}"
)
messagebox.showerror(
"Assistant Error",
f"Assistant {action_label} failed:\n{error_text}",
)
self.root.after(0, on_error)
threading.Thread(target=worker, daemon=True).start()
def main() -> None:
root = tk.Tk()
root.geometry("1400x900")
app = AssistantBuilderApp(root)
root.mainloop()
if __name__ == "__main__":
main()
You’ll see it does a function that is almost mandatory for Assistants and code interpreter to actually “work”, when the mount point only gets IDs, and the AI has no knowledge of what’s available without listing with code - it provides the original file names to the AI.