From 61d87f2b4812febb6cad3a324d22b63b74fb962c Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 23 Jan 2025 21:24:58 -0800 Subject: [PATCH 01/15] readme take one --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d380ec2..9489de9 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,69 @@ -# Cleanlab Codex + + +# Cleanlab Codex - Closing the AI Knowledge Gap [![PyPI - Version](https://img.shields.io/pypi/v/cleanlab-codex.svg)](https://pypi.org/project/cleanlab-codex) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cleanlab-codex.svg)](https://pypi.org/project/cleanlab-codex) ------ +Codex enables you to seamlessly leverage knowledge from Subject Matter Experts (SMEs) to improve your RAG/Agentic applications. -## Table of Contents +The `cleanlab-codex` library provides a simple interface to integrate Codex's capabilities into your RAG application. +See immediate impact with just a few lines of code! -- [Installation](#installation) -- [License](#license) +## Demo -## Installation +Install the package: ```console pip install cleanlab-codex ``` +Integrating Codex into your RAG application as a tool is as simple as: + +```python +from cleanlab_codex import CodexTool + +def rag(question, system_prompt, tools) -> str: + """Your RAG/Agentic code here""" + ... + +# Initialize the Codex tool +codex_tool = CodexTool.from_access_key("your-access-key") + +# Update your system prompt to include information on how to use the Codex tool +system_prompt = f"""Answer the user's Question based on the following Context. If the Context doesn't adequately address the Question, use the {codex_tool.tool_name} tool to ask an outside expert.""" + +# Convert the Codex tool to a framework-specific tool +framework_specific_codex_tool = codex_tool.to__tool() # i.e. codex_tool.to_llamaindex_tool(), codex_tool.to_openai_tool(), etc. + +# Pass the Codex tool to your RAG/Agentic framework +response = rag(question, system_prompt, [framework_specific_codex_tool]) +``` + +(Note: exact code will depend on the RAG/Agentic framework you are using) + + + +## Why Codex? +- **Identify Knowledge Gaps**: Codex captures knowledge gaps in your application so that you can easily identify which questions require expert input. +- **Efficiently Leverage SMEs**: Codex ensures the SMEs see the most critical knowledge gaps first. +- **Easy Integration**: Integrate Codex into your RAG/Agentic application with just a few lines of code. +- **Immediate Impact**: SME responses instantly enhance your AI applications. + +## How does Codex interact with my AI application? + + + +## What impact will I see? + + +## Documentation + +Comprehensive documentation along with tutorials and examples can be found [here](https://help.cleanlab.ai/codex). + +## Contributing + + ## License `cleanlab-codex` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. From e71f89f3a879101463b6bbc60aefb6885e6ac40a Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 23 Jan 2025 21:25:06 -0800 Subject: [PATCH 02/15] docs update wip --- src/cleanlab_codex/codex.py | 31 +++++++++------ src/cleanlab_codex/codex_tool.py | 59 +++++++++++++++++++++++----- src/cleanlab_codex/internal/utils.py | 8 +++- 3 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index c16fe8b..7316986 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -1,18 +1,23 @@ +"""Client for Cleanlab Codex.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING as _TYPE_CHECKING, Optional from cleanlab_codex.internal.project import create_project, query_project from cleanlab_codex.internal.utils import init_codex_client -if TYPE_CHECKING: +if _TYPE_CHECKING: from cleanlab_codex.types.entry import Entry, EntryCreate from cleanlab_codex.types.organization import Organization class Codex: """ - A client to interact with Cleanlab Codex. + Client for interacting with Cleanlab Codex. In order to use this client, make sure you have an account at [codex.cleanlab.ai](https://codex.cleanlab.ai). + + We recommend using the [Web UI](https://codex.cleanlab.ai) to [set up Codex projects](TODO: link to docs) and then using one of our abstractions around the client such as [`CodexTool`](/reference/python/codex_tool) to integrate Codex into your RAG/Agentic system. + This client can be used to programmatically set up Codex projects. The [`query`](#method-query) method can also be used directly if none of our existing abstractions are sufficient for your use case. """ def __init__(self, key: str): @@ -34,7 +39,7 @@ def list_organizations(self) -> list[Organization]: """List the organizations the authenticated user is a member of. Returns: - list[Organization]: A list of organizations the authenticated user is a member of. + list[[Organization]]: A list of organizations the authenticated user is a member of. See [`Organization`](/reference/python/codex_types#class-organization) for more information. Raises: AuthenticationError: If the client is not authenticated with a user-level API Key. @@ -47,7 +52,7 @@ def create_project(self, name: str, organization_id: str, description: Optional[ Args: name (str): The name of the project. organization_id (str): The ID of the organization to create the project in. Must be authenticated as a member of this organization. - description (:obj:`str`, optional): The description of the project. + description (str, optional): The description of the project. Returns: int: The ID of the created project. @@ -63,7 +68,7 @@ def add_entries(self, entries: list[EntryCreate], project_id: str) -> None: """Add a list of entries to the Codex project. Args: - entries (list[EntryCreate]): The entries to add to the Codex project. + entries (list[EntryCreate]): The entries to add to the Codex project. See [`EntryCreate`](/reference/python/codex_types#class-entrycreate). project_id (int): The ID of the project to add the entries to. Raises: @@ -84,7 +89,7 @@ def create_project_access_key( Args: project_id (int): The ID of the project to create the access key for. access_key_name (str): The name of the access key. - access_key_description (:obj:`str`, optional): The description of the access key. + access_key_description (str, optional): The description of the access key. Returns: str: The access key token. @@ -99,7 +104,7 @@ def query( self, question: str, *, - project_id: Optional[str] = None, # TODO: update to uuid once project IDs are changed to UUIDs + project_id: Optional[str] = None, fallback_answer: Optional[str] = None, read_only: bool = False, ) -> tuple[Optional[str], Optional[Entry]]: @@ -107,17 +112,17 @@ def query( Args: question (str): The question to ask the Codex API. - project_id (:obj:`int`, optional): The ID of the project to query. + project_id (int, optional): The ID of the project to query. If the client is authenticated with a user-level API Key, this is required. If the client is authenticated with a project-level Access Key, this is optional. The client will use the Access Key's project ID by default. - fallback_answer (:obj:`str`, optional): Optional fallback answer to return if Codex is unable to answer the question. - read_only (:obj:`bool`, optional): Whether to query the Codex API in read-only mode. If True, the question will not be added to the Codex project for SME review. + fallback_answer (str, optional): Optional fallback answer to return if Codex is unable to answer the question. + read_only (bool, optional): Whether to query the Codex API in read-only mode. If True, the question will not be added to the Codex project for SME review. This can be useful for testing purposes before when setting up your project configuration. Returns: tuple[Optional[str], Optional[Entry]]: A tuple representing the answer for the query and the existing or new entry in the Codex project. - If Codex is able to answer the question, the first element will be the answer returned by Codex and the second element will be the existing entry in the Codex project. - If Codex is unable to answer the question, the first element will be `fallback_answer` if provided, otherwise None, and the second element will be a new entry in the Codex project. + If Codex is able to answer the question, the first element will be the answer returned by Codex and the second element will be the existing [`Entry`](/reference/python/codex_types#class-entry) in the Codex project. + If Codex is unable to answer the question, the first element will be `fallback_answer` if provided, otherwise None. The second element will be a new [`Entry`](/reference/python/codex_types#class-entry) in the Codex project. """ return query_project( client=self._client, diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index 92c3282..6f68171 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -1,3 +1,5 @@ +"""Tool abstraction for Cleanlab Codex.""" + from __future__ import annotations from typing import Any, ClassVar, Optional @@ -38,7 +40,16 @@ def from_access_key( project_id: Optional[str] = None, fallback_answer: Optional[str] = DEFAULT_FALLBACK_ANSWER, ) -> CodexTool: - """Creates a CodexTool from an access key. The project ID that the CodexTool will use is the one that is associated with the access key.""" + """Creates a CodexTool from an access key. The project ID that the CodexTool will use is the one that is associated with the access key. + + Args: + access_key (str): The access key for the Codex project. + project_id (str, optional): The ID of the project to use. If not provided, the project ID will be inferred from the access key. If provided, the project ID must be the ID of the project that the access key is associated with. + fallback_answer (str, optional): The fallback answer to use if the Codex project cannot answer the question. + + Returns: + CodexTool: The CodexTool. + """ return cls( codex_client=Codex(key=access_key), project_id=project_id, @@ -56,6 +67,14 @@ def from_client( """Creates a CodexTool from a Codex client. If the Codex client is initialized with a project access key, the CodexTool will use the project ID that is associated with the access key. If the Codex client is initialized with a user API key, a project ID must be provided. + + Args: + codex_client (Codex): The Codex client to use. + project_id (str, optional): The ID of the project to use. If not provided and the Codex client is authenticated with a project-level access key, the project ID will be inferred from the access key. + fallback_answer (str, optional): The fallback answer to use if the Codex project cannot answer the question. + + Returns: + CodexTool: The CodexTool. """ return cls( codex_client=codex_client, @@ -65,17 +84,31 @@ def from_client( @property def tool_name(self) -> str: - """The name to use for the tool when passing to an LLM.""" + """The name to use for the tool when passing to an LLM. This is the name the LLM will use when determining whether to call the tool. + + Note: We recommend using the default tool name which we've benchmarked. Only override this if you have a specific reason.""" return self._tool_name + @tool_name.setter + def tool_name(self, value: str) -> None: + """Sets the name to use for the tool when passing to an LLM.""" + self._tool_name = value + @property def tool_description(self) -> str: - """The description to use for the tool when passing to an LLM.""" + """The description to use for the tool when passing to an LLM. This is the description that the LLM will see when determining whether to call the tool. + + Note: We recommend using the default tool description which we've benchmarked. Only override this if you have a specific reason.""" return self._tool_description + @tool_description.setter + def tool_description(self, value: str) -> None: + """Sets the description to use for the tool when passing to an LLM.""" + self._tool_description = value + @property def fallback_answer(self) -> Optional[str]: - """The fallback answer to use if the Codex project cannot answer the question.""" + """The fallback answer to use if the Codex project cannot answer the question. This will be returned from by the tool if the Codex project does not have an answer to the question.""" return self._fallback_answer @fallback_answer.setter @@ -90,12 +123,14 @@ def query(self, question: str) -> Optional[str]: question: The question to ask the advisor. This should be the same as the original user question, except in cases where the user question is missing information that could be additionally clarified. Returns: - The answer to the question, or None if the answer is not available. + The answer to the question if available. If no answer is available, the fallback answer is returned if provided, otherwise None is returned. """ - return self._codex_client.query(question, project_id=self._project_id, fallback_answer=self._fallback_answer)[0] + return self._codex_client.query( + question, project_id=self._project_id, fallback_answer=self._fallback_answer + )[0] def to_openai_tool(self) -> dict[str, Any]: - """Converts the tool to an OpenAI tool.""" + """Converts the tool to an [OpenAI tool](https://platform.openai.com/docs/guides/function-calling#defining-functions).""" from cleanlab_codex.utils import format_as_openai_tool return format_as_openai_tool( @@ -106,7 +141,10 @@ def to_openai_tool(self) -> dict[str, Any]: ) def to_smolagents_tool(self) -> Any: - """Converts the tool to a smolagents tool.""" + """Converts the tool to a [smolagents tool](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool). + + Note: You must have the [`smolagents` library installed](https://github.com/huggingface/smolagents/tree/main?tab=readme-ov-file#quick-demo) to use this method. + """ from cleanlab_codex.utils.smolagents import CodexTool as SmolagentsCodexTool return SmolagentsCodexTool( @@ -117,7 +155,10 @@ def to_smolagents_tool(self) -> Any: ) def to_llamaindex_tool(self) -> Any: - """Converts the tool to a LlamaIndex FunctionTool.""" + """Converts the tool to a [LlamaIndex FunctionTool](https://docs.llamaindex.ai/en/stable/module_guides/deploying/agents/tools/#functiontool). + + Note: You must have the [`llama-index` library installed](https://docs.llamaindex.ai/en/stable/getting_started/installation/) to use this method. + """ from llama_index.core.tools import FunctionTool from cleanlab_codex.utils.llamaindex import get_function_schema diff --git a/src/cleanlab_codex/internal/utils.py b/src/cleanlab_codex/internal/utils.py index 6b13196..ea6d7f6 100644 --- a/src/cleanlab_codex/internal/utils.py +++ b/src/cleanlab_codex/internal/utils.py @@ -11,6 +11,10 @@ def is_access_key(key: str) -> bool: def init_codex_client(key: str) -> _Codex: if is_access_key(key): - return _Codex(access_key=key) + client = _Codex(access_key=key) + client.projects.access_keys.retrieve_project_id() # check if the access key is valid + return client - return _Codex(api_key=key) + client = _Codex(api_key=key) + client.users.myself.api_key.retrieve() # check if the api key is valid + return client From 31488ca6def8295f2486bdb8fd8e6f5dad1f097f Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Jan 2025 11:28:44 -0800 Subject: [PATCH 03/15] temp update codex-sdk dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b036139..1e033a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ - "codex-sdk @ git+ssh://git@github.com/cleanlab/codex-python.git@8121c341af5ecca80a946ae3ba7e0f74036ca8d9", # TODO: update this once we've published SDK + "codex-sdk @ git+ssh://git@github.com/stainless-sdks/codex-python.git@axl1313/dev", # TODO: update this once we've published SDK "pydantic>=1.9.0, <3", ] From 0feb579bdaf639fbdeed59f89cc1d504c5face15 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Jan 2025 16:49:45 -0800 Subject: [PATCH 04/15] update codex sdk dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e033a2..db983cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ - "codex-sdk @ git+ssh://git@github.com/stainless-sdks/codex-python.git@axl1313/dev", # TODO: update this once we've published SDK + "codex-sdk==0.1.0a8", "pydantic>=1.9.0, <3", ] From 48dd42123dec23b7e232604a0aa2751d462e8d32 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Jan 2025 17:22:40 -0800 Subject: [PATCH 05/15] fix format --- src/cleanlab_codex/codex.py | 5 +++-- src/cleanlab_codex/codex_tool.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index 7316986..eb90990 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING as _TYPE_CHECKING, Optional +from typing import TYPE_CHECKING as _TYPE_CHECKING +from typing import Optional from cleanlab_codex.internal.project import create_project, query_project from cleanlab_codex.internal.utils import init_codex_client @@ -16,7 +17,7 @@ class Codex: """ Client for interacting with Cleanlab Codex. In order to use this client, make sure you have an account at [codex.cleanlab.ai](https://codex.cleanlab.ai). - We recommend using the [Web UI](https://codex.cleanlab.ai) to [set up Codex projects](TODO: link to docs) and then using one of our abstractions around the client such as [`CodexTool`](/reference/python/codex_tool) to integrate Codex into your RAG/Agentic system. + We recommend using the [Web UI](https://codex.cleanlab.ai) to [set up Codex projects](TODO: link to docs) and then using one of our abstractions around the client such as [`CodexTool`](/reference/python/codex_tool) to integrate Codex into your RAG/Agentic system. This client can be used to programmatically set up Codex projects. The [`query`](#method-query) method can also be used directly if none of our existing abstractions are sufficient for your use case. """ diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index 6f68171..e3e8890 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -125,9 +125,7 @@ def query(self, question: str) -> Optional[str]: Returns: The answer to the question if available. If no answer is available, the fallback answer is returned if provided, otherwise None is returned. """ - return self._codex_client.query( - question, project_id=self._project_id, fallback_answer=self._fallback_answer - )[0] + return self._codex_client.query(question, project_id=self._project_id, fallback_answer=self._fallback_answer)[0] def to_openai_tool(self) -> dict[str, Any]: """Converts the tool to an [OpenAI tool](https://platform.openai.com/docs/guides/function-calling#defining-functions).""" From 42b40d350de2f43d847a3f8282133d6536365368 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Jan 2025 17:28:55 -0800 Subject: [PATCH 06/15] fix tests --- tests/internal/test_utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/internal/test_utils.py b/tests/internal/test_utils.py index 8448796..6c6cd23 100644 --- a/tests/internal/test_utils.py +++ b/tests/internal/test_utils.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch from cleanlab_codex.internal.utils import init_codex_client, is_access_key @@ -12,14 +12,18 @@ def test_is_access_key(): def test_init_codex_client_access_key(): - with patch("cleanlab_codex.internal.utils._Codex", autospec=True) as mock_codex: + mock_client = MagicMock() + with patch("cleanlab_codex.internal.utils._Codex", autospec=True, return_value=mock_client) as mock_init: + mock_client.projects.access_keys.retrieve_project_id.return_value = "test_project_id" client = init_codex_client(DUMMY_ACCESS_KEY) - mock_codex.assert_called_once_with(access_key=DUMMY_ACCESS_KEY) + mock_init.assert_called_once_with(access_key=DUMMY_ACCESS_KEY) assert client is not None def test_init_codex_client_api_key(): - with patch("cleanlab_codex.internal.utils._Codex", autospec=True) as mock_codex: + mock_client = MagicMock() + with patch("cleanlab_codex.internal.utils._Codex", autospec=True, return_value=mock_client) as mock_init: + mock_client.users.myself.api_key.retrieve.return_value = "test_project_id" client = init_codex_client(DUMMY_API_KEY) - mock_codex.assert_called_once_with(api_key=DUMMY_API_KEY) + mock_init.assert_called_once_with(api_key=DUMMY_API_KEY) assert client is not None From f3210fee4292c3bf2eb4f36bc5f28c4cca419331 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Jan 2025 17:30:08 -0800 Subject: [PATCH 07/15] remove extra brackets --- src/cleanlab_codex/codex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index eb90990..854f9ba 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -40,7 +40,7 @@ def list_organizations(self) -> list[Organization]: """List the organizations the authenticated user is a member of. Returns: - list[[Organization]]: A list of organizations the authenticated user is a member of. See [`Organization`](/reference/python/codex_types#class-organization) for more information. + list[Organization]: A list of organizations the authenticated user is a member of. See [`Organization`](/reference/python/codex_types#class-organization) for more information. Raises: AuthenticationError: If the client is not authenticated with a user-level API Key. From 3ccdbcf1036f8ae7e96fc1bb8a8c7babe97c8028 Mon Sep 17 00:00:00 2001 From: Angela Date: Fri, 31 Jan 2025 15:48:50 -0800 Subject: [PATCH 08/15] update docstrings --- src/cleanlab_codex/codex_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index e3e8890..2ce09ff 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -128,7 +128,8 @@ def query(self, question: str) -> Optional[str]: return self._codex_client.query(question, project_id=self._project_id, fallback_answer=self._fallback_answer)[0] def to_openai_tool(self) -> dict[str, Any]: - """Converts the tool to an [OpenAI tool](https://platform.openai.com/docs/guides/function-calling#defining-functions).""" + """Converts the tool to the expected format for an [OpenAI function tool](https://platform.openai.com/docs/guides/function-calling). + See more information on defining functions for OpenAI tool calls [here](https://platform.openai.com/docs/guides/function-calling#defining-functions).""" from cleanlab_codex.utils import format_as_openai_tool return format_as_openai_tool( From 097360865ad0b13e2d7e9358a1bce50f111eda67 Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 3 Feb 2025 10:07:24 -0800 Subject: [PATCH 09/15] address comments --- README.md | 2 -- src/cleanlab_codex/codex_tool.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 3d96aa9..c218b88 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - - # Cleanlab Codex - Closing the AI Knowledge Gap [![Build Status](https://github.com/cleanlab/cleanlab-codex/actions/workflows/ci.yml/badge.svg)](https://github.com/cleanlab/cleanlab-codex/actions/workflows/ci.yml) [![PyPI - Version](https://img.shields.io/pypi/v/cleanlab-codex.svg)](https://pypi.org/project/cleanlab-codex) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cleanlab-codex.svg)](https://pypi.org/project/cleanlab-codex) diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index 2ce09ff..a45be98 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -108,7 +108,7 @@ def tool_description(self, value: str) -> None: @property def fallback_answer(self) -> Optional[str]: - """The fallback answer to use if the Codex project cannot answer the question. This will be returned from by the tool if the Codex project does not have an answer to the question.""" + """The fallback answer to use if the Codex project cannot answer the question. This will be returned by the tool if the Codex project does not have an answer to the question.""" return self._fallback_answer @fallback_answer.setter From 627fd552b3e298a2e6dddd5a375f9d14d09b153a Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 4 Feb 2025 15:05:51 -0800 Subject: [PATCH 10/15] add docstrings for types --- src/cleanlab_codex/codex.py | 3 +- src/cleanlab_codex/internal/organization.py | 12 ++++++++ src/cleanlab_codex/internal/project.py | 7 ++--- src/cleanlab_codex/types/entry.py | 34 +++++++++++++++++++-- src/cleanlab_codex/types/organization.py | 22 ++++++++++++- src/cleanlab_codex/types/project.py | 24 ++++++++++++++- 6 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 src/cleanlab_codex/internal/organization.py diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index 65bafd0..79e5413 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING as _TYPE_CHECKING from typing import Optional +from cleanlab_codex.internal.organization import list_organizations from cleanlab_codex.internal.project import create_project, query_project from cleanlab_codex.internal.utils import init_codex_client @@ -45,7 +46,7 @@ def list_organizations(self) -> list[Organization]: Raises: AuthenticationError: If the client is not authenticated with a user-level API Key. """ - return self._client.users.myself.organizations.list().organizations + return list_organizations(self._client) def create_project(self, name: str, organization_id: str, description: Optional[str] = None) -> str: """Create a new Codex project. diff --git a/src/cleanlab_codex/internal/organization.py b/src/cleanlab_codex/internal/organization.py new file mode 100644 index 0000000..1dfecb3 --- /dev/null +++ b/src/cleanlab_codex/internal/organization.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from codex import Codex as _Codex + +from cleanlab_codex.types.organization import Organization + + +def list_organizations(client: _Codex) -> list[Organization]: + return [Organization.model_validate(org) for org in client.users.myself.organizations.list().organizations] diff --git a/src/cleanlab_codex/internal/project.py b/src/cleanlab_codex/internal/project.py index 0b421bd..c4e17a0 100644 --- a/src/cleanlab_codex/internal/project.py +++ b/src/cleanlab_codex/internal/project.py @@ -5,8 +5,7 @@ if TYPE_CHECKING: from codex import Codex as _Codex - from cleanlab_codex.types.entry import Entry - +from cleanlab_codex.types.entry import Entry from cleanlab_codex.types.project import ProjectConfig @@ -40,7 +39,7 @@ def query_project( elif project_id is None: raise MissingProjectIdError - query_res = client.projects.entries.query(project_id, question=question) + query_res = Entry.model_validate(client.projects.entries.query(project_id, question=question)) if query_res is not None: if query_res.answer is not None: return query_res.answer, query_res @@ -48,7 +47,7 @@ def query_project( return fallback_answer, query_res if not read_only: - created_entry = client.projects.entries.add_question(project_id, question=question) + created_entry = Entry.model_validate(client.projects.entries.add_question(project_id, question=question)) return fallback_answer, created_entry return fallback_answer, None diff --git a/src/cleanlab_codex/types/entry.py b/src/cleanlab_codex/types/entry.py index 76bf59e..1756322 100644 --- a/src/cleanlab_codex/types/entry.py +++ b/src/cleanlab_codex/types/entry.py @@ -1,4 +1,34 @@ -from codex.types.projects.entry import Entry -from codex.types.projects.entry_create_params import EntryCreateParams as EntryCreate +"""Types for Codex entries.""" + +from codex.types.projects.entry import Entry as _Entry +from codex.types.projects.entry_create_params import EntryCreateParams + + +class EntryCreate(EntryCreateParams): + """ + Input type for creating a new Entry in a Codex project. Use this class to add a new Question-Answer pair to a project. + + ```python + class EntryCreate: + question: str + answer: Optional[str] = None + ``` + """ + + +class Entry(_Entry): + """ + Type representing an Entry in a Codex project. This is the complete data structure returned from the Codex API, including system-generated fields like ID and timestamps. + + ```python + class Entry: + id: str + question: str + answer: Optional[str] = None + created_at: datetime + answer_at: Optional[datetime] = None + ``` + """ + __all__ = ["EntryCreate", "Entry"] diff --git a/src/cleanlab_codex/types/organization.py b/src/cleanlab_codex/types/organization.py index 0848c67..d2436d2 100644 --- a/src/cleanlab_codex/types/organization.py +++ b/src/cleanlab_codex/types/organization.py @@ -1,3 +1,23 @@ -from codex.types.users.myself.user_organizations_schema import Organization +"""Types for Codex organizations.""" + +from codex.types.users.myself.user_organizations_schema import Organization as _Organization + + +class Organization(_Organization): + """ + Type representing an organization in Codex. + + ```python + class Organization: + id: str + name: str + payment_status: Literal[ + "NULL", "FIRST_OVERAGE_LENIENT", "SECOND_OVERAGE_USAGE_BLOCKED" + ] + created_at: datetime + updated_at: datetime + ``` + """ + __all__ = ["Organization"] diff --git a/src/cleanlab_codex/types/project.py b/src/cleanlab_codex/types/project.py index d6f8745..70658f3 100644 --- a/src/cleanlab_codex/types/project.py +++ b/src/cleanlab_codex/types/project.py @@ -1,3 +1,25 @@ -from codex.types.project_create_params import Config as ProjectConfig +"""Types for Codex projects.""" + +from codex.types.project_create_params import Config + + +class ProjectConfig(Config): + """ + Type representing options that can be configured for a Codex project. + + ```python + class ProjectConfig(TypedDict): + max_distance: float = 0.1 + ``` + --- + + #### property max_distance + + Distance threshold used to determine if two questions are similar when querying existing Entries in a project. + The metric used is cosine distance. Valid threshold values range from 0 (identical vectors) to 1 (orthogonal vectors). + While cosine distance can extend to 2 (opposite vectors), we limit this value to 1 since finding matches that are less similar than "unrelated" (orthogonal) + content would not improve results of the system querying the Codex project. + """ + __all__ = ["ProjectConfig"] From d24cd4d8901862cc26a70c00ef3c1aa167954402 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 4 Feb 2025 15:17:13 -0800 Subject: [PATCH 11/15] link to web app tutorial --- src/cleanlab_codex/codex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index 79e5413..e9f400e 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -18,7 +18,7 @@ class Codex: """ Client for interacting with Cleanlab Codex. In order to use this client, make sure you have an account at [codex.cleanlab.ai](https://codex.cleanlab.ai). - We recommend using the [Web UI](https://codex.cleanlab.ai) to [set up Codex projects](TODO: link to docs) and then using one of our abstractions around the client such as [`CodexTool`](/reference/python/codex_tool) to integrate Codex into your RAG/Agentic system. + We recommend using the [Web UI](https://codex.cleanlab.ai) to [set up Codex projects](/codex/sme_tutorials/getting_started) and then using one of our abstractions around the client such as [`CodexTool`](/reference/python/codex_tool) to integrate Codex into your RAG/Agentic system. This client can be used to programmatically set up Codex projects. The [`query`](#method-query) method can also be used directly if none of our existing abstractions are sufficient for your use case. """ From 8fbbec847aeb562e7c8236bfd2dc0a6cf23a1c04 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 4 Feb 2025 15:20:06 -0800 Subject: [PATCH 12/15] fix links, typo --- src/cleanlab_codex/codex.py | 8 ++++---- src/cleanlab_codex/codex_tool.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cleanlab_codex/codex.py b/src/cleanlab_codex/codex.py index e9f400e..6554d16 100644 --- a/src/cleanlab_codex/codex.py +++ b/src/cleanlab_codex/codex.py @@ -41,7 +41,7 @@ def list_organizations(self) -> list[Organization]: """List the organizations the authenticated user is a member of. Returns: - list[Organization]: A list of organizations the authenticated user is a member of. See [`Organization`](/reference/python/codex_types#class-organization) for more information. + list[Organization]: A list of organizations the authenticated user is a member of. See [`Organization`](/reference/python/types.organization#class-organization) for more information. Raises: AuthenticationError: If the client is not authenticated with a user-level API Key. @@ -70,7 +70,7 @@ def add_entries(self, entries: list[EntryCreate], project_id: str) -> None: """Add a list of entries to the Codex project. Args: - entries (list[EntryCreate]): The entries to add to the Codex project. See [`EntryCreate`](/reference/python/codex_types#class-entrycreate). + entries (list[EntryCreate]): The entries to add to the Codex project. See [`EntryCreate`](/reference/python/types.entry#class-entrycreate). project_id (int): The ID of the project to add the entries to. Raises: @@ -123,8 +123,8 @@ def query( Returns: tuple[Optional[str], Optional[Entry]]: A tuple representing the answer for the query and the existing or new entry in the Codex project. - If Codex is able to answer the question, the first element will be the answer returned by Codex and the second element will be the existing [`Entry`](/reference/python/codex_types#class-entry) in the Codex project. - If Codex is unable to answer the question, the first element will be `fallback_answer` if provided, otherwise None. The second element will be a new [`Entry`](/reference/python/codex_types#class-entry) in the Codex project. + If Codex is able to answer the question, the first element will be the answer returned by Codex and the second element will be the existing [`Entry`](/reference/python/types.entry#class-entry) in the Codex project. + If Codex is unable to answer the question, the first element will be `fallback_answer` if provided, otherwise None. The second element will be a new [`Entry`](/reference/python/types.entry#class-entry) in the Codex project. """ return query_project( client=self._client, diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index a45be98..3d63169 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -11,7 +11,7 @@ class CodexTool: """A tool that connects to a Codex project to answer questions.""" _tool_name = "ask_advisor" - _tool_description = "Asks an all-knowing advisor this query in cases where it cannot be answered from the provided Context. If the answer is avalible, this returns None." + _tool_description = "Asks an all-knowing advisor this query in cases where it cannot be answered from the provided Context. If the answer is available, this returns None." _tool_properties: ClassVar[dict[str, Any]] = { "question": { "type": "string", From 1f685206b2944a04f85914639e4016c97030a42b Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 4 Feb 2025 15:21:02 -0800 Subject: [PATCH 13/15] update smolagents link --- src/cleanlab_codex/codex_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cleanlab_codex/codex_tool.py b/src/cleanlab_codex/codex_tool.py index 3d63169..dcae7a4 100644 --- a/src/cleanlab_codex/codex_tool.py +++ b/src/cleanlab_codex/codex_tool.py @@ -142,7 +142,7 @@ def to_openai_tool(self) -> dict[str, Any]: def to_smolagents_tool(self) -> Any: """Converts the tool to a [smolagents tool](https://huggingface.co/docs/smolagents/reference/tools#smolagents.Tool). - Note: You must have the [`smolagents` library installed](https://github.com/huggingface/smolagents/tree/main?tab=readme-ov-file#quick-demo) to use this method. + Note: You must have the [`smolagents` library installed](https://github.com/huggingface/smolagents) to use this method. """ from cleanlab_codex.utils.smolagents import CodexTool as SmolagentsCodexTool From ae9a6d2fde30b3d1329f51f9d9e37e93703fd026 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 4 Feb 2025 15:23:10 -0800 Subject: [PATCH 14/15] remove readme todos --- README.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/README.md b/README.md index c218b88..a4ee6c5 100644 --- a/README.md +++ b/README.md @@ -43,24 +43,14 @@ response = rag(question, system_prompt, [framework_specific_codex_tool]) ## Why Codex? - **Identify Knowledge Gaps**: Codex captures knowledge gaps in your application so that you can easily identify which questions require expert input. -- **Efficiently Leverage SMEs**: Codex ensures the SMEs see the most critical knowledge gaps first. +- **Efficiently Leverage SMEs**: Codex ensures the SMEs see the most critical knowledge gaps first. - **Easy Integration**: Integrate Codex into your RAG/Agentic application with just a few lines of code. - **Immediate Impact**: SME responses instantly enhance your AI applications. -## How does Codex interact with my AI application? - - - -## What impact will I see? - - ## Documentation Comprehensive documentation along with tutorials and examples can be found [here](https://help.cleanlab.ai/codex). -## Contributing - - ## License `cleanlab-codex` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. From 395d0365770f7c2e11a7c82c179a251b050c731a Mon Sep 17 00:00:00 2001 From: Angela Date: Thu, 6 Feb 2025 14:21:22 -0800 Subject: [PATCH 15/15] generate class type annotations for docstrings --- src/cleanlab_codex/client.py | 2 +- src/cleanlab_codex/internal/sdk_client.py | 53 ++++++++++++++++++ src/cleanlab_codex/internal/utils.py | 67 +++++++++-------------- src/cleanlab_codex/project.py | 2 +- src/cleanlab_codex/types/entry.py | 36 +++++------- src/cleanlab_codex/types/organization.py | 26 ++++----- src/cleanlab_codex/types/project.py | 13 +++-- tests/internal/test_utils.py | 10 ++-- 8 files changed, 118 insertions(+), 91 deletions(-) create mode 100644 src/cleanlab_codex/internal/sdk_client.py diff --git a/src/cleanlab_codex/client.py b/src/cleanlab_codex/client.py index 0d08df7..2bd7920 100644 --- a/src/cleanlab_codex/client.py +++ b/src/cleanlab_codex/client.py @@ -6,7 +6,7 @@ from typing import Optional from cleanlab_codex.internal.organization import list_organizations -from cleanlab_codex.internal.utils import client_from_api_key +from cleanlab_codex.internal.sdk_client import client_from_api_key from cleanlab_codex.project import Project if _TYPE_CHECKING: diff --git a/src/cleanlab_codex/internal/sdk_client.py b/src/cleanlab_codex/internal/sdk_client.py new file mode 100644 index 0000000..f645bcb --- /dev/null +++ b/src/cleanlab_codex/internal/sdk_client.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import os +import re + +from codex import Codex as _Codex + +ACCESS_KEY_PATTERN = r"^sk-.*-.*$" + + +class MissingAuthKeyError(ValueError): + """Raised when no API key or access key is provided.""" + + def __str__(self) -> str: + return "No API key or access key provided" + + +def is_access_key(key: str) -> bool: + return re.match(ACCESS_KEY_PATTERN, key) is not None + + +def client_from_api_key(key: str | None = None) -> _Codex: + """ + Initialize a Codex SDK client using a user-level API key. + + Args: + key (str | None): The API key to use to authenticate the client. If not provided, the client will be authenticated using the `CODEX_API_KEY` environment variable. + + Returns: + _Codex: The initialized Codex client. + """ + if not (key := key or os.getenv("CODEX_API_KEY")): + raise MissingAuthKeyError + + client = _Codex(api_key=key) + client.users.myself.api_key.retrieve() # check if the api key is valid + return client + + +def client_from_access_key(key: str | None = None) -> _Codex: + """ + Initialize a Codex SDK client using a project-level access key. + + Args: + key (str | None): The access key to use to authenticate the client. If not provided, the client will be authenticated using the `CODEX_ACCESS_KEY` environment variable. + + Returns: + _Codex: The initialized Codex client. + """ + if not (key := key or os.getenv("CODEX_ACCESS_KEY")): + raise MissingAuthKeyError + + return _Codex(access_key=key) diff --git a/src/cleanlab_codex/internal/utils.py b/src/cleanlab_codex/internal/utils.py index f645bcb..1b3a11d 100644 --- a/src/cleanlab_codex/internal/utils.py +++ b/src/cleanlab_codex/internal/utils.py @@ -1,53 +1,36 @@ from __future__ import annotations -import os -import re +from typing_extensions import get_origin, get_type_hints, is_typeddict -from codex import Codex as _Codex -ACCESS_KEY_PATTERN = r"^sk-.*-.*$" +def generate_class_docstring(cls: type, name: str | None = None) -> str: + if is_typeddict(cls): + return docstring_from_type_hints(cls, name) + return docstring_from_annotations(cls, name) -class MissingAuthKeyError(ValueError): - """Raised when no API key or access key is provided.""" - def __str__(self) -> str: - return "No API key or access key provided" +def docstring_from_type_hints(cls: type, name: str | None = None) -> str: + formatted_type_hints = "\n ".join(f"{k}: {annotation_to_str(v)}" for k, v in get_type_hints(cls).items()) + return f""" +```python +class {name or cls.__name__}{is_typeddict(cls) and "(TypedDict)"}: + {formatted_type_hints} +``` +""" -def is_access_key(key: str) -> bool: - return re.match(ACCESS_KEY_PATTERN, key) is not None +def docstring_from_annotations(cls: type, name: str | None = None) -> str: + formatted_annotations = "\n ".join(f"{k}: {annotation_to_str(v)}" for k, v in cls.__annotations__.items()) + return f""" +```python +class {name or cls.__name__}: + {formatted_annotations} +``` +""" -def client_from_api_key(key: str | None = None) -> _Codex: - """ - Initialize a Codex SDK client using a user-level API key. - - Args: - key (str | None): The API key to use to authenticate the client. If not provided, the client will be authenticated using the `CODEX_API_KEY` environment variable. - - Returns: - _Codex: The initialized Codex client. - """ - if not (key := key or os.getenv("CODEX_API_KEY")): - raise MissingAuthKeyError - - client = _Codex(api_key=key) - client.users.myself.api_key.retrieve() # check if the api key is valid - return client - - -def client_from_access_key(key: str | None = None) -> _Codex: - """ - Initialize a Codex SDK client using a project-level access key. - - Args: - key (str | None): The access key to use to authenticate the client. If not provided, the client will be authenticated using the `CODEX_ACCESS_KEY` environment variable. - - Returns: - _Codex: The initialized Codex client. - """ - if not (key := key or os.getenv("CODEX_ACCESS_KEY")): - raise MissingAuthKeyError - - return _Codex(access_key=key) +def annotation_to_str(annotation: type) -> str: + if get_origin(annotation) is None: + return annotation.__name__ + return repr(annotation) diff --git a/src/cleanlab_codex/project.py b/src/cleanlab_codex/project.py index 6ad5d97..ea60326 100644 --- a/src/cleanlab_codex/project.py +++ b/src/cleanlab_codex/project.py @@ -9,7 +9,7 @@ from codex import AuthenticationError from cleanlab_codex.internal.project import query_project -from cleanlab_codex.internal.utils import client_from_access_key +from cleanlab_codex.internal.sdk_client import client_from_access_key from cleanlab_codex.types.project import ProjectConfig if _TYPE_CHECKING: diff --git a/src/cleanlab_codex/types/entry.py b/src/cleanlab_codex/types/entry.py index 1756322..8ca9525 100644 --- a/src/cleanlab_codex/types/entry.py +++ b/src/cleanlab_codex/types/entry.py @@ -3,32 +3,26 @@ from codex.types.projects.entry import Entry as _Entry from codex.types.projects.entry_create_params import EntryCreateParams +from cleanlab_codex.internal.utils import generate_class_docstring -class EntryCreate(EntryCreateParams): - """ - Input type for creating a new Entry in a Codex project. Use this class to add a new Question-Answer pair to a project. - ```python - class EntryCreate: - question: str - answer: Optional[str] = None - ``` - """ +class EntryCreate(EntryCreateParams): ... -class Entry(_Entry): - """ - Type representing an Entry in a Codex project. This is the complete data structure returned from the Codex API, including system-generated fields like ID and timestamps. +EntryCreate.__doc__ = f""" +Input type for creating a new Entry in a Codex project. Use this class to add a new Question-Answer pair to a project. - ```python - class Entry: - id: str - question: str - answer: Optional[str] = None - created_at: datetime - answer_at: Optional[datetime] = None - ``` - """ +{generate_class_docstring(EntryCreateParams, name=EntryCreate.__name__)} +""" +class Entry(_Entry): ... + + +Entry.__doc__ = f""" +Type representing an Entry in a Codex project. This is the complete data structure returned from the Codex API, including system-generated fields like ID and timestamps. + +{generate_class_docstring(_Entry, name=Entry.__name__)} +""" + __all__ = ["EntryCreate", "Entry"] diff --git a/src/cleanlab_codex/types/organization.py b/src/cleanlab_codex/types/organization.py index d2436d2..fe7b04e 100644 --- a/src/cleanlab_codex/types/organization.py +++ b/src/cleanlab_codex/types/organization.py @@ -1,23 +1,19 @@ """Types for Codex organizations.""" -from codex.types.users.myself.user_organizations_schema import Organization as _Organization +from codex.types.users.myself.user_organizations_schema import ( + Organization as _Organization, +) +from cleanlab_codex.internal.utils import generate_class_docstring -class Organization(_Organization): - """ - Type representing an organization in Codex. - ```python - class Organization: - id: str - name: str - payment_status: Literal[ - "NULL", "FIRST_OVERAGE_LENIENT", "SECOND_OVERAGE_USAGE_BLOCKED" - ] - created_at: datetime - updated_at: datetime - ``` - """ +class Organization(_Organization): ... +Organization.__doc__ = f""" +Type representing an organization in Codex. + +{generate_class_docstring(_Organization, name=Organization.__name__)} +""" + __all__ = ["Organization"] diff --git a/src/cleanlab_codex/types/project.py b/src/cleanlab_codex/types/project.py index 70658f3..e472f39 100644 --- a/src/cleanlab_codex/types/project.py +++ b/src/cleanlab_codex/types/project.py @@ -2,15 +2,16 @@ from codex.types.project_create_params import Config +from cleanlab_codex.internal.utils import generate_class_docstring -class ProjectConfig(Config): - """ + +class ProjectConfig(Config): ... + + +ProjectConfig.__doc__ = f""" Type representing options that can be configured for a Codex project. - ```python - class ProjectConfig(TypedDict): - max_distance: float = 0.1 - ``` + {generate_class_docstring(Config, name=ProjectConfig.__name__)} --- #### property max_distance diff --git a/tests/internal/test_utils.py b/tests/internal/test_utils.py index 5cafab9..2acb83c 100644 --- a/tests/internal/test_utils.py +++ b/tests/internal/test_utils.py @@ -3,7 +3,7 @@ import pytest -from cleanlab_codex.internal.utils import ( +from cleanlab_codex.internal.sdk_client import ( MissingAuthKeyError, client_from_access_key, client_from_api_key, @@ -21,7 +21,7 @@ def test_is_access_key() -> None: def test_client_from_access_key() -> None: mock_client = MagicMock() - with patch("cleanlab_codex.internal.utils._Codex", autospec=True, return_value=mock_client) as mock_init: + with patch("cleanlab_codex.internal.sdk_client._Codex", autospec=True, return_value=mock_client) as mock_init: mock_client.projects.access_keys.retrieve_project_id.return_value = "test_project_id" client = client_from_access_key(DUMMY_ACCESS_KEY) mock_init.assert_called_once_with(access_key=DUMMY_ACCESS_KEY) @@ -30,7 +30,7 @@ def test_client_from_access_key() -> None: def test_client_from_api_key() -> None: mock_client = MagicMock() - with patch("cleanlab_codex.internal.utils._Codex", autospec=True, return_value=mock_client) as mock_init: + with patch("cleanlab_codex.internal.sdk_client._Codex", autospec=True, return_value=mock_client) as mock_init: mock_client.users.myself.api_key.retrieve.return_value = "test_project_id" client = client_from_api_key(DUMMY_API_KEY) mock_init.assert_called_once_with(api_key=DUMMY_API_KEY) @@ -51,7 +51,7 @@ def test_client_from_access_key_env_var() -> None: with patch.dict(os.environ, {"CODEX_ACCESS_KEY": DUMMY_ACCESS_KEY}): mock_client = MagicMock() with patch( - "cleanlab_codex.internal.utils._Codex", + "cleanlab_codex.internal.sdk_client._Codex", autospec=True, return_value=mock_client, ) as mock_init: @@ -65,7 +65,7 @@ def test_client_from_api_key_env_var() -> None: with patch.dict(os.environ, {"CODEX_API_KEY": DUMMY_API_KEY}): mock_client = MagicMock() with patch( - "cleanlab_codex.internal.utils._Codex", + "cleanlab_codex.internal.sdk_client._Codex", autospec=True, return_value=mock_client, ) as mock_init: