Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions examples/code_interpreter_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,116 @@
print(f"{output.type}: {output.data}")
if response.data.errors:
print(f"Errors: {response.data.errors}")

# Example 4: Uploading and using a file
print("Example 4: Uploading and using a file")

# Define the file content and structure as a dictionary
file_to_upload = {
"name": "data.txt",
"encoding": "string",
"content": "This is the content of the uploaded file.",
}

# Code to read the uploaded file
code_to_read_file = """
try:
with open('data.txt', 'r') as f:
content = f.read()
print(f"Content read from data.txt: {content}")
except FileNotFoundError:
print("Error: data.txt not found.")
"""

response = code_interpreter.run(
code=code_to_read_file,
language="python",
files=[file_to_upload], # Pass the file dictionary in a list
)

# Print results
print(f"Status: {response.data.status}")
for output in response.data.outputs:
print(f"{output.type}: {output.data}")
if response.data.errors:
print(f"Errors: {response.data.errors}")
print("\n")

# Example 5: Uploading a script and running it
print("Example 5: Uploading a python script and running it")

script_content = "import sys\nprint(f'Hello from {sys.argv[0]}!')"

# Define the script file as a dictionary
script_file = {
"name": "myscript.py",
"encoding": "string",
"content": script_content,
}

code_to_run_script = "!python myscript.py"

response = code_interpreter.run(
code=code_to_run_script,
language="python",
files=[script_file], # Pass the script dictionary in a list
)

# Print results
print(f"Status: {response.data.status}")
for output in response.data.outputs:
print(f"{output.type}: {output.data}")
if response.data.errors:
print(f"Errors: {response.data.errors}")
print("\n")

# Example 6: Uploading a base64 encoded image (simulated)

print("Example 6: Uploading a base64 encoded binary file (e.g., image)")

# Example: A tiny 1x1 black PNG image, base64 encoded
# In a real scenario, you would read your binary file and base64 encode its content
tiny_png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="

image_file = {
"name": "tiny.png",
"encoding": "base64", # Use base64 encoding for binary files
"content": tiny_png_base64,
}

# Code to check if the file exists and its size (Python doesn't inherently know image dimensions from bytes alone)
code_to_check_file = (
"""
import os
import base64

file_path = 'tiny.png'
if os.path.exists(file_path):
# Read the raw bytes back
with open(file_path, 'rb') as f:
raw_bytes = f.read()
original_bytes = base64.b64decode('"""
+ tiny_png_base64
+ """')
print(f"File '{file_path}' exists.")
print(f"Size on disk: {os.path.getsize(file_path)} bytes.")
print(f"Size of original decoded base64 data: {len(original_bytes)} bytes.")

else:
print(f"File '{file_path}' does not exist.")
"""
)

response = code_interpreter.run(
code=code_to_check_file,
language="python",
files=[image_file],
)

# Print results
print(f"Status: {response.data.status}")
for output in response.data.outputs:
print(f"{output.type}: {output.data}")
if response.data.errors:
print(f"Errors: {response.data.errors}")
print("\n")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ build-backend = "poetry.masonry.api"

[tool.poetry]
name = "together"
version = "1.5.5"
version = "1.5.6"
authors = ["Together AI <[email protected]>"]
description = "Python client for Together's Cloud Platform!"
readme = "README.md"
Expand Down
32 changes: 28 additions & 4 deletions src/together/resources/code_interpreter.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations

from typing import Dict, Literal, Optional
from typing import Any, Dict, List, Literal, Optional
from pydantic import ValidationError

from together.abstract import api_requestor
from together.together_response import TogetherResponse
from together.types import TogetherClient, TogetherRequest
from together.types.code_interpreter import ExecuteResponse
from together.types.code_interpreter import ExecuteResponse, FileInput


class CodeInterpreter:
Expand All @@ -19,29 +20,52 @@ def run(
code: str,
language: Literal["python"],
session_id: Optional[str] = None,
files: Optional[List[Dict[str, Any]]] = None,
) -> ExecuteResponse:
"""Execute a code snippet.
"""Execute a code snippet, optionally with files.

Args:
code (str): Code snippet to execute
language (str): Programming language for the code to execute. Currently only supports Python.
session_id (str, optional): Identifier of the current session. Used to make follow-up calls.
files (List[Dict], optional): Files to upload to the session before executing the code.

Returns:
ExecuteResponse: Object containing execution results and outputs

Raises:
ValidationError: If any dictionary in the `files` list does not conform to the
required structure or types.
"""
requestor = api_requestor.APIRequestor(
client=self._client,
)

data: Dict[str, str] = {
data: Dict[str, Any] = {
"code": code,
"language": language,
}

if session_id is not None:
data["session_id"] = session_id

if files is not None:
serialized_files = []
try:
for file_dict in files:
# Validate the dictionary by creating a FileInput instance
validated_file = FileInput(**file_dict)
# Serialize the validated model back to a dict for the API call
serialized_files.append(validated_file.model_dump())
except ValidationError as e:
raise ValueError(f"Invalid file input format: {e}") from e
except TypeError as e:
raise ValueError(
f"Invalid file input: Each item in 'files' must be a dictionary. Error: {e}"
) from e

data["files"] = serialized_files

# Use absolute URL to bypass the /v1 prefix
response, _, _ = requestor.request(
options=TogetherRequest(
Expand Down
11 changes: 11 additions & 0 deletions src/together/types/code_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
from together.types.endpoints import TogetherJSONModel


class FileInput(TogetherJSONModel):
"""File input to be uploaded to the code interpreter session."""

name: str = Field(description="The name of the file.")
encoding: Literal["string", "base64"] = Field(
description="Encoding of the file content. Use 'string' for text files and 'base64' for binary files."
)
content: str = Field(description="The content of the file, encoded as specified.")


class InterpreterOutput(TogetherJSONModel):
"""Base class for interpreter output types."""

Expand Down Expand Up @@ -40,6 +50,7 @@ class ExecuteResponse(TogetherJSONModel):


__all__ = [
"FileInput",
"InterpreterOutput",
"ExecuteResponseData",
"ExecuteResponse",
Expand Down
126 changes: 126 additions & 0 deletions tests/unit/test_code_interpreter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest
from pydantic import ValidationError

from together.resources.code_interpreter import CodeInterpreter
from together.together_response import TogetherResponse
Expand Down Expand Up @@ -326,3 +328,127 @@ def test_code_interpreter_session_management(mocker):

# Second call should have session_id
assert calls[1][1]["options"].params["session_id"] == "new_session"


def test_code_interpreter_run_with_files(mocker):

mock_requestor = mocker.MagicMock()
response_data = {
"data": {
"session_id": "test_session_files",
"status": "success",
"outputs": [{"type": "stdout", "data": "File content read"}],
}
}
mock_headers = {
"cf-ray": "test-ray-id-files",
"x-ratelimit-remaining": "98",
"x-hostname": "test-host",
"x-total-time": "42.0",
}
mock_response = TogetherResponse(data=response_data, headers=mock_headers)
mock_requestor.request.return_value = (mock_response, None, None)
mocker.patch(
"together.abstract.api_requestor.APIRequestor", return_value=mock_requestor
)

# Create code interpreter instance
client = mocker.MagicMock()
interpreter = CodeInterpreter(client)

# Define files
files_to_upload = [
{"name": "test.txt", "encoding": "string", "content": "Hello from file!"},
{"name": "image.png", "encoding": "base64", "content": "aW1hZ2UgZGF0YQ=="},
]

# Test run method with files (passing list of dicts)
response = interpreter.run(
code='with open("test.txt") as f: print(f.read())',
language="python",
files=files_to_upload, # Pass the list of dictionaries directly
)

# Verify the response
assert isinstance(response, ExecuteResponse)
assert response.data.session_id == "test_session_files"
assert response.data.status == "success"
assert len(response.data.outputs) == 1
assert response.data.outputs[0].type == "stdout"

# Verify API request includes files (expected_files_payload remains the same)
mock_requestor.request.assert_called_once_with(
options=mocker.ANY,
stream=False,
)
request_options = mock_requestor.request.call_args[1]["options"]
assert request_options.method == "POST"
assert request_options.url == "/tci/execute"
expected_files_payload = [
{"name": "test.txt", "encoding": "string", "content": "Hello from file!"},
{"name": "image.png", "encoding": "base64", "content": "aW1hZ2UgZGF0YQ=="},
]
assert request_options.params == {
"code": 'with open("test.txt") as f: print(f.read())',
"language": "python",
"files": expected_files_payload,
}


def test_code_interpreter_run_with_invalid_file_dict_structure(mocker):
"""Test that run raises ValueError for missing keys in file dict."""
client = mocker.MagicMock()
interpreter = CodeInterpreter(client)

invalid_files = [
{"name": "test.txt", "content": "Missing encoding"} # Missing 'encoding'
]

with pytest.raises(ValueError, match="Invalid file input format"):
interpreter.run(
code="print('test')",
language="python",
files=invalid_files,
)


def test_code_interpreter_run_with_invalid_file_dict_encoding(mocker):
"""Test that run raises ValueError for invalid encoding value."""
client = mocker.MagicMock()
interpreter = CodeInterpreter(client)

invalid_files = [
{
"name": "test.txt",
"encoding": "utf-8",
"content": "Invalid encoding",
} # Invalid 'encoding' value
]

with pytest.raises(ValueError, match="Invalid file input format"):
interpreter.run(
code="print('test')",
language="python",
files=invalid_files,
)


def test_code_interpreter_run_with_invalid_file_list_item(mocker):
"""Test that run raises ValueError for non-dict item in files list."""
client = mocker.MagicMock()
interpreter = CodeInterpreter(client)

invalid_files = [
{"name": "good.txt", "encoding": "string", "content": "Good"},
"not a dictionary", # Invalid item type
]

with pytest.raises(
ValueError,
match="Invalid file input: Each item in 'files' must be a dictionary",
):
interpreter.run(
code="print('test')",
language="python",
files=invalid_files,
)