Skip to content

Commit bdedb48

Browse files
Remove support for deprecated entries data model (#88)
* Remove support for deprecated entries data model * re-trigger ci
1 parent fa9cc5c commit bdedb48

File tree

6 files changed

+14
-278
lines changed

6 files changed

+14
-278
lines changed

src/cleanlab_codex/codex_tool.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from typing_extensions import Annotated
88

9-
from cleanlab_codex.internal.analytics import IntegrationType, _AnalyticsMetadata
109
from cleanlab_codex.project import Project
1110
from cleanlab_codex.utils.errors import MissingDependencyError
1211
from cleanlab_codex.utils.function import (
@@ -111,11 +110,16 @@ def query(
111110
Returns:
112111
The answer to the question if available. If no answer is available, this returns a fallback answer or None.
113112
"""
114-
return self._project.query(
115-
question=question,
116-
fallback_answer=self._fallback_answer,
117-
_analytics_metadata=_AnalyticsMetadata(integration_type=IntegrationType.TOOL),
118-
)[0]
113+
# We will cut codex-as-a-tool and all client docs in a follow-up PR. This is a temporary setting to avoid throwing errors
114+
return (
115+
self._project.validate(
116+
query=question,
117+
response=self._fallback_answer or "",
118+
context="",
119+
prompt="",
120+
).expert_answer
121+
or self._fallback_answer
122+
)
119123

120124
def to_openai_tool(self) -> dict[str, Any]:
121125
"""Converts the tool to the expected format for an [OpenAI function tool](https://platform.openai.com/docs/guides/function-calling).

src/cleanlab_codex/project.py

Lines changed: 2 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
from __future__ import annotations
44

5-
import warnings
65
from datetime import datetime
76
from typing import TYPE_CHECKING as _TYPE_CHECKING
8-
from typing import Any, Dict, List, Literal, Optional
7+
from typing import Dict, List, Literal, Optional
98

109
from codex import AuthenticationError
1110

12-
from cleanlab_codex.internal.analytics import IntegrationType, _AnalyticsMetadata
11+
from cleanlab_codex.internal.analytics import _AnalyticsMetadata
1312
from cleanlab_codex.internal.sdk_client import client_from_access_key
14-
from cleanlab_codex.types.entry import Entry
1513
from cleanlab_codex.types.project import ProjectConfig
1614

1715
if _TYPE_CHECKING:
@@ -21,20 +19,13 @@
2119
from codex.types.project_validate_params import Options as ProjectValidateOptions
2220
from codex.types.project_validate_response import ProjectValidateResponse
2321

24-
from cleanlab_codex.types.entry import EntryCreate
2522

2623
_ERROR_CREATE_ACCESS_KEY = (
2724
"Failed to create access key. Please ensure you have the necessary permissions "
2825
"and are using a user-level API key, not a project access key. "
2926
"See cleanlab_codex.Client.get_project."
3027
)
3128

32-
_ERROR_ADD_ENTRIES = (
33-
"Failed to add entries. Please ensure you have the necessary permissions "
34-
"and are using a user-level API key, not a project access key. "
35-
"See cleanlab_codex.Client.get_project."
36-
)
37-
3829

3930
class MissingProjectError(Exception):
4031
"""Raised when the project ID or access key does not match any existing project."""
@@ -47,7 +38,6 @@ class Project:
4738
"""Represents a Codex project.
4839
4940
To integrate a Codex project into your RAG/Agentic system, we recommend using one of our abstractions such as [`CodexTool`](/codex/api/python/codex_tool).
50-
The [`query`](#method-query) method can also be used directly if none of our existing abstractions are sufficient for your use case.
5141
"""
5242

5343
def __init__(self, sdk_client: _Codex, project_id: str, *, verify_existence: bool = True):
@@ -154,90 +144,6 @@ def create_access_key(
154144
except AuthenticationError as e:
155145
raise AuthenticationError(_ERROR_CREATE_ACCESS_KEY, response=e.response, body=e.body) from e
156146

157-
def add_entries(self, entries: list[EntryCreate]) -> None:
158-
"""[DEPRECATED] Add a list of entries to this Codex project. Must be authenticated with a user-level API key to use this method.
159-
See [`Client.create_project()`](/codex/api/python/client#method-create_project) or [`Client.get_project()`](/codex/api/python/client#method-get_project).
160-
161-
Args:
162-
entries (list[EntryCreate]): The entries to add to this project. See [`EntryCreate`](/codex/api/python/types.entry#class-entrycreate).
163-
164-
Raises:
165-
AuthenticationError: If the Project was created from a project-level access key instead of a [Client instance](/codex/api/python/client#class-client).
166-
"""
167-
warnings.warn(
168-
"Project.add_entries() is deprecated and will be removed in a future release. ",
169-
FutureWarning,
170-
stacklevel=2,
171-
)
172-
try:
173-
# TODO: implement batch creation of entries in backend and update this function
174-
for entry in entries:
175-
self._sdk_client.projects.entries.create(
176-
self.id,
177-
question=entry["question"],
178-
answer=entry.get("answer"),
179-
extra_headers=_AnalyticsMetadata().to_headers(),
180-
)
181-
except AuthenticationError as e:
182-
raise AuthenticationError(_ERROR_ADD_ENTRIES, response=e.response, body=e.body) from e
183-
184-
def query(
185-
self,
186-
question: str,
187-
*,
188-
fallback_answer: Optional[str] = None,
189-
metadata: Optional[dict[str, Any]] = None,
190-
_analytics_metadata: Optional[_AnalyticsMetadata] = None,
191-
) -> tuple[Optional[str], Entry]:
192-
"""[DEPRECATED] Query Codex to check if this project contains an answer to the question. If the question is not yet in the project, it will be added for SME review.
193-
194-
Args:
195-
question (str): The question to ask the Codex API.
196-
fallback_answer (str, optional): Optional fallback answer to return if Codex is unable to answer the question.
197-
metadata (dict, optional): Additional custom metadata to associate with the query.
198-
199-
Returns:
200-
tuple[Optional[str], Entry]: A tuple representing the answer for the query and the existing or new entry in the Codex project.
201-
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`](/codex/api/python/types.entry#class-entry) in the Codex project.
202-
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`](/codex/api/python/types.entry#class-entry) in the Codex project.
203-
"""
204-
warnings.warn(
205-
"Project.query() is deprecated and will be removed in a future release. Use the Project.validate() function instead.",
206-
FutureWarning,
207-
stacklevel=2,
208-
)
209-
if not _analytics_metadata:
210-
_analytics_metadata = _AnalyticsMetadata(integration_type=IntegrationType.BACKUP)
211-
212-
return self._query_project(
213-
question=question,
214-
fallback_answer=fallback_answer,
215-
client_metadata=metadata,
216-
analytics_metadata=_analytics_metadata,
217-
)
218-
219-
def _query_project(
220-
self,
221-
question: str,
222-
*,
223-
fallback_answer: Optional[str] = None,
224-
client_metadata: Optional[dict[str, Any]] = None,
225-
analytics_metadata: Optional[_AnalyticsMetadata] = None,
226-
) -> tuple[Optional[str], Entry]:
227-
extra_headers = analytics_metadata.to_headers() if analytics_metadata else None
228-
query_res = self._sdk_client.projects.entries.query(
229-
self._id,
230-
question=question,
231-
client_metadata=client_metadata,
232-
extra_headers=extra_headers,
233-
)
234-
235-
entry = Entry.model_validate(query_res.entry.model_dump())
236-
if query_res.answer is not None:
237-
return query_res.answer, entry
238-
239-
return fallback_answer, entry
240-
241147
def validate(
242148
self,
243149
context: str,

src/cleanlab_codex/types/entry.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/cleanlab_codex/types/project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ProjectConfig(Config): ...
1616
1717
#### <kbd>property</kbd> max_distance
1818
19-
Distance threshold used to determine if two questions are similar when querying existing Entries in a project.
19+
Distance threshold used to determine if two questions in a project are similar.
2020
The metric used is cosine distance. Valid threshold values range from 0 (identical vectors) to 1 (orthogonal vectors).
2121
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)
2222
content would not improve results of the system querying the Codex project.

tests/test_codex_tool.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,16 @@
22
import importlib
33
import sys
44
from typing import Any
5-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import MagicMock
66

77
import pytest
88
from langchain_core.tools.structured import StructuredTool
99
from llama_index.core.tools import FunctionTool
1010

1111
from cleanlab_codex.codex_tool import CodexTool
12-
from cleanlab_codex.internal.analytics import IntegrationType, _AnalyticsMetadata
1312
from cleanlab_codex.utils.errors import MissingDependencyError
1413

1514

16-
def test_tool_query_passes_metadata(mock_client_from_access_key: MagicMock) -> None: # noqa: ARG001
17-
mock_project = MagicMock()
18-
mock_project.query.return_value = (None, None)
19-
20-
with patch("cleanlab_codex.codex_tool.Project.from_access_key", return_value=mock_project):
21-
tool = CodexTool.from_access_key("sk-test-123")
22-
tool.query("what is the capital of France?")
23-
24-
assert mock_project.query.call_count == 1
25-
args, kwargs = mock_project.query.call_args
26-
assert kwargs["question"] == "what is the capital of France?"
27-
assert kwargs["fallback_answer"] == CodexTool.DEFAULT_FALLBACK_ANSWER
28-
assert (
29-
kwargs["_analytics_metadata"].to_headers()
30-
== _AnalyticsMetadata(integration_type=IntegrationType.TOOL).to_headers()
31-
)
32-
33-
3415
def patch_import_with_import_error(missing_module: str) -> None:
3516
def custom_import(name: str, *args: Any, **kwargs: Any) -> Any:
3617
if name.startswith(missing_module):

tests/test_project.py

Lines changed: 0 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,8 @@
77
from codex.types.projects.access_key_retrieve_project_id_response import (
88
AccessKeyRetrieveProjectIDResponse,
99
)
10-
from codex.types.projects.entry_query_response import (
11-
Entry as SDKEntry,
12-
)
13-
from codex.types.projects.entry_query_response import (
14-
EntryManagedMetadata,
15-
EntryManagedMetadataTrustworthiness,
16-
EntryQueryResponse,
17-
)
1810

1911
from cleanlab_codex.project import MissingProjectError, Project
20-
from cleanlab_codex.types.entry import EntryCreate
2112

2213
FAKE_PROJECT_ID = str(uuid.uuid4())
2314
FAKE_USER_ID = "Test User"
@@ -71,44 +62,6 @@ def test_create_project(mock_client_from_api_key: MagicMock, default_headers: di
7162
assert mock_client_from_api_key.projects.retrieve.call_count == 0
7263

7364

74-
def test_add_entries(mock_client_from_api_key: MagicMock) -> None:
75-
answered_entry_create = EntryCreate(
76-
question="What is the capital of France?",
77-
answer="Paris",
78-
)
79-
unanswered_entry_create = EntryCreate(
80-
question="What is the capital of Germany?",
81-
)
82-
project = Project(mock_client_from_api_key, FAKE_PROJECT_ID)
83-
project.add_entries([answered_entry_create, unanswered_entry_create])
84-
85-
for call, entry in zip(
86-
mock_client_from_api_key.projects.entries.create.call_args_list,
87-
[answered_entry_create, unanswered_entry_create],
88-
):
89-
assert call.args[0] == FAKE_PROJECT_ID
90-
assert call.kwargs["question"] == entry["question"]
91-
assert call.kwargs["answer"] == entry.get("answer")
92-
93-
94-
def test_add_entries_no_access_key(mock_client_from_access_key: MagicMock) -> None:
95-
mock_error = Mock(response=Mock(status=401), body={"error": "Unauthorized"})
96-
97-
mock_client_from_access_key.projects.entries.create.side_effect = AuthenticationError(
98-
"test", response=mock_error.response, body=mock_error.body
99-
)
100-
101-
answered_entry_create = EntryCreate(
102-
question="What is the capital of France?",
103-
answer="Paris",
104-
)
105-
106-
project = Project.from_access_key(DUMMY_ACCESS_KEY)
107-
108-
with pytest.raises(AuthenticationError, match="See cleanlab_codex.Client.get_project"):
109-
project.add_entries([answered_entry_create])
110-
111-
11265
def test_create_access_key(mock_client_from_api_key: MagicMock, default_headers: dict[str, str]) -> None:
11366
project = Project(mock_client_from_api_key, FAKE_PROJECT_ID)
11467
access_key_name = "Test Access Key"
@@ -144,83 +97,3 @@ def test_init_nonexistent_project_id(mock_client_from_access_key: MagicMock) ->
14497
with pytest.raises(MissingProjectError):
14598
Project(mock_client_from_access_key, FAKE_PROJECT_ID)
14699
assert mock_client_from_access_key.projects.retrieve.call_count == 1
147-
148-
149-
def test_query_question_found_fallback_answer(
150-
mock_client_from_access_key: MagicMock,
151-
) -> None:
152-
unanswered_entry = SDKEntry(
153-
id=str(uuid.uuid4()),
154-
question="What is the capital of France?",
155-
answer=None,
156-
managed_metadata=EntryManagedMetadata(trustworthiness=EntryManagedMetadataTrustworthiness(scores=[0.95])),
157-
)
158-
159-
mock_client_from_access_key.projects.entries.query.return_value = EntryQueryResponse(
160-
entry=unanswered_entry, answer=None
161-
)
162-
project = Project(mock_client_from_access_key, FAKE_PROJECT_ID)
163-
res = project.query("What is the capital of France?")
164-
assert res[0] is None
165-
assert res[1] is not None
166-
assert res[1].model_dump() == unanswered_entry.model_dump()
167-
168-
169-
def test_query_question_not_found_fallback_answer(
170-
mock_client_from_access_key: MagicMock,
171-
) -> None:
172-
mock_entry = SDKEntry(
173-
id="fake-id",
174-
question="What is the capital of France?",
175-
answer=None,
176-
managed_metadata=EntryManagedMetadata(trustworthiness=EntryManagedMetadataTrustworthiness(scores=[0.95])),
177-
)
178-
mock_client_from_access_key.projects.entries.query.return_value = EntryQueryResponse(entry=mock_entry, answer=None)
179-
180-
project = Project(mock_client_from_access_key, FAKE_PROJECT_ID)
181-
res = project.query("What is the capital of France?", fallback_answer="Paris")
182-
assert res[0] == "Paris"
183-
assert res[1] is not None
184-
assert res[1].model_dump() == mock_entry.model_dump()
185-
186-
187-
def test_query_answer_found(mock_client_from_access_key: MagicMock) -> None:
188-
answered_entry = SDKEntry(
189-
id=str(uuid.uuid4()),
190-
question="What is the capital of France?",
191-
answer="Paris",
192-
managed_metadata=EntryManagedMetadata(trustworthiness=EntryManagedMetadataTrustworthiness(scores=[0.95])),
193-
)
194-
mock_client_from_access_key.projects.entries.query.return_value = EntryQueryResponse(
195-
answer="Paris", entry=answered_entry
196-
)
197-
project = Project(mock_client_from_access_key, FAKE_PROJECT_ID)
198-
res = project.query("What is the capital of France?")
199-
assert res[0] == answered_entry.answer
200-
assert res[1] is not None
201-
assert res[1].model_dump() == answered_entry.model_dump()
202-
203-
204-
def test_query_answer_found_with_metadata(mock_client_from_access_key: MagicMock) -> None:
205-
answered_entry = SDKEntry(
206-
id=str(uuid.uuid4()),
207-
question="What is the capital of France?",
208-
answer="Paris",
209-
client_query_metadata=[{"trustworthiness_score": 0.95}],
210-
managed_metadata=EntryManagedMetadata(trustworthiness=EntryManagedMetadataTrustworthiness(scores=[0.95])),
211-
)
212-
mock_client_from_access_key.projects.entries.query.return_value = EntryQueryResponse(
213-
answer="Paris", entry=answered_entry
214-
)
215-
project = Project(mock_client_from_access_key, FAKE_PROJECT_ID)
216-
res = project.query("What is the capital of France?", metadata={"trustworthiness_score": 0.95})
217-
assert res[0] == answered_entry.answer
218-
assert res[1] is not None
219-
assert res[1].model_dump() == answered_entry.model_dump() # metadata should be included in the entry
220-
221-
222-
def test_add_entries_empty_list(mock_client_from_access_key: MagicMock) -> None:
223-
"""Test adding an empty list of entries"""
224-
project = Project(mock_client_from_access_key, FAKE_PROJECT_ID)
225-
project.add_entries([])
226-
mock_client_from_access_key.projects.entries.create.assert_not_called()

0 commit comments

Comments
 (0)