brickfrog commited on
Commit
07fe6c3
·
verified ·
1 Parent(s): c7abff0

Upload folder using huggingface_hub

Browse files
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 topic_info in (
 
 
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": f"{topic_index}.{actual_index}"
762
- if topic_index > 0
763
- else str(actual_index),
 
 
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(