feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146) - #300

Merged
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent
Jul 15, 2026
Merged

feat(flashcards): flashcard generation/import → Pydantic AI agent (#146)#300
Darkest-Teddy merged 4 commits into
mainfrom
feat/146-flashcard-agent

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes#146 (Agent-migration epic #152, milestone #2).

What

Routes all five flashcard LLM seams through one flashcard_agent — no gemini_service import remains in the flashcard path.

SeamBeforeAfter
extract_cards_from_image (OCR-split)call_geminiflashcard_agent
gemini_generate_cardscall_geminiflashcard_agent
gemini_cleanup_cardscall_geminiflashcard_agent
gemini_clozecall_geminiflashcard_agent
generate_flashcards (main AI gen)gemini_service.generate_flashcardsmoved to flashcard_import_service, runs flashcard_agent

Notes

  • New agents/flashcard.pyFlashcards { cards: list[FlashCard{front, back}] }; one agent, task instructions stay in the existing prompt templates (passed as the user message). Registered flashcard (gemini-2.5-flash) in _providers.
  • _run_flashcard_agent(prompt) filters cards missing a side (preserving _parse_card_json's behavior) and degrades to [] on agent failure — matching the old "bad LLM output → []" resilience (never raises).
  • Behavior preserved: gemini_cleanup_cards still falls back to its input on empty cleanup; extract_cards_from_image still short-circuits on empty OCR without calling the agent.
  • generate_flashcards moved off gemini_service (prompt-building verbatim); routes/flashcards.py imports it from flashcard_import_service.

Testing

  • Rewrote the 7 call_gemini patches in test_flashcard_import_service.py to mock flashcard_agent; added empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests. Route tests are unchanged (they patch the whole functions).
  • Full backend suite: 832 passed (the 2 test_storage_service failures pre-exist on main — missing SUPABASE env). ruff clean.

Out of scope: the non-LLM parsers (parse_xlsx/parse_anki_apkg/scrape_quizlet_url/dedup), and deleting the rest of services/gemini_service.py (#151).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added structured AI-powered flashcard generation using a dedicated flashcard workflow for course content, OCR imports, cleanup, and cloze cards.
    • Improved consistency by enforcing front/back card formatting and deterministic output behavior.
    • Enhanced generation by grounding prompts in source concepts, including low-mastery focus and optional extra context.
  • Bug Fixes
    • Added graceful fallbacks for invalid AI output.
    • Implemented a single retry for temporary provider errors (rate limits and transient server failures).
  • Tests
    • Updated and expanded unit tests to validate the new workflow, retry behavior, and empty-OCR short-circuit handling.

…I agent (#146)
Routes all five flashcard LLM seams through one `flashcard_agent`:
- New agents/flashcard.py (Flashcards{cards: list[FlashCard{front,back}]}).
Registered `flashcard` (flash) in _providers.
- flashcard_import_service gains `_run_flashcard_agent(prompt)` (filters empty
cards, degrades to [] on agent failure — preserving the old bad-output→[]
resilience). extract_cards_from_image / gemini_generate_cards /
gemini_cleanup_cards / gemini_cloze rewired onto it; call_gemini import gone.
gemini_cleanup_cards still falls back to its input; extract still short-circuits
on empty OCR.
- generate_flashcards (the main AI-generation path) moved here from
gemini_service (prompt-building verbatim) and runs the agent; routes/flashcards
imports it from flashcard_import_service, so no gemini_service import remains in
the flashcard path.
Tests: rewrote the 7 call_gemini patches to mock flashcard_agent; added
empty-filter, agent-failure→[], cleanup-fallback, and generate_flashcards tests.
Route tests unchanged (they patch whole functions). Full suite 832 passed (2
pre-existing storage-env failures). ruff clean.
Spec: specs/146-flashcard-agent.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f7c9c41-79d7-4513-83c6-9fb1a139ebac

📥 Commits

Reviewing files that changed from the base of the PR and between 7b57acc and 947fda2.

📒 Files selected for processing (2)
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/146-flashcard-agent.md
  • backend/tests/test_flashcard_import_service.py

📝 Walkthrough

Walkthrough

Flashcard import and generation now use a structured flashcard_agent with typed Pydantic outputs. The service adds filtering, failure handling, and transient-error retries, while routes, tests, provider mappings, and migration documentation are updated.

Changes

Flashcard agent migration

Layer / File(s)Summary
Flashcard agent contract and provider wiring
backend/agents/_providers.py, backend/agents/flashcard.py
Adds the flashcard task, typed FlashCard/Flashcards models, and a configured agent using gemini-2.5-flash.
Agent-backed flashcard service flow
backend/services/flashcard_import_service.py, backend/services/gemini_service.py, backend/routes/flashcards.py, backend/ruff.toml
Routes flashcard operations through the agent runner, adds output filtering and retry handling, and removes the legacy generator.
Migration validation and acceptance coverage
backend/tests/test_flashcard_import_service.py, specs/146-flashcard-agent.md
Updates tests for structured agent responses and documents migration requirements and acceptance checks.

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

Possibly related issues

Possibly related PRs

Suggested reviewers:darkest-teddy

Sequence Diagram(s)

sequenceDiagram
participant FlashcardsRoute
participant FlashcardImportService
participant flashcard_agent
FlashcardsRoute->>FlashcardImportService: Request flashcard generation
FlashcardImportService->>flashcard_agent: Submit structured prompt
flashcard_agent-->>FlashcardImportService: Return Flashcards
FlashcardImportService-->>FlashcardsRoute: Return filtered cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.25% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: migrating flashcard generation/import to a Pydantic AI agent.
Description check✅ PassedThe description covers the what, related issue, testing, and reviewer notes, even if it doesn't match the template exactly.
Linked Issues check✅ PassedThe PR satisfies #146 by routing flashcard seams through the agent, removing flashcard-path gemini_service imports, and adding tests.
Out of Scope Changes check✅ PassedNo clear out-of-scope changes stand out; the spec, lint baseline updates, and test refactors all support the flashcard agent migration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/146-flashcard-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging947fda2Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:12 PM

@AndresL230AndresL230 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review (workflow-backed, xhigh effort) of the flashcard → Pydantic AI agent migration.

One root cause dominates: _run_flashcard_agent's blanket except Exception: return [] turns LLM/API/transport outages into silent HTTP 200 with an empty (or un-cleaned) result across every migrated route — 12 of the finder hits collapse to this one issue. Alongside it: a dropped transient-error retry, a silent model-config default flip masked by a misleading comment, a latent async-loop swallow, and three cleanup items. Details inline, ranked by severity.

🤖 Generated with Claude Code

"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))
except Exception:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 P1 — this blanket except Exception: return [] swallows every LLM / API / transport failure.

The old call_gemini path re-raised on 429 / 500 / timeout / missing key, so routes surfaced a retryable HTTP 502. Now a Gemini outage (or an unset GEMINI_API_KEY) makes every migrated entry point — extract_cards_from_image, gemini_generate_cards, gemini_cloze, generate_flashcards — return [], so POST /api/flashcards/import/generate and /import/cloze reply 200 {"cards": []}.

Consequences:

  • The frontend can't distinguish an AI outage from 'genuinely no cards' — the user sees zero cards with nothing to retry.
  • There's no logging in the except, so operators get no signal.
  • Each route's existing 502 / 422 handler becomes dead code.

Suggest: only degrade to [] on genuine bad-output / parse cases; let transient / transport errors propagate (or at minimum logger.exception(...) before returning).

behavior) and degrades to an empty list on agent failure — matching the old
"bad LLM output → []" resilience (never raises)."""
try:
result = run_agent_sync(flashcard_agent.run(prompt))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — lost transient-error retry.call_gemini auto-retried once on 429 / 500 with a ~2s backoff (retries=1). A bare flashcard_agent.run drops that, so a single rate-limit / 5xx blip now fails the whole call instead of retrying.

🟡 P3 (latent) — async-loop swallow.run_agent_sync wraps asyncio.run, which raises RuntimeError if ever called from a running event loop; the except just below then swallows it to []. Harmless today (all callers are sync def), but the moment an async handler or on-loop task calls one of these helpers it will silently return zero cards instead of failing loudly.

Comment threadbackend/agents/_providers.py Outdated
# Social summary is short-form prose → the cheaper lite tier is enough.
"social_summary": "gemini-2.5-flash-lite",
# Flashcard generation/cleanup/cloze — content quality matters → full Flash
# (matches the legacy gemini_service default the flashcard path used).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 P2 — this comment is misleading. Only the model name carries over. The legacy call_gemini flashcard path also pinned temperature=0.7, max_output_tokens=8192, and thinking_budget=0 (thinking disabled) — none of which this agent replicates.

Flashcard generation / cleanup / cloze now run on GoogleModel defaults: dynamic thinking enabled (instead of budget 0) and provider-default temperature (instead of the pinned 0.7) → higher per-call latency / cost and less deterministic output. Either replicate those settings on the agent or drop the 'matches the legacy default' claim so it doesn't hide the flip from reviewers.

def _run_flashcard_agent(prompt: str) -> list[Card]:
"""Run the flashcard agent on a rendered prompt and return front/back dicts.

Filters out cards missing either side (preserving `_parse_card_json`'s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — _parse_card_json is now dead code. Every caller was rewired to _run_flashcard_agent; the ~18-line parser (around line 203) is unreferenced apart from this docstring mention. Its front/back strip+filter logic is duplicated inside _run_flashcard_agent, so a future edit to the dead copy silently no-ops. Recommend deleting it.

@@ -170,91 +170,3 @@ def extract_graph_update(response_text: str) -> tuple:
conversational = response_text

return conversational.strip(), graph_update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — stale ruff ignore (cleanup). Deleting generate_flashcards removed the last F541 (f-string-without-placeholder) source in this file, but the "services/gemini_service.py" = ["F541"] per-file-ignore in ruff.toml is left behind. ruff.toml states the baseline can only shrink — drop that line so an accidental placeholder-less f-string in this file fails CI again instead of passing silently.

import zipfile
from unittest.mock import MagicMock, patch
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 P3 — import json (line 3) is now unused after the rewrite dropped the json.dumps(...) payloads in favor of Flashcards objects. It's tolerated only by the F401 baseline the ruff docs ask to burn down — removing it shrinks the baseline rather than growing it.

Darkest-Teddyand others added 2 commits July 13, 2026 12:23
Resolve the conflict in backend/agents/_providers.py by keeping both sides:
the branch's `flashcard` agent task alongside main's `course_summary`,
`quiz_context`, and `concept_scan` additions in both AgentTask and _DEFAULTS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve AndresL230's review of the flashcard → Pydantic AI migration.
_run_flashcard_agent no longer swallows every failure into an empty list:
- Transport / runtime errors (missing GEMINI_API_KEY, run_agent_sync event-loop
misuse, network) now propagate so routes surface a retryable 502 instead of a
misleading 200 {"cards": []} — the existing 502/422 handlers are live again. (P1)
- Restore the one-retry-with-~2s-backoff on transient HTTP 429/5xx that the old
call_gemini(retries=1) path provided. (P2)
- The event-loop RuntimeError is no longer masked. (P3)
- Only genuine bad model output (UnexpectedModelBehavior) degrades to [], and every
failure path logs via logger.exception — never silent.
Pin the flashcard agent's model_settings (temperature=0.7, max_tokens=8192,
thinking_budget=0) to match the legacy call_gemini path instead of silently
inheriting GoogleModel defaults, and correct the misleading _providers.py comment
that claimed full legacy parity from the model name alone. (P2)
Delete the now-dead _parse_card_json (all callers use _run_flashcard_agent). (P3)
ruff.toml: drop the stale services/gemini_service.py F541 ignore (no F541 source
remains after generate_flashcards moved out), and shrink the test file's ignore to
just E402 by removing the now-unused `json` and `time` imports. (P3)
Update tests to the new contract: bad output → [], transport errors propagate,
transient 429/5xx retries once. Full backend suite green (live-Gemini integration
tests aside, which are CI-skipped without GEMINI_API_KEY).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
specs/146-flashcard-agent.md (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Spec text is stale vs. the implemented error contract.

R2 says _run_flashcard_agent "degrades to [] on agent failure ... no raise," and acceptance criterion 3 repeats "returns [] on agent failure." The shipped implementation is more nuanced: only bad/invalid model output (UnexpectedModelBehavior) degrades to []; transient HTTP 429/5xx retries once; everything else (transport, runtime, non-transient HTTP) propagates. This was an explicit, deliberate follow-up fix (per the PR's own commit messages) to avoid silently swallowing real outages — the spec should reflect that nuance so it isn't read as license to regress to blanket-catch behavior later.

📝 Suggested wording
-- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via- `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving- `_parse_card_json`'s filtering), and **degrades to `[]` on agent failure** (preserving the old- "bad output → []" resilience — no raise).+- Add `flashcard_import_service._run_flashcard_agent(prompt: str) -> list[Card]`: runs the agent via+ `run_agent_sync`, returns `{front, back}` dicts with empty-front/back filtered out (preserving+ `_parse_card_json`'s filtering), and **degrades to `[]` only on unusable/invalid model output**+ (`UnexpectedModelBehavior`). Transient HTTP 429/5xx retries once; all other failures (transport,+ runtime, non-transient HTTP) propagate so the route surfaces a retryable 502.

Also applies to: 63-72

🤖 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 `@specs/146-flashcard-agent.md` around lines 28 - 39, Update the R2
specification and acceptance criterion 3 to document the implemented
_run_flashcard_agent error contract: return [] only for invalid model output
represented by UnexpectedModelBehavior, retry transient HTTP 429/5xx failures
once, and propagate transport, runtime, and other non-transient HTTP errors.
Remove wording that implies blanket failure swallowing or unconditional [] on
agent failure.
backend/tests/test_flashcard_import_service.py (1)

378-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the documents=/context= prompt-building branches.

TestGenerateFlashcards only exercises topic/weak_concepts; the doc_blocks/concept_notes loop and extra_block logic in generate_flashcards (services/flashcard_import_service.py:322-361) — including the category.upper() call flagged above — has no test coverage.

🤖 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 `@backend/tests/test_flashcard_import_service.py` around lines 378 - 392, Add
tests in TestGenerateFlashcards that call generate_flashcards with
documents/context inputs, exercising both the doc_blocks/concept_notes loop and
extra_block prompt branch. Assert the generated prompt includes the supplied
document and concept content, and use a category value that verifies
category.upper() is applied while preserving existing topic/weak-concepts
coverage.
🤖 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.
Nitpick comments:
In `@backend/tests/test_flashcard_import_service.py`:
- Around line 378-392: Add tests in TestGenerateFlashcards that call
generate_flashcards with documents/context inputs, exercising both the
doc_blocks/concept_notes loop and extra_block prompt branch. Assert the
generated prompt includes the supplied document and concept content, and use a
category value that verifies category.upper() is applied while preserving
existing topic/weak-concepts coverage.
In `@specs/146-flashcard-agent.md`:
- Around line 28-39: Update the R2 specification and acceptance criterion 3 to
document the implemented _run_flashcard_agent error contract: return [] only for
invalid model output represented by UnexpectedModelBehavior, retry transient
HTTP 429/5xx failures once, and propagate transport, runtime, and other
non-transient HTTP errors. Remove wording that implies blanket failure
swallowing or unconditional [] on agent failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7cc72b-91a2-4970-897a-12a8dc9ae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 426a704 and 7b57acc.

📒 Files selected for processing (8)
  • backend/agents/_providers.py
  • backend/agents/flashcard.py
  • backend/routes/flashcards.py
  • backend/ruff.toml
  • backend/services/flashcard_import_service.py
  • backend/services/gemini_service.py
  • backend/tests/test_flashcard_import_service.py
  • specs/146-flashcard-agent.md
💤 Files with no reviewable changes (1)
  • backend/services/gemini_service.py

…act (#146)
Address remaining CodeRabbit nitpicks on the flashcard agent migration PR;
Andres's P1-P3 findings were already resolved in 7b57acc.
- Add a TestGenerateFlashcards case exercising the documents/concept_notes
loop and the free-text context branch, asserting the rendered prompt carries
the document block (with upper-cased category), concept notes, and context,
while preserving the existing topic/weak-concept coverage.
- Update spec R2, acceptance criterion 3, and R5 to document the implemented
_run_flashcard_agent error contract ([] only on UnexpectedModelBehavior,
retry transient 429/5xx once, propagate transport/runtime/non-transient HTTP)
instead of the stale "blanket [] on failure" wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

[P2] Agent migration: flashcard generation/import → agent

3 participants

@Jose-Gael-Cruz-Lopez@AndresL230@Darkest-Teddy