Skip to content

Flashcard Generation Endpoint 5.1#18

Open
usha-sj wants to merge 1 commit intomainfrom
sprint5/flashcard-gen-endpoint
Open

Flashcard Generation Endpoint 5.1#18
usha-sj wants to merge 1 commit intomainfrom
sprint5/flashcard-gen-endpoint

Conversation

@usha-sj
Copy link
Collaborator

@usha-sj usha-sj commented Mar 1, 2026

Feature(Flashcard Generation Endpoint 5.1): Implement endpoint to generate 10 Q/A flashcards via Gemini.

🎉 New feature (Extends application, non-breaking feature)

PR Summary
This PR introduces the Flashcard Generation Endpoint, which generates exactly 10 question-and-answer flashcards using Gemini. The endpoint validates input, enforces structured JSON output, supports difficulty levels, and stores the generated flashcards in Supabase.

Overview

What feature/problem does this PR address?
This PR introduces a new endpoint that generates 10 flashcards from the provided notes or topic, ensures the output is returned in a consistent and structured JSON format, validates the correctness and formatting of the response from Gemini, and stores each flashcard along with its difficulty level and source input.

What approach was taken?

  • Created a POST /api/v1/flashcards endpoint in FastAPI.
  • FlashcardRequest accepts either text (notes) or topic as input, with an optional difficulty field (easy, medium, hard) defaulting to medium.
  • FlashcardDifficulty introduced as a str Enum to enforce valid difficulty values.
  • Added parse_and_validate_flashcards() helper to enforce strict JSON structure from Gemini — exactly 10 flashcards, each with a non-empty question and answer.
  • Prompt builder build_flashcard_generation_prompt() added to prompts/study_gen_v1.py, consistent with existing prompt architecture.
  • Flashcards stored in Supabase flashcards table with user_id, source_text, topic, difficulty, and cards columns.
  • Unit test method: test_flashcards_unit.py (mocked) to manage quota usage.

Any important design decisions or trade-offs?

  • Prompt builder was added to study_gen_v1.py rather than inline in the route, keeping main.py consistent with the existing codebase pattern.
  • Difficulty is passed both into the prompt (to adjust question complexity) and stored in the DB (to enable future UI filtering).
  • user_id is pulled from the authenticated user via require_user, ensuring flashcards are always associated with the correct user.

Checklist

  • Added a clear description
  • Included evidence of Unit Testing
  • Updated documentation (if needed)

Unit Test Evidence

======================================================== test session starts =========================================================
tests/test_flashcards_unit.py::TestFlashcardRequest::test_valid_request_with_text PASSED                                       [  2%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_valid_request_with_topic PASSED                                      [  5%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_default_difficulty_is_medium PASSED                                  [  7%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_accepts_easy_difficulty PASSED                                       [ 10%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_accepts_hard_difficulty PASSED                                       [ 12%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_rejects_invalid_difficulty PASSED                                    [ 15%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_rejects_missing_text_and_topic PASSED                                [ 17%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_strips_whitespace_from_text PASSED                                   [ 20%]
tests/test_flashcards_unit.py::TestFlashcardRequest::test_strips_whitespace_from_topic PASSED                                  [ 23%]
tests/test_flashcards_unit.py::TestFlashcardStructure::test_response_contains_flashcards_array PASSED                          [ 25%]
tests/test_flashcards_unit.py::TestFlashcardStructure::test_response_contains_exactly_10_flashcards PASSED                     [ 28%]
tests/test_flashcards_unit.py::TestFlashcardStructure::test_each_flashcard_has_question_and_answer PASSED                      [ 30%]
tests/test_flashcards_unit.py::TestFlashcardStructure::test_question_and_answer_are_strings PASSED                             [ 33%]
tests/test_flashcards_unit.py::TestFlashcardStructure::test_accepts_topic_instead_of_text PASSED                               [ 35%]
tests/test_flashcards_unit.py::TestJSONValidation::test_missing_flashcards_key PASSED                                          [ 38%]
tests/test_flashcards_unit.py::TestJSONValidation::test_fewer_than_10_flashcards PASSED                                        [ 41%]
tests/test_flashcards_unit.py::TestJSONValidation::test_more_than_10_flashcards PASSED                                         [ 43%]
tests/test_flashcards_unit.py::TestJSONValidation::test_missing_question_field PASSED                                          [ 46%]
tests/test_flashcards_unit.py::TestJSONValidation::test_missing_answer_field PASSED                                            [ 48%]
tests/test_flashcards_unit.py::TestJSONValidation::test_empty_question_rejected PASSED                                         [ 51%]
tests/test_flashcards_unit.py::TestJSONValidation::test_empty_answer_rejected PASSED                                           [ 53%]
tests/test_flashcards_unit.py::TestJSONValidation::test_flashcard_item_not_dict PASSED                                         [ 56%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_handles_markdown_json_fence PASSED                           [ 58%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_handles_generic_code_fence PASSED                            [ 61%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_handles_whitespace PASSED                                    [ 64%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_plain_text_response_returns_500 PASSED                       [ 66%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_empty_string_returns_500 PASSED                              [ 69%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_clean_response_strips_json_fence PASSED                      [ 71%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_clean_response_strips_generic_fence PASSED                   [ 74%]
tests/test_flashcards_unit.py::TestNoHallucinatedFormatting::test_clean_response_leaves_plain_json_unchanged PASSED            [ 76%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_endpoint_exists PASSED                                     [ 79%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_accepts_text_input PASSED                                  [ 82%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_rejects_empty_body PASSED                                  [ 84%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_handles_gemini_unavailable PASSED                          [ 87%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_response_content_type_is_json PASSED                       [ 89%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_successful_response_schema PASSED                          [ 92%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_difficulty_easy_accepted PASSED                            [ 94%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_difficulty_hard_accepted PASSED                            [ 97%]
tests/test_flashcards_unit.py::TestGenerateFlashcardsEndpoint::test_invalid_difficulty_rejected PASSED                         [100%]

=================================================== 39 passed, 2 warnings in 5.35s ===================================================

Testing Method

cd backend
source venv/bin/activate  # for macOS/Linux

# Install dependencies
pip install -r requirements.txt

# Run unit tests (no API calls)
venv/bin/python -m pytest tests/test_flashcards_unit.py -v

# Check Gemini API call with Curl
curl -X POST http://localhost:8001/api/v1/flashcards -H "Content-Type: application/json" -d '{"text": "Photosynthesis is the process by which plants convert sunlight into glucose using CO2 and water. It occurs in the chloroplasts.", "difficulty": "easy"}'

Additional Notes
API Quota Warning: use test_flashcards_unit.py for regular development as curl testing consumes Gemini quota by making real API calls.
Environment Setup: Ensure GEMINI_API_KEY and SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set in the root .env file before running.

Jira Ticket
Jira Ticket(s) - [SOC-26]

@usha-sj usha-sj requested a review from Arhum2 March 1, 2026 19:17
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR implements a new endpoint for generating 10 flashcards via Gemini AI with difficulty levels and Supabase storage. The implementation includes comprehensive validation, well-structured prompts, and extensive unit tests.

Major Issues:

  • Missing database migration for the flashcards table - the code will fail at runtime when attempting to insert data into a non-existent table
  • Test coverage gap: no tests verify the database insertion logic

Positive aspects:

  • Well-structured prompt builder with clear difficulty levels
  • Thorough input validation and JSON response parsing
  • Comprehensive unit test coverage for API contract and edge cases
  • Consistent with existing codebase patterns

Confidence Score: 1/5

  • This PR will fail in production due to missing database table
  • The code attempts to insert data into a flashcards table that doesn't exist in the database. No migration file was included to create this table, which will cause the endpoint to fail with a "relation does not exist" error when called.
  • backend/main.py requires immediate attention - the database table must be created before this can work

Important Files Changed

Filename Overview
backend/main.py Added flashcard generation endpoint with validation and Supabase storage, but missing database table migration will cause runtime failure
backend/prompts/study_gen_v1.py Added well-structured prompt builder for flashcard generation with clear difficulty levels and JSON format instructions
backend/tests/test_flashcards_unit.py Comprehensive unit tests for API validation and JSON parsing, but no tests for database insertion logic

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Client sends POST request] --> B[Validate request body]
    B --> C{text or topic provided?}
    C -->|No| D[Return 422 error]
    C -->|Yes| E[Build prompt with difficulty]
    E --> F[Call Gemini API]
    F --> G{Response valid?}
    G -->|No| H[Return 500 error]
    G -->|Yes| I[Parse and validate JSON]
    I --> J{Exactly 10 flashcards?}
    J -->|No| H
    J -->|Yes| K[Insert into Supabase]
    K --> L{Insert successful?}
    L -->|No| M[Return 500 error]
    L -->|Yes| N[Return flashcards to client]
Loading

Last reviewed commit: 68850ee

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

},
"dependencies": {
"@supabase/supabase-js": "^2.97.0",
"@supabase/supabase-js": "^2.98.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Update to @supabase/supabase-js ^2.98.0 not mentioned in PR description. Verify this change is intentional.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Must have happened by accident, I can revert or if its fine, we can keep version 2.98

@usha-sj
Copy link
Collaborator Author

usha-sj commented Mar 1, 2026

@greptileai

Comment on lines +332 to +333
print(f"[flashcards] Failed to parse JSON: {e}")
print(f"[flashcards] Raw response: {raw_response}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Use logger.error() instead of print() for consistency

Suggested change
print(f"[flashcards] Failed to parse JSON: {e}")
print(f"[flashcards] Raw response: {raw_response}")
logger.error(f"[flashcards] Failed to parse JSON: {e}")
logger.debug(f"[flashcards] Raw response: {raw_response}")

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor

Choose a reason for hiding this comment

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

dosent match our coding style, ignore this

"cards": [fc.model_dump() for fc in flashcards],
}).execute()
except Exception as e:
print(f"[flashcards] DB insert failed: {e}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Use logger.error() instead of print() for consistency

Suggested change
print(f"[flashcards] DB insert failed: {e}")
logger.error(f"[flashcards] DB insert failed: {e}")

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor

Choose a reason for hiding this comment

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

dosent match our coding style, ignore this

sb = get_supabase()
try:
sb.table("flashcards").insert({
"user_id": user["user_id"] if user else "00000000-0000-0000-0000-000000000001",
Copy link
Contributor

Choose a reason for hiding this comment

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

All unauthenticated users share the same fallback user_id. When auth is not required, multiple users' flashcards will be mixed under this single UUID, causing data integrity issues.

@usha-sj usha-sj force-pushed the sprint5/flashcard-gen-endpoint branch from 5db16a4 to 68850ee Compare March 2, 2026 02:51
@usha-sj
Copy link
Collaborator Author

usha-sj commented Mar 2, 2026

@greptileai

Comment on lines +519 to +525
sb.table("flashcards").insert({
"user_id": user["user_id"] if user else "00000000-0000-0000-0000-000000000001",
"source_text": request.text,
"topic": request.topic,
"difficulty": difficulty,
"cards": [fc.model_dump() for fc in flashcards],
}).execute()
Copy link
Contributor

Choose a reason for hiding this comment

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

No migration file creates the flashcards table. This will fail at runtime with "relation does not exist".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

NA as we're using supabase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants