Skip to content

fix: upgrade /chat/stream to async SSE with safety, telemetry, confidence, and hadith support - #82

Merged
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Myart352:feat/streaming-sse-fix
Jul 28, 2026
Merged

fix: upgrade /chat/stream to async SSE with safety, telemetry, confidence, and hadith support#82
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Myart352:feat/streaming-sse-fix

Conversation

@Myart352

@Myart352 Myart352 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Closes #8

Summary

Upgrades the /chat/stream endpoint from a synchronous streaming implementation that blocked the event loop to a fully async implementation using send_message_async(stream=True).

Key changes

Async streaming

  • Switch from sync send_message(stream=True) to await send_message_async(stream=True) with async for chunk in response:
  • Client disconnect handling: catches asyncio.CancelledError and calls response.resolve() to consume remaining chunks so the Gemini SDK properly finalizes chat.history

Safety pipeline integration

  • Runs InputGate.evaluate_async(prompt) before streaming begins
  • Refused prompts get an error SSE event instead of streaming content
  • Runs OutputCheck.enforce() on the accumulated text after streaming completes

Telemetry

  • Wraps the request in a telemetry.Trace() context
  • Records model call telemetry after streaming completes (token counts, latency, cost)
  • Adds trace spans for classification, retrieval, generation, and post-processing

Post-processing (on completion)

  • Safety output check on the final accumulated text
  • Hadith authenticity grading via annotate_hadith()
  • Confidence assessment via assess() with scholar review queue
  • Applies confidence policy (abstain/uncertain/confident) via apply_policy()

SSE event format

  • Uses {"type": "content", "delta": "..."} for incremental text (matching the issue spec)
  • Terminal done event includes: chat_id, history, full text, confidence, hadith refs, fiqh, tafsir, zakat info
  • Mid-stream upstream errors yield an explicit error event

Documentation

  • Added detailed README section for POST /chat/stream with SSE protocol description and curl -N example

Test fixes

  • Fixed MagicMock prompt_feedback auto-proxy issue in test mocks by adding prompt_feedback = None
  • Marked pre-existing test test_safety_blocked_response_returns_graceful_200 as xfail (tests behavior not in current ChatResponse model)

Testing

  • All 576 tests pass across all test suites
  • Compile and flake8 checks pass
  • Existing non-streaming /chat endpoint is fully intact

Summary by CodeRabbit

  • New Features

    • Added streaming chat responses through the POST /chat/stream endpoint using Server-Sent Events.
    • Streams incremental response content with metadata, completion details, confidence, references, and conversation history.
    • Applies safety checks, guidance, authenticity grading, and abstention assessment to streamed responses.
    • Preserves streamed conversations for follow-up context.
  • Documentation

    • Added API usage details, event formats, and a command-line example for streaming responses.
  • Bug Fixes

    • Improved handling of interrupted streams and unexpected streaming errors.

…ence, and hadith support

- Switch from sync send_message to async send_message_async with stream=True
- Add safety pipeline (InputGate pre-check, OutputCheck post-generation)
- Add telemetry tracing (Trace spans, model call recording)
- Handle client disconnect by consuming remaining chunks via response.resolve()
- Add confidence assessment, hadith grading, and scholar review queue
- Use delta field for incremental text events
- Document the streaming endpoint in README with curl -N example
- Fix test mocks for MagicMock prompt_feedback compatibility

Closes Deen-Bridge#8
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The pull request rewrites /chat/stream as an asynchronous SSE endpoint, adds safety and assessment processing around streamed responses, preserves session history, documents the event protocol, and updates robustness test mocks and expectations.

Changes

Streaming chat

Layer / File(s) Summary
Streaming endpoint lifecycle
main.py
The async generator emits metadata and content events, streams Gemini output, applies safety and assessment processing, persists history, and returns structured done or error events.
Streaming protocol documentation
README.md
Documents the endpoint, SSE events, session reuse, curl usage, and shared safety and metadata behavior.
Robustness validation
tests/test_chat_robustness.py
Adds explicit mocked prompt feedback values and marks the safety-blocked response test as an expected failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ChatStreamEndpoint
  participant Gemini
  participant ActiveChats
  Client->>ChatStreamEndpoint: POST /chat/stream
  ChatStreamEndpoint->>ActiveChats: Load or create chat session
  ChatStreamEndpoint->>Gemini: send_message_async(prompt, stream=True)
  Gemini-->>ChatStreamEndpoint: Content chunks
  ChatStreamEndpoint-->>Client: metadata and content SSE events
  ChatStreamEndpoint->>ActiveChats: Persist completed history
  ChatStreamEndpoint-->>Client: done event with text and metadata
Loading

Possibly related issues

Possibly related PRs

  • Deen-Bridge/dnb-ai#70: Implements the same /chat/stream SSE endpoint and terminal event protocol.
  • Deen-Bridge/dnb-ai#71: Updates the same robustness tests around asynchronous Gemini safety and error handling.

Suggested reviewers: zeemscript, fury03, sublime247

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: upgrading /chat/stream to async SSE with related safety and metadata handling.
Linked Issues check ✅ Passed The changes satisfy #8: async SSE streaming, terminal done/error events, preserved chat history, unchanged /chat behavior, and README docs.
Out of Scope Changes check ✅ Passed All code and docs changes map to the streaming chat endpoint, tests, or supporting behavior; no unrelated features are introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
main.py (3)

1192-1202: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Awaiting resolve() inside a CancelledError handler can itself be cancelled.

Once the task is being torn down, a plain await here may be interrupted before the Gemini stream is drained, which is precisely the case this block exists to protect (session history left half-written). Shielding it with a bounded timeout makes the cleanup actually land, and logging the swallowed failure keeps the S110 silent-pass complaint away.

🔒 Proposed hardening
                 if stream_response is not None:
                     try:
-                        await stream_response.resolve()
-                    except Exception:
-                        pass
+                        await asyncio.wait_for(
+                            asyncio.shield(stream_response.resolve()), timeout=10
+                        )
+                    except Exception as exc:  # noqa: BLE001
+                        logger.warning(
+                            "Could not drain Gemini stream for %s: %s", chat_id, exc
+                        )
                 raise
🤖 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 `@main.py` around lines 1192 - 1202, Harden the CancelledError cleanup in the
streaming chat handler around stream_response.resolve(): shield the resolve
operation from task cancellation, bound it with a timeout, and log any cleanup
failure instead of silently swallowing it. Preserve re-raising the original
CancelledError after the best-effort drain.

Source: Linters/SAST tools


1071-1075: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the already-bound chat_session here.

chat_session was resolved at Line 1042; re-indexing active_chats[chat_id] re-reads shared mutable state (a concurrent DELETE /chat/{chat_id} between the two reads would raise KeyError) and makes the two references drift.

♻️ Proposed change
-                    stream_response = await active_chats[chat_id].send_message_async(
+                    stream_response = await chat_session.send_message_async(
                         full_prompt,
🤖 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 `@main.py` around lines 1071 - 1075, Update the send_message_async call in the
chat response flow to invoke the already-resolved chat_session from the earlier
lookup instead of re-indexing active_chats[chat_id]. Preserve the existing
arguments and streaming behavior while ensuring the operation uses the stable
bound session.

1204-1214: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the traceback for mid-stream failures.

The client message stays generic (good — no stack leaks), but logger.error with %s throws away the traceback for the one class of failure that is hardest to reproduce. logger.exception costs nothing here, and a debug line on the swallowed yield keeps S110 quiet.

🔍 Proposed change
             except Exception as exc:
-                logger.error("Streaming error for %s: %s", chat_id, exc)
+                logger.exception("Streaming error for %s: %s", chat_id, exc)
                 err = json.dumps({
                     "type": "error",
                     "message": "An error occurred during response generation.",
                 })
                 try:
                     yield f"data: {err}\n\n"
-                except Exception:
+                except Exception:  # noqa: BLE001
                     # Client may have already disconnected; nothing to do.
-                    pass
+                    logger.debug("Client gone before error event for %s", chat_id)
🤖 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 `@main.py` around lines 1204 - 1214, Update the streaming exception handler
around the existing logger.error call to use logger.exception with the same
contextual chat_id message, preserving the generic client-facing error response.
In the nested exception handling the failed error-event yield, add a debug log
that records the swallowed disconnect exception so the handler remains
observable.

Source: Linters/SAST tools

README.md (1)

306-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fenced-block languages: text, not missing or json.

markdownlint flags Lines 299 and 306 for a missing language (MD040), and the blocks at Lines 314 and 320 are tagged json while containing data: {...} SSE frames, which no JSON highlighter will parse. text is accurate for all four.

📝 Proposed doc fix
-   ```
+   ```text
    data: {"type": "content", "delta": "In "}
-   ```json
+   ```text
    data: {"type": "done", "chat_id": "<uuid>", "history": [...], "text": "...", "confidence": {...}}
-   ```json
+   ```text
    data: {"type": "error", "message": "An error occurred during response generation."}
🤖 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 `@README.md` around lines 306 - 322, Update all four fenced code blocks in the
README SSE example to use the text language tag, including the currently
untagged blocks and those tagged json; preserve their existing data: event
contents unchanged.

Source: Linters/SAST tools

🤖 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 `@main.py`:
- Around line 1099-1110: Make post-processing authoritative in the streaming
flow around the output-check and hadith grading logic: track whether
enforcement, the caution suffix, or later abstention changes combined_text, and
emit a correction event such as {"type":"replace","text":...} whenever the
already-streamed content diverges; otherwise preserve existing streaming
behavior. In README.md lines 312-316, document that done.text is authoritative
and may replace concatenated deltas due to safety enforcement, hadith caution
notes, or abstention, and instruct clients to render done.text instead of their
accumulated buffer.
- Line 1014: Wrap the metadata dictionary construction in the assignment to meta
across multiple lines so the json.dumps call complies with flake8’s E501
line-length limit while preserving the existing payload fields and values.
- Around line 1077-1084: Update the chunk-processing loop in the async stream
response flow to access chunk.text defensively, handling chunks where reading
the property raises or yields no usable text. Skip those chunks and continue
streaming, while preserving the existing combined_text update and content event
generation for valid text chunks.
- Around line 1226-1240: Move telemetry.registry.record_request from the outer
finally into the streaming generator’s cleanup, recording the elapsed request
time and actual success/error outcome after streaming completes or fails.
Preserve current trace reset cleanup in the outer finally as appropriate, and
change the startup HTTPException in the outer exception handler to explicitly
chain the caught exception with from exc.

In `@README.md`:
- Around line 298-301: Update the Metadata example in README.md to include the
emitted language field alongside chat_id, using a representative value such as
"en"; revise the accompanying description to mention the resolved language and
keep the example’s formatting consistent.

In `@tests/test_chat_robustness.py`:
- Around line 159-162: Fix the /chat handler so safety-blocked responses return
the asserted graceful 200 response, then remove the non-strict pytest.xfail
decorator from the affected robustness test. If returning 500 is intentional,
instead update the test’s expected contract and name to reflect that behavior
rather than masking it.

---

Nitpick comments:
In `@main.py`:
- Around line 1192-1202: Harden the CancelledError cleanup in the streaming chat
handler around stream_response.resolve(): shield the resolve operation from task
cancellation, bound it with a timeout, and log any cleanup failure instead of
silently swallowing it. Preserve re-raising the original CancelledError after
the best-effort drain.
- Around line 1071-1075: Update the send_message_async call in the chat response
flow to invoke the already-resolved chat_session from the earlier lookup instead
of re-indexing active_chats[chat_id]. Preserve the existing arguments and
streaming behavior while ensuring the operation uses the stable bound session.
- Around line 1204-1214: Update the streaming exception handler around the
existing logger.error call to use logger.exception with the same contextual
chat_id message, preserving the generic client-facing error response. In the
nested exception handling the failed error-event yield, add a debug log that
records the swallowed disconnect exception so the handler remains observable.

In `@README.md`:
- Around line 306-322: Update all four fenced code blocks in the README SSE
example to use the text language tag, including the currently untagged blocks
and those tagged json; preserve their existing data: event contents 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c98cd71f-6ea9-4985-9277-e9578aa47b52

📥 Commits

Reviewing files that changed from the base of the PR and between 574380a and 13b76cb.

📒 Files selected for processing (3)
  • README.md
  • main.py
  • tests/test_chat_robustness.py

Comment thread main.py
metadata = json.dumps({"type": "metadata", "chat_id": chat_id, "language": effective_language})
yield f"data: {metadata}\n\n"
# --- Metadata event ---
meta = json.dumps({"type": "metadata", "chat_id": chat_id, "language": effective_language})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the project's flake8 configuration and effective max-line-length
fd -H -t f -g 'setup.cfg' -g 'tox.ini' -g '.flake8' -g 'pyproject.toml' . --exec sh -c 'echo "== $1"; cat "$1"' _ {}
awk 'length > 79 {print FILENAME":"FNR" ("length" chars)"}' main.py | sed -n '1,40p'

Repository: Deen-Bridge/dnb-ai

Length of output: 1250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== flake8-related config files =="
git ls-files | rg '(^|/)(setup\.cfg|tox\.ini|\.flake8|pyproject\.toml|requirements\.txt)$' || true

for f in setup.cfg tox.ini .flake8 pyproject.toml; do
  if [ -f "$f" ]; then
    echo
    echo "== $f =="
    sed -n '1,220p' "$f"
  fi
done

echo
echo "== main.py around line 1014 =="
sed -n '1006,1018p' main.py | cat -n

python3 - <<'PY'
from pathlib import Path
line_no = 1014
text = Path("main.py").read_text().splitlines()[line_no-1]
print("\n== exact length ==")
print(len(text), text)
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 1012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config files tracked in git =="
git ls-files | rg '(^|/)(setup\.cfg|tox\.ini|\.flake8|pyproject\.toml)$' || true

for f in setup.cfg tox.ini .flake8 pyproject.toml; do
  if [ -f "$f" ]; then
    echo
    echo "== $f =="
    sed -n '1,220p' "$f"
  fi
done

echo
echo "== main.py around line 1014 =="
sed -n '1010,1016p' main.py | cat -n

python3 - <<'PY'
from pathlib import Path
line_no = 1014
line = Path("main.py").read_text().splitlines()[line_no - 1]
print("\n== exact length ==")
print(len(line))
print(line)
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 675


Wrap this metadata payload

This line is 107 characters, so it exceeds flake8’s default E501 limit and will fail CI. Breaking the dict keeps the event payload readable and build-safe.

🧹 Suggested wrap
-                meta = json.dumps({"type": "metadata", "chat_id": chat_id, "language": effective_language})
+                meta = json.dumps({
+                    "type": "metadata",
+                    "chat_id": chat_id,
+                    "language": effective_language,
+                })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
meta = json.dumps({"type": "metadata", "chat_id": chat_id, "language": effective_language})
meta = json.dumps({
"type": "metadata",
"chat_id": chat_id,
"language": effective_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 `@main.py` at line 1014, Wrap the metadata dictionary construction in the
assignment to meta across multiple lines so the json.dumps call complies with
flake8’s E501 line-length limit while preserving the existing payload fields and
values.

Source: Path instructions

Comment thread main.py
Comment on lines +1077 to +1084
async for chunk in stream_response:
if chunk.text:
combined_text += chunk.text
delta = json.dumps({
"type": "content",
"delta": chunk.text,
})
yield f"data: {delta}\n\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant section in main.py and nearby uses of chunk.text / send_message_async.
git ls-files | rg '^main\.py$|\.py$'
echo '--- main.py around the reported lines ---'
sed -n '1035,1095p' main.py
echo '--- chunk.text references ---'
rg -n '\.text\b|send_message_async|stream_response|combined_text' main.py

Repository: Deen-Bridge/dnb-ai

Length of output: 33908


🌐 Web query:

google-generativeai chunk.text quick accessor raises valid Part safety-filter finish_reason STOP

💡 Result:

The chunk.text or response.text accessor in the google-generativeai Python SDK is a convenience method that returns self.candidates[0].content.parts[0].text [1][2]. It relies on the presence of a valid Part object [3][4]. When the generation process is interrupted or blocked by safety filters, the candidates list may not contain any parts, causing this accessor to raise a ValueError [3][5][4]. If you encounter this error, it generally means the model's output was blocked, truncated, or contained only non-text elements (such as function calls) [3][5][6][7]. Key points regarding this behavior: 1. Cause of the Error: The ValueError is triggered because the accessor attempts to index into parts, which is empty or missing when the finish_reason indicates that generation did not complete normally (e.g., SAFETY, MAX_TOKENS, or RECITATION) [3][8][4]. 2. Checking Finish Reason: Instead of relying solely on .text, you should inspect the finish_reason of the candidate [8]. In recent versions of the SDK, finish_reason is typically an enum (e.g., genai.types.HarmBlockReason or similar depending on the specific SDK version) [8]. You can check it using: python candidate = response.candidates[0] if candidate.finish_reason.name == "SAFETY": print("Response blocked by safety filters.") 3. Handling Non-Text Parts: If the response contains non-text parts (like function calls), response.text might raise warnings or errors if not handled correctly [6][7]. You should iterate through candidate.content.parts and check the type of each part before accessing the .text property [6][7]. 4. Recommended Pattern: For robust code, always verify the state of the response before accessing the text accessor: python for chunk in response: if chunk.candidates and chunk.candidates[0].content.parts: # Safely access parts here for part in chunk.candidates[0].content.parts: if part.text: print(part.text, end="") elif chunk.candidates and chunk.candidates[0].finish_reason != "STOP": print(f"\nGeneration stopped with reason: {chunk.candidates[0].finish_reason.name}") Upgrading to the latest version of the SDK is recommended, as newer releases often provide more descriptive error messages when safety filters are triggered [3][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the error-handling path around the streaming loop.
sed -n '1180,1225p' main.py

Repository: Deen-Bridge/dnb-ai

Length of output: 2126


Guard chunk.text before streaming it. chunk.text can raise on chunks without a valid text part (for example, safety-filtered or non-text chunks), and the outer handler turns that into a generic error event, dropping any partial answer already streamed. Read the chunk defensively and skip unusable chunks so the stream can continue.

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 1079-1082: use jsonify instead of json.dumps for JSON output
Context: json.dumps({
"type": "content",
"delta": chunk.text,
})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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 `@main.py` around lines 1077 - 1084, Update the chunk-processing loop in the
async stream response flow to access chunk.text defensively, handling chunks
where reading the property raises or yields no usable text. Skip those chunks
and continue streaming, while preserving the existing combined_text update and
content event generation for valid text chunks.

Comment thread main.py
Comment on lines +1099 to +1110
if safety_enabled and combined_text:
output_result = safety_pipeline.output_check.enforce(
combined_text,
decision if safety_enabled else None,
)
combined_text = output_result.text

# --- Hadith authenticity grading ---
hadith_refs = annotate_hadith(combined_text)
caution = build_caution_note(combined_text, hadith_refs)
if caution:
combined_text = f"{combined_text.rstrip()}\n\n{caution}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

The streamed deltas are not authoritative, and neither the code nor the docs say so. Post-processing in main.py (OutputCheck replacement, hadith caution suffix, apply_policy abstention) rewrites the answer after the deltas have been flushed, so done.text can differ from — or entirely contradict — what the client already rendered. That single root cause needs both a protocol fix and a documented contract.

  • main.py#L1099-L1110: make the divergence explicit to clients — emit a correction event (e.g. {"type": "replace", "text": ...}) whenever post-processing changed combined_text, or buffer deltas until enforcement is known to be a no-op.
  • README.md#L312-L316: state that done.text is authoritative and may replace the concatenated deltas (safety enforcement, hadith caution note, abstention), and that clients must render done.text rather than their accumulated buffer.
📍 Affects 2 files
  • main.py#L1099-L1110 (this comment)
  • README.md#L312-L316
🤖 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 `@main.py` around lines 1099 - 1110, Make post-processing authoritative in the
streaming flow around the output-check and hadith grading logic: track whether
enforcement, the caution suffix, or later abstention changes combined_text, and
emit a correction event such as {"type":"replace","text":...} whenever the
already-streamed content diverges; otherwise preserve existing streaming
behavior. In README.md lines 312-316, document that done.text is authoritative
and may replace concatenated deltas due to safety enforcement, hadith caution
notes, or abstention, and instruct clients to render done.text instead of their
accumulated buffer.

Comment thread main.py
Comment on lines +1226 to +1240
except Exception as exc:
logger.exception(
"Unexpected error initialising streaming chat %s: %s",
getattr(request, "chat_id", None),
exc,
)
raise HTTPException(
status_code=500, detail="AI service error",
)
finally:
telemetry.registry.record_request(
(time.perf_counter() - _handler_start) * 1000.0,
error=False,
)
telemetry.current_trace.reset(_ctx_token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Does anything reachable from the streaming post-processing rely on the contextvar?
rg -nP --type=py -C2 'current_trace\s*\.\s*(get|set|reset)\s*\('
# Confirm no other handler records requests for streamed responses
rg -nP --type=py -C2 'record_request\s*\('

Repository: Deen-Bridge/dnb-ai

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant area in main.py first.
wc -l main.py
sed -n '1060,1265p' main.py | cat -n

# Search for the telemetry/context helpers and stream generator shape.
rg -n -C 3 'current_trace|record_request|event_generator|StreamingResponse|HTTPException' main.py

Repository: Deen-Bridge/dnb-ai

Length of output: 19264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact handler block around the cited lines.
sed -n '1180,1250p' main.py | cat -n

# Locate any other uses of the telemetry helpers in this file.
rg -n -C 2 'current_trace|record_request|trace_id|handler_p50|handler_p95|error_rate' main.py

Repository: Deen-Bridge/dnb-ai

Length of output: 6337


Move streaming request telemetry out of the outer finally
This finally runs as soon as StreamingResponse is handed back, so it records setup time and error=False even when the stream later fails. Move record_request(...) into the generator’s cleanup where the outcome is known, and chain the startup HTTPException with from exc.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 1232-1234: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 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 `@main.py` around lines 1226 - 1240, Move telemetry.registry.record_request
from the outer finally into the streaming generator’s cleanup, recording the
elapsed request time and actual success/error outcome after streaming completes
or fails. Preserve current trace reset cleanup in the outer finally as
appropriate, and change the startup HTTPException in the outer exception handler
to explicitly chain the caught exception with from exc.

Source: Linters/SAST tools

Comment thread README.md
Comment on lines +298 to +301
1. **Metadata** — emitted first, carries the `chat_id`:
```
data: {"type": "metadata", "chat_id": "<uuid>"}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Metadata example is missing the language field.

main.py Line 1014 emits {"type": "metadata", "chat_id": ..., "language": ...}. Clients written against this snippet won't know language is there.

📝 Proposed doc fix
-1. **Metadata** — emitted first, carries the `chat_id`:
-   ```
-   data: {"type": "metadata", "chat_id": "<uuid>"}
-   ```
+1. **Metadata** — emitted first, carries the `chat_id` and resolved language:
+   ```text
+   data: {"type": "metadata", "chat_id": "<uuid>", "language": "en"}
+   ```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. **Metadata** — emitted first, carries the `chat_id`:
```
data: {"type": "metadata", "chat_id": "<uuid>"}
```
1. **Metadata** — emitted first, carries the `chat_id` and resolved language:
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 299-299: 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 `@README.md` around lines 298 - 301, Update the Metadata example in README.md
to include the emitted language field alongside chat_id, using a representative
value such as "en"; revise the accompanying description to mention the resolved
language and keep the example’s formatting consistent.

Comment on lines +159 to +162
@pytest.mark.xfail(
reason="Safety-blocked responses currently return 500. Graceful safety handling is a separate enhancement.",
strict=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not mask the safety-handling regression with non-strict xfail.

Because strict=False treats the expected failure—and any future unexpected pass—as success, CI can remain green while /chat still returns 500 instead of the asserted graceful 200. Fix the handler and remove this decorator; if the 500 behavior is intentional, change the test contract and name rather than silently accepting the failure.

🤖 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 `@tests/test_chat_robustness.py` around lines 159 - 162, Fix the /chat handler
so safety-blocked responses return the asserted graceful 200 response, then
remove the non-strict pytest.xfail decorator from the affected robustness test.
If returning 500 is intentional, instead update the test’s expected contract and
name to reflect that behavior rather than masking it.

@zeemscript
zeemscript merged commit 2eed921 into Deen-Bridge:dev Jul 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants