Repository: DigitalKin-ai/kin-kernel (language: python)

Issue:
Fix stubs and Licence
# Fix Library stubs
- add py.typed 
- update setup.py  

# Fix Licence badge
- Update reamde.md



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index b1644a9..e7c869b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 [![PyPI version](https://badge.fury.io/py/kin-kernel.svg)](https://badge.fury.io/py/kin-kernel)
 [![Python version](https://img.shields.io/pypi/pyversions/kin-kernel.svg)](https://pypi.python.org/pypi/kin-kernel/)
-[![License](https://img.shields.io/github/license/DigitalKin/kin-kernel.svg)](https://github.com/DigitalKin-ai/kin-kernel/blob/main/LICENSE)
+[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-nc-sa/4.0/)
 [![DigitalKin](https://img.shields.io/badge/DigitalKin-connect-001945)](https://vision.digitalkin.ai)
 [![Discord](https://img.shields.io/badge/Discord-join-blue)](https://discord.com/invite/yVstHVcx)
 
diff --git a/docs/modules.rst b/docs/modules.rst
index 34acf9a..70f1e1d 100644
--- a/docs/modules.rst
+++ b/docs/modules.rst
@@ -2,18 +2,35 @@ Modules
 =======
 .. include:: _sidebar.rst
 
+Global
+======
+
 .. automodule:: kinkernel
    :no-index:
    :members:
    :undoc-members:
    :show-inheritance:
 
+Cells
+======
+
 .. automodule:: kinkernel.cells
    :members:
    :undoc-members:
    :show-inheritance:
 
+Config
+======
+
 .. automodule:: kinkernel.config
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Tools
+=====
+
+.. automodule:: kinkernel.tools
    :members:
    :undoc-members:
    :show-inheritance:
\ No newline at end of file
diff --git a/kinkernel/_version.py b/kinkernel/_version.py
index f9b20fa..06fc0b6 100644
--- a/kinkernel/_version.py
+++ b/kinkernel/_version.py
@@ -1,4 +1,4 @@
 """
 kinkernel/_version.py
 """
-__version__ = "0.0.3"
+__version__ = "0.0.4"
diff --git a/kinkernel/tools/__init__.py b/kinkernel/tools/__init__.py
new file mode 100644
index 0000000..9e37819
--- /dev/null
+++ b/kinkernel/tools/__init__.py
@@ -0,0 +1,22 @@
+"""
+This package provides tools for wrapping and interacting with OpenAI's Functions.
+
+The `OpenAiFunctionCell` class offers a high-level interface to execute BaseCell objects
+asynchronously and to access their properties and input arguments schema in order to use them with OpenAI
+functions.
+
+The `cell_to_openai_function` function is a utility that converts a list of BaseCell objects
+into a list of `OpenAiFunctionCell` instances for easier manipulation.
+
+Classes:
+    OpenAiFunctionCell: Wraps a BaseCell to provide a convenient interface for execution and property access.
+
+Functions:
+    cell_to_openai_function(cells): Converts a list of BaseCell objects into a list of OpenAiFunctionCell instances.
+"""
+from kinkernel.tools.openai_function_cell import (
+    OpenAiFunctionCell,
+    cell_to_openai_function,
+)
+
+__all__ = ["OpenAiFunctionCell", "cell_to_openai_function"]
diff --git a/kinkernel/tools/openai_function_cell.py b/kinkernel/tools/openai_function_cell.py
new file mode 100644
index 0000000..d2fdd3b
--- /dev/null
+++ b/kinkernel/tools/openai_function_cell.py
@@ -0,0 +1,108 @@
+"""
+This module provides a wrapper class OpenAiFunctionCell to connect DK BaseCell with OpenAI's Function Calls.
+It allows for the invocation of these cells asynchronously and provides access to their arguments.
+"""
+
+import json
+
+from typing import Union, Dict, Any, List
+
+from kinkernel.cells import BaseCell
+
+
+class OpenAiFunctionCell:
+    """
+    A class representing an OpenAI function calls that wraps around a DK BaseCell object, providing
+    a convenient interface to access its properties and execute it asynchronously.
+
+    Attributes:
+        cell (BaseCell): The underlying BaseCell instance.
+        name (str): The role name of the cell.
+        description (str): The description of the cell.
+    """
+
+    def __init__(self, cell: BaseCell):
+        """
+        Initialize the OpenAiFunctionCell with a given BaseCell.
+
+        Args:
+            cell (BaseCell): The BaseCell instance to wrap.
+        """
+        self.cell = cell
+        self.name = self.cell.get_role()
+        self.description = self.cell.get_description()
+
+    @property
+    def args(self) -> dict:
+        """
+        Retrieve the input arguments schema for the cell.
+
+        Returns:
+            Dict[str, Any]: A dictionary representing the input arguments schema.
+
+        Raises:
+            ValueError: If the input schema is not found or cannot be parsed.
+        """
+        if self.cell.get_input_format() is None:
+            raise ValueError(
+                f"No input schema found in the cell: {self.cell.get_role()}"
+            )
+
+        try:
+            input_schema: Dict[str, Any] = json.loads(self.cell.get_input_format())[
+                "properties"
+            ]
+            return input_schema
+        except json.JSONDecodeError as e:
+            raise ValueError(f"Invalid JSON input schema: {e}") from e
+
+    async def ainvoke(
+        self, input: Union[str, Dict[str, Any]]  # pylint: disable=W0622
+    ) -> Any:
+        """
+        Asynchronously invoke the cell with the given input.
+
+        Args:
+            input (Union[str, Dict[str, Any]]): The input data to pass to the cell. Can be a JSON string or a dictionary.
+
+        Returns:
+            Any: The output content from the cell after execution.
+
+        Raises:
+            TypeError: If the input cannot be serialized to JSON.
+            KeyError: If the expected output key 'content' is missing from the cell's response.
+        """
+        try:
+            input_json = json.dumps(input)
+        except TypeError as e:
+            print("herhreherhreherherhrh")
+            raise TypeError(f"Input could not be serialized to JSON: {e}") from e
+
+        cell_output = await self.cell.run(input_json)
+        if "content" not in cell_output:
+            raise KeyError(
+                f"The 'content' key is missing in the cell's output: {cell_output}"
+            )
+        return cell_output["content"]
+
+    def __repr__(self) -> str:
+        """
+        Represent the OpenAiFunctionCell as a string with its name.
+
+        Returns:
+            str: The string representation of the OpenAiFunctionCell.
+        """
+        return f"{self.name}()"
+
+
+def cell_to_openai_function(cells: List[BaseCell]) -> List[OpenAiFunctionCell]:
+    """
+    Convert a list of BaseCell instances to a list of OpenAiFunctionCell instances.
+
+    Args:
+        cells (List[BaseCell]): The list of BaseCell instances to convert.
+
+    Returns:
+        List[OpenAiFunctionCell]: A list of OpenAiFunctionCell instances.
+    """
+    return [OpenAiFunctionCell(cell) for cell in cells]
diff --git a/linter.sh b/linter.sh
new file mode 100755
index 0000000..c343c37
--- /dev/null
+++ b/linter.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Function to run commands with pipenv
+run_with_pipenv() {
+    pipenv run flake8 kinkernel
+    pipenv run black kinkernel --check --diff
+    pipenv run black kinkernel
+    pipenv run mypy kinkernel
+    pipenv run pylint kinkernel
+}
+
+# Function to run commands without pipenv
+run_without_pipenv() {
+    flake8 kinkernel
+    black kinkernel --check --diff
+    black kinkernel
+    mypy kinkernel
+    pylint kinkernel
+}
+
+# Ask the user if they want to use pipenv
+read -p "Do you want to use pipenv? (y/n): " use_pipenv
+
+# Check the user input
+if [[ "$use_pipenv" =~ ^[Yy]$ ]]; then
+    echo "Running commands with pipenv..."
+    run_with_pipenv
+elif [[ "$use_pipenv" =~ ^[Nn]$ ]]; then
+    echo "Running commands without pipenv..."
+    run_without_pipenv
+else
+    echo "Invalid input. Please answer 'y' or 'n'."
+    exit 1
+fi
+
+exit 0
\ No newline at end of file
diff --git a/py.typed b/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/setup.py b/setup.py
index 1e89518..da05dac 100644
--- a/setup.py
+++ b/setup.py
@@ -37,4 +37,5 @@
     ],
     keywords="kin-kernel cells autonomous-agents IoA DigitalKin",
     include_package_data=True,
+    package_data={"kinkernel": ["py.typed"]},
 )


Test patch:
diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/tools/test_openai_function_cell.py b/tests/tools/test_openai_function_cell.py
new file mode 100644
index 0000000..e18b322
--- /dev/null
+++ b/tests/tools/test_openai_function_cell.py
@@ -0,0 +1,152 @@
+import pytest
+import json
+
+# Import the module and classes/functions to be tested
+from kinkernel.tools.openai_function_cell import (
+    OpenAiFunctionCell,
+    cell_to_openai_function,
+)
+
+
+# Mock BaseCell to avoid actual API calls
+class MockBaseCell:
+    def get_role(self):
+        return "test_role"
+
+    def get_description(self):
+        return "test_description"
+
+    def get_input_format(self):
+        return json.dumps(
+            {"properties": {"arg1": {"type": "string"}, "arg2": {"type": "number"}}}
+        )
+
+    async def run(self, input_json):
+        return {"content": f"Processed {input_json}"}
+
+
+@pytest.fixture
+def base_cell():
+    return MockBaseCell()
+
+
+@pytest.fixture
+def open_ai_function_cell(base_cell):
+    return OpenAiFunctionCell(base_cell)
+
+
+def test_openai_function_cell_init(base_cell):
+    cell = OpenAiFunctionCell(base_cell)
+    assert cell.name == "test_role"
+    assert cell.description == "test_description"
+
+
+def test_openai_function_cell_args(open_ai_function_cell):
+    expected_args = {"arg1": {"type": "string"}, "arg2": {"type": "number"}}
+    assert open_ai_function_cell.args == expected_args
+
+
+@pytest.mark.asyncio
+async def test_openai_function_cell_ainvoke(open_ai_function_cell):
+    input_data = {"arg1": "value1", "arg2": 42}
+    result = await open_ai_function_cell.ainvoke(input_data)
+    assert result == f"Processed {json.dumps(input_data)}"
+
+
+def test_cell_to_openai_function(base_cell):
+    cells = [base_cell, base_cell]
+    openai_function_cells = cell_to_openai_function(cells)
+    assert len(openai_function_cells) == 2
+    for func_cell in openai_function_cells:
+        assert isinstance(func_cell, OpenAiFunctionCell)
+
+
+# #############################
+# No input format
+# #############################
+
+
+# Mock BaseCell to avoid actual API calls
+class MockBaseCellFail:
+    def get_role(self):
+        return "test_role"
+
+    def get_description(self):
+        return "test_description"
+
+    def get_input_format(self):
+        # Return None to simulate a missing input format
+        return None
+
+    async def run(self, input_json):
+        # Simulate different responses based on input
+        if input_json == json.dumps({"error": "simulate"}):
+            return {}  # Missing 'content' key
+        return {"content": f"Processed {input_json}"}
+
+
+@pytest.fixture
+def base_cell_fail():
+    return MockBaseCellFail()
+
+
+@pytest.fixture
+def open_ai_function_cell_fail(base_cell_fail):
+    return OpenAiFunctionCell(base_cell_fail)
+
+
+def test_openai_function_cell_missing_args(open_ai_function_cell_fail):
+    with pytest.raises(ValueError) as excinfo:
+        _ = open_ai_function_cell_fail.args
+    assert "No input schema found" in str(excinfo.value)
+
+
+@pytest.mark.asyncio
+async def test_openai_function_cell_ainvoke_error(open_ai_function_cell_fail):
+    with pytest.raises(KeyError) as excinfo:
+        await open_ai_function_cell_fail.ainvoke({"error": "simulate"})
+    assert "The 'content' key is missing" in str(excinfo.value)
+
+
+@pytest.mark.asyncio
+async def test_openai_function_cell_ainvoke_serialization_error(
+    open_ai_function_cell_fail,
+):
+    with pytest.raises(TypeError) as excinfo:
+        # Pass an object that cannot be serialized to JSON
+        await open_ai_function_cell_fail.ainvoke(lambda x: x)
+    assert "Input could not be serialized to JSON:" in str(excinfo.value)
+
+
+def test_repr(open_ai_function_cell_fail):
+    assert repr(open_ai_function_cell_fail) == "test_role()"
+
+
+# #############################
+# Invalid input format
+# #############################
+
+
+# Extend the MockBaseCell to simulate a JSON decoding error
+class MockBaseCellWithInvalidJSON(MockBaseCell):
+    def get_input_format(self):
+        # Return an invalid JSON string
+        return "{invalid_json}"
+
+
+@pytest.fixture
+def base_cell_with_invalid_json():
+    return MockBaseCellWithInvalidJSON()
+
+
+@pytest.fixture
+def open_ai_function_cell_with_invalid_json(base_cell_with_invalid_json):
+    return OpenAiFunctionCell(base_cell_with_invalid_json)
+
+
+def test_openai_function_cell_invalid_json_args(
+    open_ai_function_cell_with_invalid_json,
+):
+    with pytest.raises(ValueError) as excinfo:
+        _ = open_ai_function_cell_with_invalid_json.args
+    assert "Invalid JSON input schema" in str(excinfo.value)