Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The platform is composed of three services:
| Method | Route | Purpose |
|--------|-------|---------|
| `POST` | `/chat` | Start or continue a chat session |
| `POST` | `/chat/stream` | Stream chat responses as Server-Sent Events |
| `DELETE` | `/chat/{chat_id}` | Delete a chat session |
| `GET` | `/memory/{user_id}` | Retrieve a stored user profile (transparency) |
| `DELETE` | `/memory/{user_id}` | Completely erase a stored user profile |
Expand Down Expand Up @@ -87,6 +88,17 @@ uvicorn main:app --reload

The API runs at `http://localhost:8000` — interactive docs at `http://localhost:8000/docs`.

### Streaming chat endpoint

Use the streaming endpoint to receive incremental chat output as Server-Sent Events:

```bash
curl -N -X POST http://localhost:8000/chat/stream \
-H 'Content-Type: application/json' \
-d '{"message": "Explain the concept of tawakkul in Islam", "chat_id": "demo"}'
```

Each `delta` event contains a JSON payload like `{"delta": "..."}` and the terminal `done` event contains the `chat_id` and the completed text.

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

Document the error event too.

The endpoint emits event: error, but this contract only describes delta and done; clients need the failure payload and terminal semantics to handle upstream failures correctly.

🤖 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` at line 83, Update the streaming endpoint event documentation in
README.md to include the error event, describing its failure payload and that it
terminates the stream. Keep the existing delta and done event descriptions
intact.

### Docker

The included `Dockerfile` produces a production-ready image with Python 3.12,
Expand Down
55 changes: 55 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import json
import os
from typing import List, Optional, Dict, Any
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import asyncio
import json
import logging
Expand Down Expand Up @@ -946,6 +952,55 @@ async def event_generator():
raise HTTPException(status_code=500, detail="Internal server error") from e


@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
if not GEMINI_API_KEY:
raise HTTPException(status_code=500, detail="GEMINI_API_KEY is not configured.")

chat_id = request.chat_id or "default"
if chat_id not in sessions:
model = get_model()
sessions[chat_id] = model.start_chat(history=[])

chat_session = sessions[chat_id]

async def event_stream():
full_text_parts: List[str] = []
try:
response_stream = None
if hasattr(chat_session, "send_message_async"):
response_stream = await chat_session.send_message_async(request.message, stream=True)
else:
response_stream = chat_session.send_message(request.message, stream=True)

async for chunk in response_stream:
text = getattr(chunk, "text", None)
if text is None and isinstance(chunk, dict):
text = chunk.get("text")
if text is None:
continue

full_text_parts.append(text)
payload = json.dumps({"delta": text})
yield f"event: delta\ndata: {payload}\n\n"
except Exception as exc: # pragma: no cover - defensive path
error_payload = json.dumps({"error": str(exc)})
yield f"event: error\ndata: {error_payload}\n\n"
Comment on lines +986 to +988

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 | ⚡ Quick win

Do not expose raw upstream exceptions in the public SSE contract.

Provider exception strings can disclose internal request or configuration details. Emit a stable public error code/message and log the original exception server-side; update the test to assert that safe contract instead.

  • main.py#L189-L191: replace str(exc) in the event payload with a stable, non-sensitive error response.
  • tests/test_chat_streaming.py#L67-L70: assert the stable public error code/message rather than "upstream failed".
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 189-189: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"error": str(exc)})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 Ruff (0.15.21)

[warning] 189-189: Do not catch blind exception: Exception

(BLE001)

📍 Affects 2 files
  • main.py#L189-L191 (this comment)
  • tests/test_chat_streaming.py#L67-L70
🤖 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 189 - 191, In main.py lines 189-191, update the
exception path in the streaming handler to log the original exception
server-side and emit only a stable, non-sensitive public error code/message
instead of str(exc). In tests/test_chat_streaming.py lines 67-70, update the
streaming error assertion to expect that stable public contract rather than
“upstream failed”.

return

final_payload = json.dumps({"chat_id": chat_id, "text": "".join(full_text_parts)})
yield f"event: done\ndata: {final_payload}\n\n"

return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)


@app.delete("/chat/{chat_id}")
async def delete_chat(chat_id: str):
try:
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ python-dotenv==0.21.0
google-generativeai==0.8.3
pydantic>=2.5.3
stellar-sdk>=12.0.0
pytest
pytest-asyncio
Comment on lines +7 to +8

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
set -euo pipefail

fd -a 'requirements*.txt|pyproject.toml|pytest.ini|tox.ini|.python-version|.tool-versions|Dockerfile' . -E venv
rg -n --hidden -g '!venv/**' \
  'pytest(-asyncio)?|setup-python|python-version' \
  requirements.txt .github 2>/dev/null || true

Repository: Deen-Bridge/dnb-ai

Length of output: 1573


🏁 Script executed:

sed -n '1,120p' requirements.txt && printf '\n---\n' && sed -n '1,120p' .github/workflows/ci.yml && printf '\n---\n' && sed -n '1,120p' pytest.ini

Repository: Deen-Bridge/dnb-ai

Length of output: 3537


Pin pytest and pytest-asyncio in the test environment. requirements.txt should carry exact versions for these new test deps, and the CI job currently installs them again with pip install pytest flake8 pytest-asyncio, which leaves the test setup open to upstream drift.

🤖 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 `@requirements.txt` around lines 7 - 8, Pin pytest and pytest-asyncio to exact
versions in requirements.txt, and update the CI installation step to rely on
those pinned requirements instead of reinstalling unversioned packages. Preserve
the existing flake8 installation behavior.

httpx
Comment thread
coderabbitai[bot] marked this conversation as resolved.
PyYAML==6.0.2
numpy==2.2.4
redis>=5.0.0,<6.0.0
Expand Down
70 changes: 70 additions & 0 deletions tests/test_chat_streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest
from fastapi.testclient import TestClient

import main


class FakeChunk:
def __init__(self, text: str):
self.text = text


class FakeChatSession:
def __init__(self):
self.history = []

async def send_message_async(self, message, stream=True):
self.history.append({"role": "user", "text": message})

async def generator():
yield FakeChunk("Hello")
yield FakeChunk(" world")

return generator()


class FailingChatSession(FakeChatSession):
async def send_message_async(self, message, stream=True):
self.history.append({"role": "user", "text": message})

async def generator():
yield FakeChunk("Hello")
raise RuntimeError("upstream failed")

return generator()


@pytest.fixture(autouse=True)
def reset_app_state(monkeypatch):
main.sessions.clear()
monkeypatch.setattr(main, "GEMINI_API_KEY", "fake-key")
monkeypatch.setattr(main, "CITATION_VERIFY_MODE", "off")
monkeypatch.setattr(main, "get_model", lambda: None)


def test_chat_stream_endpoint_emits_sse_events(monkeypatch):
chat_session = FakeChatSession()
monkeypatch.setattr(main, "get_model", lambda: type("Model", (), {"start_chat": lambda self, history=[]: chat_session})())

client = TestClient(main.app)
response = client.post("/chat/stream", json={"message": "Hello", "chat_id": "stream-test"})

assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
assert "event: delta" in response.text
assert "Hello" in response.text
assert "event: done" in response.text
assert '"chat_id": "stream-test"' in response.text


def test_chat_stream_endpoint_emits_error_event_on_upstream_failure(monkeypatch):
chat_session = FailingChatSession()
monkeypatch.setattr(main, "get_model", lambda: type("Model", (), {"start_chat": lambda self, history=[]: chat_session})())

client = TestClient(main.app)
response = client.post("/chat/stream", json={"message": "Hello", "chat_id": "broken-stream"})

assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
assert "event: error" in response.text
assert "upstream failed" in response.text
Loading