Skip to content

Commit 9b3b9ea

Browse files
committed
Return minimal ChatResponse from chatbot call_openai activity
The activity previously returned `openai.types.responses.Response` directly. The OpenAI API currently returns `"prompt_cache_retention": "in_memory"` (underscore), but openai SDK v2.32.0 declares that field as `Literal["in-memory", "24h"]`. The openai client parses laxly so the activity succeeds, but Temporal's `pydantic_data_converter` uses strict `TypeAdapter(Response).validate_json` on the way into the workflow and rejects the underscore value, failing every workflow task. Define minimal `ChatResponse` and `ToolCall` pydantic models in `activities.py` exposing only the fields the workflow uses (id, output_text, tool_calls). The activity projects the openai Response down to this shape so the sample is no longer coupled to SDK drift in fields it doesn't use. Update the workflow loop to iterate `response.tool_calls` directly and the test mocks/helpers to build `ChatResponse` instead of constructing openai Response objects.
1 parent 1b84cf3 commit 9b3b9ea

4 files changed

Lines changed: 42 additions & 61 deletions

File tree

langsmith_tracing/chatbot/activities.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@
66
from langsmith import traceable
77
from langsmith.wrappers import wrap_openai
88
from openai import AsyncOpenAI
9-
from openai.types.responses import Response
9+
from pydantic import BaseModel, Field
1010
from temporalio import activity
1111

1212

13+
class ToolCall(BaseModel):
14+
call_id: str
15+
name: str
16+
arguments: str
17+
18+
19+
class ChatResponse(BaseModel):
20+
id: str
21+
output_text: str = ""
22+
tool_calls: list[ToolCall] = Field(default_factory=list)
23+
24+
1325
@dataclass
1426
class OpenAIRequest:
1527
model: str
@@ -25,7 +37,7 @@ class OpenAIRequest:
2537

2638
@traceable(name="Call OpenAI", run_type="llm")
2739
@activity.defn
28-
async def call_openai(request: OpenAIRequest) -> Response:
40+
async def call_openai(request: OpenAIRequest) -> ChatResponse:
2941
"""Call OpenAI Responses API. Retries handled by Temporal, not the OpenAI client."""
3042
# wrap_openai patches the client so each API call (e.g. responses.create)
3143
# creates its own child span with model parameters and token usage.
@@ -42,4 +54,13 @@ async def call_openai(request: OpenAIRequest) -> Response:
4254
response_args["tools"] = request.tools
4355
if request.previous_response_id:
4456
response_args["previous_response_id"] = request.previous_response_id
45-
return await client.responses.create(**response_args)
57+
response = await client.responses.create(**response_args)
58+
return ChatResponse(
59+
id=response.id,
60+
output_text=response.output_text or "",
61+
tool_calls=[
62+
ToolCall(call_id=item.call_id, name=item.name, arguments=item.arguments)
63+
for item in response.output
64+
if item.type == "function_call"
65+
],
66+
)

langsmith_tracing/chatbot/workflows.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,25 +147,23 @@ async def _traced():
147147
self._previous_response_id = response.id
148148

149149
tool_results = []
150-
for item in response.output:
151-
if item.type != "function_call":
152-
continue
153-
args = json.loads(item.arguments)
154-
if item.name == "save_note":
150+
for tc in response.tool_calls:
151+
args = json.loads(tc.arguments)
152+
if tc.name == "save_note":
155153
result = self._save_note(args["name"], args["content"])
156154
tool_results.append(
157155
{
158156
"type": "function_call_output",
159-
"call_id": item.call_id,
157+
"call_id": tc.call_id,
160158
"output": result,
161159
}
162160
)
163-
elif item.name == "read_note":
161+
elif tc.name == "read_note":
164162
result = self._read_note(args["name"])
165163
tool_results.append(
166164
{
167165
"type": "function_call_output",
168-
"call_id": item.call_id,
166+
"call_id": tc.call_id,
169167
"output": result,
170168
}
171169
)

tests/langsmith_tracing/helpers.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,8 @@
11
"""Shared test helpers for LangSmith tracing tests."""
22

3-
from openai.types.responses import Response
4-
from openai.types.responses.response_output_message import ResponseOutputMessage
5-
from openai.types.responses.response_output_text import ResponseOutputText
3+
from langsmith_tracing.chatbot.activities import ChatResponse
64

75

8-
def make_text_response(text: str) -> Response:
9-
"""Build a minimal OpenAI Response with a text output."""
10-
return Response.model_construct(
11-
id="resp_mock",
12-
created_at=0.0,
13-
model="gpt-4o-mini",
14-
object="response",
15-
output=[
16-
ResponseOutputMessage.model_construct(
17-
id="msg_mock",
18-
type="message",
19-
role="assistant",
20-
status="completed",
21-
content=[
22-
ResponseOutputText.model_construct(
23-
type="output_text",
24-
text=text,
25-
annotations=[],
26-
)
27-
],
28-
)
29-
],
30-
parallel_tool_calls=False,
31-
tool_choice="auto",
32-
tools=[],
33-
)
6+
def make_text_response(text: str) -> ChatResponse:
7+
"""Build a minimal ChatResponse with a text output."""
8+
return ChatResponse(id="resp_mock", output_text=text)

tests/langsmith_tracing/test_chatbot.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,29 @@
11
import json
22
import uuid
33

4-
from openai.types.responses import Response
5-
from openai.types.responses.response_function_tool_call import (
6-
ResponseFunctionToolCall,
7-
)
84
from temporalio import activity
95
from temporalio.client import Client
106
from temporalio.contrib.langsmith import LangSmithPlugin
117
from temporalio.testing import WorkflowEnvironment
128
from temporalio.worker import Worker
139

14-
from langsmith_tracing.chatbot.activities import OpenAIRequest
10+
from langsmith_tracing.chatbot.activities import ChatResponse, OpenAIRequest, ToolCall
1511
from langsmith_tracing.chatbot.workflows import ChatbotWorkflow
1612
from tests.langsmith_tracing.helpers import make_text_response
1713

1814

1915
def _make_function_call_response(
2016
name: str, arguments: dict, call_id: str = "call_123"
21-
) -> Response:
22-
return Response.model_construct(
17+
) -> ChatResponse:
18+
return ChatResponse(
2319
id="resp_tool",
24-
created_at=0.0,
25-
model="gpt-4o-mini",
26-
object="response",
27-
output=[
28-
ResponseFunctionToolCall.model_construct(
29-
id="fc_mock",
30-
type="function_call",
20+
tool_calls=[
21+
ToolCall(
22+
call_id=call_id,
3123
name=name,
3224
arguments=json.dumps(arguments),
33-
call_id=call_id,
34-
status="completed",
3525
)
3626
],
37-
parallel_tool_calls=False,
38-
tool_choice="auto",
39-
tools=[],
4027
)
4128

4229

@@ -45,7 +32,7 @@ async def test_chatbot_save_note(client: Client, env: WorkflowEnvironment):
4532
call_count = 0
4633

4734
@activity.defn(name="call_openai")
48-
async def mock_call_openai(request: OpenAIRequest) -> Response:
35+
async def mock_call_openai(request: OpenAIRequest) -> ChatResponse:
4936
nonlocal call_count
5037
call_count += 1
5138
if call_count == 1:
@@ -86,7 +73,7 @@ async def test_chatbot_read_note(client: Client, env: WorkflowEnvironment):
8673
call_count = 0
8774

8875
@activity.defn(name="call_openai")
89-
async def mock_call_openai(request: OpenAIRequest) -> Response:
76+
async def mock_call_openai(request: OpenAIRequest) -> ChatResponse:
9077
nonlocal call_count
9178
call_count += 1
9279
if call_count == 1:

0 commit comments

Comments
 (0)