Skip to content

Commit 6121621

Browse files
zainhasorangetin
andauthored
Tci file support (#301)
* add file support for TCI - updated run method with files and validation - added tests for files - added a FileInput type for validation help * add examples of file usage * small fixes * bump version to v1.5.6 --------- Co-authored-by: orangetin <[email protected]>
1 parent 367d2bd commit 6121621

File tree

5 files changed

+279
-5
lines changed

5 files changed

+279
-5
lines changed

examples/code_interpreter_demo.py

+113
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,116 @@
5151
print(f"{output.type}: {output.data}")
5252
if response.data.errors:
5353
print(f"Errors: {response.data.errors}")
54+
55+
# Example 4: Uploading and using a file
56+
print("Example 4: Uploading and using a file")
57+
58+
# Define the file content and structure as a dictionary
59+
file_to_upload = {
60+
"name": "data.txt",
61+
"encoding": "string",
62+
"content": "This is the content of the uploaded file.",
63+
}
64+
65+
# Code to read the uploaded file
66+
code_to_read_file = """
67+
try:
68+
with open('data.txt', 'r') as f:
69+
content = f.read()
70+
print(f"Content read from data.txt: {content}")
71+
except FileNotFoundError:
72+
print("Error: data.txt not found.")
73+
"""
74+
75+
response = code_interpreter.run(
76+
code=code_to_read_file,
77+
language="python",
78+
files=[file_to_upload], # Pass the file dictionary in a list
79+
)
80+
81+
# Print results
82+
print(f"Status: {response.data.status}")
83+
for output in response.data.outputs:
84+
print(f"{output.type}: {output.data}")
85+
if response.data.errors:
86+
print(f"Errors: {response.data.errors}")
87+
print("\n")
88+
89+
# Example 5: Uploading a script and running it
90+
print("Example 5: Uploading a python script and running it")
91+
92+
script_content = "import sys\nprint(f'Hello from {sys.argv[0]}!')"
93+
94+
# Define the script file as a dictionary
95+
script_file = {
96+
"name": "myscript.py",
97+
"encoding": "string",
98+
"content": script_content,
99+
}
100+
101+
code_to_run_script = "!python myscript.py"
102+
103+
response = code_interpreter.run(
104+
code=code_to_run_script,
105+
language="python",
106+
files=[script_file], # Pass the script dictionary in a list
107+
)
108+
109+
# Print results
110+
print(f"Status: {response.data.status}")
111+
for output in response.data.outputs:
112+
print(f"{output.type}: {output.data}")
113+
if response.data.errors:
114+
print(f"Errors: {response.data.errors}")
115+
print("\n")
116+
117+
# Example 6: Uploading a base64 encoded image (simulated)
118+
119+
print("Example 6: Uploading a base64 encoded binary file (e.g., image)")
120+
121+
# Example: A tiny 1x1 black PNG image, base64 encoded
122+
# In a real scenario, you would read your binary file and base64 encode its content
123+
tiny_png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
124+
125+
image_file = {
126+
"name": "tiny.png",
127+
"encoding": "base64", # Use base64 encoding for binary files
128+
"content": tiny_png_base64,
129+
}
130+
131+
# Code to check if the file exists and its size (Python doesn't inherently know image dimensions from bytes alone)
132+
code_to_check_file = (
133+
"""
134+
import os
135+
import base64
136+
137+
file_path = 'tiny.png'
138+
if os.path.exists(file_path):
139+
# Read the raw bytes back
140+
with open(file_path, 'rb') as f:
141+
raw_bytes = f.read()
142+
original_bytes = base64.b64decode('"""
143+
+ tiny_png_base64
144+
+ """')
145+
print(f"File '{file_path}' exists.")
146+
print(f"Size on disk: {os.path.getsize(file_path)} bytes.")
147+
print(f"Size of original decoded base64 data: {len(original_bytes)} bytes.")
148+
149+
else:
150+
print(f"File '{file_path}' does not exist.")
151+
"""
152+
)
153+
154+
response = code_interpreter.run(
155+
code=code_to_check_file,
156+
language="python",
157+
files=[image_file],
158+
)
159+
160+
# Print results
161+
print(f"Status: {response.data.status}")
162+
for output in response.data.outputs:
163+
print(f"{output.type}: {output.data}")
164+
if response.data.errors:
165+
print(f"Errors: {response.data.errors}")
166+
print("\n")

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ build-backend = "poetry.masonry.api"
1212

1313
[tool.poetry]
1414
name = "together"
15-
version = "1.5.5"
15+
version = "1.5.6"
1616
authors = ["Together AI <[email protected]>"]
1717
description = "Python client for Together's Cloud Platform!"
1818
readme = "README.md"

src/together/resources/code_interpreter.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

3-
from typing import Dict, Literal, Optional
3+
from typing import Any, Dict, List, Literal, Optional
4+
from pydantic import ValidationError
45

56
from together.abstract import api_requestor
67
from together.together_response import TogetherResponse
78
from together.types import TogetherClient, TogetherRequest
8-
from together.types.code_interpreter import ExecuteResponse
9+
from together.types.code_interpreter import ExecuteResponse, FileInput
910

1011

1112
class CodeInterpreter:
@@ -19,29 +20,52 @@ def run(
1920
code: str,
2021
language: Literal["python"],
2122
session_id: Optional[str] = None,
23+
files: Optional[List[Dict[str, Any]]] = None,
2224
) -> ExecuteResponse:
23-
"""Execute a code snippet.
25+
"""Execute a code snippet, optionally with files.
2426
2527
Args:
2628
code (str): Code snippet to execute
2729
language (str): Programming language for the code to execute. Currently only supports Python.
2830
session_id (str, optional): Identifier of the current session. Used to make follow-up calls.
31+
files (List[Dict], optional): Files to upload to the session before executing the code.
2932
3033
Returns:
3134
ExecuteResponse: Object containing execution results and outputs
35+
36+
Raises:
37+
ValidationError: If any dictionary in the `files` list does not conform to the
38+
required structure or types.
3239
"""
3340
requestor = api_requestor.APIRequestor(
3441
client=self._client,
3542
)
3643

37-
data: Dict[str, str] = {
44+
data: Dict[str, Any] = {
3845
"code": code,
3946
"language": language,
4047
}
4148

4249
if session_id is not None:
4350
data["session_id"] = session_id
4451

52+
if files is not None:
53+
serialized_files = []
54+
try:
55+
for file_dict in files:
56+
# Validate the dictionary by creating a FileInput instance
57+
validated_file = FileInput(**file_dict)
58+
# Serialize the validated model back to a dict for the API call
59+
serialized_files.append(validated_file.model_dump())
60+
except ValidationError as e:
61+
raise ValueError(f"Invalid file input format: {e}") from e
62+
except TypeError as e:
63+
raise ValueError(
64+
f"Invalid file input: Each item in 'files' must be a dictionary. Error: {e}"
65+
) from e
66+
67+
data["files"] = serialized_files
68+
4569
# Use absolute URL to bypass the /v1 prefix
4670
response, _, _ = requestor.request(
4771
options=TogetherRequest(

src/together/types/code_interpreter.py

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
from together.types.endpoints import TogetherJSONModel
88

99

10+
class FileInput(TogetherJSONModel):
11+
"""File input to be uploaded to the code interpreter session."""
12+
13+
name: str = Field(description="The name of the file.")
14+
encoding: Literal["string", "base64"] = Field(
15+
description="Encoding of the file content. Use 'string' for text files and 'base64' for binary files."
16+
)
17+
content: str = Field(description="The content of the file, encoded as specified.")
18+
19+
1020
class InterpreterOutput(TogetherJSONModel):
1121
"""Base class for interpreter output types."""
1222

@@ -40,6 +50,7 @@ class ExecuteResponse(TogetherJSONModel):
4050

4151

4252
__all__ = [
53+
"FileInput",
4354
"InterpreterOutput",
4455
"ExecuteResponseData",
4556
"ExecuteResponse",

tests/unit/test_code_interpreter.py

+126
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import pytest
4+
from pydantic import ValidationError
35

46
from together.resources.code_interpreter import CodeInterpreter
57
from together.together_response import TogetherResponse
@@ -326,3 +328,127 @@ def test_code_interpreter_session_management(mocker):
326328

327329
# Second call should have session_id
328330
assert calls[1][1]["options"].params["session_id"] == "new_session"
331+
332+
333+
def test_code_interpreter_run_with_files(mocker):
334+
335+
mock_requestor = mocker.MagicMock()
336+
response_data = {
337+
"data": {
338+
"session_id": "test_session_files",
339+
"status": "success",
340+
"outputs": [{"type": "stdout", "data": "File content read"}],
341+
}
342+
}
343+
mock_headers = {
344+
"cf-ray": "test-ray-id-files",
345+
"x-ratelimit-remaining": "98",
346+
"x-hostname": "test-host",
347+
"x-total-time": "42.0",
348+
}
349+
mock_response = TogetherResponse(data=response_data, headers=mock_headers)
350+
mock_requestor.request.return_value = (mock_response, None, None)
351+
mocker.patch(
352+
"together.abstract.api_requestor.APIRequestor", return_value=mock_requestor
353+
)
354+
355+
# Create code interpreter instance
356+
client = mocker.MagicMock()
357+
interpreter = CodeInterpreter(client)
358+
359+
# Define files
360+
files_to_upload = [
361+
{"name": "test.txt", "encoding": "string", "content": "Hello from file!"},
362+
{"name": "image.png", "encoding": "base64", "content": "aW1hZ2UgZGF0YQ=="},
363+
]
364+
365+
# Test run method with files (passing list of dicts)
366+
response = interpreter.run(
367+
code='with open("test.txt") as f: print(f.read())',
368+
language="python",
369+
files=files_to_upload, # Pass the list of dictionaries directly
370+
)
371+
372+
# Verify the response
373+
assert isinstance(response, ExecuteResponse)
374+
assert response.data.session_id == "test_session_files"
375+
assert response.data.status == "success"
376+
assert len(response.data.outputs) == 1
377+
assert response.data.outputs[0].type == "stdout"
378+
379+
# Verify API request includes files (expected_files_payload remains the same)
380+
mock_requestor.request.assert_called_once_with(
381+
options=mocker.ANY,
382+
stream=False,
383+
)
384+
request_options = mock_requestor.request.call_args[1]["options"]
385+
assert request_options.method == "POST"
386+
assert request_options.url == "/tci/execute"
387+
expected_files_payload = [
388+
{"name": "test.txt", "encoding": "string", "content": "Hello from file!"},
389+
{"name": "image.png", "encoding": "base64", "content": "aW1hZ2UgZGF0YQ=="},
390+
]
391+
assert request_options.params == {
392+
"code": 'with open("test.txt") as f: print(f.read())',
393+
"language": "python",
394+
"files": expected_files_payload,
395+
}
396+
397+
398+
def test_code_interpreter_run_with_invalid_file_dict_structure(mocker):
399+
"""Test that run raises ValueError for missing keys in file dict."""
400+
client = mocker.MagicMock()
401+
interpreter = CodeInterpreter(client)
402+
403+
invalid_files = [
404+
{"name": "test.txt", "content": "Missing encoding"} # Missing 'encoding'
405+
]
406+
407+
with pytest.raises(ValueError, match="Invalid file input format"):
408+
interpreter.run(
409+
code="print('test')",
410+
language="python",
411+
files=invalid_files,
412+
)
413+
414+
415+
def test_code_interpreter_run_with_invalid_file_dict_encoding(mocker):
416+
"""Test that run raises ValueError for invalid encoding value."""
417+
client = mocker.MagicMock()
418+
interpreter = CodeInterpreter(client)
419+
420+
invalid_files = [
421+
{
422+
"name": "test.txt",
423+
"encoding": "utf-8",
424+
"content": "Invalid encoding",
425+
} # Invalid 'encoding' value
426+
]
427+
428+
with pytest.raises(ValueError, match="Invalid file input format"):
429+
interpreter.run(
430+
code="print('test')",
431+
language="python",
432+
files=invalid_files,
433+
)
434+
435+
436+
def test_code_interpreter_run_with_invalid_file_list_item(mocker):
437+
"""Test that run raises ValueError for non-dict item in files list."""
438+
client = mocker.MagicMock()
439+
interpreter = CodeInterpreter(client)
440+
441+
invalid_files = [
442+
{"name": "good.txt", "encoding": "string", "content": "Good"},
443+
"not a dictionary", # Invalid item type
444+
]
445+
446+
with pytest.raises(
447+
ValueError,
448+
match="Invalid file input: Each item in 'files' must be a dictionary",
449+
):
450+
interpreter.run(
451+
code="print('test')",
452+
language="python",
453+
files=invalid_files,
454+
)

0 commit comments

Comments
 (0)