From f902b402dec06d3a219a58a7f373458494ef5cb8 Mon Sep 17 00:00:00 2001 From: Zain Hasan Date: Tue, 29 Apr 2025 17:47:02 -0400 Subject: [PATCH 1/4] add file support for TCI - updated run method with files and validation - added tests for files - added a FileInput type for validation help --- src/together/resources/code_interpreter.py | 28 ++++- src/together/types/code_interpreter.py | 10 ++ tests/unit/test_code_interpreter.py | 116 +++++++++++++++++++++ 3 files changed, 151 insertions(+), 3 deletions(-) diff --git a/src/together/resources/code_interpreter.py b/src/together/resources/code_interpreter.py index c37a8343..58dedb81 100644 --- a/src/together/resources/code_interpreter.py +++ b/src/together/resources/code_interpreter.py @@ -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: @@ -19,16 +20,22 @@ 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, @@ -42,6 +49,21 @@ def run( 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( diff --git a/src/together/types/code_interpreter.py b/src/together/types/code_interpreter.py index 6f960f7c..f6a58579 100644 --- a/src/together/types/code_interpreter.py +++ b/src/together/types/code_interpreter.py @@ -6,6 +6,15 @@ 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.""" @@ -40,6 +49,7 @@ class ExecuteResponse(TogetherJSONModel): __all__ = [ + "FileInput", "InterpreterOutput", "ExecuteResponseData", "ExecuteResponse", diff --git a/tests/unit/test_code_interpreter.py b/tests/unit/test_code_interpreter.py index 525a2da0..31300742 100644 --- a/tests/unit/test_code_interpreter.py +++ b/tests/unit/test_code_interpreter.py @@ -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 @@ -326,3 +328,117 @@ 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, + ) From c5dbb69b7ecc1174134cb3d84e4c5d8e8e46841f Mon Sep 17 00:00:00 2001 From: Zain Hasan Date: Tue, 29 Apr 2025 18:34:25 -0400 Subject: [PATCH 2/4] add examples of file usage --- examples/code_interpreter_demo.py | 109 ++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py index ac4f705d..64c8529e 100644 --- a/examples/code_interpreter_demo.py +++ b/examples/code_interpreter_demo.py @@ -51,3 +51,112 @@ 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") From df096ffa56e2511e18aba3bfac32cbb8c474de21 Mon Sep 17 00:00:00 2001 From: Zain Hasan Date: Tue, 29 Apr 2025 18:45:46 -0400 Subject: [PATCH 3/4] small fixes --- examples/code_interpreter_demo.py | 12 ++++++++---- src/together/resources/code_interpreter.py | 6 ++++-- src/together/types/code_interpreter.py | 1 + tests/unit/test_code_interpreter.py | 20 +++++++++++++++----- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py index 64c8529e..6930c8eb 100644 --- a/examples/code_interpreter_demo.py +++ b/examples/code_interpreter_demo.py @@ -103,7 +103,7 @@ response = code_interpreter.run( code=code_to_run_script, language="python", - files=[script_file], # Pass the script dictionary in a list + files=[script_file], # Pass the script dictionary in a list ) # Print results @@ -124,12 +124,13 @@ image_file = { "name": "tiny.png", - "encoding": "base64", # Use base64 encoding for binary files + "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 = """ +code_to_check_file = ( + """ import os import base64 @@ -138,7 +139,9 @@ # Read the raw bytes back with open(file_path, 'rb') as f: raw_bytes = f.read() - original_bytes = base64.b64decode('""" + tiny_png_base64 + """') + 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.") @@ -146,6 +149,7 @@ else: print(f"File '{file_path}' does not exist.") """ +) response = code_interpreter.run( code=code_to_check_file, diff --git a/src/together/resources/code_interpreter.py b/src/together/resources/code_interpreter.py index 58dedb81..655543e9 100644 --- a/src/together/resources/code_interpreter.py +++ b/src/together/resources/code_interpreter.py @@ -41,7 +41,7 @@ def run( client=self._client, ) - data: Dict[str, str] = { + data: Dict[str, Any] = { "code": code, "language": language, } @@ -60,7 +60,9 @@ def run( 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 + raise ValueError( + f"Invalid file input: Each item in 'files' must be a dictionary. Error: {e}" + ) from e data["files"] = serialized_files diff --git a/src/together/types/code_interpreter.py b/src/together/types/code_interpreter.py index f6a58579..619be03a 100644 --- a/src/together/types/code_interpreter.py +++ b/src/together/types/code_interpreter.py @@ -6,6 +6,7 @@ from together.types.endpoints import TogetherJSONModel + class FileInput(TogetherJSONModel): """File input to be uploaded to the code interpreter session.""" diff --git a/tests/unit/test_code_interpreter.py b/tests/unit/test_code_interpreter.py index 31300742..19a1c48c 100644 --- a/tests/unit/test_code_interpreter.py +++ b/tests/unit/test_code_interpreter.py @@ -366,7 +366,7 @@ def test_code_interpreter_run_with_files(mocker): 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 + files=files_to_upload, # Pass the list of dictionaries directly ) # Verify the response @@ -394,13 +394,14 @@ def test_code_interpreter_run_with_files(mocker): "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' + {"name": "test.txt", "content": "Missing encoding"} # Missing 'encoding' ] with pytest.raises(ValueError, match="Invalid file input format"): @@ -410,13 +411,18 @@ def test_code_interpreter_run_with_invalid_file_dict_structure(mocker): 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 + { + "name": "test.txt", + "encoding": "utf-8", + "content": "Invalid encoding", + } # Invalid 'encoding' value ] with pytest.raises(ValueError, match="Invalid file input format"): @@ -426,6 +432,7 @@ def test_code_interpreter_run_with_invalid_file_dict_encoding(mocker): 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() @@ -433,10 +440,13 @@ def test_code_interpreter_run_with_invalid_file_list_item(mocker): invalid_files = [ {"name": "good.txt", "encoding": "string", "content": "Good"}, - "not a dictionary" # Invalid item type + "not a dictionary", # Invalid item type ] - with pytest.raises(ValueError, match="Invalid file input: Each item in 'files' must be a dictionary"): + with pytest.raises( + ValueError, + match="Invalid file input: Each item in 'files' must be a dictionary", + ): interpreter.run( code="print('test')", language="python", From f8476199cf5d1e32432e366743af39e696c94225 Mon Sep 17 00:00:00 2001 From: orangetin <126978607+orangetin@users.noreply.github.com> Date: Wed, 30 Apr 2025 08:54:08 -0700 Subject: [PATCH 4/4] bump version to v1.5.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 91d7b417..4b0f5ed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "together" -version = "1.5.5" +version = "1.5.6" authors = ["Together AI "] description = "Python client for Together's Cloud Platform!" readme = "README.md"