Upload folder using huggingface_hub
Browse files- ankigen_core/card_generator.py +61 -4
- app.py +7 -0
- tests/integration/test_app_interactions.py +1 -0
- tests/unit/test_card_generator.py +36 -0
ankigen_core/card_generator.py
CHANGED
@@ -176,6 +176,50 @@ async def generate_cards_batch(
|
|
176 |
raise # Re-raise for the main function to handle
|
177 |
|
178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
async def orchestrate_card_generation( # MODIFIED: Added async
|
180 |
client_manager: OpenAIClientManager, # Expect the manager
|
181 |
cache: ResponseCache, # Expect the cache instance
|
@@ -190,6 +234,7 @@ async def orchestrate_card_generation( # MODIFIED: Added async
|
|
190 |
cards_per_topic: int,
|
191 |
preference_prompt: str,
|
192 |
generate_cloze: bool,
|
|
|
193 |
):
|
194 |
"""Orchestrates the card generation process based on UI inputs."""
|
195 |
|
@@ -490,6 +535,10 @@ async def orchestrate_card_generation( # MODIFIED: Added async
|
|
490 |
"structured_output_completion returned None, defaulting to empty card list for text mode."
|
491 |
)
|
492 |
processed_cards = process_raw_cards_data(raw_cards)
|
|
|
|
|
|
|
|
|
493 |
formatted_cards = format_cards_for_dataframe(
|
494 |
processed_cards, topic_name=source_text_display_name, start_index=1
|
495 |
)
|
@@ -529,7 +578,9 @@ async def orchestrate_card_generation( # MODIFIED: Added async
|
|
529 |
# progress_total_batches = len(topics_for_generation)
|
530 |
# current_batch_num = 0
|
531 |
|
532 |
-
for
|
|
|
|
|
533 |
topics_for_generation
|
534 |
): # This loop will be skipped if text_mode populated flattened_data directly
|
535 |
# current_batch_num += 1
|
@@ -551,6 +602,10 @@ async def orchestrate_card_generation( # MODIFIED: Added async
|
|
551 |
system_prompt, # System prompt defined above based on mode
|
552 |
generate_cloze,
|
553 |
)
|
|
|
|
|
|
|
|
|
554 |
# Assign topic name to cards before formatting for DataFrame
|
555 |
formatted_batch = format_cards_for_dataframe(
|
556 |
batch_cards,
|
@@ -758,9 +813,11 @@ def format_cards_for_dataframe(
|
|
758 |
difficulty_str = strip_html_tags(str(difficulty))
|
759 |
|
760 |
formatted_card = {
|
761 |
-
"Index":
|
762 |
-
|
763 |
-
|
|
|
|
|
764 |
"Topic": strip_html_tags(topic_name), # Ensure topic is also plain
|
765 |
"Card_Type": strip_html_tags(card_type),
|
766 |
"Question": question, # Already stripped during Card object creation
|
|
|
176 |
raise # Re-raise for the main function to handle
|
177 |
|
178 |
|
179 |
+
async def judge_card(
|
180 |
+
openai_client,
|
181 |
+
cache: ResponseCache,
|
182 |
+
model: str,
|
183 |
+
card: Card,
|
184 |
+
) -> bool:
|
185 |
+
"""Use an LLM to validate a single card."""
|
186 |
+
system_prompt = (
|
187 |
+
"You review flashcards and decide if the question is clear and useful. "
|
188 |
+
'Respond with a JSON object like {"is_valid": true}.'
|
189 |
+
)
|
190 |
+
user_prompt = f"Question: {card.front.question}\nAnswer: {card.back.answer}"
|
191 |
+
try:
|
192 |
+
result = await structured_output_completion(
|
193 |
+
openai_client=openai_client,
|
194 |
+
model=model,
|
195 |
+
response_format={"type": "json_object"},
|
196 |
+
system_prompt=system_prompt,
|
197 |
+
user_prompt=user_prompt,
|
198 |
+
cache=cache,
|
199 |
+
)
|
200 |
+
if isinstance(result, dict):
|
201 |
+
return bool(result.get("is_valid", True))
|
202 |
+
except Exception as e: # pragma: no cover - network or parse errors
|
203 |
+
logger.warning(f"LLM judge failed for card '{card.front.question}': {e}")
|
204 |
+
return True
|
205 |
+
|
206 |
+
|
207 |
+
async def judge_cards(
|
208 |
+
openai_client,
|
209 |
+
cache: ResponseCache,
|
210 |
+
model: str,
|
211 |
+
cards: List[Card],
|
212 |
+
) -> List[Card]:
|
213 |
+
"""Filter cards using the LLM judge."""
|
214 |
+
validated: List[Card] = []
|
215 |
+
for card in cards:
|
216 |
+
if await judge_card(openai_client, cache, model, card):
|
217 |
+
validated.append(card)
|
218 |
+
else:
|
219 |
+
logger.info(f"Card rejected by judge: {card.front.question}")
|
220 |
+
return validated
|
221 |
+
|
222 |
+
|
223 |
async def orchestrate_card_generation( # MODIFIED: Added async
|
224 |
client_manager: OpenAIClientManager, # Expect the manager
|
225 |
cache: ResponseCache, # Expect the cache instance
|
|
|
234 |
cards_per_topic: int,
|
235 |
preference_prompt: str,
|
236 |
generate_cloze: bool,
|
237 |
+
use_llm_judge: bool = False,
|
238 |
):
|
239 |
"""Orchestrates the card generation process based on UI inputs."""
|
240 |
|
|
|
535 |
"structured_output_completion returned None, defaulting to empty card list for text mode."
|
536 |
)
|
537 |
processed_cards = process_raw_cards_data(raw_cards)
|
538 |
+
if use_llm_judge and processed_cards:
|
539 |
+
processed_cards = await judge_cards(
|
540 |
+
openai_client, cache, model, processed_cards
|
541 |
+
)
|
542 |
formatted_cards = format_cards_for_dataframe(
|
543 |
processed_cards, topic_name=source_text_display_name, start_index=1
|
544 |
)
|
|
|
578 |
# progress_total_batches = len(topics_for_generation)
|
579 |
# current_batch_num = 0
|
580 |
|
581 |
+
for (
|
582 |
+
topic_info
|
583 |
+
) in (
|
584 |
topics_for_generation
|
585 |
): # This loop will be skipped if text_mode populated flattened_data directly
|
586 |
# current_batch_num += 1
|
|
|
602 |
system_prompt, # System prompt defined above based on mode
|
603 |
generate_cloze,
|
604 |
)
|
605 |
+
if use_llm_judge and batch_cards:
|
606 |
+
batch_cards = await judge_cards(
|
607 |
+
openai_client, cache, model, batch_cards
|
608 |
+
)
|
609 |
# Assign topic name to cards before formatting for DataFrame
|
610 |
formatted_batch = format_cards_for_dataframe(
|
611 |
batch_cards,
|
|
|
813 |
difficulty_str = strip_html_tags(str(difficulty))
|
814 |
|
815 |
formatted_card = {
|
816 |
+
"Index": (
|
817 |
+
f"{topic_index}.{actual_index}"
|
818 |
+
if topic_index > 0
|
819 |
+
else str(actual_index)
|
820 |
+
),
|
821 |
"Topic": strip_html_tags(topic_name), # Ensure topic is also plain
|
822 |
"Card_Type": strip_html_tags(card_type),
|
823 |
"Question": question, # Already stripped during Card object creation
|
app.py
CHANGED
@@ -295,6 +295,10 @@ def create_ankigen_interface():
|
|
295 |
label="Generate Cloze Cards (Experimental)",
|
296 |
value=False,
|
297 |
)
|
|
|
|
|
|
|
|
|
298 |
|
299 |
generate_button = gr.Button("Generate Cards", variant="primary")
|
300 |
|
@@ -490,6 +494,7 @@ def create_ankigen_interface():
|
|
490 |
cards_per_topic_val,
|
491 |
preference_prompt_val,
|
492 |
generate_cloze_checkbox_val,
|
|
|
493 |
progress=gr.Progress(track_tqdm=True), # Added progress tracker
|
494 |
):
|
495 |
# Recreate the partial function call, but now it can be awaited
|
@@ -509,6 +514,7 @@ def create_ankigen_interface():
|
|
509 |
cards_per_topic_val,
|
510 |
preference_prompt_val,
|
511 |
generate_cloze_checkbox_val,
|
|
|
512 |
)
|
513 |
|
514 |
generate_button.click(
|
@@ -524,6 +530,7 @@ def create_ankigen_interface():
|
|
524 |
cards_per_topic,
|
525 |
preference_prompt,
|
526 |
generate_cloze_checkbox,
|
|
|
527 |
],
|
528 |
outputs=[output, total_cards_html],
|
529 |
show_progress="full",
|
|
|
295 |
label="Generate Cloze Cards (Experimental)",
|
296 |
value=False,
|
297 |
)
|
298 |
+
llm_judge_checkbox = gr.Checkbox(
|
299 |
+
label="Use LLM Judge",
|
300 |
+
value=False,
|
301 |
+
)
|
302 |
|
303 |
generate_button = gr.Button("Generate Cards", variant="primary")
|
304 |
|
|
|
494 |
cards_per_topic_val,
|
495 |
preference_prompt_val,
|
496 |
generate_cloze_checkbox_val,
|
497 |
+
llm_judge_checkbox_val,
|
498 |
progress=gr.Progress(track_tqdm=True), # Added progress tracker
|
499 |
):
|
500 |
# Recreate the partial function call, but now it can be awaited
|
|
|
514 |
cards_per_topic_val,
|
515 |
preference_prompt_val,
|
516 |
generate_cloze_checkbox_val,
|
517 |
+
llm_judge_checkbox_val,
|
518 |
)
|
519 |
|
520 |
generate_button.click(
|
|
|
530 |
cards_per_topic,
|
531 |
preference_prompt,
|
532 |
generate_cloze_checkbox,
|
533 |
+
llm_judge_checkbox,
|
534 |
],
|
535 |
outputs=[output, total_cards_html],
|
536 |
show_progress="full",
|
tests/integration/test_app_interactions.py
CHANGED
@@ -393,6 +393,7 @@ def get_orchestrator_mock_inputs(generation_mode="subject", api_key="sk-test"):
|
|
393 |
"cards_per_topic": 3, # For subject mode / text mode / web mode
|
394 |
"preference_prompt": "Test preferences",
|
395 |
"generate_cloze": False,
|
|
|
396 |
}
|
397 |
|
398 |
|
|
|
393 |
"cards_per_topic": 3, # For subject mode / text mode / web mode
|
394 |
"preference_prompt": "Test preferences",
|
395 |
"generate_cloze": False,
|
396 |
+
"use_llm_judge": False,
|
397 |
}
|
398 |
|
399 |
|
tests/unit/test_card_generator.py
CHANGED
@@ -203,6 +203,7 @@ def base_orchestrator_args(api_key="valid_key", **kwargs):
|
|
203 |
"cards_per_topic": 5, # Corresponds to num_cards in generate_cards_batch
|
204 |
"preference_prompt": "Pref prompt", # Corresponds to system_prompt
|
205 |
"generate_cloze": False,
|
|
|
206 |
}
|
207 |
base_args.update(kwargs) # Update with any provided kwargs
|
208 |
return base_args
|
@@ -276,6 +277,41 @@ async def test_orchestrate_subject_mode(
|
|
276 |
# assert status.strip() == expected_html_status.strip()
|
277 |
|
278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
@patch("ankigen_core.card_generator.structured_output_completion")
|
280 |
@patch("ankigen_core.card_generator.generate_cards_batch")
|
281 |
async def test_orchestrate_text_mode(
|
|
|
203 |
"cards_per_topic": 5, # Corresponds to num_cards in generate_cards_batch
|
204 |
"preference_prompt": "Pref prompt", # Corresponds to system_prompt
|
205 |
"generate_cloze": False,
|
206 |
+
"use_llm_judge": False,
|
207 |
}
|
208 |
base_args.update(kwargs) # Update with any provided kwargs
|
209 |
return base_args
|
|
|
277 |
# assert status.strip() == expected_html_status.strip()
|
278 |
|
279 |
|
280 |
+
@patch("ankigen_core.card_generator.judge_cards")
|
281 |
+
@patch("ankigen_core.card_generator.structured_output_completion")
|
282 |
+
@patch("ankigen_core.card_generator.generate_cards_batch")
|
283 |
+
async def test_orchestrate_subject_mode_with_judge(
|
284 |
+
mock_gcb,
|
285 |
+
mock_soc,
|
286 |
+
mock_judge,
|
287 |
+
mock_client_manager_fixture,
|
288 |
+
mock_response_cache_fixture,
|
289 |
+
):
|
290 |
+
"""Test orchestrate_card_generation calls judge_cards when enabled."""
|
291 |
+
manager, client = mock_client_manager_fixture
|
292 |
+
cache = mock_response_cache_fixture
|
293 |
+
args = base_orchestrator_args(generation_mode="subject", use_llm_judge=True)
|
294 |
+
|
295 |
+
mock_soc.return_value = {
|
296 |
+
"topics": [{"name": "T1", "difficulty": "d", "description": "d"}]
|
297 |
+
}
|
298 |
+
sample_card = Card(
|
299 |
+
front=CardFront(question="Q1"),
|
300 |
+
back=CardBack(answer="A1", explanation="E1", example="Ex1"),
|
301 |
+
)
|
302 |
+
mock_gcb.return_value = [sample_card]
|
303 |
+
mock_judge.return_value = [sample_card]
|
304 |
+
|
305 |
+
with patch("gradio.Info"), patch("gradio.Warning"):
|
306 |
+
await card_generator.orchestrate_card_generation(
|
307 |
+
client_manager=manager,
|
308 |
+
cache=cache,
|
309 |
+
**args,
|
310 |
+
)
|
311 |
+
|
312 |
+
mock_judge.assert_called_once_with(client, cache, args["model_name"], [sample_card])
|
313 |
+
|
314 |
+
|
315 |
@patch("ankigen_core.card_generator.structured_output_completion")
|
316 |
@patch("ankigen_core.card_generator.generate_cards_batch")
|
317 |
async def test_orchestrate_text_mode(
|