Skip to content

Commit

Permalink
feat(routes): add support for structured ouptuts (#59)
Browse files Browse the repository at this point in the history
* chore(examples): add langchain-openai example from #58

* chore(types): add `refusal` to choice

* chore(deps): add langchain-openai as dev dep for testing

* chore(docs): remove not about support for older SDK versions

* chore(routes): move beta routes to separate dir

* chore(types): add structured output type additions

* feat(routes): add beta parsed chat route

* feat(examples): add parsed chat completion examples
  • Loading branch information
mharrisb1 authored Aug 12, 2024
1 parent 1d6be48 commit 3a6b0e7
Show file tree
Hide file tree
Showing 24 changed files with 517 additions and 259 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ Pytest plugin for automatically mocking OpenAI requests. Powered by [RESPX](http

[![sdk support](https://img.shields.io/badge/SDK_Support-v1.32+-white?logo=openai&logoColor=black&labelColor=white)](https://github.com/openai/openai-python)

> [!NOTE]
> For working with OpenAI Python SDK versions `>=1.25,<1.32` please use version [`v0.6`](https://github.com/mharrisb1/openai-responses-python/releases/tag/v0.6.1)
## Supported Endpoints

- [Chat](https://platform.openai.com/docs/api-reference/chat)
Expand Down
41 changes: 41 additions & 0 deletions examples/test_langchain_openai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from langchain_openai import ChatOpenAI
from pydantic.v1 import SecretStr

import openai_responses
from openai_responses import OpenAIMock


@openai_responses.mock()
def test_langchain_chat_openai_invoke(openai_mock: OpenAIMock):
openai_mock.chat.completions.create.response = {
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"content": "J'adore la programmation.",
"role": "assistant",
},
}
]
}

llm = ChatOpenAI(
name="My Custom Chatbot",
model="gpt-4o",
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key=SecretStr("sk-fake123"),
)

messages = [
(
"system",
"You are a helpful assistant that translates English to French. Translate the user sentence.",
),
("human", "I love programming."),
]
ai_msg = llm.invoke(messages)
assert ai_msg.content == "J'adore la programmation." # type: ignore
112 changes: 112 additions & 0 deletions examples/test_parsed_chat_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from typing import Optional
from datetime import datetime

import openai
from pydantic import BaseModel

import openai_responses
from openai_responses import OpenAIMock


class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]


@openai_responses.mock()
def test_create_parsed_chat_completion_with_response_format(openai_mock: OpenAIMock):
openai_mock.beta.chat.completions.create.response = {
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"content": CalendarEvent(
name="Example Event",
date=datetime.now().strftime("%Y-%m-%d"),
participants=[
"Alice",
"Bob",
],
).model_dump_json(),
"role": "assistant",
},
}
]
}

client = openai.Client(api_key="sk-fake123")

completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract the event information."},
{
"role": "user",
"content": "Alice and Bob are going to a science fair on today.",
},
],
response_format=CalendarEvent,
)

event = completion.choices[0].message.parsed
assert event
assert event.name == "Example Event"
assert datetime.strptime(event.date, "%Y-%m-%d").date() == datetime.now().date()
assert len(event.participants) == 2


@openai_responses.mock()
def test_create_parsed_chat_completion_with_tools(openai_mock: OpenAIMock):
openai_mock.beta.chat.completions.create.response = {
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"arguments": CalendarEvent(
name="Example Event",
date=datetime.now().strftime("%Y-%m-%d"),
participants=[
"Alice",
"Bob",
],
).model_dump_json(),
"name": "CalendarEvent",
},
}
],
},
}
]
}

client = openai.Client(api_key="sk-fake123")

completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract the event information."},
{
"role": "user",
"content": "Alice and Bob are going to a science fair on today.",
},
],
tools=[openai.pydantic_function_tool(CalendarEvent)],
)

event: Optional[CalendarEvent] = (
completion.choices[0].message.tool_calls[0].function.parsed_arguments # type: ignore
)

assert event
assert event.name == "Example Event"
assert datetime.strptime(event.date, "%Y-%m-%d").date() == datetime.now().date()
assert len(event.participants) == 2
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pytest = "^8.1.1"
pytest-asyncio = "^0.23.6"
mkdocs-material = "^9.5.18"
tox = "^4.14.2"
langchain-openai = "^0.1.20"

[build-system]
requires = ["poetry-core"]
Expand Down
141 changes: 3 additions & 138 deletions src/openai_responses/_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,62 +12,10 @@
FileRetrieveContentRoute,
)
from .models import ModelListRoute, ModelRetrieveRoute
from .assistants import (
AssistantCreateRoute,
AssistantListRoute,
AssistantRetrieveRoute,
AssistantUpdateRoute,
AssistantDeleteRoute,
)
from .threads import (
ThreadCreateRoute,
ThreadRetrieveRoute,
ThreadUpdateRoute,
ThreadDeleteRoute,
)
from .messages import (
MessageCreateRoute,
MessageListRoute,
MessageRetrieveRoute,
MessageUpdateRoute,
MessageDeleteRoute,
)
from .runs import (
RunCreateRoute,
ThreadCreateAndRun,
RunListRoute,
RunRetrieveRoute,
RunUpdateRoute,
RunSubmitToolOutputsRoute,
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,
)
from .vector_store_file_batches import (
VectorStoreFileBatchCreateRoute,
VectorStoreFileBatchRetrieveRoute,
VectorStoreFileBatchCancelRoute,
VectorStoreFileBatchListFilesRoute,
)

__all__ = [
"BetaRoutes",
"ChatRoutes",
"EmbeddingsRoutes",
"FileRoutes",
]
from .beta import BetaRoutes

__all__ = ["BetaRoutes", "ChatRoutes", "EmbeddingsRoutes", "FileRoutes", "ModelRoutes"]


class ChatRoutes:
Expand Down Expand Up @@ -98,86 +46,3 @@ class ModelRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.list = ModelListRoute(router, state)
self.retrieve = ModelRetrieveRoute(router, state)


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:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = AssistantCreateRoute(router, state)
self.list = AssistantListRoute(router, state)
self.retrieve = AssistantRetrieveRoute(router, state)
self.update = AssistantUpdateRoute(router, state)
self.delete = AssistantDeleteRoute(router, state)


class ThreadRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = ThreadCreateRoute(router, state)
self.retrieve = ThreadRetrieveRoute(router, state)
self.update = ThreadUpdateRoute(router, state)
self.delete = ThreadDeleteRoute(router, state)
self.create_and_run = ThreadCreateAndRun(router, state)

self.messages = MessageRoutes(router, state)
self.runs = RunRoutes(router, state)


class MessageRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = MessageCreateRoute(router, state)
self.list = MessageListRoute(router, state)
self.retrieve = MessageRetrieveRoute(router, state)
self.update = MessageUpdateRoute(router, state)
self.delete = MessageDeleteRoute(router, state)


class RunRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = RunCreateRoute(router, state)
self.list = RunListRoute(router, state)
self.retrieve = RunRetrieveRoute(router, state)
self.update = RunUpdateRoute(router, state)
self.submit_tool_outputs = RunSubmitToolOutputsRoute(router, state)
self.cancel = RunCancelRoute(router, state)

self.steps = RunStepRoutes(router, state)


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)
self.file_batches = VectorStoreFileBatchRoutes(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)


class VectorStoreFileBatchRoutes:
def __init__(self, router: respx.MockRouter, state: StateStore) -> None:
self.create = VectorStoreFileBatchCreateRoute(router, state)
self.retrieve = VectorStoreFileBatchRetrieveRoute(router, state)
self.cancel = VectorStoreFileBatchCancelRoute(router, state)
self.list_files = VectorStoreFileBatchListFilesRoute(router, state)
Loading

0 comments on commit 3a6b0e7

Please sign in to comment.