Skip to content

feat: cap conversation history with token budget to prevent unbounded prompt growth - #49

Open
Abdul-dev-creator wants to merge 4 commits into
Deen-Bridge:devfrom
Abdul-dev-creator:fix/issue-13-token-budget
Open

feat: cap conversation history with token budget to prevent unbounded prompt growth#49
Abdul-dev-creator wants to merge 4 commits into
Deen-Bridge:devfrom
Abdul-dev-creator:fix/issue-13-token-budget

Conversation

@Abdul-dev-creator

@Abdul-dev-creator Abdul-dev-creator commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR adds a token budget system to cap conversation history and prevent unbounded prompt growth in chat sessions. Before each Gemini call, the oldest turn-pairs are dropped until both the token budget and turn cap are satisfied.

Related Issue

Closes #13

Changes

History Budget and Truncation

  • ADD history.py with token estimation and history trimming
  • MAX_HISTORY_TOKENS (default 16000) and MAX_HISTORY_TURNS (default 50) env config
  • Turn-count cap enforced first as cheap backstop
  • Oldest user+model turn-pairs dropped together (never orphaned)
  • System instruction never counted or trimmed

API Surface

  • ADD include_history field on ChatRequest (default true)
  • ADD truncated field on ChatResponse

Documentation

  • New env vars documented in README

Tests

  • ADD 13 unit tests for truncation boundary, pair-granularity, and env configurability

Summary by CodeRabbit

  • New Features
    • Added token-and-turn capped chat history trimming with a truncated response flag.
    • Added confidence assessment and optional scholar review queueing for low-confidence religious answers.
    • Added study/quiz & flashcard generation endpoints.
    • Added hadith citation annotation plus fiqh question detection.
    • Added semantic cache and confidence policy endpoints.
  • Documentation
    • Expanded setup/environment docs, including history budget, confidence, and scholar-review configuration.
  • Bug Fixes
    • Ensures history trimming never leaves orphan turn pairs.
  • Tests
    • Added extensive offline unit/integration coverage for truncation, confidence, safety/policy, hadith, fiqh, study, cache, and review queue.
  • Chores
    • Expanded CI linting, syntax checks, and targeted test suites.

Oberonskii and others added 3 commits July 21, 2026 20:39
…g response

Two bugs fixed in main.py:

1. The /ping endpoint returned a Python set literal instead of a
   proper JSON dict, which cannot be serialized. Changed to return
   {"status": "ok"} as a proper JSON object.

2. The prompt construction used {ISLAMIC_CONTEXT, request.prompt}
   which Python interprets as a tuple, causing the model to receive
   a stringified tuple instead of clean text. Fixed by:
   - Passing ISLAMIC_CONTEXT as system_instruction to GenerativeModel
   - Building the prompt string without the tuple syntax
…d-ping-response

fix: resolve tuple artifact in prompt construction and malformed /ping response
… prompt growth

- Add MAX_HISTORY_TOKENS (default 16000) and MAX_HISTORY_TURNS (default 50) env config
- Trim oldest turn-pairs at pair granularity before each Gemini call
- Surface truncated boolean in ChatResponse
- Add include_history flag to ChatRequest (default true) to omit payload bloat
- Use cheap local estimate (chars/4) avoiding extra network calls
- Document new env vars in README
- Add 13 unit tests covering truncation boundary, pair-granularity invariant, and env configurability

Closes Deen-Bridge#13
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds configurable chat-history trimming, confidence and scholar-review workflows, hadith grading, fiqh classification, semantic caching, study generation, and layered safety enforcement, with expanded documentation, dependencies, CI coverage, and offline tests.

Changes

Chat intelligence and response controls

Layer / File(s) Summary
History budgeting and chat integration
history.py, main.py, README.md, tests/test_history_truncation.py
Chat history is trimmed by configurable turn and estimated-token limits, optionally returned, and reported through truncated; chat responses also integrate hadith, confidence, cache, and review metadata.
Confidence, caching, and scholar review
confidence.py, semantic_cache.py, review.py, review_store.py, tests/test_confidence.py, tests/test_semantic_cache.py, tests/test_review_queue.py
Confidence bands control abstention and review queueing, semantic cache behavior provides similarity-based reuse, and reviewer verdicts update durable or in-memory queue state with optional feedback export.
Hadith grading and fiqh classification
hadith.py, fiqh.py, scripts/build_hadith_data.py, data/hadith/*, tests/test_hadith.py, tests/test_fiqh.py
Hadith references are normalized, graded, annotated, and cautioned from bundled metadata; fiqh prompts support keyword classification and madhhab normalization.

Study content generation

Layer / File(s) Summary
Study schemas and generation workflow
study.py, tests/test_study.py, tests/fixtures/*
The study router validates quiz and flashcard requests, converts schemas for Gemini, retries invalid generation results, and exposes /study/generate with offline fixture coverage.

Layered content safety

Layer / File(s) Summary
Safety policy contracts and configuration
safety/policy.yaml, safety/policy.py, safety/POLICY.md, safety/__init__.py, tests/redteam/prompts.yaml, tests/redteam/test_live.py
A versioned YAML policy defines four safety categories, actions, failure modes, guidance, refusal text, and red-team operating rules.
Safety classification and output enforcement
safety/input_gate.py, safety/output_check.py, safety/pipeline.py, tests/redteam/test_pipeline.py
Input gating validates classifier results and fail-closed behavior; output enforcement replaces policy violations or appends scholar referral guidance through synchronous and asynchronous pipeline paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • Deen-Bridge/dnb-ai#34 — Overlaps with the /chat safety-derived response metadata and moderation flow.
  • Deen-Bridge/dnb-ai#35 — Directly overlaps with the study router and /study/generate endpoint.
  • Deen-Bridge/dnb-ai#36 — Directly overlaps with semantic-cache integration, cache metrics, and chat-flow wiring.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Large new modules for safety, study, fiqh, hadith, review, and semantic cache are unrelated to #13's history-cap scope. Split unrelated feature work into separate PRs and keep this change set focused on conversation-history truncation, the response flag, docs, and tests.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested history token/turn caps, truncation flag, include_history toggle, docs, and tests for #13.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: capping conversation history with a token budget to prevent prompt growth.
✨ 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

🤖 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 `@history.py`:
- Around line 13-14: Validate MAX_HISTORY_TOKENS and MAX_HISTORY_TURNS while
parsing their environment values, requiring non-negative integers and raising a
clear configuration error for invalid or negative values. Keep the existing
defaults and history-processing behavior unchanged for valid settings.
- Around line 40-44: Update the history token calculation around estimate_tokens
to iterate over every part in each message and sum the tokens from all parts
that expose text, rather than only parts[0]. Preserve the zero-token behavior
for messages or parts without text, and add a regression test covering multipart
Gemini messages exceeding MAX_HISTORY_TOKENS.

In `@main.py`:
- Around line 145-147: Update the async handler around trim_history and the
synchronous chat.send_message call to use the SDK’s asynchronous API or
asyncio.to_thread, preventing model responses from blocking the event loop.
Preserve the existing per-chat serialization behavior while offloading only the
blocking Gemini request.

In `@tests/test_history_truncation.py`:
- Around line 169-171: Update test_budget_configurable_via_env to set the
relevant environment variables with monkeypatch and reload the history module
before invoking trim_history. Remove the direct patches to
history.MAX_HISTORY_TOKENS and history.MAX_HISTORY_TURNS, and call trim_history
from the reloaded module so the test validates environment parsing.
- Around line 115-148: Strengthen test_no_orphaned_turns_after_truncation by
asserting every retained history pair is ordered user then model, not only that
the length is even. In test_preserves_recent_context_after_truncation, replace
the nonexistent "Old question 1" marker with content actually present in the
fixture, such as "very old" or the repeated C marker, and assert that old
content was removed while recent context remains.
- Line 8: Add pytest as an explicitly pinned dependency in requirements.txt so
tests/test_history_truncation.py can import it in clean CI environments.
🪄 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: f1cb4c63-2164-4479-8a8e-ca1b43a9ccb0

📥 Commits

Reviewing files that changed from the base of the PR and between cc79344 and fdf6572.

📒 Files selected for processing (4)
  • README.md
  • history.py
  • main.py
  • tests/test_history_truncation.py

Comment thread history.py
Comment on lines +13 to +14
MAX_HISTORY_TOKENS = int(os.getenv("MAX_HISTORY_TOKENS", "16000"))
MAX_HISTORY_TURNS = int(os.getenv("MAX_HISTORY_TURNS", "50"))

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

Reject invalid history limits at startup.

A negative MAX_HISTORY_TURNS makes the loop at Line 34 remain true after history is emptied, so the next pair-pop raises IndexError. Negative token limits also silently discard all history. Parse these settings as non-negative integers and raise a clear configuration error otherwise.

Proposed fix
+def _read_non_negative_int(name: str, default: int) -> int:
+    value = int(os.getenv(name, str(default)))
+    if value < 0:
+        raise ValueError(f"{name} must be non-negative")
+    return value
+
-MAX_HISTORY_TOKENS = int(os.getenv("MAX_HISTORY_TOKENS", "16000"))
-MAX_HISTORY_TURNS = int(os.getenv("MAX_HISTORY_TURNS", "50"))
+MAX_HISTORY_TOKENS = _read_non_negative_int("MAX_HISTORY_TOKENS", 16000)
+MAX_HISTORY_TURNS = _read_non_negative_int("MAX_HISTORY_TURNS", 50)
📝 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
MAX_HISTORY_TOKENS = int(os.getenv("MAX_HISTORY_TOKENS", "16000"))
MAX_HISTORY_TURNS = int(os.getenv("MAX_HISTORY_TURNS", "50"))
def _read_non_negative_int(name: str, default: int) -> int:
value = int(os.getenv(name, str(default)))
if value < 0:
raise ValueError(f"{name} must be non-negative")
return value
MAX_HISTORY_TOKENS = _read_non_negative_int("MAX_HISTORY_TOKENS", 16000)
MAX_HISTORY_TURNS = _read_non_negative_int("MAX_HISTORY_TURNS", 50)
🤖 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 `@history.py` around lines 13 - 14, Validate MAX_HISTORY_TOKENS and
MAX_HISTORY_TURNS while parsing their environment values, requiring non-negative
integers and raising a clear configuration error for invalid or negative values.
Keep the existing defaults and history-processing behavior unchanged for valid
settings.

Comment thread history.py
Comment on lines +40 to +44
total = sum(
estimate_tokens(m.parts[0].text)
if hasattr(m, "parts") and m.parts and hasattr(m.parts[0], "text")
else 0
for m in chat_session.history

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and inspect the relevant section with line numbers.
fd -a '^history\.py$' .

Repository: Deen-Bridge/dnb-ai

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the target file and relevant tests, then inspect the surrounding code.
python3 - <<'PY'
import os
from pathlib import Path

matches = []
for p in Path('.').rglob('history.py'):
    matches.append(str(p))
print("HISTORY_FILES:")
for m in matches:
    print(m)

print("\nTEST_FILES:")
for p in Path('.').rglob('*test*'):
    if p.is_file() and p.suffix in {'.py', '.ts', '.js'}:
        if 'history' in p.name.lower() or 'token' in p.name.lower() or 'gemini' in p.name.lower():
            print(p)
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 228


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git ls-files | rg '(^|/)history\.py$|test|gemini|token'

Repository: Deen-Bridge/dnb-ai

Length of output: 518


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect the relevant section with line numbers.
file=$(git ls-files | rg '(^|/)history\.py$' | head -n 1)
echo "FILE=$file"
if [ -n "$file" ]; then
  nl -ba "$file" | sed -n '1,140p'
fi

echo
echo "TESTS:"
git ls-files | rg '(^|/).*(test|spec).*\.(py|js|ts)$|(^|/)tests?/' | rg 'history|token|gemini|chat'

Repository: Deen-Bridge/dnb-ai

Length of output: 212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=$(git ls-files | rg '(^|/)history\.py$' | head -n 1)
echo "FILE=$file"
nl -ba "$file" | sed -n '1,160p'

Repository: Deen-Bridge/dnb-ai

Length of output: 212


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "hello"

Repository: Deen-Bridge/dnb-ai

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('history.py'), Path('tests/test_history_truncation.py')]:
    print(f"\n### {path} ###")
    text = path.read_text()
    lines = text.splitlines()
    # Print all lines with numbers if reasonably small, otherwise first 220 lines.
    start = 1
    end = min(len(lines), 260)
    for i in range(start, end + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 9875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the specific logic around history truncation and token counting.
python3 - <<'PY'
from pathlib import Path

for file in ['history.py', 'tests/test_history_truncation.py']:
    p = Path(file)
    print(f"\nFILE: {file}")
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if 'estimate_tokens' in line or 'parts' in line or 'history' in line or 'truncate' in line or 'token' in line:
            print(f"{i:4d}: {line}")
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 4907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [Path('history.py'), Path('tests/test_history_truncation.py')]:
    print(f"\n### {path} ###")
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if i <= 220:
            print(f"{i:4d}: {line}")
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 9875


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path

for file in ('history.py', 'tests/test_history_truncation.py'):
    p = Path(file)
    print(f"\n=== {file} ===")
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if i <= 220:
            print(f"{i:4d}: {line}")
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 9875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for file in ('history.py', 'tests/test_history_truncation.py'):
    p = Path(file)
    print(f"\n=== {file} ===")
    lines = p.read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if i <= 220:
            print(f"{i:4d}: {line}")
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 9875


Count all text parts in history trimming. Lines 40-44 only budget parts[0], so multipart Gemini messages can slip past MAX_HISTORY_TOKENS. Sum every text part and add a multipart regression test.

🤖 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 `@history.py` around lines 40 - 44, Update the history token calculation around
estimate_tokens to iterate over every part in each message and sum the tokens
from all parts that expose text, rather than only parts[0]. Preserve the
zero-token behavior for messages or parts without text, and add a regression
test covering multipart Gemini messages exceeding MAX_HISTORY_TOKENS.

Comment thread main.py Outdated
Comment on lines +145 to +147
# Trim history to stay within budget (oldest turn-pairs dropped)
truncated = trim_history(chat)

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and the relevant symbols.
git ls-files | rg '^main\.py$|(^|/)main\.py$|(^|/)requirements\.txt$|(^|/)pyproject\.toml$|(^|/)poetry\.lock$|(^|/)Pipfile(\.lock)?$'
echo '--- outline main.py ---'
ast-grep outline main.py --view expanded || true
echo '--- search for send_message / Gemini / async endpoint ---'
rg -n "send_message|Gemini|async def|trim_history|ChatRequest|ChatResponse" main.py

Repository: Deen-Bridge/dnb-ai

Length of output: 1724


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant slice of main.py with line numbers.
sed -n '120,190p' main.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 3270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any local dependency hints for the Gemini client.
rg -n "google\.generativeai|genai|GenerativeModel|asyncio\.to_thread|to_thread|send_message_async" .

Repository: Deen-Bridge/dnb-ai

Length of output: 311


Offload the Gemini call from the async handler. chat.send_message(...) is synchronous, so this endpoint blocks the event loop while the model responds. Use the SDK’s async API or wrap it with asyncio.to_thread, and keep per-chat serialization intact.

🤖 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 145 - 147, Update the async handler around trim_history
and the synchronous chat.send_message call to use the SDK’s asynchronous API or
asyncio.to_thread, preventing model responses from blocking the event loop.
Preserve the existing per-chat serialization behavior while offloading only the
blocking Gemini request.

Source: Path instructions


from unittest.mock import MagicMock, PropertyMock

import pytest

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked files of interest ---'
git ls-files | rg '(^|/)(requirements(\.txt)?|pyproject\.toml|setup\.py|setup.cfg|tox.ini|Pipfile|poetry\.lock|tests/test_history_truncation\.py)$'

printf '\n%s\n' '--- tests/test_history_truncation.py ---'
cat -n tests/test_history_truncation.py

printf '\n%s\n' '--- manifest search for pytest ---'
rg -n --hidden --glob '!**/.git/**' '\bpytest\b' requirements.txt requirements*.txt pyproject.toml setup.py setup.cfg tox.ini Pipfile poetry.lock 2>/dev/null || true

Repository: Deen-Bridge/dnb-ai

Length of output: 8153


Pin pytest in requirements.txt. This test module imports pytest directly, but the tracked manifest doesn’t declare it, so a clean CI environment can fail before the test suite starts.

🤖 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_history_truncation.py` at line 8, Add pytest as an explicitly
pinned dependency in requirements.txt so tests/test_history_truncation.py can
import it in clean CI environments.

Source: Path instructions

Comment on lines +115 to +148
def test_no_orphaned_turns_after_truncation(self, monkeypatch):
"""History must always have pairs — never a lone user or model turn."""
monkeypatch.setattr("history.MAX_HISTORY_TOKENS", 10)
monkeypatch.setattr("history.MAX_HISTORY_TURNS", 50)
pairs = [
("AAAAA AAAAA AA", "BBBBB BBBBB BB"),
("CCCCC CCCCC CC", "DDDDD DDDDD DD"),
("EEEEE EEEEE EE", "FFFFF FFFFF FF"),
]
chat = _make_chat_session(pairs)
trim_history(chat)
assert len(chat.history) % 2 == 0

def test_returns_true_when_truncation_happens(self, monkeypatch):
monkeypatch.setattr("history.MAX_HISTORY_TOKENS", 3)
monkeypatch.setattr("history.MAX_HISTORY_TURNS", 50)
chat = _make_chat_session([("AAAA AAAA", "BBBB BBBB")])
assert trim_history(chat) is True

def test_preserves_recent_context_after_truncation(self, monkeypatch):
monkeypatch.setattr("history.MAX_HISTORY_TOKENS", 20)
monkeypatch.setattr("history.MAX_HISTORY_TURNS", 50)
pairs = [
("A" * 40 + " very old", "B" * 40 + " very old response"),
("C" * 40 + " old", "D" * 40 + " old response"),
("Recent question", "Recent answer"),
]
chat = _make_chat_session(pairs)
trim_history(chat)
remaining = " ".join(
m.parts[0].text for m in chat.history
)
assert "Recent" in remaining
assert "Old question 1" not in remaining

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

Assert actual pair ordering and removed content.

len(history) % 2 == 0 allows invalid model,user sequences, and "Old question 1" is never added to the fixture, so Line 148 passes even if old turns remain. Assert each retained pair is user then model, and check for real old markers such as "very old" or "C" * 40.

🤖 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_history_truncation.py` around lines 115 - 148, Strengthen
test_no_orphaned_turns_after_truncation by asserting every retained history pair
is ordered user then model, not only that the length is even. In
test_preserves_recent_context_after_truncation, replace the nonexistent "Old
question 1" marker with content actually present in the fixture, such as "very
old" or the repeated C marker, and assert that old content was removed while
recent context remains.

Comment on lines +169 to +171
def test_budget_configurable_via_env(self, monkeypatch):
monkeypatch.setattr("history.MAX_HISTORY_TOKENS", 1000)
monkeypatch.setattr("history.MAX_HISTORY_TURNS", 3)

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

Test environment loading rather than patched globals.

This test patches already-imported module constants, so it never validates os.getenv() parsing or defaults. Set the environment variables and reload history before exercising the reloaded trim_history.

🤖 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_history_truncation.py` around lines 169 - 171, Update
test_budget_configurable_via_env to set the relevant environment variables with
monkeypatch and reload the history module before invoking trim_history. Remove
the direct patches to history.MAX_HISTORY_TOKENS and history.MAX_HISTORY_TURNS,
and call trim_history from the reloaded module so the test validates environment
parsing.

@zeemscript

Copy link
Copy Markdown
Contributor

@Abdul-dev-creator please fix conflict and point pr to dev branch

Resolve conflicts and integrate token-budget history trimming into the
refactored chat flow (safety pipeline, fiqh, semantic cache, confidence).

Closes Deen-Bridge#13
@Abdul-dev-creator
Abdul-dev-creator changed the base branch from main to dev July 23, 2026 19:17

@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: 14

🧹 Nitpick comments (6)
study.py (2)

523-529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead code: redundant length check.

LessonTextSource.lesson_text (Line 99-104) already enforces max_length=MAX_LESSON_TEXT_LENGTH via Pydantic, so FastAPI rejects an oversized payload with a 422 before this handler ever runs (confirmed by test_oversized_lesson_text_is_rejected_by_pydantic, tests/test_study.py Lines 184-187). This block can never execute.

🧹 Proposed cleanup
-    if isinstance(request.source, LessonTextSource):
-        text_len = len(request.source.lesson_text)
-        if text_len > MAX_LESSON_TEXT_LENGTH:
-            raise HTTPException(
-                status_code=422,
-                detail=f"lesson_text must not exceed {MAX_LESSON_TEXT_LENGTH} characters (got {text_len})",
-            )
-
🤖 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 `@study.py` around lines 523 - 529, Remove the redundant text-length validation
block from the request handler, including the unused text_len calculation and
HTTPException, since LessonTextSource already enforces MAX_LESSON_TEXT_LENGTH
through Pydantic before the handler runs. Preserve the surrounding
LessonTextSource handling unchanged.

401-404: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing exception chaining (raise ... from).

Static analysis (Ruff B904) flags this: re-raising as ValueError without from e discards the original traceback context, making debugging harder.

🔧 Proposed fix
     try:
         data = json.loads(response_text)
     except json.JSONDecodeError as e:
-        raise ValueError(f"Invalid JSON: {e}")
+        raise ValueError(f"Invalid JSON: {e}") from e
🤖 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 `@study.py` around lines 401 - 404, Update the JSON parsing exception handler
around json.loads in the response-processing flow to chain the new ValueError
explicitly from the caught JSONDecodeError using raise ... from e. Preserve the
existing error message and exception type.

Source: Linters/SAST tools

tests/test_fiqh.py (1)

155-167: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add negative fixture assertions for non-fiqh prompts.

The fixture tests currently verify positive fiqh matches, but non-fiqh rows only check that expect_schools is empty. Assert that classify_fiqh(case["prompt"]) is False for those rows, and assert the public classifier for positive rows as well.

This strengthens the fixture-driven classification contract and guards against the broad matching issue above.

🤖 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_fiqh.py` around lines 155 - 167, Update the fixture assertions in
test_non_fiqh_cases_have_empty_schools to also require
classify_fiqh(case["prompt"]) is False for every non-fiqh row. In the positive
fiqh fixture test, or an equivalent positive-row assertion, require
classify_fiqh(case["prompt"]) is True while preserving the existing
keyword_match check.
requirements.txt (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning redis exactly like its new neighbors.

PyYAML==6.0.2 and numpy==2.2.4 are exact-pinned, but redis>=5.0.0 is open-ended — a future major redis release could get pulled in on a fresh install without a corresponding diff, which works against reproducible builds.

♻️ Suggested fix
-redis>=5.0.0
+redis==5.2.1

(swap in whatever version you've actually tested against)

🤖 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 - 9, Pin the redis dependency exactly in
requirements.txt instead of leaving it open-ended at redis>=5.0.0. Use the
specific redis version tested with the project, matching the exact-pinning style
of PyYAML and numpy.
.github/workflows/ci.yml (1)

36-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a catch-all pytest step as a safety net.

The per-area pytest steps are great for readable CI logs, but they only run the specific files listed — any future test file that isn't explicitly wired in here will simply never execute in CI, silently. A trailing "Run full test suite" step (e.g. pytest -q tests/) would guard against that without needing to remove the granular steps.

🤖 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 @.github/workflows/ci.yml around lines 36 - 55, Add a trailing full-suite
pytest step after the existing per-area test steps, using the tests directory as
the target (for example, “Run full test suite” with pytest -q tests/). Keep all
granular steps unchanged so their readable CI output is preserved.
hadith.py (1)

322-342: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

@lru_cache on an instance method pins self forever.

_index is decorated with @lru_cache(maxsize=None) directly on the method (line 328), which caches (self, collection) tuples — every BundledGradingSource instance ever constructed stays alive for the process lifetime since it's a cache key. Today there's a single module-level _default_source, so it's harmless in practice, but it's the exact anti-pattern Ruff's B019 flags, and it'll bite silently if this class is ever instantiated per-request or per-test with a custom data_dir.

♻️ Suggested fix — cache per-instance without leaking `self` via `lru_cache`
 class BundledGradingSource(GradingSource):
     def __init__(self, data_dir: Path = DATA_DIR):
         self._data_dir = data_dir
+        self._index_cache: Dict[str, Optional[Dict]] = {}

-    `@lru_cache`(maxsize=None)
     def _index(self, collection: str) -> Optional[Dict[str, Dict]]:
+        if collection in self._index_cache:
+            return self._index_cache[collection]
         path = self._data_dir / f"{collection}.json"
         if not path.exists():
+            self._index_cache[collection] = None
             return None
         with open(path, encoding="utf-8") as f:
             payload = json.load(f)

         by_number: Dict[int, Dict] = {}
         by_book_number: Dict[Tuple[int, int], Dict] = {}
         for record in payload.get("hadiths", []):
             by_number[record["n"]] = record
             if record.get("book") is not None and record.get("bn") is not None:
                 by_book_number[(record["book"], record["bn"])] = record
-        return {"by_number": by_number, "by_book_number": by_book_number}
+        result = {"by_number": by_number, "by_book_number": by_book_number}
+        self._index_cache[collection] = result
+        return result
🤖 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 `@hadith.py` around lines 322 - 342, Remove the direct `@lru_cache` decoration
from BundledGradingSource._index so cached keys do not retain self indefinitely.
Replace it with per-instance caching initialized by
BundledGradingSource.__init__, while preserving collection-based lookup results
and the existing missing-file behavior.

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 @.github/workflows/ci.yml:
- Line 31: Update the CI lint and compile target lists in the workflow to
include semantic_cache.py and the test modules tests/test_semantic_cache.py,
tests/test_fiqh.py, tests/test_study.py, and tests/test_hadith.py. Ensure both
flake8 and compileall cover these files while preserving the existing checks and
options.

In `@fiqh.py`:
- Around line 95-96: Resolve the flake8 violations at fiqh.py lines 95-96 by
splitting the long MADHHAB_LEAD_INSTRUCTION string without changing its content;
at tests/test_fiqh.py line 12, remove the unused VALID_MADHHABS import or use
it; and at lines 165-167, wrap the long parametrization decorator and assertion
while preserving test behavior.
- Around line 42-67: Update keyword_match() to use token- and phrase-boundary
matching instead of raw substring checks, preventing generic entries such as
“can i” and “interest” from matching unrelated text. Replace overly broad
FIQH_KEYWORDS entries with domain-scoped patterns while preserving legitimate
fiqh classification, and add negative regression cases for prompts such as
password resets and “This is interesting.”

In `@hadith.py`:
- Around line 322-363: Update BundledGradingSource.get() so that when book is
provided, it only returns the by_book_number match and returns None if that pair
is absent; reserve the by_number fallback for calls without a book argument,
preventing unmatched book citations from receiving an unrelated grade.
- Around line 460-491: Update build_caution_note so the unverified-reference
trigger applies when all cited references are unverified, including answers with
multiple citations, rather than only when references has length one. Preserve
the existing flagged-reference handling and caution wording, and add coverage in
tests/test_hadith.py for two unverified citations.

In `@main.py`:
- Around line 215-218: Update the async cache-read path around normalize_text,
embed_text, and semantic_cache.get to avoid calling synchronous embed_text on
the event loop; use the available asynchronous embedding API with await, or
offload the synchronous call via asyncio.to_thread. Apply the same non-blocking
treatment to the cache-write embedding path around the referenced embedding
call, preserving existing cache behavior.

In `@README.md`:
- Around line 72-75: Update the README setup instructions so the copied
.env.example contents are preserved: remove the redundant GEMINI_API_KEY echo
step, or change it to append with >> and explicitly tell users to edit the
placeholder key. Keep the cp .env.example .env command as the source of the
complete environment configuration.

In `@safety/pipeline.py`:
- Around line 32-38: Update SafetyPipeline.run_async to offload the synchronous
_complete call, including generator execution, to the project’s existing
worker-thread or executor mechanism so Gemini network calls do not block the
event loop. Preserve the current arguments and returned SafetyResult, and add a
test verifying the generator executes outside the event-loop thread.

In `@safety/policy.yaml`:
- Around line 75-91: Update the DB-SAFE-004 policy entry’s output_patterns to
mirror its existing harmful religious-content patterns, enabling OutputCheck to
replace generated religious justifications for violence. Add a replacement test
covering this category and verify the generated harmful output is replaced.

In `@semantic_cache.py`:
- Around line 139-157: Add a shared lock to SemanticCache and use it to
synchronize all reads and mutations of the paired _entries and _access_times
lists across get(), put(), and _find_best_match(). Ensure each operation holds
the lock for its complete iteration or update, including eviction, so concurrent
calls from the event loop and asyncio.to_thread cannot leave the lists
inconsistent.

In `@study.py`:
- Around line 412-436: Update the quiz and flashcard validation branches to
reject empty lists as well as non-list values, preserving the existing “must be
a non-empty array” errors and avoiding insertion of empty results. Apply the
same emptiness check in both branches; leave the broader exception handling
unchanged unless needed for this validation fix.
- Around line 258-277: Update the async generate_study_content flow to execute
the synchronous _generate_content → GeminiGenerator.generate chain via
run_in_threadpool, ensuring model.generate_content does not block the event loop
while preserving the existing response and error behavior.
- Around line 307-313: Update _build_prompt so lesson_text is not hard-truncated
to 500 characters; preserve the full LessonTextSource.lesson_text up to its
20,000-character limit, or replace the fixed cap with the existing
MAX_LESSON_TEXT_LENGTH configuration and document the token-budget behavior if
truncation is required.

In `@tests/redteam/test_live.py`:
- Around line 16-19: Update the live audit setup in test_live.py to use the
google-genai client instead of the deprecated google.generativeai import,
configure it with the existing GEMINI_API_KEY, and select a currently supported
Gemini model. Update requirements.txt to replace the google-generativeai==0.8.3
dependency with the corresponding google-genai package, preserving the test’s
existing assertions and opt-in behavior.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 36-55: Add a trailing full-suite pytest step after the existing
per-area test steps, using the tests directory as the target (for example, “Run
full test suite” with pytest -q tests/). Keep all granular steps unchanged so
their readable CI output is preserved.

In `@hadith.py`:
- Around line 322-342: Remove the direct `@lru_cache` decoration from
BundledGradingSource._index so cached keys do not retain self indefinitely.
Replace it with per-instance caching initialized by
BundledGradingSource.__init__, while preserving collection-based lookup results
and the existing missing-file behavior.

In `@requirements.txt`:
- Around line 7-9: Pin the redis dependency exactly in requirements.txt instead
of leaving it open-ended at redis>=5.0.0. Use the specific redis version tested
with the project, matching the exact-pinning style of PyYAML and numpy.

In `@study.py`:
- Around line 523-529: Remove the redundant text-length validation block from
the request handler, including the unused text_len calculation and
HTTPException, since LessonTextSource already enforces MAX_LESSON_TEXT_LENGTH
through Pydantic before the handler runs. Preserve the surrounding
LessonTextSource handling unchanged.
- Around line 401-404: Update the JSON parsing exception handler around
json.loads in the response-processing flow to chain the new ValueError
explicitly from the caught JSONDecodeError using raise ... from e. Preserve the
existing error message and exception type.

In `@tests/test_fiqh.py`:
- Around line 155-167: Update the fixture assertions in
test_non_fiqh_cases_have_empty_schools to also require
classify_fiqh(case["prompt"]) is False for every non-fiqh row. In the positive
fiqh fixture test, or an equivalent positive-row assertion, require
classify_fiqh(case["prompt"]) is True while preserving the existing
keyword_match check.
🪄 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: 9380f9a8-e54f-4abd-8f1a-5d84dd634f2c

📥 Commits

Reviewing files that changed from the base of the PR and between fdf6572 and 2444e53.

📒 Files selected for processing (47)
  • .env.example
  • .github/workflows/ci.yml
  • .gitignore
  • CONTRIBUTING.md
  • README.md
  • confidence.py
  • data/hadith/PROVENANCE.md
  • data/hadith/abudawud.json
  • data/hadith/bukhari.json
  • data/hadith/ibnmajah.json
  • data/hadith/malik.json
  • data/hadith/muslim.json
  • data/hadith/nasai.json
  • data/hadith/tirmidhi.json
  • fiqh.py
  • hadith.py
  • main.py
  • requirements.txt
  • review.py
  • review_store.py
  • safety/POLICY.md
  • safety/__init__.py
  • safety/input_gate.py
  • safety/output_check.py
  • safety/pipeline.py
  • safety/policy.py
  • safety/policy.yaml
  • scripts/build_hadith_data.py
  • semantic_cache.py
  • study.py
  • tests/__init__.py
  • tests/conftest.py
  • tests/fixtures/fiqh_cases.jsonl
  • tests/fixtures/flashcards_valid.json
  • tests/fixtures/invalid_json.json
  • tests/fixtures/quiz_valid.json
  • tests/fixtures/schema_invalid.json
  • tests/fixtures/semantic_invalid.json
  • tests/redteam/prompts.yaml
  • tests/redteam/test_live.py
  • tests/redteam/test_pipeline.py
  • tests/test_confidence.py
  • tests/test_fiqh.py
  • tests/test_hadith.py
  • tests/test_review_queue.py
  • tests/test_semantic_cache.py
  • tests/test_study.py

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 14

🧹 Nitpick comments (6)
study.py (2)

523-529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead code: redundant length check.

LessonTextSource.lesson_text (Line 99-104) already enforces max_length=MAX_LESSON_TEXT_LENGTH via Pydantic, so FastAPI rejects an oversized payload with a 422 before this handler ever runs (confirmed by test_oversized_lesson_text_is_rejected_by_pydantic, tests/test_study.py Lines 184-187). This block can never execute.

🧹 Proposed cleanup
-    if isinstance(request.source, LessonTextSource):
-        text_len = len(request.source.lesson_text)
-        if text_len > MAX_LESSON_TEXT_LENGTH:
-            raise HTTPException(
-                status_code=422,
-                detail=f"lesson_text must not exceed {MAX_LESSON_TEXT_LENGTH} characters (got {text_len})",
-            )
-
🤖 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 `@study.py` around lines 523 - 529, Remove the redundant text-length validation
block from the request handler, including the unused text_len calculation and
HTTPException, since LessonTextSource already enforces MAX_LESSON_TEXT_LENGTH
through Pydantic before the handler runs. Preserve the surrounding
LessonTextSource handling unchanged.

401-404: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing exception chaining (raise ... from).

Static analysis (Ruff B904) flags this: re-raising as ValueError without from e discards the original traceback context, making debugging harder.

🔧 Proposed fix
     try:
         data = json.loads(response_text)
     except json.JSONDecodeError as e:
-        raise ValueError(f"Invalid JSON: {e}")
+        raise ValueError(f"Invalid JSON: {e}") from e
🤖 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 `@study.py` around lines 401 - 404, Update the JSON parsing exception handler
around json.loads in the response-processing flow to chain the new ValueError
explicitly from the caught JSONDecodeError using raise ... from e. Preserve the
existing error message and exception type.

Source: Linters/SAST tools

tests/test_fiqh.py (1)

155-167: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add negative fixture assertions for non-fiqh prompts.

The fixture tests currently verify positive fiqh matches, but non-fiqh rows only check that expect_schools is empty. Assert that classify_fiqh(case["prompt"]) is False for those rows, and assert the public classifier for positive rows as well.

This strengthens the fixture-driven classification contract and guards against the broad matching issue above.

🤖 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_fiqh.py` around lines 155 - 167, Update the fixture assertions in
test_non_fiqh_cases_have_empty_schools to also require
classify_fiqh(case["prompt"]) is False for every non-fiqh row. In the positive
fiqh fixture test, or an equivalent positive-row assertion, require
classify_fiqh(case["prompt"]) is True while preserving the existing
keyword_match check.
requirements.txt (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning redis exactly like its new neighbors.

PyYAML==6.0.2 and numpy==2.2.4 are exact-pinned, but redis>=5.0.0 is open-ended — a future major redis release could get pulled in on a fresh install without a corresponding diff, which works against reproducible builds.

♻️ Suggested fix
-redis>=5.0.0
+redis==5.2.1

(swap in whatever version you've actually tested against)

🤖 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 - 9, Pin the redis dependency exactly in
requirements.txt instead of leaving it open-ended at redis>=5.0.0. Use the
specific redis version tested with the project, matching the exact-pinning style
of PyYAML and numpy.
.github/workflows/ci.yml (1)

36-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a catch-all pytest step as a safety net.

The per-area pytest steps are great for readable CI logs, but they only run the specific files listed — any future test file that isn't explicitly wired in here will simply never execute in CI, silently. A trailing "Run full test suite" step (e.g. pytest -q tests/) would guard against that without needing to remove the granular steps.

🤖 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 @.github/workflows/ci.yml around lines 36 - 55, Add a trailing full-suite
pytest step after the existing per-area test steps, using the tests directory as
the target (for example, “Run full test suite” with pytest -q tests/). Keep all
granular steps unchanged so their readable CI output is preserved.
hadith.py (1)

322-342: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

@lru_cache on an instance method pins self forever.

_index is decorated with @lru_cache(maxsize=None) directly on the method (line 328), which caches (self, collection) tuples — every BundledGradingSource instance ever constructed stays alive for the process lifetime since it's a cache key. Today there's a single module-level _default_source, so it's harmless in practice, but it's the exact anti-pattern Ruff's B019 flags, and it'll bite silently if this class is ever instantiated per-request or per-test with a custom data_dir.

♻️ Suggested fix — cache per-instance without leaking `self` via `lru_cache`
 class BundledGradingSource(GradingSource):
     def __init__(self, data_dir: Path = DATA_DIR):
         self._data_dir = data_dir
+        self._index_cache: Dict[str, Optional[Dict]] = {}

-    `@lru_cache`(maxsize=None)
     def _index(self, collection: str) -> Optional[Dict[str, Dict]]:
+        if collection in self._index_cache:
+            return self._index_cache[collection]
         path = self._data_dir / f"{collection}.json"
         if not path.exists():
+            self._index_cache[collection] = None
             return None
         with open(path, encoding="utf-8") as f:
             payload = json.load(f)

         by_number: Dict[int, Dict] = {}
         by_book_number: Dict[Tuple[int, int], Dict] = {}
         for record in payload.get("hadiths", []):
             by_number[record["n"]] = record
             if record.get("book") is not None and record.get("bn") is not None:
                 by_book_number[(record["book"], record["bn"])] = record
-        return {"by_number": by_number, "by_book_number": by_book_number}
+        result = {"by_number": by_number, "by_book_number": by_book_number}
+        self._index_cache[collection] = result
+        return result
🤖 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 `@hadith.py` around lines 322 - 342, Remove the direct `@lru_cache` decoration
from BundledGradingSource._index so cached keys do not retain self indefinitely.
Replace it with per-instance caching initialized by
BundledGradingSource.__init__, while preserving collection-based lookup results
and the existing missing-file behavior.

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 @.github/workflows/ci.yml:
- Line 31: Update the CI lint and compile target lists in the workflow to
include semantic_cache.py and the test modules tests/test_semantic_cache.py,
tests/test_fiqh.py, tests/test_study.py, and tests/test_hadith.py. Ensure both
flake8 and compileall cover these files while preserving the existing checks and
options.

In `@fiqh.py`:
- Around line 95-96: Resolve the flake8 violations at fiqh.py lines 95-96 by
splitting the long MADHHAB_LEAD_INSTRUCTION string without changing its content;
at tests/test_fiqh.py line 12, remove the unused VALID_MADHHABS import or use
it; and at lines 165-167, wrap the long parametrization decorator and assertion
while preserving test behavior.
- Around line 42-67: Update keyword_match() to use token- and phrase-boundary
matching instead of raw substring checks, preventing generic entries such as
“can i” and “interest” from matching unrelated text. Replace overly broad
FIQH_KEYWORDS entries with domain-scoped patterns while preserving legitimate
fiqh classification, and add negative regression cases for prompts such as
password resets and “This is interesting.”

In `@hadith.py`:
- Around line 322-363: Update BundledGradingSource.get() so that when book is
provided, it only returns the by_book_number match and returns None if that pair
is absent; reserve the by_number fallback for calls without a book argument,
preventing unmatched book citations from receiving an unrelated grade.
- Around line 460-491: Update build_caution_note so the unverified-reference
trigger applies when all cited references are unverified, including answers with
multiple citations, rather than only when references has length one. Preserve
the existing flagged-reference handling and caution wording, and add coverage in
tests/test_hadith.py for two unverified citations.

In `@main.py`:
- Around line 215-218: Update the async cache-read path around normalize_text,
embed_text, and semantic_cache.get to avoid calling synchronous embed_text on
the event loop; use the available asynchronous embedding API with await, or
offload the synchronous call via asyncio.to_thread. Apply the same non-blocking
treatment to the cache-write embedding path around the referenced embedding
call, preserving existing cache behavior.

In `@README.md`:
- Around line 72-75: Update the README setup instructions so the copied
.env.example contents are preserved: remove the redundant GEMINI_API_KEY echo
step, or change it to append with >> and explicitly tell users to edit the
placeholder key. Keep the cp .env.example .env command as the source of the
complete environment configuration.

In `@safety/pipeline.py`:
- Around line 32-38: Update SafetyPipeline.run_async to offload the synchronous
_complete call, including generator execution, to the project’s existing
worker-thread or executor mechanism so Gemini network calls do not block the
event loop. Preserve the current arguments and returned SafetyResult, and add a
test verifying the generator executes outside the event-loop thread.

In `@safety/policy.yaml`:
- Around line 75-91: Update the DB-SAFE-004 policy entry’s output_patterns to
mirror its existing harmful religious-content patterns, enabling OutputCheck to
replace generated religious justifications for violence. Add a replacement test
covering this category and verify the generated harmful output is replaced.

In `@semantic_cache.py`:
- Around line 139-157: Add a shared lock to SemanticCache and use it to
synchronize all reads and mutations of the paired _entries and _access_times
lists across get(), put(), and _find_best_match(). Ensure each operation holds
the lock for its complete iteration or update, including eviction, so concurrent
calls from the event loop and asyncio.to_thread cannot leave the lists
inconsistent.

In `@study.py`:
- Around line 412-436: Update the quiz and flashcard validation branches to
reject empty lists as well as non-list values, preserving the existing “must be
a non-empty array” errors and avoiding insertion of empty results. Apply the
same emptiness check in both branches; leave the broader exception handling
unchanged unless needed for this validation fix.
- Around line 258-277: Update the async generate_study_content flow to execute
the synchronous _generate_content → GeminiGenerator.generate chain via
run_in_threadpool, ensuring model.generate_content does not block the event loop
while preserving the existing response and error behavior.
- Around line 307-313: Update _build_prompt so lesson_text is not hard-truncated
to 500 characters; preserve the full LessonTextSource.lesson_text up to its
20,000-character limit, or replace the fixed cap with the existing
MAX_LESSON_TEXT_LENGTH configuration and document the token-budget behavior if
truncation is required.

In `@tests/redteam/test_live.py`:
- Around line 16-19: Update the live audit setup in test_live.py to use the
google-genai client instead of the deprecated google.generativeai import,
configure it with the existing GEMINI_API_KEY, and select a currently supported
Gemini model. Update requirements.txt to replace the google-generativeai==0.8.3
dependency with the corresponding google-genai package, preserving the test’s
existing assertions and opt-in behavior.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 36-55: Add a trailing full-suite pytest step after the existing
per-area test steps, using the tests directory as the target (for example, “Run
full test suite” with pytest -q tests/). Keep all granular steps unchanged so
their readable CI output is preserved.

In `@hadith.py`:
- Around line 322-342: Remove the direct `@lru_cache` decoration from
BundledGradingSource._index so cached keys do not retain self indefinitely.
Replace it with per-instance caching initialized by
BundledGradingSource.__init__, while preserving collection-based lookup results
and the existing missing-file behavior.

In `@requirements.txt`:
- Around line 7-9: Pin the redis dependency exactly in requirements.txt instead
of leaving it open-ended at redis>=5.0.0. Use the specific redis version tested
with the project, matching the exact-pinning style of PyYAML and numpy.

In `@study.py`:
- Around line 523-529: Remove the redundant text-length validation block from
the request handler, including the unused text_len calculation and
HTTPException, since LessonTextSource already enforces MAX_LESSON_TEXT_LENGTH
through Pydantic before the handler runs. Preserve the surrounding
LessonTextSource handling unchanged.
- Around line 401-404: Update the JSON parsing exception handler around
json.loads in the response-processing flow to chain the new ValueError
explicitly from the caught JSONDecodeError using raise ... from e. Preserve the
existing error message and exception type.

In `@tests/test_fiqh.py`:
- Around line 155-167: Update the fixture assertions in
test_non_fiqh_cases_have_empty_schools to also require
classify_fiqh(case["prompt"]) is False for every non-fiqh row. In the positive
fiqh fixture test, or an equivalent positive-row assertion, require
classify_fiqh(case["prompt"]) is True while preserving the existing
keyword_match check.
🪄 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: 9380f9a8-e54f-4abd-8f1a-5d84dd634f2c

📥 Commits

Reviewing files that changed from the base of the PR and between fdf6572 and 2444e53.

📒 Files selected for processing (47)
  • .env.example
  • .github/workflows/ci.yml
  • .gitignore
  • CONTRIBUTING.md
  • README.md
  • confidence.py
  • data/hadith/PROVENANCE.md
  • data/hadith/abudawud.json
  • data/hadith/bukhari.json
  • data/hadith/ibnmajah.json
  • data/hadith/malik.json
  • data/hadith/muslim.json
  • data/hadith/nasai.json
  • data/hadith/tirmidhi.json
  • fiqh.py
  • hadith.py
  • main.py
  • requirements.txt
  • review.py
  • review_store.py
  • safety/POLICY.md
  • safety/__init__.py
  • safety/input_gate.py
  • safety/output_check.py
  • safety/pipeline.py
  • safety/policy.py
  • safety/policy.yaml
  • scripts/build_hadith_data.py
  • semantic_cache.py
  • study.py
  • tests/__init__.py
  • tests/conftest.py
  • tests/fixtures/fiqh_cases.jsonl
  • tests/fixtures/flashcards_valid.json
  • tests/fixtures/invalid_json.json
  • tests/fixtures/quiz_valid.json
  • tests/fixtures/schema_invalid.json
  • tests/fixtures/semantic_invalid.json
  • tests/redteam/prompts.yaml
  • tests/redteam/test_live.py
  • tests/redteam/test_pipeline.py
  • tests/test_confidence.py
  • tests/test_fiqh.py
  • tests/test_hadith.py
  • tests/test_review_queue.py
  • tests/test_semantic_cache.py
  • tests/test_study.py
🛑 Comments failed to post (14)
.github/workflows/ci.yml (1)

31-31: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Lint/compile checks don't cover all the new files this PR adds tests for.

tests/test_semantic_cache.py, tests/test_fiqh.py, tests/test_study.py, and tests/test_hadith.py all get their own pytest step below (lines 39-49), but none of them — nor the semantic_cache.py module itself — appear in the flake8 or compileall target lists. That means style violations or outright syntax errors in these new files won't fail the build the way they should.

🔧 Suggested fix
-        run: flake8 main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py confidence.py review.py review_store.py tests/test_confidence.py tests/test_review_queue.py scripts/build_hadith_data.py --max-line-length=120 --ignore=E501,W503
+        run: flake8 main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py confidence.py review.py review_store.py semantic_cache.py tests/test_confidence.py tests/test_review_queue.py tests/test_hadith.py tests/test_fiqh.py tests/test_study.py tests/test_semantic_cache.py scripts/build_hadith_data.py --max-line-length=120 --ignore=E501,W503

       - name: Check syntax
-        run: python -m compileall -q main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py confidence.py review.py review_store.py scripts/build_hadith_data.py
+        run: python -m compileall -q main.py stellar.py safety tests/redteam study.py fiqh.py hadith.py confidence.py review.py review_store.py semantic_cache.py scripts/build_hadith_data.py

As per path instructions, "CI enforces flake8, so style violations fail the build" — worth keeping that guarantee true for every new file landing in this PR.

Also applies to: 33-34

🤖 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 @.github/workflows/ci.yml at line 31, Update the CI lint and compile target
lists in the workflow to include semantic_cache.py and the test modules
tests/test_semantic_cache.py, tests/test_fiqh.py, tests/test_study.py, and
tests/test_hadith.py. Ensure both flake8 and compileall cover these files while
preserving the existing checks and options.

Source: Path instructions

fiqh.py (2)

42-67: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid raw substring matching for fiqh classification.

keyword_match() classifies unrelated prompts such as “Can I reset my password?” ("can i") and “This is interesting” ("interest") as fiqh. Use token/phrase boundaries and replace generic phrases with domain-scoped patterns, then add negative regression cases.

Cross-file impact: main.py uses this result to add fiqh context and mark the request high-stakes, so these false positives alter live chat behavior.

🤖 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 `@fiqh.py` around lines 42 - 67, Update keyword_match() to use token- and
phrase-boundary matching instead of raw substring checks, preventing generic
entries such as “can i” and “interest” from matching unrelated text. Replace
overly broad FIQH_KEYWORDS entries with domain-scoped patterns while preserving
legitimate fiqh classification, and add negative regression cases for prompts
such as password resets and “This is interesting.”

95-96: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve the flake8 violations before merging.

These changed Python files currently contain lint violations that can fail CI:

  • fiqh.py#L95-L96: split the long MADHHAB_LEAD_INSTRUCTION string.
  • tests/test_fiqh.py#L12-L12: remove or use the unused VALID_MADHHABS import.
  • tests/test_fiqh.py#L165-L167: wrap the long parametrization decorator and assertion.
📍 Affects 2 files
  • fiqh.py#L95-L96 (this comment)
  • tests/test_fiqh.py#L12-L12
  • tests/test_fiqh.py#L165-L167
🤖 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 `@fiqh.py` around lines 95 - 96, Resolve the flake8 violations at fiqh.py lines
95-96 by splitting the long MADHHAB_LEAD_INSTRUCTION string without changing its
content; at tests/test_fiqh.py line 12, remove the unused VALID_MADHHABS import
or use it; and at lines 165-167, wrap the long parametrization decorator and
assertion while preserving test behavior.

Source: Path instructions

hadith.py (2)

322-363: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

get() can silently return the wrong hadith's grade when a book number is given but doesn't match.

Nice, clean lookup structure overall! One correctness gap though: when book is provided but (book, number) isn't found in by_book_number, get() falls through to by_number.get(number) (line 352-353) and treats number as a global sequential number. If a citation names a book that doesn't contain that number (typo, wrong book index, etc.), this can return a completely unrelated hadith's GradeRecord — and the caller (annotate()) will present that grade as if it belonged to the cited hadith. Given the whole point of this module is to avoid misattributing authenticity grades, this fallback works against that goal.

🐛 Proposed fix — don't fall back to global-number when a book was specified but not matched
     def get(self, collection: str, number: int, book: Optional[int] = None) -> Optional[GradeRecord]:
         index = self._index(collection)
         if index is None:
             return None
-        record = None
         if book is not None:
             record = index["by_book_number"].get((book, number))
-        if record is None:
+        else:
             record = index["by_number"].get(number)
         if record is None:
             return None

Worth adding a regression test in tests/test_hadith.py for "book specified but wrong/unmatched → None, not a wrong record" — happy to draft one if useful.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 332-332: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🪛 Ruff (0.15.21)

[warning] 328-328: Use of functools.lru_cache or functools.cache on methods can lead to memory leaks

(B019)

🤖 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 `@hadith.py` around lines 322 - 363, Update BundledGradingSource.get() so that
when book is provided, it only returns the by_book_number match and returns None
if that pair is absent; reserve the by_number fallback for calls without a book
argument, preventing unmatched book citations from receiving an unrelated grade.

460-491: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Caution note is skipped when several citations are all unverified.

sole_unverified only fires when there's exactly one reference and it's unverified (line 470-472). If an answer cites two or more hadith and none of them resolve against the bundled dataset, flagged is empty (unverified refs aren't individually flagged) and sole_unverified is False (since len(references) != 1) — so build_caution_note returns None even though the entire evidentiary basis is unverified. That seems to run against the module's stated "never overstate authenticity" policy.

🛡️ Suggested fix — trigger on "all unverified", not just "sole unverified"
     flagged = [r for r in references if r.flagged]
-    sole_unverified = (
-        len(references) == 1 and not references[0].verified
-    )
+    all_unverified = bool(references) and all(not r.verified for r in references)

-    if not flagged and not sole_unverified:
+    if not flagged and not all_unverified:
         return None

     lines = ["Note on hadith authenticity in this answer:"]
     for r in flagged:
         lines.append(f"- \"{r.raw}\" — {r.note}.")
-    if sole_unverified:
-        lines.append(
-            f"- \"{references[0].raw}\" is the only hadith cited here and its grading "
-            "could not be verified against the bundled dataset — treat it as unverified, "
-            "not as an authentic narration."
-        )
+    if all_unverified:
+        if len(references) == 1:
+            lines.append(
+                f"- \"{references[0].raw}\" is the only hadith cited here and its grading "
+                "could not be verified against the bundled dataset — treat it as unverified, "
+                "not as an authentic narration."
+            )
+        else:
+            lines.append(
+                "- None of the hadith cited here could be verified against the bundled "
+                "dataset — treat all of them as unverified, not as authentic narrations."
+            )

Would also be worth a test in tests/test_hadith.py covering "two citations, both unverified" to lock this in.

📝 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.

def build_caution_note(text: str, references: List[HadithReference]) -> Optional[str]:
    """Compose a single caution suffix for the response, or None if not needed.

    Two triggers, per the grading policy:
    - a da'if/mawdu' or non-marfu hadith cited without an existing caveat
      nearby (i.e. presented as unqualified evidence), and
    - an unverified/ungraded reference that is the *sole* hadith support in
      the whole answer.
    """
    flagged = [r for r in references if r.flagged]
    all_unverified = bool(references) and all(not r.verified for r in references)

    if not flagged and not all_unverified:
        return None

    lines = ["Note on hadith authenticity in this answer:"]
    for r in flagged:
        lines.append(f"- \"{r.raw}\" — {r.note}.")
    if all_unverified:
        if len(references) == 1:
            lines.append(
                f"- \"{references[0].raw}\" is the only hadith cited here and its grading "
                "could not be verified against the bundled dataset — treat it as unverified, "
                "not as an authentic narration."
            )
        else:
            lines.append(
                "- None of the hadith cited here could be verified against the bundled "
                "dataset — treat all of them as unverified, not as authentic narrations."
            )
    lines.append(
        "Please verify with a qualified scholar or a hadith reference (e.g. Sunnah.com) "
        "before relying on this as evidence."
    )
    return "\n".join(lines)
🤖 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 `@hadith.py` around lines 460 - 491, Update build_caution_note so the
unverified-reference trigger applies when all cited references are unverified,
including answers with multiple citations, rather than only when references has
length one. Preserve the existing flagged-reference handling and caution
wording, and add coverage in tests/test_hadith.py for two unverified citations.
main.py (1)

215-218: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

does google-generativeai 0.8.3 provide an async embed_content coroutine

💡 Result:

Yes, the google-generativeai library (version 0.8.3) does provide an asynchronous method for generating embeddings, named embed_content_async [1][2]. In this library, you can call embed_content_async as a coroutine to perform asynchronous embedding requests [2]. However, users should be aware of the following context regarding this library: 1. Deprecation Status: The google-generativeai package is now deprecated [3]. It has been succeeded by the new, unified Google Gen AI SDK (google-genai) [4][3]. 2. Known Issues: Asynchronous methods in the legacy google-generativeai library have been reported to experience instability and errors in certain environments (such as Google Colab or when using REST transport) [5][6]. 3. Recommended Migration: Google strongly recommends migrating to the new Google Gen AI SDK (google-genai), which features a modern, more robust asynchronous API accessed via client.aio (e.g., client.aio.models.embed_content) [7][8][4][9][3].

Citations:


Avoid the synchronous embedding call in the async handler. embed_text() is a network round-trip, so calling it directly in main.py:215-218 blocks the event loop; the same applies to the cache-write path at main.py:372-375. If this helper still uses google-generativeai, switch to embed_content_async/await where available, or offload the sync call with asyncio.to_thread so other requests can keep flowing.

🤖 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 215 - 218, Update the async cache-read path around
normalize_text, embed_text, and semantic_cache.get to avoid calling synchronous
embed_text on the event loop; use the available asynchronous embedding API with
await, or offload the synchronous call via asyncio.to_thread. Apply the same
non-blocking treatment to the cache-write embedding path around the referenced
embedding call, preserving existing cache behavior.

Source: Path instructions

README.md (1)

72-75: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The new cp step gets clobbered two lines later. Nice addition documenting the template copy — but Line 75's echo "GEMINI_API_KEY=..." > .env uses a truncating redirect (>), which overwrites the .env you just copied on Line 73 and throws away every optional variable in the template. A reader following the steps literally ends up with a one-line .env. Either drop the echo line or switch it to append (>>) and remind the user to edit the key.

📝 Suggested fix
 # Copy environment template and add your API key
 cp .env.example .env
-
-echo "GEMINI_API_KEY=your_api_key_here" > .env
+# then edit .env and set GEMINI_API_KEY
📝 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.

# Copy environment template and add your API key
cp .env.example .env
# then edit .env and set GEMINI_API_KEY
🤖 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 72 - 75, Update the README setup instructions so the
copied .env.example contents are preserved: remove the redundant GEMINI_API_KEY
echo step, or change it to append with >> and explicitly tell users to edit the
placeholder key. Keep the cp .env.example .env command as the source of the
complete environment configuration.
safety/pipeline.py (1)

32-38: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Offload generation from the event loop.

run_async offloads classification but then calls _complete() directly; _complete() synchronously invokes generator. A Gemini network call therefore blocks concurrent async requests. Offload _complete (and add a test that the generator runs outside the loop).

Proposed fix
+import asyncio
 from dataclasses import dataclass
@@
         started = perf_counter()
         decision = await self.input_gate.evaluate_async(prompt)
-        return self._complete(prompt, generator, decision, started)
+        return await asyncio.to_thread(
+            self._complete, prompt, generator, decision, started
+        )

As per path instructions, “network calls should be awaited or offloaded”.

📝 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.

    async def run_async(
        self, prompt: str, generator: Callable[[str], str]
    ) -> SafetyResult:
        """Run the pipeline while keeping classification off the event loop."""
        started = perf_counter()
        decision = await self.input_gate.evaluate_async(prompt)
        return await asyncio.to_thread(
            self._complete, prompt, generator, decision, started
        )
🤖 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 `@safety/pipeline.py` around lines 32 - 38, Update SafetyPipeline.run_async to
offload the synchronous _complete call, including generator execution, to the
project’s existing worker-thread or executor mechanism so Gemini network calls
do not block the event loop. Preserve the current arguments and returned
SafetyResult, and add a test verifying the generator executes outside the
event-loop thread.

Source: Path instructions

safety/policy.yaml (1)

75-91: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add output enforcement for harmful religious content.

DB-SAFE-004 has no output_patterns, so OutputCheck cannot replace generated religious justifications for violence. Mirror the harm patterns here and add a generated-output replacement test for this category.

🤖 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 `@safety/policy.yaml` around lines 75 - 91, Update the DB-SAFE-004 policy
entry’s output_patterns to mirror its existing harmful religious-content
patterns, enabling OutputCheck to replace generated religious justifications for
violence. Add a replacement test covering this category and verify the generated
harmful output is replaced.
semantic_cache.py (1)

139-157: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm cache is entered from both an async handler and a worker thread.
rg -nP '(get_cache\(\)|semantic_cache)\.(get|put)\s*\(' --type=py
rg -nP 'asyncio\.to_thread\([^)]*cache' --type=py

Repository: Deen-Bridge/dnb-ai

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant files =="
git ls-files | rg '(^|/)(semantic_cache|main|review)\.py$|semantic_cache\.py$'

echo
echo "== semantic_cache.py outline =="
ast-grep outline semantic_cache.py --view expanded || true

echo
echo "== call sites for cache methods =="
rg -n --hidden --glob '*.py' '\b(get|put)\(' semantic_cache.py main.py review.py || true
rg -n --hidden --glob '*.py' 'get_cache\(\)|SemanticCache|semantic_cache' . || true

echo
echo "== async/thread usage around cache =="
rg -n --hidden --glob '*.py' 'asyncio\.to_thread|ThreadPoolExecutor|run_in_executor|to_thread\(' . || true

Repository: Deen-Bridge/dnb-ai

Length of output: 5292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== review.py around cache write =="
sed -n '120,175p' review.py | cat -n

echo
echo "== review.py around to_thread usage =="
sed -n '260,290p' review.py | cat -n

echo
echo "== main.py around cache read/write =="
sed -n '200,390p' main.py | cat -n

echo
echo "== semantic_cache.py methods =="
sed -n '115,230p' semantic_cache.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 17707


Guard SemanticCache with a lock. main.py uses this singleton on the event loop, and review.py calls get_cache().put(...) from asyncio.to_thread(...). The unsynchronized _entries / _access_times mutations in get(), put(), and _find_best_match() can race and leave the paired lists out of sync or fail mid-iteration.

🤖 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 `@semantic_cache.py` around lines 139 - 157, Add a shared lock to SemanticCache
and use it to synchronize all reads and mutations of the paired _entries and
_access_times lists across get(), put(), and _find_best_match(). Ensure each
operation holds the lock for its complete iteration or update, including
eviction, so concurrent calls from the event loop and asyncio.to_thread cannot
leave the lists inconsistent.
study.py (3)

258-277: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the referenced file and inspect the relevant sections with line numbers.
git ls-files | rg '^study\.py$|/study\.py$|study\.py$' || true
wc -l study.py
sed -n '220,340p' study.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 4997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file around the cited endpoint and generator implementation.
sed -n '220,340p' study.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 4955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the endpoint and call chain around the second cited range.
sed -n '480,545p' study.py | cat -n

# Also inspect tests for any concurrency/offload expectations.
sed -n '1,260p' tests/test_study.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 12767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '480,545p' study.py | cat -n
sed -n '1,260p' tests/test_study.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 12767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the helper that the async route calls.
sed -n '340,490p' study.py | cat -n

Repository: Deen-Bridge/dnb-ai

Length of output: 6612


Offload Gemini generation from the async route. generate_study_content is async, but it still runs the sync _generate_content()GeminiGenerator.generate()model.generate_content(...) chain on the event loop. That blocks other requests while the network call is in flight. Use run_in_threadpool for the current sync path, or switch to generate_content_async and make the helper chain async.

🤖 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 `@study.py` around lines 258 - 277, Update the async generate_study_content
flow to execute the synchronous _generate_content → GeminiGenerator.generate
chain via run_in_threadpool, ensuring model.generate_content does not block the
event loop while preserving the existing response and error behavior.

Source: Path instructions


307-313: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the relevant file and symbols.
git ls-files | rg '(^|/)study\.py$|MAX_LESSON_TEXT_LENGTH|LessonTextSource|_build_prompt|lesson_text'

# Inspect outline if available, otherwise fall back to targeted reads.
if [ -f study.py ]; then
  echo "=== study.py lines around 280-340 ==="
  sed -n '280,340p' study.py
fi

# Search for the constant and prompt-building references.
rg -n "MAX_LESSON_TEXT_LENGTH|LessonTextSource|_build_prompt|lesson_text[:=]" .

Repository: Deen-Bridge/dnb-ai

Length of output: 3719


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files | rg '(^|/)study\.py$|MAX_LESSON_TEXT_LENGTH|LessonTextSource|_build_prompt|lesson_text'

if [ -f study.py ]; then
  echo "=== study.py lines around 280-340 ==="
  sed -n '280,340p' study.py
fi

rg -n "MAX_LESSON_TEXT_LENGTH|LessonTextSource|_build_prompt|lesson_text[:=]" .

Repository: Deen-Bridge/dnb-ai

Length of output: 3719


🏁 Script executed:

#!/bin/bash
set -euo pipefail
pwd
ls -1

Repository: Deen-Bridge/dnb-ai

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the prompt-building tests.
sed -n '200,280p' tests/test_study.py

# Search for any docs or comments explaining the 500-char prompt limit.
rg -n "500|token budget|prompt budget|truncate|truncat|lesson_text" README.md CONTRIBUTING.md study.py tests/test_study.py main.py review.py fiqh.py hadith.py history.py stellar.py safety scripts data

Repository: Deen-Bridge/dnb-ai

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tests/test_study.py around prompt-building tests ==="
sed -n '212,265p' tests/test_study.py

echo "=== focused docs/search for prompt size / truncation notes ==="
rg -n -m 20 "prompt budget|token budget|500 chars|500-character|500 char|truncate|truncat|MAX_LESSON_TEXT_LENGTH|lesson_text" README.md CONTRIBUTING.md study.py tests/test_study.py main.py review.py review_store.py semantic_cache.py

Repository: Deen-Bridge/dnb-ai

Length of output: 3993


Forward more of lesson_text into the prompt. _build_prompt cuts lessons to 500 chars even though LessonTextSource.lesson_text allows 20,000. That silently drops most of larger submissions, so generated quizzes/flashcards can miss the actual source. If the cap is intentional for token budget, make it configurable and document it; otherwise remove it or tie it to MAX_LESSON_TEXT_LENGTH.

🤖 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 `@study.py` around lines 307 - 313, Update _build_prompt so lesson_text is not
hard-truncated to 500 characters; preserve the full LessonTextSource.lesson_text
up to its 20,000-character limit, or replace the fixed cap with the existing
MAX_LESSON_TEXT_LENGTH configuration and document the token-budget behavior if
truncation is required.

412-436: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty quizzes/flashcards array silently passes validation.

The error message says "'quizzes' must be a non-empty array" (Line 415) but the code only checks isinstance(raw, list) — an empty list [] passes this check and produces result["quizzes"] = [], contradicting the message and silently returning fewer items than the requested count. Consider adding an explicit emptiness check (and no test currently covers this case).

🔧 Proposed fix
         raw = data.get("quizzes")
-        if not isinstance(raw, list):
+        if not isinstance(raw, list) or not raw:
             errors.append("'quizzes' must be a non-empty array")

(Apply the analogous change to the flashcards branch below.)

Separately, static analysis flags blind except Exception at Lines 421 and 434 (Ruff BLE001) — narrowing to pydantic.ValidationError (plus perhaps TypeError) would make failure modes more explicit, though the current broad catch is defensible here since it feeds the retry loop.

🧰 Tools
🪛 Ruff (0.15.21)

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

(BLE001)


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

(BLE001)

🤖 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 `@study.py` around lines 412 - 436, Update the quiz and flashcard validation
branches to reject empty lists as well as non-list values, preserving the
existing “must be a non-empty array” errors and avoiding insertion of empty
results. Apply the same emptiness check in both branches; leave the broader
exception handling unchanged unless needed for this validation fix.

Source: Linters/SAST tools

tests/redteam/test_live.py (1)

16-19: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the official Gemini API documentation, which supported model and Python google-genaiSDK API should replacegemini-2.5-flash-preview-05-20andgoogle-generativeai for a JSON classification audit?

💡 Result:

To migrate from the legacy google-generativeai SDK and the gemini-2.5-flash-preview-05-20 model for your JSON classification audit, you should adopt the following: 1. Recommended SDK: You must migrate to the google-genai Python SDK [1][2]. The google-generativeai library is considered a legacy library, is no longer actively maintained, and is deprecated [2]. You can install the new SDK using pip install google-genai [3]. 2. Recommended Model: The gemini-2.5-flash-preview-05-20 model has been deprecated [4]. For a current production replacement, you should use gemini-3.6-flash [4]. The google-genai SDK introduces a new, centralized Client object architecture, which simplifies credential management and provides a more consistent interface for interacting with models, including features like structured JSON responses using Pydantic [1][5]. Migration guides provided by Google offer direct before-and-after code examples to facilitate this transition [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="tests/redteam/test_live.py"

printf '\n== file outline ==\n'
ast-grep outline "$FILE" --view expanded || true

printf '\n== relevant lines ==\n'
nl -ba "$FILE" | sed -n '1,120p'

printf '\n== SDK/model references in repo ==\n'
rg -n "google-generativeai|google-genai|gemini-2\.5-flash-preview-05-20|gemini-3\.6-flash" .

Repository: Deen-Bridge/dnb-ai

Length of output: 341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== test_live.py ==\n'
sed -n '1,120p' tests/redteam/test_live.py

printf '\n== dependency references ==\n'
rg -n "google-generativeai|google-genai" requirements* pyproject.toml poetry.lock uv.lock Pipfile* . -g '!**/.git/**' || true

Repository: Deen-Bridge/dnb-ai

Length of output: 1537


Replace the deprecated Gemini client and model in the live audit.

tests/redteam/test_live.py still imports google.generativeai and instantiates gemini-2.5-flash-preview-05-20, and requirements.txt pins google-generativeai==0.8.3. Move this test to google-genai and a supported model so the opt-in audit can reach its assertions instead of failing up front.

🤖 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/redteam/test_live.py` around lines 16 - 19, Update the live audit setup
in test_live.py to use the google-genai client instead of the deprecated
google.generativeai import, configure it with the existing GEMINI_API_KEY, and
select a currently supported Gemini model. Update requirements.txt to replace
the google-generativeai==0.8.3 dependency with the corresponding google-genai
package, preserving the test’s existing assertions and opt-in behavior.

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.

[Enhancement] Cap conversation history with a token budget to prevent unbounded prompt growth

2 participants