diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py new file mode 100644 index 0000000..ac4f705 --- /dev/null +++ b/examples/code_interpreter_demo.py @@ -0,0 +1,53 @@ +from together import Together + +client = Together() + +# Create a code interpreter instance +code_interpreter = client.code_interpreter + +# Example 1: Simple print statement +print("Example 1: Simple print") +response = code_interpreter.run(code='print("Hello from Together!")', language="python") +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 2: Using session for maintaining state +print("Example 2: Using session for state") +response1 = code_interpreter.run(code="x = 42", language="python") +session_id = response1.data.session_id + +response2 = code_interpreter.run( + code='print(f"The value of x is {x}")', language="python", session_id=session_id +) +for output in response2.data.outputs: + print(f"{output.type}: {output.data}") +if response2.data.errors: + print(f"Errors: {response2.data.errors}") +print("\n") + +# Example 3: More complex computation +print("Example 3: Complex computation") +code = """ +!pip install numpy +import numpy as np + +# Create a random matrix +matrix = np.random.rand(3, 3) +print("Random matrix:") +print(matrix) + +# Calculate eigenvalues +eigenvalues = np.linalg.eigvals(matrix) +print("\\nEigenvalues:") +print(eigenvalues) +""" + +response = code_interpreter.run(code=code, language="python") +for output in response.data.outputs: + print(f"{output.type}: {output.data}") +if response.data.errors: + print(f"Errors: {response.data.errors}") diff --git a/poetry.lock b/poetry.lock index ec39219..d4585fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1669,6 +1669,24 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["tests"] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytest-watch" version = "4.2.0" @@ -2623,4 +2641,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "aa3f2327e6f33fcb7224b089f4319728cf800c8d71913e4851d817403a2484e4" +content-hash = "a5e8c66dcdc0cbb934e23aa5fad38b0790b8e83f1242d6d6c49538011f017e06" diff --git a/pyproject.toml b/pyproject.toml index 2b79f58..ea29d8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "together" version = "1.5.3" -authors = [ - "Together AI " -] +authors = ["Together AI "] description = "Python client for Together's Cloud Platform!" readme = "README.md" license = "Apache-2.0" @@ -65,6 +63,7 @@ optional = true [tool.poetry.group.tests.dependencies] pytest = ">=7.4.2,<9.0.0" pytest-watch = "^4.2.0" +pytest-mock = "^3.14.0" tox = "^4.14.1" [tool.poetry.group.examples] diff --git a/src/together/client.py b/src/together/client.py index cc86dc0..08d829d 100644 --- a/src/together/client.py +++ b/src/together/client.py @@ -7,6 +7,7 @@ from together import resources from together.constants import BASE_URL, MAX_RETRIES, TIMEOUT_SECS from together.error import AuthenticationError +from together.resources.code_interpreter import CodeInterpreter from together.types import TogetherClient from together.utils import enforce_trailing_slash from together.utils.api_helpers import get_google_colab_secret @@ -22,6 +23,7 @@ class Together: fine_tuning: resources.FineTuning rerank: resources.Rerank audio: resources.Audio + code_interpreter: CodeInterpreter # client options client: TogetherClient @@ -87,6 +89,7 @@ def __init__( self.rerank = resources.Rerank(self.client) self.audio = resources.Audio(self.client) self.endpoints = resources.Endpoints(self.client) + self.code_interpreter = CodeInterpreter(self.client) class AsyncTogether: @@ -98,6 +101,7 @@ class AsyncTogether: models: resources.AsyncModels fine_tuning: resources.AsyncFineTuning rerank: resources.AsyncRerank + code_interpreter: CodeInterpreter # client options client: TogetherClient @@ -161,6 +165,7 @@ def __init__( self.models = resources.AsyncModels(self.client) self.fine_tuning = resources.AsyncFineTuning(self.client) self.rerank = resources.AsyncRerank(self.client) + self.code_interpreter = CodeInterpreter(self.client) Client = Together diff --git a/src/together/resources/code_interpreter.py b/src/together/resources/code_interpreter.py new file mode 100644 index 0000000..c37a834 --- /dev/null +++ b/src/together/resources/code_interpreter.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Dict, Literal, Optional + +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 + + +class CodeInterpreter: + """Code Interpreter resource for executing code snippets.""" + + def __init__(self, client: TogetherClient) -> None: + self._client = client + + def run( + self, + code: str, + language: Literal["python"], + session_id: Optional[str] = None, + ) -> ExecuteResponse: + """Execute a code snippet. + + 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. + + Returns: + ExecuteResponse: Object containing execution results and outputs + """ + requestor = api_requestor.APIRequestor( + client=self._client, + ) + + data: Dict[str, str] = { + "code": code, + "language": language, + } + + if session_id is not None: + data["session_id"] = session_id + + # Use absolute URL to bypass the /v1 prefix + response, _, _ = requestor.request( + options=TogetherRequest( + method="POST", + url="/tci/execute", + params=data, + ), + stream=False, + ) + + assert isinstance(response, TogetherResponse) + + # Return the response data directly since our types match the API structure + return ExecuteResponse(**response.data) diff --git a/src/together/types/code_interpreter.py b/src/together/types/code_interpreter.py new file mode 100644 index 0000000..6f960f7 --- /dev/null +++ b/src/together/types/code_interpreter.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import Any, Dict, Literal, Union + +from pydantic import Field + +from together.types.endpoints import TogetherJSONModel + + +class InterpreterOutput(TogetherJSONModel): + """Base class for interpreter output types.""" + + type: Literal["stdout", "stderr", "error", "display_data", "execute_result"] = ( + Field(description="The type of output") + ) + data: Union[str, Dict[str, Any]] = Field(description="The output data") + + +class ExecuteResponseData(TogetherJSONModel): + """Data from code execution response.""" + + outputs: list[InterpreterOutput] = Field( + description="List of outputs from execution", default_factory=list + ) + errors: Union[str, None] = Field( + description="Any errors that occurred during execution", default=None + ) + session_id: str = Field( + description="Identifier of the current session. Used to make follow-up calls." + ) + status: str = Field(description="Status of the execution", default="completed") + + +class ExecuteResponse(TogetherJSONModel): + """Response from code execution.""" + + data: ExecuteResponseData = Field( + description="The response data containing outputs and session information" + ) + + +__all__ = [ + "InterpreterOutput", + "ExecuteResponseData", + "ExecuteResponse", +] diff --git a/tests/unit/test_code_interpreter.py b/tests/unit/test_code_interpreter.py new file mode 100644 index 0000000..525a2da --- /dev/null +++ b/tests/unit/test_code_interpreter.py @@ -0,0 +1,328 @@ +from __future__ import annotations + + +from together.resources.code_interpreter import CodeInterpreter +from together.together_response import TogetherResponse +from together.types.code_interpreter import ( + ExecuteResponse, + ExecuteResponseData, + InterpreterOutput, +) + + +def test_interpreter_output_validation(): + # Test stdout output + stdout = InterpreterOutput(type="stdout", data="Hello, world!") + assert stdout.type == "stdout" + assert stdout.data == "Hello, world!" + + # Test stderr output + stderr = InterpreterOutput(type="stderr", data="Warning message") + assert stderr.type == "stderr" + assert stderr.data == "Warning message" + + # Test error output + error = InterpreterOutput(type="error", data="Error occurred") + assert error.type == "error" + assert error.data == "Error occurred" + + # Test display_data output with dict data + display_data = InterpreterOutput( + type="display_data", + data={ + "text/plain": "Hello", + "text/html": "

Hello

", + }, + ) + assert display_data.type == "display_data" + assert isinstance(display_data.data, dict) + assert display_data.data.get("text/plain") == "Hello" + assert display_data.data.get("text/html") == "

Hello

" + + # Test execute_result output + execute_result = InterpreterOutput(type="execute_result", data="42") + assert execute_result.type == "execute_result" + assert execute_result.data == "42" + + +def test_execute_response_validation(): + # Test valid response + outputs = [ + InterpreterOutput(type="stdout", data="Hello"), + InterpreterOutput(type="stderr", data="Warning"), + ] + response = ExecuteResponse( + data=ExecuteResponseData( + session_id="test_session", + status="success", + outputs=outputs, + ) + ) + assert response.data.session_id == "test_session" + assert response.data.status == "success" + assert len(response.data.outputs) == 2 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[1].type == "stderr" + + +def test_code_interpreter_run(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "Hello, world!"}], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "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) + + # Test run method + response = interpreter.run( + code='print("Hello, world!")', + language="python", + session_id="test_session", + ) + + # Verify the response + assert isinstance(response, ExecuteResponse) + assert response.data.session_id == "test_session" + assert response.data.status == "success" + assert len(response.data.outputs) == 1 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[0].data == "Hello, world!" + + # Verify API request + 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" + assert request_options.params == { + "code": 'print("Hello, world!")', + "language": "python", + "session_id": "test_session", + } + + +def test_code_interpreter_run_without_session(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [], + } + } + mock_headers = { + "cf-ray": "test-ray-id-2", + "x-ratelimit-remaining": "99", + "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) + + # Test run method without session_id + response = interpreter.run( + code="x = 1", + language="python", + ) + + # Verify the response + assert isinstance(response, ExecuteResponse) + assert response.data.session_id == "new_session" + + # Verify API request doesn't include session_id + request_options = mock_requestor.request.call_args[1]["options"] + assert request_options.params == { + "code": "x = 1", + "language": "python", + } + + +def test_code_interpreter_error_handling(mocker): + # Mock the API requestor to simulate an error + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "error", + "outputs": [{"type": "error", "data": "Division by zero"}], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "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) + + # Test run method with code that would cause an error + response = interpreter.run( + code="1/0", # This will cause a division by zero error + language="python", + session_id="test_session", + ) + + # Verify the error response + assert isinstance(response, ExecuteResponse) + assert response.data.status == "error" + assert len(response.data.outputs) == 1 + assert response.data.outputs[0].type == "error" + assert "Division by zero" in response.data.outputs[0].data + + +def test_code_interpreter_multiple_outputs(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "success", + "outputs": [ + {"type": "stdout", "data": "First line"}, + {"type": "stderr", "data": "Warning message"}, + {"type": "execute_result", "data": "42"}, + ], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "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) + + # Test run method with code that produces multiple outputs + response = interpreter.run( + code='print("First line")\nimport sys\nsys.stderr.write("Warning message")\n42', + language="python", + session_id="test_session", + ) + + # Verify the response with multiple outputs + assert isinstance(response, ExecuteResponse) + assert response.data.status == "success" + assert len(response.data.outputs) == 3 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[1].type == "stderr" + assert response.data.outputs[2].type == "execute_result" + + +def test_code_interpreter_session_management(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + + # First response - create new session + response_data1 = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "First execution"}], + } + } + + # Second response - use existing session + response_data2 = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "Second execution"}], + } + } + + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + + mock_response1 = TogetherResponse(data=response_data1, headers=mock_headers) + mock_response2 = TogetherResponse(data=response_data2, headers=mock_headers) + mock_requestor.request.side_effect = [ + (mock_response1, None, None), + (mock_response2, None, None), + ] + + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # First execution - no session ID + response1 = interpreter.run( + code='print("First execution")', + language="python", + ) + + # Second execution - using session ID from first execution + response2 = interpreter.run( + code='print("Second execution")', + language="python", + session_id=response1.data.session_id, + ) + + # Verify both responses + assert response1.data.session_id == "new_session" + assert response2.data.session_id == "new_session" + assert len(response1.data.outputs) == 1 + assert len(response2.data.outputs) == 1 + assert response1.data.outputs[0].data == "First execution" + assert response2.data.outputs[0].data == "Second execution" + + # Verify API calls + assert mock_requestor.request.call_count == 2 + calls = mock_requestor.request.call_args_list + + # First call should not have session_id + assert "session_id" not in calls[0][1]["options"].params + + # Second call should have session_id + assert calls[1][1]["options"].params["session_id"] == "new_session"