Skip to content

Commit

Permalink
feat(routes): add vector store and vector store file routes (#48)
Browse files Browse the repository at this point in the history
* chore(poetry): bump faker-openai-api-provider minor version

* feat(stores): add vector store state store

* feat(types): add partial vector store

* feat(routes): add create vector store route

* chore(docs): mark create vector store route as supported

* chore(docs): mark vector stores route as supported

* feat(routes): add remaining vector store routes

* chore(docs): add note about vector store endpoints support

* feat(routes): add vector store files create route

* feat(routes): add vector store files list route

* feat(types): make partial pagination generic

* feat(routes): add vector store file retrieve route

* feat(routes): add vector store file delete route

* chore(docs): add note about vector store file endpoints support

* fix(types): typeddict with variadic generic support

https://stackoverflow.com/a/78065164
  • Loading branch information
mharrisb1 authored May 29, 2024
1 parent 8f1a187 commit 12f80da
Show file tree
Hide file tree
Showing 23 changed files with 891 additions and 90 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Pytest plugin for automatically mocking OpenAI requests. Powered by [RESPX](http
- [Messages](https://platform.openai.com/docs/api-reference/messages)
- [Runs](https://platform.openai.com/docs/api-reference/runs)
- [Run Steps](https://platform.openai.com/docs/api-reference/run-steps)
- [Vector Stores](https://platform.openai.com/docs/api-reference/vector-stores)
- [Vector Store Files](https://platform.openai.com/docs/api-reference/vector-stores-files)

View full support coverage [here](https://mharrisb1.github.io/openai-responses-python/coverage).

Expand Down
18 changes: 9 additions & 9 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,16 @@ The end-goal of this library is to eventually support all OpenAI API routes. See
| List run steps | :material-check:{ .green } | - | Stateful |
| Retrieve run step | :material-check:{ .green } | - | Stateful |
| **Vector Stores** |
| Create vector store | :material-close:{ .red } | - | - |
| List vector stores | :material-close:{ .red } | - | - |
| Retrieve vector store | :material-close:{ .red } | - | - |
| Modify vector store | :material-close:{ .red } | - | - |
| Delete vector store | :material-close:{ .red } | - | - |
| Create vector store | :material-check:{ .green } | - | Stateful |
| List vector stores | :material-check:{ .green } | - | Stateful |
| Retrieve vector store | :material-check:{ .green } | - | Stateful |
| Modify vector store | :material-check:{ .green } | - | Stateful |
| Delete vector store | :material-check:{ .green } | - | Stateful |
| **Vector Store Files** |
| Create vector store file | :material-close:{ .red } | - | - |
| List vector store files | :material-close:{ .red } | - | - |
| Retrieve vector store file | :material-close:{ .red } | - | - |
| Delete vector store file | :material-close:{ .red } | - | - |
| Create vector store file | :material-check:{ .green } | - | Stateful |
| List vector store files | :material-check:{ .green } | - | Stateful |
| Retrieve vector store file | :material-check:{ .green } | - | Stateful |
| Delete vector store file | :material-check:{ .green } | - | Stateful |
| **Vector Store File Batches** |
| Create vector store file batch | :material-close:{ .red } | - | - |
| Retrieve vector store file batch | :material-close:{ .red } | - | - |
Expand Down
110 changes: 110 additions & 0 deletions examples/test_vector_store_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import openai

import openai_responses
from openai_responses import OpenAIMock


@openai_responses.mock()
def test_create_vector_store_file(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")
file = client.files.create(
file=open("examples/example.json", "rb"),
purpose="assistants",
)

vector_store_file = client.beta.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id,
)

assert vector_store_file.vector_store_id == vector_store.id
assert vector_store_file.id == file.id

assert openai_mock.files.create.route.call_count == 1
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.create.route.call_count == 1


@openai_responses.mock()
def test_list_vector_store_files(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")

for _ in range(10):
file = client.files.create(
file=open("examples/example.json", "rb"),
purpose="assistants",
)

client.beta.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id,
)

vector_store_files = client.beta.vector_stores.files.list(vector_store.id)

assert len(vector_store_files.data) == 10

assert openai_mock.files.create.route.call_count == 10
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.create.route.call_count == 10
assert openai_mock.beta.vector_stores.files.list.route.call_count == 1


@openai_responses.mock()
def test_retrieve_vector_store_file(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")
file = client.files.create(
file=open("examples/example.json", "rb"),
purpose="assistants",
)

vector_store_file = client.beta.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id,
)

found = client.beta.vector_stores.files.retrieve(
vector_store_file.id,
vector_store_id=vector_store.id,
)

assert found.id == vector_store_file.id

assert openai_mock.files.create.route.call_count == 1
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.retrieve.route.call_count == 1


@openai_responses.mock()
def test_delete_vector_store_file(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")
file = client.files.create(
file=open("examples/example.json", "rb"),
purpose="assistants",
)

vector_store_file = client.beta.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id,
)

deleted = client.beta.vector_stores.files.delete(
vector_store_file.id,
vector_store_id=vector_store.id,
)

assert deleted.deleted

assert openai_mock.files.create.route.call_count == 1
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.create.route.call_count == 1
assert openai_mock.beta.vector_stores.files.delete.route.call_count == 1
70 changes: 70 additions & 0 deletions examples/test_vector_stores.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import openai

import openai_responses
from openai_responses import OpenAIMock


@openai_responses.mock()
def test_create_vector_store(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")

assert vector_store.name == "Support FAQ"
assert openai_mock.beta.vector_stores.create.route.call_count == 1


@openai_responses.mock()
def test_list_vector_stores(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

for i in range(10):
client.beta.vector_stores.create(name=f"vector-store-{i}")

vector_stores = client.beta.vector_stores.list()

assert len(vector_stores.data) == 10
assert openai_mock.beta.vector_stores.create.route.call_count == 10
assert openai_mock.beta.vector_stores.list.route.call_count == 1


@openai_responses.mock()
def test_retrieve_vector_store(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")

found = client.beta.vector_stores.retrieve(vector_store.id)

assert vector_store.name == "Support FAQ"
assert found.name == vector_store.name
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.retrieve.route.call_count == 1


@openai_responses.mock()
def test_update_vector_store(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")

updated = client.beta.vector_stores.update(
vector_store.id,
name="Support FAQ Updated",
)

assert updated.id == vector_store.id
assert updated.name == "Support FAQ Updated"
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.update.route.call_count == 1


@openai_responses.mock()
def test_delete_vector_store(openai_mock: OpenAIMock):
client = openai.Client(api_key="sk-fake123")

vector_store = client.beta.vector_stores.create(name="Support FAQ")

assert client.beta.vector_stores.delete(vector_store.id).deleted
assert openai_mock.beta.vector_stores.create.route.call_count == 1
assert openai_mock.beta.vector_stores.delete.route.call_count == 1
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ openai_responses = "openai_responses.plugin"

[tool.poetry.dependencies]
python = ">=3.9,<4.0"
faker = ">=24.2.0,<25.0.0"
faker-openai-api-provider = "0.2.0"
openai = "^1.25"
respx = "^0.20.2"
faker-openai-api-provider = "^0.1.0"
requests-toolbelt = "^1.0.0"
respx = "^0.20.2"

[tool.poetry.group.dev.dependencies]
black = "^24.2.0"
Expand Down
33 changes: 33 additions & 0 deletions src/openai_responses/_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@
RunCancelRoute,
)
from .run_steps import RunStepListRoute, RunStepRetrieveRoute
from .vector_stores import (
VectorStoreCreateRoute,
VectorStoreListRoute,
VectorStoreRetrieveRoute,
VectorStoreUpdateRoute,
VectorStoreDeleteRoute,
)
from .vector_store_files import (
VectorStoreFileCreateRoute,
VectorStoreFileListRoute,
VectorStoreFileRetrieveRoute,
VectorStoreFileDeleteRoute,
)

__all__ = [
"BetaRoutes",
Expand Down Expand Up @@ -85,6 +98,7 @@ class BetaRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.assistants = AssistantsRoutes(router, state)
self.threads = ThreadRoutes(router, state)
self.vector_stores = VectorStoreRoutes(router, state)


class AssistantsRoutes:
Expand Down Expand Up @@ -133,3 +147,22 @@ class RunStepRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.list = RunStepListRoute(router, state)
self.retrieve = RunStepRetrieveRoute(router, state)


class VectorStoreRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = VectorStoreCreateRoute(router, state)
self.list = VectorStoreListRoute(router, state)
self.retrieve = VectorStoreRetrieveRoute(router, state)
self.update = VectorStoreUpdateRoute(router, state)
self.delete = VectorStoreDeleteRoute(router, state)

self.files = VectorStoreFileRoutes(router, state)


class VectorStoreFileRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = VectorStoreFileCreateRoute(router, state)
self.list = VectorStoreFileListRoute(router, state)
self.retrieve = VectorStoreFileRetrieveRoute(router, state)
self.delete = VectorStoreFileDeleteRoute(router, state)
11 changes: 4 additions & 7 deletions src/openai_responses/_routes/assistants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
from ._base import StatefulRoute

from ..stores import StateStore
from .._types.partials.assistants import (
PartialAssistant,
PartialAssistantList,
PartialAssistantDeleted,
)
from .._types.partials.sync_cursor_page import PartialSyncCursorPage
from .._types.partials.assistants import PartialAssistant, PartialAssistantDeleted

from .._utils.faker import faker
from .._utils.serde import json_loads, model_dict, model_parse
Expand Down Expand Up @@ -62,7 +59,7 @@ def _build(partial: PartialAssistant, request: httpx.Request) -> Assistant:


class AssistantListRoute(
StatefulRoute[SyncCursorPage[Assistant], PartialAssistantList]
StatefulRoute[SyncCursorPage[Assistant], PartialSyncCursorPage[PartialAssistant]]
):
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
super().__init__(
Expand Down Expand Up @@ -96,7 +93,7 @@ def _handler(self, request: httpx.Request, route: respx.Route) -> httpx.Response

@staticmethod
def _build(
partial: PartialAssistantList,
partial: PartialSyncCursorPage[PartialAssistant],
request: httpx.Request,
) -> SyncCursorPage[Assistant]:
raise NotImplementedError
Expand Down
13 changes: 6 additions & 7 deletions src/openai_responses/_routes/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
from ._base import StatefulRoute

from ..stores import StateStore
from .._types.partials.messages import (
PartialMessage,
PartialMessageList,
PartialMessageDeleted,
)
from .._types.partials.sync_cursor_page import PartialSyncCursorPage
from .._types.partials.messages import PartialMessage, PartialMessageDeleted

from .._utils.faker import faker
from .._utils.serde import json_loads, model_dict, model_parse
Expand Down Expand Up @@ -100,7 +97,9 @@ def _build(partial: PartialMessage, request: httpx.Request) -> Message:
return model_parse(Message, defaults | partial | content)


class MessageListRoute(StatefulRoute[SyncCursorPage[Message], PartialMessageList]):
class MessageListRoute(
StatefulRoute[SyncCursorPage[Message], PartialSyncCursorPage[PartialMessage]]
):
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
super().__init__(
route=router.get(
Expand Down Expand Up @@ -153,7 +152,7 @@ def _handler(

@staticmethod
def _build(
partial: PartialMessageList,
partial: PartialSyncCursorPage[PartialMessage],
request: httpx.Request,
) -> SyncCursorPage[Message]:
raise NotImplementedError
Expand Down
9 changes: 6 additions & 3 deletions src/openai_responses/_routes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
from ._base import StatefulRoute

from ..stores import StateStore
from .._types.partials.models import PartialModel, PartialModelList
from .._types.partials.models import PartialModel
from .._types.partials.sync_cursor_page import PartialSyncCursorPage

from .._utils.serde import model_dict


class ModelListRoute(StatefulRoute[SyncCursorPage[Model], PartialModelList]):
class ModelListRoute(
StatefulRoute[SyncCursorPage[Model], PartialSyncCursorPage[PartialModel]]
):
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
super().__init__(
route=router.get(url__regex="/models"),
Expand All @@ -32,7 +35,7 @@ def _handler(self, request: httpx.Request, route: respx.Route) -> httpx.Response

@staticmethod
def _build(
partial: PartialModelList,
partial: PartialSyncCursorPage[PartialModel],
request: httpx.Request,
) -> SyncCursorPage[Model]:
raise NotImplementedError
Expand Down
Loading

0 comments on commit 12f80da

Please sign in to comment.