feat(chat-agent): Add a cooking chat example in python#4
Conversation
📝 WalkthroughWalkthroughAdds a synchronous Cooking Assistant chat REPL with per-message Traceloop tracing, OpenAI conversation history, graceful exit handling, a ChangesCooking Assistant Chat
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant main
participant Traceloop
participant handle_turn
participant OpenAI
User->>main: Enter chat message
main->>Traceloop: Set conversation_id and turn
main->>handle_turn: Submit history and message
handle_turn->>OpenAI: Request chat completion
OpenAI-->>handle_turn: Return assistant reply
handle_turn-->>main: Return reply
main-->>User: Display response
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
python/cooking_chat.py (1)
29-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winMake prompt-content tracing an explicit privacy choice.
Traceloop records prompts and completions in span attributes by default. This REPL accepts free-form dietary or health-related input, so document that behavior and expose the documented opt-out, or disable content capture by default. (docs.traceloop.com)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cooking_chat.py` around lines 29 - 33, Update the Traceloop.init configuration to make prompt and completion content capture an explicit privacy choice: document the default recording behavior and expose the documented opt-out, or configure content capture disabled by default. Keep the existing app_name and batching settings unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/cooking_chat.py`:
- Around line 58-68: Update handle_turn() to build the API request from a
temporary message list without mutating history, then append both user and
assistant messages only after a successful response. In main(), catch expected
OpenAI timeout, rate-limit, and network exceptions around each turn, report the
failure, and continue the REPL loop so retries do not duplicate messages or
terminate the session.
- Around line 51-62: Update handle_turn to bound the conversation history before
constructing the messages argument, retaining only a recent window of turns or
using the existing summarization mechanism if available. Preserve the system
prompt and in-place accumulation behavior while ensuring older history cannot
cause unbounded request growth.
- Around line 96-98: Update the Traceloop.set_association_properties call to
convert turn to a string with str(turn), while leaving conversation_id
unchanged.
- Around line 65-67: Update the response handling around
response.choices[0].message in the cooking chat flow to handle None content
before appending to history. Use the message refusal as the fallback when
available, or stop with a clear error if neither text content nor refusal
exists; only append a non-null assistant message to history.
In `@python/README.md`:
- Around line 120-126: Add a `text` or `console` language tag to the opening
fence of the chickpeas-and-spinach conversation example in the README, leaving
the example content unchanged.
---
Nitpick comments:
In `@python/cooking_chat.py`:
- Around line 29-33: Update the Traceloop.init configuration to make prompt and
completion content capture an explicit privacy choice: document the default
recording behavior and expose the documented opt-out, or configure content
capture disabled by default. Keep the existing app_name and batching settings
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e2b48f75-eb23-421d-b084-3918d3386486
📒 Files selected for processing (3)
python/README.mdpython/cooking_chat.pypython/pyproject.toml
| def handle_turn(history: list[dict], user_message: str) -> str: | ||
| """Handle a single user->bot exchange. | ||
|
|
||
| Called at the top level (no enclosing span), so each invocation is a root | ||
| span == one trace per message exchange. `history` is mutated in place so the | ||
| conversation accumulates memory across turns. | ||
| """ | ||
| history.append({"role": "user", "content": user_message}) | ||
|
|
||
| response = client.chat.completions.create( | ||
| model=MODEL, | ||
| messages=[{"role": "system", "content": SYSTEM_PROMPT}, *history], |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and inspect relevant sections with line numbers.
git ls-files python/cooking_chat.py
printf '\n--- file excerpt ---\n'
cat -n python/cooking_chat.py | sed -n '1,220p'
printf '\n--- history-related search ---\n'
rg -n "history|summary|token|truncate|budget|max_" python/cooking_chat.pyRepository: traceloop/agent-examples
Length of output: 4766
Bound the conversation history in handle_turn. history is sent back in full on every turn, so a long session will keep increasing request size, latency, and cost and can eventually hit the model context limit. Trim to a recent window or summarize older turns before building messages.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/cooking_chat.py` around lines 51 - 62, Update handle_turn to bound the
conversation history before constructing the messages argument, retaining only a
recent window of turns or using the existing summarization mechanism if
available. Preserve the system prompt and in-place accumulation behavior while
ensuring older history cannot cause unbounded request growth.
| history.append({"role": "user", "content": user_message}) | ||
|
|
||
| response = client.chat.completions.create( | ||
| model=MODEL, | ||
| messages=[{"role": "system", "content": SYSTEM_PROMPT}, *history], | ||
| temperature=0.7, | ||
| ) | ||
| reply = response.choices[0].message.content | ||
|
|
||
| history.append({"role": "assistant", "content": reply}) | ||
| return reply |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
sed -n '1,180p' python/cooking_chat.pyRepository: traceloop/agent-examples
Length of output: 3438
Keep API failures inside the REPL
handle_turn()appends the user message before the API call, so a retry can duplicate it.- Build the request from a temporary message list, append to
historyonly after success, and catch expected OpenAI timeout/rate-limit/network errors inmain()so a failed turn doesn’t exit the loop.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/cooking_chat.py` around lines 58 - 68, Update handle_turn() to build
the API request from a temporary message list without mutating history, then
append both user and assistant messages only after a successful response. In
main(), catch expected OpenAI timeout, rate-limit, and network exceptions around
each turn, report the failure, and continue the REPL loop so retries do not
duplicate messages or terminate the session.
| reply = response.choices[0].message.content | ||
|
|
||
| history.append({"role": "assistant", "content": reply}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
cd python
uv run python - <<'PY'
from openai.types.chat import ChatCompletionMessage
print(ChatCompletionMessage.model_fields["content"])
print(ChatCompletionMessage.model_fields.get("refusal"))
PYRepository: traceloop/agent-examples
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,180p' python/cooking_chat.pyRepository: traceloop/agent-examples
Length of output: 3438
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== dependency pins ==\n'
git ls-files | rg '(^|/)(pyproject\.toml|requirements(\.txt)?|poetry\.lock|uv\.lock|Pipfile|Pipfile\.lock)$' || true
printf '\n== openai references ==\n'
rg -n "openai|ChatCompletionMessage|refusal|tool_call|tool-call" python . -g '!**/.git/**' || trueRepository: traceloop/agent-examples
Length of output: 15767
🌐 Web query:
OpenAI Chat Completions assistant message content refusal optional schema content=None refusal field next request invalid
💡 Result:
In the OpenAI Chat Completions API, the refusal field is an optional property of an assistant message, distinct from the content field [1][2][3]. When a model generates a refusal (e.g., due to safety policies or when using Structured Outputs), the API may return a message where content is null and refusal contains the string explanation of the refusal [2][3][4]. Key technical points regarding this behavior include: 1. Relationship between Content and Refusal: The content field is not always required to be present if a refusal is provided, particularly in cases where the model has explicitly refused to answer [2][3]. In many programmatic implementations, content is defined as Optional[str] and refusal as Optional[str] [5][2][6]. 2. API Design: The refusal field is intended to be surfaced to the user or handled by application logic when it appears [4]. Applications consuming the API should check for the presence of the refusal field rather than assuming content will contain the model's response [4][7]. 3. Handling in SDKs: Various SDKs (such as Python, Node, and Java) reflect this structure, providing both fields as optional [2][3][6]. Issues reported in developer communities often stem from downstream applications or wrappers that do not explicitly check for or gracefully handle the refusal field when content is null, sometimes causing errors or infinite retry loops [8][9][7]. If you receive a response with content: None and refusal: "...", it indicates a model-generated refusal. Your application logic should be updated to detect the refusal property, handle it as a legitimate assistant response (e.g., by displaying the refusal message), and stop further processing for that turn [4][8].
Citations:
- 1: https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create/
- 2: https://github.com/openai/openai-python/blob/main/src/openai/types/chat/chat_completion_message.py
- 3: https://github.com/openai/openai-node/blob/e228aaad/src/resources/chat/completions/completions.ts
- 4: https://developers.openai.com/api/docs/guides/structured-outputs
- 5: https://developers.openai.com/api/reference/python/resources/chat/subresources/completions/methods/create/
- 6: https://javadoc.io/static/com.openai/openai-java/4.0.0/com/openai/models/chat/completions/ChatCompletionMessage.html
- 7: Properly handle refusal in OpenAI StructuredOutput refusal to raise
OpenAIRefusalErrorlangchain-ai/langchain#34616 - 8: Model refusals with structured output aren't handled openai/openai-agents-python#3055
- 9: openai-completions: chat-completions refusal field dropped, assistant turn resolves to empty content openclaw/openclaw#102321
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '684,704p' python/travel_agent.pyRepository: traceloop/agent-examples
Length of output: 939
Handle assistant turns without text content
response.choices[0].message.content can be None on refusal/tool-call responses. Don’t append that directly to history; fall back to message.refusal or stop with a clear error before storing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/cooking_chat.py` around lines 65 - 67, Update the response handling
around response.choices[0].message in the cooking chat flow to handle None
content before appending to history. Use the message refusal as the fallback
when available, or stop with a clear error if neither text content nor refusal
exists; only append a non-null assistant message to history.
| Traceloop.set_association_properties( | ||
| {"conversation_id": conversation_id, "turn": turn} | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
cd python
uv run python - <<'PY'
import inspect
from traceloop.sdk import Traceloop
print(inspect.signature(Traceloop.set_association_properties))
print(inspect.getsource(Traceloop.set_association_properties))
PYRepository: traceloop/agent-examples
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant file and nearby context
wc -l python/cooking_chat.py
cat -n python/cooking_chat.py | sed -n '1,180p'
# Find Traceloop usage and dependency pinning
rg -n "set_association_properties|traceloop" python -S
# Inspect dependency files for the SDK version
fd -a "pyproject.toml|requirements*.txt|poetry.lock|uv.lock|Pipfile.lock" pythonRepository: traceloop/agent-examples
Length of output: 6660
🌐 Web query:
Traceloop set_association_properties association properties string values turn integer docs
💡 Result:
According to the official Traceloop documentation, the association properties used to attach unique identifiers to traces must be strings [1][2]. Specifically, the documentation explicitly states that "the only requirement is that the key and the value are strings" [1][2]. If you pass non-string values (such as integers) to set_association_properties, they may not be handled as expected by the backend or could cause errors, as the system design typically enforces string-based key-value pairs for these associations [1][2]. To ensure your properties are correctly captured and searchable in the Traceloop UI, you should explicitly cast or convert any integer values to strings before passing them to the set_association_properties method. Example of correct usage: Traceloop.set_association_properties({ "user_id": str(12345), "chat_id": "chat_abc" })
Citations:
- 1: https://www.traceloop.com/docs/openllmetry/tracing/association
- 2: https://docs.traceloop.com/docs/openllmetry/tracing/association
Pass turn as a string. Traceloop association properties require string values, so turn should be str(turn); otherwise the session-grouping metadata can be dropped or rejected.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/cooking_chat.py` around lines 96 - 98, Update the
Traceloop.set_association_properties call to convert turn to a string with
str(turn), while leaving conversation_id unchanged.
| ``` | ||
| you > what can I make with chickpeas and spinach? | ||
| chef > ... | ||
| you > make it vegetarian and quicker | ||
| chef > ... | ||
| you > exit | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language tag to the fenced example.
markdownlint-cli2 reports MD040 for this block. Use text or console after the opening fence.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 120-120: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/README.md` around lines 120 - 126, Add a `text` or `console` language
tag to the opening fence of the chickpeas-and-spinach conversation example in
the README, leaving the example content unchanged.
Source: Linters/SAST tools
Summary by CodeRabbit
New Features
cooking-chatcommand to launch the assistant.Documentation