generate.py•5.67 kB
import os
import json
import logging
from typing import Dict, List, Any, Set
from dotenv import load_dotenv
from openai import OpenAI
# Configure logging
logger = logging.getLogger(__name__)
load_dotenv()
# Validate environment variables
openai_api_key = os.getenv("OPENAI_API_KEY")
if not openai_api_key:
raise ValueError("OPENAI_API_KEY environment variable is required")
open_client = OpenAI(api_key=openai_api_key)
functions = [
{
"name": "generate_anki_notes",
"description": "Produce flashcards ready for AnkiConnect addNotes",
"parameters": {
"type": "object",
"properties": {
"notes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"deckName": {"type": "string"},
"modelName": {"type": "string"},
"fields": {
"type": "object",
"properties": {
"Front": {"type": "string"},
"Back": {"type": "string"}
},
"required": ["Front","Back"]
},
"options": {
"type":"object",
"properties": {"allowDuplicate": {"type":"boolean"}},
"default": {"allowDuplicate": False}
},
"tags": {
"type":"array",
"items": {"type":"string"},
"default": []
}
},
"required": ["deckName","modelName","fields"]
}
}
},
"required": ["notes"]
}
}
]
async def generate_flashcards_gpt(page_name: str, topics: Set[str], content: Dict[str, str]) -> List[Dict[str, Any]]:
"""
Generates flashcards using the OpenAI Models.
Takes page topics and Q&A content, returns structured Anki cards.
"""
if not page_name or not content:
logger.warning("Empty page name or content provided to GPT")
return []
logger.info(f"Generating flashcards for {page_name} with {len(content)} Q&A pairs")
topics_str = ", ".join(list(topics)) if topics else "general study topics"
system = {
"role": "system",
"content": f"""
You are an expert tutor and curriculum designer specializing in {topics_str}.
Your job is to take a student's raw Q&A notes (provided as a JSON dict) and:
• Refine each question for clarity and focus on core concepts
• Correct or enhance answers with accurate, relevant details
• Keep language concise and suitable for flashcard study
• Ensure questions test understanding, not just memorization
Create Anki flashcards by calling the function "generate_anki_notes" with these parameters:
- deckName: "{page_name}"
- modelName: "Basic" (always use this note type)
- fields.Front: refined, clear question
- fields.Back: comprehensive but concise answer
- options.allowDuplicate: false
- tags: [] (empty array)
Generate high-quality study cards that will help with long-term retention.
"""
}
user = {
"role": "user",
"content": f"Topics covered: {topics_str}\n\nStudent's Q&A notes to convert to flashcards:\n{json.dumps(content, indent=2)}"
}
try:
resp = open_client.chat.completions.create(
model="gpt-4o-mini",
messages=[system, user],
functions=functions,
function_call={"name": "generate_anki_notes"}
)
if not resp.choices or not resp.choices[0].message.function_call:
logger.error("No function call received from OpenAI")
return []
except Exception as e:
logger.error(f"OpenAI API error: {str(e)}")
raise
try:
args = json.loads(resp.choices[0].message.function_call.arguments)
notes = args.get("notes", [])
if not notes:
logger.warning("No notes generated by OpenAI")
return []
# Ensure all cards have correct deck name
flashcards = enforce_deck_name(notes, page_name)
logger.info(f"Generated {len(flashcards)} flashcards")
return flashcards
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Error parsing OpenAI response: {str(e)}")
return []
def enforce_deck_name(cards: List[Dict[str, Any]], correct_deck: str) -> List[Dict[str, Any]]:
"""
Overwrite every card's deckName with the correct one.
This guarantees no stray values get through and validates card structure.
"""
validated_cards = []
for i, card in enumerate(cards):
if not isinstance(card, dict):
logger.warning(f"Skipping invalid card at index {i}: not a dict")
continue
# Ensure required fields exist
if 'fields' not in card or 'Front' not in card.get('fields', {}) or 'Back' not in card.get('fields', {}):
logger.warning(f"Skipping invalid card at index {i}: missing required fields")
continue
# Set correct values
card['deckName'] = correct_deck
card['modelName'] = 'Basic' # Ensure consistent model
# Ensure options and tags exist
if 'options' not in card:
card['options'] = {}
card['options']['allowDuplicate'] = False
if 'tags' not in card:
card['tags'] = []
validated_cards.append(card)
logger.info(f"Validated {len(validated_cards)} cards out of {len(cards)}")
return validated_cards