Repository: i-am-bee/beeai-framework
Pull request #583: Add code interpreter
Author: login: akihikokuroda
Labels: name: python
description: Python related functionality
name: data-analysis:collaboration
description: Used for data analysis: was worked on collaboratively over video calls
Created: 2025-03-13T15:07:27Z | Merged: 2025-03-25T17:25:34Z

<!--
Thank you for your pull request. Please review and complete the sections below.
-->

### Which issue(s) does this pull-request address?

<!--
Please include a link to an issue in the tracker.  The issue describes the problem to be solved.  If there is no issue raised for this PR then either raise one with a summary and description of the problem or add a summary and description of the problem here
-->

Ref: #423 

### Description

<!-- Provide a description of the change, pay special attention to describing any breaking changes.  The description describes the resolution to the problem described in the linked issue (or to the problem outlined in this PR). -->
This is still in progress.  
- The input schema definition and sending the request to the interpreter are working.  
- Sending the files seems working but not sure if it is doing the same thing as typescript ones.
- Handing of the result from the interpreter is not done yet. 
  
### Checklist

#### General
- [x] I have read the appropriate contributor guide: [Python](https://github.com/i-am-bee/beeai-framework/blob/main/CONTRIBUTING.md)
/ [TypeScript](https://github.com/i-am-bee/beeai-framework/blob/main/CONTRIBUTING.md)
- [x] I have signed off on my commit: [Python instructions](https://github.com/i-am-bee/beeai-framework/blob/main/python/CONTRIBUTING.md#developer-certificate-of-origin-dco) / [TypeScript instructions](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/CONTRIBUTING.md#developer-certificate-of-origin-dco)
- [x] Commit messages and PR title follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- [x] Appropriate label(s) added to PR: `Python` for Python changes, `TypeScript` for TypeScript changes

#### Code quality checks
- [x] Linting passes: Python `poe lint` or `poe lint --fix` / TypeScript `yarn lint` or `yarn lint:fix`
- [x] Formatting is applied: Python `poe format` or `poe format --fix` / TypeScript: `yarn format` or `yarn format:fix`
- [x] Static type checks pass: Python `poe type-check`

#### Testing
- [x] Unit tests pass: Python `poe test --type unit` / TypeScript `yarn test:unit`
- [x] E2E tests pass: Python `poe test --type e2e` / TypeScript: `yarn test:e2e`
- [ ] Integration tests pass: Python `poe test --type integration`
- [x] Tests are included (for bug fixes or new features)

#### Documentation
- [ ] Documentation is updated
- [ ] Embedme embeds code examples in docs. To update after edits, run: Python `poe docs --type build`



Diff:
diff --git a/python/beeai_framework/tools/code/__init__.py b/python/beeai_framework/tools/code/__init__.py
new file mode 100644
index 000000000..b9d2143e5
--- /dev/null
+++ b/python/beeai_framework/tools/code/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from beeai_framework.tools.code.python import PythonTool
+from beeai_framework.tools.code.storage import LocalPythonStorage, PythonStorage
+
+__all__ = ["LocalPythonStorage", "PythonStorage", "PythonTool"]
diff --git a/python/beeai_framework/tools/code/output.py b/python/beeai_framework/tools/code/output.py
new file mode 100644
index 000000000..42742dd63
--- /dev/null
+++ b/python/beeai_framework/tools/code/output.py
@@ -0,0 +1,61 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from beeai_framework.tools import ToolOutput
+from beeai_framework.tools.code.storage import PythonFile
+
+
+class PythonToolOutput(ToolOutput):
+    FILE_PREFIX = "urn:bee:file"
+
+    def __init__(self, stdout: str, stderr: str, exit_code: int, output_files: list[PythonFile]) -> None:
+        super().__init__()
+
+        self.stdout = stdout
+        self.stderr = stderr
+        self.exit_code = exit_code
+        self.output_files = output_files
+
+    def is_empty(self) -> bool:
+        return False
+
+    def get_text_content(self) -> str:
+        execution_status = (
+            "The code executed successfully."
+            if self.exit_code == 0
+            else f"The code exited with error code {self.exit_code}."
+        )
+        stdout = f"Standard output:\n\n```\n{self.stdout}\n```" if self.stdout and self.stdout.strip() else None
+        stderr = f"Error output:\n\n```\n{self.stderr}\n```" if self.stderr and self.stderr.strip() else None
+
+        def is_image(filename: str) -> bool:
+            return any(filename.lower().endswith(ext) for ext in [".png", ".jpg", ".jpeg", ".gif", ".bmp"])
+
+        files = (
+            "The following files were created or modified. The user does not see them yet. \
+            To present a file to the user, send them the link below, verbatim:\n"
+            + "\n".join(
+                f"{'!' if is_image(file.filename) else ''}[ {file.filename} ]\
+                ({PythonToolOutput.FILE_PREFIX}:{file.id})"
+                for file in self.output_files
+            )
+            if self.output_files
+            else None
+        )
+        message = ""
+        for x in [execution_status, stdout, stderr, files]:
+            if bool(x):
+                message = message + str(x) + "\n"
+        return message
diff --git a/python/beeai_framework/tools/code/python.py b/python/beeai_framework/tools/code/python.py
new file mode 100644
index 000000000..a95765424
--- /dev/null
+++ b/python/beeai_framework/tools/code/python.py
@@ -0,0 +1,158 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, InstanceOf
+
+from beeai_framework import UserMessage
+from beeai_framework.backend.chat import ChatModel
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate
+from beeai_framework.tools import ToolInputValidationError
+from beeai_framework.tools.code.output import PythonToolOutput
+from beeai_framework.tools.code.storage import PythonFile, PythonStorage
+from beeai_framework.tools.tool import Tool
+from beeai_framework.tools.types import ToolRunOptions
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonToolTemplate(BaseModel):
+    input: str
+
+
+class PreProcess(BaseModel):
+    llm: InstanceOf[ChatModel]
+    prompt_template: InstanceOf[PromptTemplate[PythonToolTemplate]]
+
+
+class PythonToolInput(BaseModel):
+    language: Language = Field(description="Use shell for ffmpeg, pandoc, yt-dlp")
+    code: str = Field(description="full source code file that will be executed")
+    input_files: list[str] = Field(
+        description="""To access an existing file, you must specify it;
+otherwise, the file will not be accessible.
+IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+    )
+
+
+class PythonTool(Tool[PythonToolInput, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = PythonToolInput
+    description = """
+Run Python and/or shell code and return the console output. Use for isolated calculations,
+computations, data or file manipulation but still prefer assistant's capabilities
+(IMPORTANT: Do not use for text analysis or summarization).
+Files provided by the user, or created in a previous run, will be accessible
+ if and only if they are specified in the input. It is necessary to always print() results.
+The following shell commands are available:
+Use ffmpeg to convert videos.
+Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+ for video and `-x --audio-format mp3` for audio-only.
+Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+ you can create PDFs by writing markdown and then converting.
+In Python, the following modules are available:
+Use numpy, pandas, scipy and sympy for working with data.
+Use matplotlib to plot charts.
+Use pillow (import PIL) to create and manipulate images.
+Use moviepy for complex manipulations with videos.
+Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+Use pdf2image to convert PDF to images.
+Other Python libraries are also available -- however, prefer using the ones above.
+Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+Do not attempt to install libraries manually -- it will not work.
+Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, code_interpreter_url: str, storage: PythonStorage, preprocess: PreProcess | None = None) -> None:
+        super().__init__()
+        self._code_interpreter_url = code_interpreter_url
+        self._storage = storage
+        self._preprocess = preprocess
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    @staticmethod
+    async def _call_code_interpreter(url: str, body: dict[str, Any]) -> Any:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+        async with httpx.AsyncClient() as client:
+            response = await client.post(url, headers=headers, json=body)
+            response.raise_for_status()
+            return response.json()
+
+    async def _run(
+        self, tool_input: PythonToolInput, options: ToolRunOptions | None, context: RunContext
+    ) -> PythonToolOutput:
+        async def get_source_code() -> str:
+            if self._preprocess:
+                response = await self._preprocess.llm.create(
+                    messages=[
+                        UserMessage(self._preprocess.prompt_template.render(PythonToolTemplate(input=tool_input.code)))
+                    ],
+                    abort_signal=context.signal,
+                )
+                return response.get_text_content()
+            return tool_input.code
+
+        execute_url = self._code_interpreter_url + "/v1/execute"
+        prefix = "/workspace/"
+
+        files = await self._storage.list_files()
+
+        valid_filenames = [f.filename for f in files]
+        for input_file in tool_input.input_files:
+            if input_file not in valid_filenames:
+                raise ToolInputValidationError(
+                    f"Provided file does not exist: {input_file}\n"
+                    + f"Accessible files include: {', '.join(valid_filenames)}"
+                )
+
+        unique_files: list[PythonFile] = [file for file in files if file.filename in tool_input.input_files]
+        await self._storage.upload(unique_files)
+
+        files_dict = {prefix + file.filename: file.python_id for file in unique_files}
+
+        result = await self._call_code_interpreter(
+            url=execute_url,
+            body={
+                "source_code": await get_source_code(),
+                "files": files_dict,
+            },
+        )
+
+        files_output: list[PythonFile] = []
+        if result["files"]:
+            for file_path, python_id in result["files"].items():
+                if file_path.startswith(prefix):
+                    filename = file_path.removeprefix(prefix)
+                    if all(filename != f.filename or python_id != f.python_id for f in unique_files):
+                        files_output.append(PythonFile(filename=filename, id=python_id, python_id=python_id))
+
+            await self._storage.download(files_output)
+
+        return PythonToolOutput(result["stdout"], result["stderr"], result["exit_code"], files_output)
diff --git a/python/beeai_framework/tools/code/storage.py b/python/beeai_framework/tools/code/storage.py
new file mode 100644
index 000000000..0460603ff
--- /dev/null
+++ b/python/beeai_framework/tools/code/storage.py
@@ -0,0 +1,119 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import ABC, abstractmethod
+
+from pydantic import BaseModel
+
+
+class PythonFile(BaseModel):
+    id: str
+    python_id: str
+    filename: str
+
+
+class PythonStorage(ABC):
+    """
+    Abstract class for managing files in Python code interpreter.
+    """
+
+    @abstractmethod
+    async def list_files(self) -> list[PythonFile]:
+        """
+        List all files that code interpreter can use.
+        """
+        pass
+
+    @abstractmethod
+    async def upload(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Prepare subset of available files to code interpreter.
+        """
+        pass
+
+    @abstractmethod
+    async def download(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Process updated/modified/deleted files from code interpreter response.
+        """
+        pass
+
+
+class LocalPythonStorage(PythonStorage):
+    def __init__(
+        self, *, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str] | None = None
+    ) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files or []
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    def init(self) -> None:
+        os.makedirs(self._local_working_dir, exist_ok=True)
+        os.makedirs(self._interpreter_working_dir, exist_ok=True)
+
+    async def list_files(self) -> list[PythonFile]:
+        self.init()
+        files = os.listdir(self._local_working_dir)
+        python_files = []
+        for file in files:
+            python_id = self._compute_hash(os.path.join(self._local_working_dir, file))
+            python_files.append(
+                PythonFile(
+                    filename=file,
+                    id=python_id,
+                    python_id=python_id,
+                )
+            )
+        return python_files
+
+    async def upload(self, files: list[PythonFile]) -> list[PythonFile]:
+        self.init()
+
+        for file in files:
+            shutil.copyfile(
+                os.path.join(self._local_working_dir, file.filename),
+                os.path.join(self._interpreter_working_dir, file.python_id),
+            )
+        return files
+
+    async def download(self, files: list[PythonFile]) -> list[PythonFile]:
+        self.init()
+
+        for file in files:
+            shutil.copyfile(
+                os.path.join(self._interpreter_working_dir, file.python_id),
+                os.path.join(self._local_working_dir, file.filename),
+            )
+        return files
+
+    @staticmethod
+    def _compute_hash(file_path: str) -> str:
+        with open(file_path, "rb") as f:
+            digest = hashlib.file_digest(f, "sha256")
+            return digest.hexdigest()
diff --git a/python/examples/agents/react.py b/python/examples/agents/react.py
index 3380a454c..a7c9264ba 100644
--- a/python/examples/agents/react.py
+++ b/python/examples/agents/react.py
@@ -2,6 +2,7 @@
 import logging
 import os
 import sys
+import tempfile
 import traceback
 from typing import Any
 
@@ -16,6 +17,7 @@
 from beeai_framework.errors import FrameworkError
 from beeai_framework.logger import Logger
 from beeai_framework.memory.token_memory import TokenMemory
+from beeai_framework.tools.code import LocalPythonStorage, PythonTool
 from beeai_framework.tools.search import DuckDuckGoSearchTool, WikipediaTool
 from beeai_framework.tools.tool import AnyTool
 from beeai_framework.tools.weather.openmeteo import OpenMeteoTool
@@ -53,8 +55,15 @@ def create_agent() -> ReActAgent:
     # Add code interpreter tool if URL is configured
     code_interpreter_url = os.getenv("CODE_INTERPRETER_URL")
     if code_interpreter_url:
-        # Note: Python tool implementation would go here
-        pass
+        tools.append(
+            PythonTool(
+                code_interpreter_url,
+                LocalPythonStorage(
+                    local_working_dir=tempfile.mkdtemp("code_interpreter_source"),
+                    interpreter_working_dir=os.getenv("CODE_INTERPRETER_TMPDIR", "./tmp/code_interpreter_target"),
+                ),
+            )
+        )
 
     # Create agent with memory and tools
     agent = ReActAgent(llm=llm, tools=tools, memory=TokenMemory(llm))
@@ -91,7 +100,7 @@ async def main() -> None:
             f"The code interpreter tool is enabled. Please ensure that it is running on {code_interpreter_url}",
         )
 
-    reader.write("🛠️ System: ", "Agent initialized with Wikipedia tool.")
+    reader.write("🛠️ System: ", "Agent initialized with Wikipedia, DuckDuckGo, and Weather tools.")
 
     # Main interaction loop with user input
     for prompt in reader:
diff --git a/python/examples/tools/python_tool.py b/python/examples/tools/python_tool.py
new file mode 100644
index 000000000..353b4a428
--- /dev/null
+++ b/python/examples/tools/python_tool.py
@@ -0,0 +1,42 @@
+import asyncio
+import os
+import sys
+import tempfile
+import traceback
+
+from dotenv import load_dotenv
+
+from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel
+from beeai_framework.agents.react.agent import ReActAgent
+from beeai_framework.errors import FrameworkError
+from beeai_framework.memory import UnconstrainedMemory
+from beeai_framework.tools.code import LocalPythonStorage, PythonTool
+
+# Load environment variables
+load_dotenv()
+
+
+async def main() -> None:
+    llm = OllamaChatModel("llama3.1")
+    storage = LocalPythonStorage(
+        local_working_dir=tempfile.mkdtemp("code_interpreter_source"),
+        interpreter_working_dir=os.getenv("CODE_INTERPRETER_TMPDIR", "./tmp/code_interpreter_target"),
+    )
+    python_tool = PythonTool(
+        code_interpreter_url=os.getenv("CODE_INTERPRETER_URL", "http://127.0.0.1:50081"),
+        storage=storage,
+    )
+    agent = ReActAgent(llm=llm, tools=[python_tool], memory=UnconstrainedMemory())
+    result = await agent.run("Calculate 5036 * 12856 and save the result to answer.txt")
+    print(result.result.text)
+
+    result = await agent.run("Read the content of answer.txt?")
+    print(result.result.text)
+
+
+if __name__ == "__main__":
+    try:
+        asyncio.run(main())
+    except FrameworkError as e:
+        traceback.print_exc()
+        sys.exit(e.explain())
diff --git a/python/tests/examples/test_examples.py b/python/tests/examples/test_examples.py
index 88e50e9f5..7a6ee1b9f 100644
--- a/python/tests/examples/test_examples.py
+++ b/python/tests/examples/test_examples.py
@@ -46,6 +46,8 @@
             "workflows/searx_agent.py",
             # Requires BeeAI platform to be running
             "agents/experimental/remote.py",
+            # Requires Code Interpreter to be running
+            "tools/python_tool.py" if os.getenv("CODE_INTERPRETER_URL") is None else None,
         ],
     )
 )
diff --git a/python/tests/tools/test_python.py b/python/tests/tools/test_python.py
new file mode 100644
index 000000000..da62adf25
--- /dev/null
+++ b/python/tests/tools/test_python.py
@@ -0,0 +1,119 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+from collections.abc import Generator
+from typing import Any
+from unittest.mock import patch
+
+import pytest
+import pytest_asyncio
+
+from beeai_framework.tools.code import LocalPythonStorage, PythonTool
+
+
+@pytest_asyncio.fixture
+def test_dirs() -> Generator[tuple[str, str], Any, None]:
+    local_dir = "local_dir"
+    interpreter_dir = "interpreter_dir"
+    os.makedirs(local_dir, exist_ok=True)
+    os.makedirs(interpreter_dir, exist_ok=True)
+
+    # Create some test files
+    test_files = [os.path.join(local_dir, f"file{i}.txt") for i in range(1, 4)]
+    for file in test_files:
+        with open(file, "w") as f:
+            f.write(f"Content of {file}")
+    with open(os.path.join(interpreter_dir, "dummyID"), "w") as f:
+        f.write("Hello, World!")
+
+    yield local_dir, interpreter_dir
+
+    # Clean up: remove the temporary directory and its contents
+    shutil.rmtree(local_dir)
+    shutil.rmtree(interpreter_dir)
+
+
+@pytest_asyncio.fixture
+async def tool(test_dirs: tuple[str, str]) -> PythonTool:
+    tool = PythonTool(
+        code_interpreter_url="dummyURL",
+        storage=LocalPythonStorage(local_working_dir=test_dirs[0], interpreter_working_dir=test_dirs[1]),
+    )
+    return tool
+
+
+@pytest.mark.e2e
+@pytest.mark.asyncio
+async def test_without_file(tool: PythonTool) -> None:
+    with patch(
+        "beeai_framework.tools.code.PythonTool._call_code_interpreter",
+        return_value={"stdout": "2\n", "stderr": "", "exit_code": 0, "files": {}},
+    ):
+        result = await tool.run(
+            {
+                "language": "python",
+                "code": "print(str(1+1))",
+                "input_files": [],
+            }
+        )
+    assert result.stdout
+    assert "2" in result.stdout
+
+
+@pytest.mark.e2e
+@pytest.mark.asyncio
+async def test_with_file(tool: PythonTool) -> None:
+    with patch(
+        "beeai_framework.tools.code.PythonTool._call_code_interpreter",
+        return_value={
+            "stdout": "",
+            "stderr": "",
+            "exit_code": 0,
+            "files": {"/workspace/file1.txt": "dummyID"},
+        },
+    ):
+        first_result = await tool.run(
+            {
+                "language": "python",
+                "code": """
+with open('file1.txt', 'w') as f:
+    f.write("Hello, World!")
+""",
+                "input_files": [],
+            }
+        )
+
+    assert len(first_result.output_files) == 1
+    assert first_result.output_files[0].filename == "file1.txt"
+
+    with patch(
+        "beeai_framework.tools.code.PythonTool._call_code_interpreter",
+        return_value={
+            "stdout": "4\n",
+            "stderr": "",
+            "exit_code": 0,
+            "files": {"/workspace/file1.txt": "dummyID"},
+        },
+    ):
+        result = await tool.run(
+            {
+                "language": "python",
+                "code": "print(str(3+1))",
+                "input_files": ["file1.txt"],
+            }
+        )
+    assert result.stdout
+    assert "4" in result.stdout


Reviews:
user: login: Tomas2D
body: Good work so far! 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:54:28Z
user: login: ajbozarth
body: We should move the files in `tools` into a subdirectory like we did with search and weather.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-19T00:07:21Z
user: login: akihikokuroda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T00:17:51Z
user: login: akihikokuroda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T00:26:57Z
user: login: akihikokuroda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T00:31:03Z
user: login: akihikokuroda
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T01:18:01Z
user: login: Tomas2D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:21:34Z
user: login: Tomas2D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:23:39Z
user: login: ajbozarth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:59:39Z
user: login: Tomas2D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:55:08Z
user: login: Tomas2D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:55:37Z
user: login: ajbozarth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T22:35:20Z
user: login: ajbozarth
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-24T21:56:26Z
user: login: planetf1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:22:04Z
user: login: Tomas2D
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:39:16Z
user: login: Tomas2D
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T16:44:45Z

Review comments:
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: Make all methods async.
created_at: 2025-03-18T20:52:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
+    """
+    Abstract class for managing files in Python code interpreter.
+    """
+
+    def __init__(self) -> None:
+        super().__init__()
+
+    @abstractmethod
+    def list_files(self) -> list[PythonFile]:
+        """
+        List all files that code interpreter can use.
+        """
+        pass
+
+    @abstractmethod
+    def upload(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Prepare subset of available files to code interpreter.
+        """
+        pass
+
+    @abstractmethod
+    def download(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Process updated/modified/deleted files from code interpreter response.
+        """
+        pass
+
+
+class LocalPythonStorage(PythonStorage):
+    def __init__(self, input: Input) -> None:
+        super().__init__()
+        self.input = input
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: Use keyword arguments instead of the `Input` class.
created_at: 2025-03-18T20:52:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: Replace with Pydantic's BaseModel.

```py
class PythonFile(BaseModel):
  id: str
  python_id: str
  filename: str
```
created_at: 2025-03-18T20:53:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: Is this indented correctly? I'd say it should either be indented once more to match the current block or not be indented at all
created_at: 2025-03-18T23:56:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: same comment about indentation
created_at: 2025-03-18T23:57:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: what is the rational for ignoring mypy for this line?
created_at: 2025-03-18T23:58:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+                ),
+            )
+        self.input_schema = create_model(
+            "PythonToolInput",
+            language=(Language, Field(description="Use shell for ffmpeg, pandoc, yt-dlp")),
+            code=(str, Field(description="full source code file that will be executed")),
+            inputFiles=input_files,
+        )
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    async def _run(self, input: Any, options: ToolRunOptions | None, context: RunContext) -> PythonToolOutput:
+        async def get_source_code() -> Any:
+            if self.options and self.options.get("preprocess"):
+                response = await self.options["preprocess"]["llm"].create(
+                    {
+                        "messages": PromptTemplate(
+                            PromptTemplateInput(schema=UserPromptTemplateInput, template="{{input}}")
+                        ),
+                        "abortSignal": context.signal,
+                    }
+                )
+                return response.getTextContent().trim()
+            return input.code
+
+        async def call_code_interpreter(url: str, body: dict[str, Any], files: dict[str, io.BufferedReader]) -> Any:
+            headers = {"Accept": "application/json", "Content-Type": "application/json"}
+            async with httpx.AsyncClient() as client:
+                # response = await client.post(url, headers=headers, data=json.dumps(body), files=files)
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: I believe you meant to delete this line given it's the same as the one below
created_at: 2025-03-18T23:59:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+                ),
+            )
+        self.input_schema = create_model(
+            "PythonToolInput",
+            language=(Language, Field(description="Use shell for ffmpeg, pandoc, yt-dlp")),
+            code=(str, Field(description="full source code file that will be executed")),
+            inputFiles=input_files,
+        )
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    async def _run(self, input: Any, options: ToolRunOptions | None, context: RunContext) -> PythonToolOutput:
+        async def get_source_code() -> Any:
+            if self.options and self.options.get("preprocess"):
+                response = await self.options["preprocess"]["llm"].create(
+                    {
+                        "messages": PromptTemplate(
+                            PromptTemplateInput(schema=UserPromptTemplateInput, template="{{input}}")
+                        ),
+                        "abortSignal": context.signal,
+                    }
+                )
+                return response.getTextContent().trim()
+            return input.code
+
+        async def call_code_interpreter(url: str, body: dict[str, Any], files: dict[str, io.BufferedReader]) -> Any:
+            headers = {"Accept": "application/json", "Content-Type": "application/json"}
+            async with httpx.AsyncClient() as client:
+                # response = await client.post(url, headers=headers, data=json.dumps(body), files=files)
+                response = await client.post(url, headers=headers, data=json.dumps(body), files=files)  # type:  ignore[arg-type]
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: what is the rational for ignoring mypy for this line?
created_at: 2025-03-19T00:00:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+                ),
+            )
+        self.input_schema = create_model(
+            "PythonToolInput",
+            language=(Language, Field(description="Use shell for ffmpeg, pandoc, yt-dlp")),
+            code=(str, Field(description="full source code file that will be executed")),
+            inputFiles=input_files,
+        )
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    async def _run(self, input: Any, options: ToolRunOptions | None, context: RunContext) -> PythonToolOutput:
+        async def get_source_code() -> Any:
+            if self.options and self.options.get("preprocess"):
+                response = await self.options["preprocess"]["llm"].create(
+                    {
+                        "messages": PromptTemplate(
+                            PromptTemplateInput(schema=UserPromptTemplateInput, template="{{input}}")
+                        ),
+                        "abortSignal": context.signal,
+                    }
+                )
+                return response.getTextContent().trim()
+            return input.code
+
+        async def call_code_interpreter(url: str, body: dict[str, Any], files: dict[str, io.BufferedReader]) -> Any:
+            headers = {"Accept": "application/json", "Content-Type": "application/json"}
+            async with httpx.AsyncClient() as client:
+                # response = await client.post(url, headers=headers, data=json.dumps(body), files=files)
+                response = await client.post(url, headers=headers, data=json.dumps(body), files=files)  # type:  ignore[arg-type]
+                response.raise_for_status()
+                return response.json()
+
+        if self.options and self.options.get("codeInterpreter") and self.options["codeInterpreter"].get("url"):
+            url = self.options["codeInterpreter"]["url"] + "/v1/execute"
+            prefix = "/workspace/"
+            unique_files = {}
+            if self.options and self.options.get("storage"):
+                for file in self.files:
+                    if file.filename in [python_file.value for python_file in input.inputFiles]:
+                        unique_files[file.filename] = file
+                self.options["storage"].upload(unique_files.values())
+
+            url = self.options["codeInterpreter"]["url"] + "/v1/execute"
+            files = {}
+            for file in unique_files.values():
+                files[prefix + file.filename] = open("localTmp/" + file.filename, "rb")  # noqa: ASYNC230,SIM115
path: python/beeai_framework/tools/python.py
user: login: ajbozarth
body: should we really be ignoring these lint issues?
created_at: 2025-03-19T00:01:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
path: python/beeai_framework/tools/storage.py
user: login: ajbozarth
body: echoing @Tomas2D I believe this should be a BaseModel as well
created_at: 2025-03-19T00:02:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+import asyncio
+import sys
+import traceback
+
+from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel
+from beeai_framework.agents.react.agent import ReActAgent
+from beeai_framework.errors import FrameworkError
+from beeai_framework.memory import UnconstrainedMemory
+from beeai_framework.tools.python import PythonTool
+from beeai_framework.tools.storage import Input, LocalPythonStorage
+
+
+async def main() -> None:
+    llm = OllamaChatModel("llama3.1")
+    p = PythonTool(
+        {
+            "codeInterpreter": {"url": "http://127.0.0.1:50081"},
path: python/examples/tools/python_tool.py
user: login: ajbozarth
body: Since this requires an external service to be running it will cause the e2e tests to fail, you should add this example to the excludes list in `tests/examples/test_examples.py` with a reason comment
created_at: 2025-03-19T00:04:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import asyncio
+import sys
+import traceback
+
+from beeai_framework.errors import FrameworkError
+from beeai_framework.tools.python import PythonTool
+from beeai_framework.tools.storage import Input, LocalPythonStorage
+
+
+async def main() -> None:
+    p = PythonTool(
+        {
+            "codeInterpreter": {"url": "http://127.0.0.1:50081"},
+            "storage": LocalPythonStorage(Input("./localTmp", "./pythonTmp", [])),
+        }
+    )
+    result = await p.run(
+        {
+            "language": "python",
+            "code": "print(str(1+1))",
+            "inputFiles": ["a", "c"],
+        }
+    )
+    print(result)
+
+    result = await p.run(
+        {
+            "language": "python",
+            "code": "print(str(3+1))",
+            "inputFiles": ["a", "c"],
+        }
+    )
+    print(result)
+
+
+if __name__ == "__main__":
+    try:
+        asyncio.run(main())
+    except FrameworkError as e:
+        traceback.print_exc()
+        sys.exit(e.explain())
path: python/tests/tools/test_python.py
user: login: ajbozarth
body: This is not a test, it should be rewritten in the format of a unit test
created_at: 2025-03-19T00:07:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import asyncio
+import sys
+import traceback
+
+from beeai_framework.errors import FrameworkError
+from beeai_framework.tools.python import PythonTool
+from beeai_framework.tools.storage import Input, LocalPythonStorage
+
+
+async def main() -> None:
+    p = PythonTool(
+        {
+            "codeInterpreter": {"url": "http://127.0.0.1:50081"},
+            "storage": LocalPythonStorage(Input("./localTmp", "./pythonTmp", [])),
+        }
+    )
+    result = await p.run(
+        {
+            "language": "python",
+            "code": "print(str(1+1))",
+            "inputFiles": ["a", "c"],
+        }
+    )
+    print(result)
+
+    result = await p.run(
+        {
+            "language": "python",
+            "code": "print(str(3+1))",
+            "inputFiles": ["a", "c"],
+        }
+    )
+    print(result)
+
+
+if __name__ == "__main__":
+    try:
+        asyncio.run(main())
+    except FrameworkError as e:
+        traceback.print_exc()
+        sys.exit(e.explain())
path: python/tests/tools/test_python.py
user: login: akihikokuroda
body: Updated for tests.
created_at: 2025-03-19T00:17:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+                ),
+            )
+        self.input_schema = create_model(
+            "PythonToolInput",
+            language=(Language, Field(description="Use shell for ffmpeg, pandoc, yt-dlp")),
+            code=(str, Field(description="full source code file that will be executed")),
+            inputFiles=input_files,
+        )
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    async def _run(self, input: Any, options: ToolRunOptions | None, context: RunContext) -> PythonToolOutput:
+        async def get_source_code() -> Any:
+            if self.options and self.options.get("preprocess"):
+                response = await self.options["preprocess"]["llm"].create(
+                    {
+                        "messages": PromptTemplate(
+                            PromptTemplateInput(schema=UserPromptTemplateInput, template="{{input}}")
+                        ),
+                        "abortSignal": context.signal,
+                    }
+                )
+                return response.getTextContent().trim()
+            return input.code
+
+        async def call_code_interpreter(url: str, body: dict[str, Any], files: dict[str, io.BufferedReader]) -> Any:
+            headers = {"Accept": "application/json", "Content-Type": "application/json"}
+            async with httpx.AsyncClient() as client:
+                # response = await client.post(url, headers=headers, data=json.dumps(body), files=files)
+                response = await client.post(url, headers=headers, data=json.dumps(body), files=files)  # type:  ignore[arg-type]
+                response.raise_for_status()
+                return response.json()
+
+        if self.options and self.options.get("codeInterpreter") and self.options["codeInterpreter"].get("url"):
+            url = self.options["codeInterpreter"]["url"] + "/v1/execute"
+            prefix = "/workspace/"
+            unique_files = {}
+            if self.options and self.options.get("storage"):
+                for file in self.files:
+                    if file.filename in [python_file.value for python_file in input.inputFiles]:
+                        unique_files[file.filename] = file
+                self.options["storage"].upload(unique_files.values())
+
+            url = self.options["codeInterpreter"]["url"] + "/v1/execute"
+            files = {}
+            for file in unique_files.values():
+                files[prefix + file.filename] = open("localTmp/" + file.filename, "rb")  # noqa: ASYNC230,SIM115
path: python/beeai_framework/tools/python.py
user: login: akihikokuroda
body: I couldn't find any other way to open multiple files and pass the file object to the API call.
created_at: 2025-03-19T00:26:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
+            input_files = (
+                list[python_files],
+                Field(
+                    description="""To access an existing file, you must specify it;
+                    otherwise, the file will not be accessible.
+                    IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+                ),
+            )
+        self.input_schema = create_model(
+            "PythonToolInput",
+            language=(Language, Field(description="Use shell for ffmpeg, pandoc, yt-dlp")),
+            code=(str, Field(description="full source code file that will be executed")),
+            inputFiles=input_files,
+        )
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    async def _run(self, input: Any, options: ToolRunOptions | None, context: RunContext) -> PythonToolOutput:
+        async def get_source_code() -> Any:
+            if self.options and self.options.get("preprocess"):
+                response = await self.options["preprocess"]["llm"].create(
+                    {
+                        "messages": PromptTemplate(
+                            PromptTemplateInput(schema=UserPromptTemplateInput, template="{{input}}")
+                        ),
+                        "abortSignal": context.signal,
+                    }
+                )
+                return response.getTextContent().trim()
+            return input.code
+
+        async def call_code_interpreter(url: str, body: dict[str, Any], files: dict[str, io.BufferedReader]) -> Any:
+            headers = {"Accept": "application/json", "Content-Type": "application/json"}
+            async with httpx.AsyncClient() as client:
+                # response = await client.post(url, headers=headers, data=json.dumps(body), files=files)
+                response = await client.post(url, headers=headers, data=json.dumps(body), files=files)  # type:  ignore[arg-type]
path: python/beeai_framework/tools/python.py
user: login: akihikokuroda
body: I'm not sure but changing the type causes the request failure.  This call is passing both `data` and `files`.  Addin g the files in the body also causes a request failure.
created_at: 2025-03-19T00:31:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: akihikokuroda
body: The `list_files` is called in the `__init__` function of the `PythonTool`.  We probably create a factory function for    `pythonTool` class.
created_at: 2025-03-19T01:18:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,157 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import io
+import json
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, create_model
+
+from beeai_framework.agents.react.runners.default.prompts import (
+    UserPromptTemplateInput,
+)
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate, PromptTemplateInput
+from beeai_framework.tools.output import PythonToolOutput
+from beeai_framework.tools.tool import Tool, ToolRunOptions  # type: ignore[attr-defined]
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonTool(Tool[BaseModel, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = BaseModel
+    description = """
+    Run Python and/or shell code and return the console output. Use for isolated calculations,
+    computations, data or file manipulation but still prefer assistant's capabilities
+    (IMPORTANT: Do not use for text analysis or summarization).
+    Files provided by the user, or created in a previous run, will be accessible
+    if and only if they are specified in the input. It is necessary to always print() results.
+    The following shell commands are available:
+    Use ffmpeg to convert videos.
+    Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+    for video and `-x --audio-format mp3` for audio-only.
+    Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+    you can create PDFs by writing markdown and then converting.
+    In Python, the following modules are available:
+    Use numpy, pandas, scipy and sympy for working with data.
+    Use matplotlib to plot charts.
+    Use pillow (import PIL) to create and manipulate images.
+    Use moviepy for complex manipulations with videos.
+    Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+    Use pdf2image to convert PDF to images.
+    Other Python libraries are also available -- however, prefer using the ones above.
+    Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+    Do not attempt to install libraries manually -- it will not work.
+    Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+    Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, options: dict[str, Any] | None = None) -> None:
+        super().__init__(options)
+        input_files = None
+        if options and options.get("storage"):
+            self.files = options["storage"].list_files()
+            filenames = [file.filename for file in self.files]
+            members = {}
+            for filename in filenames:
+                members[filename.upper()] = filename
+                python_files = Enum("PythonFiles", members)  # type: ignore[misc]
path: python/beeai_framework/tools/python.py
user: login: Tomas2D
body: btw, there is a factory function for that (`create_strenum`)
created_at: 2025-03-19T07:21:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: We don't need this class anymore. I pointed out earlier.
created_at: 2025-03-19T07:22:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: Follow the TS implementation where listing files is done asynchronously in `input_schema`.
created_at: 2025-03-19T07:23:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: ajbozarth
body: In TS we use `fs.promises`, an asynchronous fileIO library. In python there is no equivalent async fileIO library, there are a few third party ones but I would rather not use third party libs for fileIO. Is there a reason we need the fileIO to be async?
created_at: 2025-03-19T18:59:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: We don't need async for working with files. But we will need async to enable our users to implement their storage, which might be external like S3 and thus would require HTTP communication, which ideally should be asynchronous.
created_at: 2025-03-19T20:55:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: Tomas2D
body: btw: the class should be abstract 
created_at: 2025-03-19T20:55:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import abstractmethod
+
+
+class PythonFile:
+    def __init__(self, filename: str, id: str, python_id: str) -> None:
+        self._id = id
+        self._python_id = python_id
+        self._filename = filename
+
+    @property
+    def id(self) -> str:
+        return self._id
+
+    @id.setter
+    def id(self, value: str) -> None:
+        self._id = value
+
+    @property
+    def python_id(self) -> str:
+        return self._python_id
+
+    @python_id.setter
+    def python_id(self, value: str) -> None:
+        self._python_id = value
+
+    @property
+    def filename(self) -> str:
+        return self._filename
+
+    @filename.setter
+    def filename(self, value: str) -> None:
+        self._filename = value
+
+
+class Input:
+    def __init__(self, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str]) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @local_working_dir.setter
+    def local_working_dir(self, v: str) -> None:
+        self._local_working_dir = v
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @interpreter_working_dir.setter
+    def interpreter_working_dir(self, v: str) -> None:
+        self._interpreter_working_eir = v
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    @ignored_files.setter
+    def ignored_files(self, v: list[str]) -> None:
+        self._ignored_files = v
+
+
+class PythonStorage:
path: python/beeai_framework/tools/storage.py
user: login: ajbozarth
body: So the issue that @akihikokuroda stated above is still an issue for this. no matter what I do I must call `list_files` (which should be async) in either __init__ or in the property getter `input_schema`. Currently I put it in `input_schema`, but in python it is sync, unlike in TS. I started to make the abstract property on Tool async to allow this, but it slowly became a mess, so I wanted to discuss if that is actually the solution before I make a mess of the code making `input_schema` async on all our tools and calls to it
created_at: 2025-03-21T22:35:20Z
author_association: MEMBER
diff_hunk: @@ -46,6 +46,8 @@
             "workflows/searx_agent.py",
             # Requires BeeAI platform to be running
             "agents/experimental/remote.py",
+            # Requires Code Interpreter to be running
path: python/tests/examples/test_examples.py
user: login: planetf1
body: we may need a condition here (or a soft fail) (check if port open?)
or split the tests that require backends into a different group
created_at: 2025-03-25T10:19:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, InstanceOf
+
+from beeai_framework import UserMessage
+from beeai_framework.backend.chat import ChatModel
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate
+from beeai_framework.tools.code.output import PythonToolOutput
+from beeai_framework.tools.code.storage import PythonFile, PythonStorage
+from beeai_framework.tools.tool import Tool
+from beeai_framework.tools.types import ToolRunOptions
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonToolTemplate(BaseModel):
+    input: str
+
+
+class PreProcess(BaseModel):
+    llm: InstanceOf[ChatModel]
+    prompt_template: InstanceOf[PromptTemplate[PythonToolTemplate]]
+
+
+class PythonToolInput(BaseModel):
+    language: Language = Field(description="Use shell for ffmpeg, pandoc, yt-dlp")
+    code: str = Field(description="full source code file that will be executed")
+    input_files: list[str] = Field(
+        description="""To access an existing file, you must specify it;
+otherwise, the file will not be accessible.
+IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+    )
+
+
+class PythonTool(Tool[PythonToolInput, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = PythonToolInput
+    description = """
+Run Python and/or shell code and return the console output. Use for isolated calculations,
+computations, data or file manipulation but still prefer assistant's capabilities
+(IMPORTANT: Do not use for text analysis or summarization).
+Files provided by the user, or created in a previous run, will be accessible
+ if and only if they are specified in the input. It is necessary to always print() results.
+The following shell commands are available:
+Use ffmpeg to convert videos.
+Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+ for video and `-x --audio-format mp3` for audio-only.
+Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+ you can create PDFs by writing markdown and then converting.
+In Python, the following modules are available:
+Use numpy, pandas, scipy and sympy for working with data.
+Use matplotlib to plot charts.
+Use pillow (import PIL) to create and manipulate images.
+Use moviepy for complex manipulations with videos.
+Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+Use pdf2image to convert PDF to images.
+Other Python libraries are also available -- however, prefer using the ones above.
+Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+Do not attempt to install libraries manually -- it will not work.
+Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, code_interpreter_url: str, storage: PythonStorage, preprocess: PreProcess | None = None) -> None:
+        super().__init__()
+        self._code_interpreter_url = code_interpreter_url
+        self._storage = storage
+        self._preprocess = preprocess
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    @staticmethod
+    async def _call_code_interpreter(url: str, body: dict[str, Any]) -> Any:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+        async with httpx.AsyncClient() as client:
+            response = await client.post(url, headers=headers, json=body)
+            response.raise_for_status()
+            return response.json()
+
+    async def _run(
+        self, tool_input: PythonToolInput, options: ToolRunOptions | None, context: RunContext
+    ) -> PythonToolOutput:
+        async def get_source_code() -> str:
+            if self._preprocess:
+                response = await self._preprocess.llm.create(
+                    messages=[
+                        UserMessage(self._preprocess.prompt_template.render(PythonToolTemplate(input=tool_input.code)))
+                    ],
+                    abort_signal=context.signal,
+                )
+                return response.get_text_content()
+            return tool_input.code
+
+        execute_url = self._code_interpreter_url + "/v1/execute"
+        prefix = "/workspace/"
+
+        files = await self._storage.list_files()
+        if not set(tool_input.input_files).issubset([f.filename for f in files]):
+            raise ValueError(f"Invalid input files: {tool_input.input_files}")
path: python/beeai_framework/tools/code/python.py
user: login: Tomas2D
body: I assume this should be `ToolInputValidationError`.
created_at: 2025-03-25T13:34:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, InstanceOf
+
+from beeai_framework import UserMessage
+from beeai_framework.backend.chat import ChatModel
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate
+from beeai_framework.tools.code.output import PythonToolOutput
+from beeai_framework.tools.code.storage import PythonFile, PythonStorage
+from beeai_framework.tools.tool import Tool
+from beeai_framework.tools.types import ToolRunOptions
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonToolTemplate(BaseModel):
+    input: str
+
+
+class PreProcess(BaseModel):
+    llm: InstanceOf[ChatModel]
+    prompt_template: InstanceOf[PromptTemplate[PythonToolTemplate]]
+
+
+class PythonToolInput(BaseModel):
+    language: Language = Field(description="Use shell for ffmpeg, pandoc, yt-dlp")
+    code: str = Field(description="full source code file that will be executed")
+    input_files: list[str] = Field(
+        description="""To access an existing file, you must specify it;
+otherwise, the file will not be accessible.
+IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+    )
+
+
+class PythonTool(Tool[PythonToolInput, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = PythonToolInput
+    description = """
+Run Python and/or shell code and return the console output. Use for isolated calculations,
+computations, data or file manipulation but still prefer assistant's capabilities
+(IMPORTANT: Do not use for text analysis or summarization).
+Files provided by the user, or created in a previous run, will be accessible
+ if and only if they are specified in the input. It is necessary to always print() results.
+The following shell commands are available:
+Use ffmpeg to convert videos.
+Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+ for video and `-x --audio-format mp3` for audio-only.
+Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+ you can create PDFs by writing markdown and then converting.
+In Python, the following modules are available:
+Use numpy, pandas, scipy and sympy for working with data.
+Use matplotlib to plot charts.
+Use pillow (import PIL) to create and manipulate images.
+Use moviepy for complex manipulations with videos.
+Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+Use pdf2image to convert PDF to images.
+Other Python libraries are also available -- however, prefer using the ones above.
+Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+Do not attempt to install libraries manually -- it will not work.
+Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, code_interpreter_url: str, storage: PythonStorage, preprocess: PreProcess | None = None) -> None:
+        super().__init__()
+        self._code_interpreter_url = code_interpreter_url
+        self._storage = storage
+        self._preprocess = preprocess
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    @staticmethod
+    async def _call_code_interpreter(url: str, body: dict[str, Any]) -> Any:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+        async with httpx.AsyncClient() as client:
+            response = await client.post(url, headers=headers, json=body)
+            response.raise_for_status()
+            return response.json()
+
+    async def _run(
+        self, tool_input: PythonToolInput, options: ToolRunOptions | None, context: RunContext
+    ) -> PythonToolOutput:
+        async def get_source_code() -> str:
+            if self._preprocess:
+                response = await self._preprocess.llm.create(
+                    messages=[
+                        UserMessage(self._preprocess.prompt_template.render(PythonToolTemplate(input=tool_input.code)))
+                    ],
+                    abort_signal=context.signal,
+                )
+                return response.get_text_content()
+            return tool_input.code
+
+        execute_url = self._code_interpreter_url + "/v1/execute"
+        prefix = "/workspace/"
+
+        files = await self._storage.list_files()
+        if not set(tool_input.input_files).issubset([f.filename for f in files]):
+            raise ValueError(f"Invalid input files: {tool_input.input_files}")
+
+        unique_files: list[PythonFile] = []
+        for file in files:
+            if file.filename in tool_input.input_files or []:
+                unique_files.append(file)
+        await self._storage.upload(unique_files)
path: python/beeai_framework/tools/code/python.py
user: login: Tomas2D
body: ```python
unique_files = [file for file in files if file.filename in (tool_input.input_files or [])]
await self._storage.upload(unique_files)
```
created_at: 2025-03-25T13:35:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import Enum
+from typing import Any
+
+import httpx
+from pydantic import BaseModel, Field, InstanceOf
+
+from beeai_framework import UserMessage
+from beeai_framework.backend.chat import ChatModel
+from beeai_framework.context import RunContext
+from beeai_framework.emitter.emitter import Emitter
+from beeai_framework.logger import Logger
+from beeai_framework.template import PromptTemplate
+from beeai_framework.tools.code.output import PythonToolOutput
+from beeai_framework.tools.code.storage import PythonFile, PythonStorage
+from beeai_framework.tools.tool import Tool
+from beeai_framework.tools.types import ToolRunOptions
+
+logger = Logger(__name__)
+
+
+class Language(Enum):
+    PYTHON = "python"
+    SHELL = "shell"
+
+
+class PythonToolTemplate(BaseModel):
+    input: str
+
+
+class PreProcess(BaseModel):
+    llm: InstanceOf[ChatModel]
+    prompt_template: InstanceOf[PromptTemplate[PythonToolTemplate]]
+
+
+class PythonToolInput(BaseModel):
+    language: Language = Field(description="Use shell for ffmpeg, pandoc, yt-dlp")
+    code: str = Field(description="full source code file that will be executed")
+    input_files: list[str] = Field(
+        description="""To access an existing file, you must specify it;
+otherwise, the file will not be accessible.
+IMPORTANT: If the file is not provided in the input, it will not be accessible."""
+    )
+
+
+class PythonTool(Tool[PythonToolInput, ToolRunOptions, PythonToolOutput]):
+    name = "Python"
+    input_schema = PythonToolInput
+    description = """
+Run Python and/or shell code and return the console output. Use for isolated calculations,
+computations, data or file manipulation but still prefer assistant's capabilities
+(IMPORTANT: Do not use for text analysis or summarization).
+Files provided by the user, or created in a previous run, will be accessible
+ if and only if they are specified in the input. It is necessary to always print() results.
+The following shell commands are available:
+Use ffmpeg to convert videos.
+Use yt-dlp to download videos, and unless specified otherwise use `-S vcodec:h264,res,acodec:m4a`
+ for video and `-x --audio-format mp3` for audio-only.
+Use pandoc to convert documents between formats (like MD, DOC, DOCX, PDF) -- and don't forget that
+ you can create PDFs by writing markdown and then converting.
+In Python, the following modules are available:
+Use numpy, pandas, scipy and sympy for working with data.
+Use matplotlib to plot charts.
+Use pillow (import PIL) to create and manipulate images.
+Use moviepy for complex manipulations with videos.
+Use PyPDF2, pikepdf, or fitz to manipulate PDFs.
+Use pdf2image to convert PDF to images.
+Other Python libraries are also available -- however, prefer using the ones above.
+Prefer using qualified imports -- `import library; library.thing()` instead of `import thing from library`.
+Do not attempt to install libraries manually -- it will not work.
+Each invocation of Python runs in a completely fresh VM -- it will not remember anything from before.
+Do not use this tool multiple times in a row, always write the full code you want to run in a single invocation."""
+
+    def __init__(self, code_interpreter_url: str, storage: PythonStorage, preprocess: PreProcess | None = None) -> None:
+        super().__init__()
+        self._code_interpreter_url = code_interpreter_url
+        self._storage = storage
+        self._preprocess = preprocess
+
+    def _create_emitter(self) -> Emitter:
+        return Emitter.root().child(
+            namespace=["tool", "python", "code_interpreter"],
+            creator=self,
+        )
+
+    @staticmethod
+    async def _call_code_interpreter(url: str, body: dict[str, Any]) -> Any:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+        async with httpx.AsyncClient() as client:
+            response = await client.post(url, headers=headers, json=body)
+            response.raise_for_status()
+            return response.json()
+
+    async def _run(
+        self, tool_input: PythonToolInput, options: ToolRunOptions | None, context: RunContext
+    ) -> PythonToolOutput:
+        async def get_source_code() -> str:
+            if self._preprocess:
+                response = await self._preprocess.llm.create(
+                    messages=[
+                        UserMessage(self._preprocess.prompt_template.render(PythonToolTemplate(input=tool_input.code)))
+                    ],
+                    abort_signal=context.signal,
+                )
+                return response.get_text_content()
+            return tool_input.code
+
+        execute_url = self._code_interpreter_url + "/v1/execute"
+        prefix = "/workspace/"
+
+        files = await self._storage.list_files()
+        if not set(tool_input.input_files).issubset([f.filename for f in files]):
+            raise ValueError(f"Invalid input files: {tool_input.input_files}")
+
+        unique_files: list[PythonFile] = []
+        for file in files:
+            if file.filename in tool_input.input_files or []:
+                unique_files.append(file)
+        await self._storage.upload(unique_files)
+
+        files_dict = {}
+        for file in unique_files:
+            files_dict[prefix + file.filename] = file.python_id
path: python/beeai_framework/tools/code/python.py
user: login: Tomas2D
body: ```py
files_dict = {prefix + file.filename: file.python_id for file in unique_files}
```
created_at: 2025-03-25T13:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+from abc import ABC, abstractmethod
+
+from pydantic import BaseModel
+
+
+class PythonFile(BaseModel):
+    id: str
+    python_id: str
+    filename: str
+
+
+class PythonStorage(ABC):
+    """
+    Abstract class for managing files in Python code interpreter.
+    """
+
+    @abstractmethod
+    async def list_files(self) -> list[PythonFile]:
+        """
+        List all files that code interpreter can use.
+        """
+        pass
+
+    @abstractmethod
+    async def upload(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Prepare subset of available files to code interpreter.
+        """
+        pass
+
+    @abstractmethod
+    async def download(self, files: list[PythonFile]) -> list[PythonFile]:
+        """
+        Process updated/modified/deleted files from code interpreter response.
+        """
+        pass
+
+
+class LocalPythonStorage(PythonStorage):
+    def __init__(
+        self, *, local_working_dir: str, interpreter_working_dir: str, ignored_files: list[str] | None = None
+    ) -> None:
+        self._local_working_dir = local_working_dir
+        self._interpreter_working_dir = interpreter_working_dir
+        self._ignored_files = ignored_files or []
+
+    @property
+    def local_working_dir(self) -> str:
+        return self._local_working_dir
+
+    @property
+    def interpreter_working_dir(self) -> str:
+        return self._interpreter_working_dir
+
+    @property
+    def ignored_files(self) -> list[str]:
+        return self._ignored_files
+
+    def init(self) -> None:
+        os.makedirs(self._local_working_dir, exist_ok=True)
+        os.makedirs(self._interpreter_working_dir, exist_ok=True)
+
+    async def list_files(self) -> list[PythonFile]:
+        self.init()
+        files = os.listdir(self._local_working_dir)
+        python_files = []
+        for file in files:
+            python_id = self._compute_hash(os.path.join(self._local_working_dir, file))
+            python_files.append(
+                PythonFile(
+                    filename=file,
+                    id=python_id,
+                    python_id=python_id,
+                )
+            )
+        return python_files
+
+    async def upload(self, files: list[PythonFile]) -> list[PythonFile]:
+        self.init()
+
+        for file in files:
+            shutil.copyfile(
+                os.path.join(self._local_working_dir, file.filename),
+                os.path.join(self._interpreter_working_dir, file.python_id),
+            )
+        return files
+
+    async def download(self, files: list[PythonFile]) -> list[PythonFile]:
+        self.init()
+
+        for file in files:
+            shutil.copyfile(
+                os.path.join(self._interpreter_working_dir, file.python_id),
+                os.path.join(self._local_working_dir, file.filename),
+            )
+        return files
+
+    def clean_up(self, rmtree: bool = False) -> None:
+        if rmtree:
+            shutil.rmtree(self._local_working_dir)
+            shutil.rmtree(self._interpreter_working_dir)
+        else:
+            os.rmdir(self._local_working_dir)
+            os.rmdir(self._interpreter_working_dir)
path: python/beeai_framework/tools/code/storage.py
user: login: Tomas2D
body: We should never remove the directories. This can break the mounting in the Docker. Remove the content only.
created_at: 2025-03-25T13:36:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,6 +46,8 @@
             "workflows/searx_agent.py",
             # Requires BeeAI platform to be running
             "agents/experimental/remote.py",
+            # Requires Code Interpreter to be running
path: python/tests/examples/test_examples.py
user: login: Tomas2D
body: just check for the env variable
created_at: 2025-03-25T13:37:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+# Copyright 2025 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from beeai_framework.tools.code.python import PythonTool
+from beeai_framework.tools.code.storage import LocalPythonStorage
+
+__all__ = ["LocalPythonStorage", "PythonTool"]
path: python/beeai_framework/tools/code/__init__.py
user: login: Tomas2D
body: You are missing the `PythonStorage`
created_at: 2025-03-25T13:38:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,32 @@
+import asyncio
+import sys
+import traceback
+
+from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel
+from beeai_framework.agents.react.agent import ReActAgent
+from beeai_framework.errors import FrameworkError
+from beeai_framework.memory import UnconstrainedMemory
+from beeai_framework.tools.code import LocalPythonStorage, PythonTool
+
+
+async def main() -> None:
+    llm = OllamaChatModel("llama3.1")
+    storage = LocalPythonStorage(local_working_dir="./localTmp", interpreter_working_dir="./pythonTmp")
path: python/examples/tools/python_tool.py
user: login: Tomas2D
body: Can you make paths same as the TS starter?
created_at: 2025-03-25T13:39:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: ajbozarth
created_at: 2025-03-19T16:46:03Z
body: Per discussion at the team call I'll be picking this up to finish as @akihikokuroda leaves on vacation at EOD
author_association: MEMBER
user: login: ajbozarth
created_at: 2025-03-20T18:37:35Z
body: I've pushed some of my changes addressing review so far, including:
- moving the tool files into a subdirectory
- updating `PythonFile` to a `BaseModel`
- deleting `Input` and replacing it with key word args

Remaining items I'm still working on:
- [x] making `PythonStorage` async
- [x] addressing mypy and lint issues like:
   - [x] replacing Enum with `create_strenum`
   - [ ] ~replacing file io with asynchronous file io~
- [x] updating tests to mock the service instead of calling it
- [x] temp dir cleanup after running tests
author_association: MEMBER
user: login: ajbozarth
created_at: 2025-03-20T23:06:48Z
body: pushed my progress for the day, still have quite a bit of work to do.
author_association: MEMBER
user: login: ajbozarth
created_at: 2025-03-21T22:32:25Z
body: I've push my update for the day, after working on getting the files code working I am now ready to mock the code interpreter in pytest. I'll also add ac comment inline about my blocker for making `PythonStorage` async
author_association: MEMBER
user: login: ajbozarth
created_at: 2025-03-24T21:42:31Z
body: I've pushed an update with the following changes:
- made `PythonStorage` async
- removed the special handling around `input_schema` so the `input_files` key always exists and is a `list[str]` instead of a `list[StrEnum]`
- Updated the parsing of `list_files()` to check that the `input_files` all exist in `list_files()` and throw a `ValueError` if they don't (this replaces the validation lost in the above change)
- mocked the code interpreter in the tests

After receiving help from @Tomas2D I've figure out how to interact with the fileIO of the code interpreter, so I'll be doing some testing to make sure what I have works properly with file IO. As long as it works as expected this is done
author_association: MEMBER
user: login: ajbozarth
created_at: 2025-03-24T21:54:02Z
body: I have confirmed that fileIO works as expected with the current code
author_association: MEMBER