diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py index ac4f705d..6930c8eb 100644 --- a/examples/code_interpreter_demo.py +++ b/examples/code_interpreter_demo.py @@ -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") 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" diff --git a/src/together/resources/code_interpreter.py b/src/together/resources/code_interpreter.py index c37a8343..655543e9 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,22 +20,28 @@ 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, } @@ -42,6 +49,23 @@ 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..619be03a 100644 --- a/src/together/types/code_interpreter.py +++ b/src/together/types/code_interpreter.py @@ -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.""" @@ -40,6 +50,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..19a1c48c 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,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, + )