brickfrog commited on
Commit
9b095d8
·
verified ·
1 Parent(s): efaa99d

Upload folder using huggingface_hub

Browse files
ankigen_core/agents/base.py CHANGED
@@ -36,7 +36,7 @@ class AgentConfig:
36
 
37
  name: str
38
  instructions: str
39
- model: str = "gpt-4.1"
40
  temperature: float = 0.7
41
  max_tokens: Optional[int] = None
42
  timeout: float = 30.0
@@ -67,8 +67,19 @@ class BaseAgentWrapper:
67
 
68
  set_default_openai_client(self.openai_client, use_for_tracing=False)
69
 
70
- # Create model settings with temperature
71
- model_settings = ModelSettings(temperature=self.config.temperature)
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  # Use clean instructions without JSON formatting hacks
74
  clean_instructions = self.config.instructions
 
36
 
37
  name: str
38
  instructions: str
39
+ model: str = "gpt-5.1"
40
  temperature: float = 0.7
41
  max_tokens: Optional[int] = None
42
  timeout: float = 30.0
 
67
 
68
  set_default_openai_client(self.openai_client, use_for_tracing=False)
69
 
70
+ # Create model settings with temperature and GPT-5.1 reasoning support
71
+ model_settings_kwargs = {"temperature": self.config.temperature}
72
+
73
+ # GPT-5.1 (not chat-latest) supports reasoning_effort
74
+ if (
75
+ self.config.model.startswith("gpt-5")
76
+ and "chat-latest" not in self.config.model
77
+ ):
78
+ from openai.types.shared import Reasoning
79
+
80
+ model_settings_kwargs["reasoning"] = Reasoning(effort="none")
81
+
82
+ model_settings = ModelSettings(**model_settings_kwargs)
83
 
84
  # Use clean instructions without JSON formatting hacks
85
  clean_instructions = self.config.instructions
ankigen_core/agents/config.py CHANGED
@@ -91,7 +91,7 @@ class AgentConfigManager:
91
 
92
  # Default models for each agent type
93
  default_models = {
94
- "subject_expert_model": "gpt-4.1",
95
  }
96
 
97
  # Simple mapping: agent_name -> agent_name_model
@@ -111,7 +111,7 @@ class AgentConfigManager:
111
  config = AgentConfig(
112
  name=agent_data.get("name", agent_name),
113
  instructions=agent_data.get("instructions", ""),
114
- model=agent_data.get("model", "gpt-4"),
115
  temperature=agent_data.get("temperature", 0.7),
116
  max_tokens=agent_data.get("max_tokens"),
117
  timeout=agent_data.get("timeout", 30.0),
@@ -176,7 +176,7 @@ class AgentConfigManager:
176
  config = AgentConfig(
177
  name=agent_name,
178
  instructions=agent_data.get("instructions", ""),
179
- model=agent_data.get("model", "gpt-4.1"),
180
  temperature=agent_data.get("temperature", 0.7),
181
  max_tokens=agent_data.get("max_tokens"),
182
  timeout=agent_data.get("timeout", 30.0),
 
91
 
92
  # Default models for each agent type
93
  default_models = {
94
+ "subject_expert_model": "gpt-5.1",
95
  }
96
 
97
  # Simple mapping: agent_name -> agent_name_model
 
111
  config = AgentConfig(
112
  name=agent_data.get("name", agent_name),
113
  instructions=agent_data.get("instructions", ""),
114
+ model=agent_data.get("model", "gpt-5.1"),
115
  temperature=agent_data.get("temperature", 0.7),
116
  max_tokens=agent_data.get("max_tokens"),
117
  timeout=agent_data.get("timeout", 30.0),
 
176
  config = AgentConfig(
177
  name=agent_name,
178
  instructions=agent_data.get("instructions", ""),
179
+ model=agent_data.get("model", "gpt-5.1"),
180
  temperature=agent_data.get("temperature", 0.7),
181
  max_tokens=agent_data.get("max_tokens"),
182
  timeout=agent_data.get("timeout", 30.0),
ankigen_core/agents/schemas.py CHANGED
@@ -167,7 +167,7 @@ class AutoConfigSchema(BaseModel):
167
  )
168
  model_choice: str = Field(
169
  ...,
170
- description="Recommended model: 'gpt-4.1' for complex topics, 'gpt-4.1-nano' for simpler topics",
171
  )
172
 
173
  # Analysis metadata
 
167
  )
168
  model_choice: str = Field(
169
  ...,
170
+ description="Recommended model: 'gpt-5.1' for all tasks (uses reasoning_effort=none for speed)",
171
  )
172
 
173
  # Analysis metadata
ankigen_core/auto_config.py CHANGED
@@ -6,6 +6,7 @@ from openai import AsyncOpenAI
6
  from ankigen_core.logging import logger
7
  from ankigen_core.context7 import Context7Client
8
  from ankigen_core.agents.schemas import AutoConfigSchema
 
9
 
10
 
11
  class AutoConfigService:
@@ -70,20 +71,15 @@ Extract:
70
  Provide a brief rationale for your choices."""
71
 
72
  try:
73
- response = await openai_client.beta.chat.completions.parse(
74
- model="gpt-4.1-nano", # Use nano for this analysis task
75
- messages=[
76
- {"role": "system", "content": system_prompt},
77
- {"role": "user", "content": user_prompt},
78
- ],
79
- response_format=AutoConfigSchema,
80
  temperature=0.3, # Lower temperature for more consistent analysis
81
  )
82
 
83
- if not response.choices or not response.choices[0].message.parsed:
84
- raise ValueError("Failed to get valid response from OpenAI")
85
-
86
- config = response.choices[0].message.parsed
87
  logger.info(
88
  f"Subject analysis complete: library='{config.library_search_term}', "
89
  f"topics={config.topic_number}, cards/topic={config.cards_per_topic}"
@@ -100,7 +96,7 @@ Provide a brief rationale for your choices."""
100
  cards_per_topic=8,
101
  learning_preferences="Focus on fundamental concepts and core principles with practical examples",
102
  generate_cloze=False,
103
- model_choice="gpt-4.1-nano",
104
  subject_type="concepts",
105
  scope="medium",
106
  rationale="Using default settings due to analysis error",
 
6
  from ankigen_core.logging import logger
7
  from ankigen_core.context7 import Context7Client
8
  from ankigen_core.agents.schemas import AutoConfigSchema
9
+ from ankigen_core.llm_interface import structured_agent_call
10
 
11
 
12
  class AutoConfigService:
 
71
  Provide a brief rationale for your choices."""
72
 
73
  try:
74
+ config = await structured_agent_call(
75
+ openai_client=openai_client,
76
+ model="gpt-5.1",
77
+ instructions=system_prompt,
78
+ user_input=user_prompt,
79
+ output_type=AutoConfigSchema,
 
80
  temperature=0.3, # Lower temperature for more consistent analysis
81
  )
82
 
 
 
 
 
83
  logger.info(
84
  f"Subject analysis complete: library='{config.library_search_term}', "
85
  f"topics={config.topic_number}, cards/topic={config.cards_per_topic}"
 
96
  cards_per_topic=8,
97
  learning_preferences="Focus on fundamental concepts and core principles with practical examples",
98
  generate_cloze=False,
99
+ model_choice="gpt-5.1",
100
  subject_type="concepts",
101
  scope="medium",
102
  rationale="Using default settings due to analysis error",
ankigen_core/card_generator.py CHANGED
@@ -29,15 +29,20 @@ logger.info("Agent system loaded successfully")
29
 
30
  # --- Constants --- (Moved from app.py)
31
  AVAILABLE_MODELS = [
 
 
 
 
 
32
  {
33
  "value": "gpt-4.1",
34
- "label": "GPT-4.1 (Best Quality)",
35
- "description": "Highest quality, large context window",
36
  },
37
  {
38
  "value": "gpt-4.1-nano",
39
- "label": "GPT-4.1 Nano (Ultra Fast)",
40
- "description": "Ultra-fast and cost-effective",
41
  },
42
  ]
43
 
 
29
 
30
  # --- Constants --- (Moved from app.py)
31
  AVAILABLE_MODELS = [
32
+ {
33
+ "value": "gpt-5.1",
34
+ "label": "GPT-5.1 (Best Quality)",
35
+ "description": "Latest model with adaptive reasoning, 400K context",
36
+ },
37
  {
38
  "value": "gpt-4.1",
39
+ "label": "GPT-4.1 (Legacy)",
40
+ "description": "Previous generation, large context window",
41
  },
42
  {
43
  "value": "gpt-4.1-nano",
44
+ "label": "GPT-4.1 Nano (Legacy Fast)",
45
+ "description": "Previous generation, ultra-fast",
46
  },
47
  ]
48
 
ankigen_core/cli.py CHANGED
@@ -131,7 +131,7 @@ async def generate_cards_from_config(
131
  generation_mode="subject",
132
  source_text="",
133
  url_input="",
134
- model_name=config.get("model_choice", "gpt-4.1-nano"),
135
  topic_number=config.get("topic_number", 3),
136
  cards_per_topic=config.get("cards_per_topic", 5),
137
  preference_prompt=config.get("preference_prompt", ""),
@@ -207,7 +207,10 @@ def export_cards(
207
  )
208
  @click.option(
209
  "--model",
210
- type=click.Choice(["gpt-4.1", "gpt-4.1-nano"], case_sensitive=False),
 
 
 
211
  help="Model to use for generation (auto-selected if not specified)",
212
  )
213
  @click.option(
 
131
  generation_mode="subject",
132
  source_text="",
133
  url_input="",
134
+ model_name=config.get("model_choice", "gpt-5.1"),
135
  topic_number=config.get("topic_number", 3),
136
  cards_per_topic=config.get("cards_per_topic", 5),
137
  preference_prompt=config.get("preference_prompt", ""),
 
207
  )
208
  @click.option(
209
  "--model",
210
+ type=click.Choice(
211
+ ["gpt-5.1", "gpt-4.1", "gpt-4.1-nano"],
212
+ case_sensitive=False,
213
+ ),
214
  help="Model to use for generation (auto-selected if not specified)",
215
  )
216
  @click.option(
ankigen_core/exporters.py CHANGED
@@ -84,7 +84,253 @@ ANKI_CLOZE_MODEL_NAME = "AnkiGen Cloze"
84
  DEFAULT_BASIC_MODEL_ID = random.randrange(1 << 30, 1 << 31)
85
  DEFAULT_CLOZE_MODEL_ID = random.randrange(1 << 30, 1 << 31)
86
 
87
- # --- Full Model Definitions with CSS (Restored) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  BASIC_MODEL = genanki.Model(
90
  DEFAULT_BASIC_MODEL_ID, # Use the generated ID
@@ -167,188 +413,7 @@ BASIC_MODEL = genanki.Model(
167
  """,
168
  }
169
  ],
170
- css="""
171
- /* Base styles */
172
- .card {
173
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
174
- font-size: 16px;
175
- line-height: 1.6;
176
- color: #1a1a1a;
177
- max-width: 800px;
178
- margin: 0 auto;
179
- padding: 20px;
180
- background: #ffffff;
181
- }
182
-
183
- @media (max-width: 768px) {
184
- .card {
185
- font-size: 14px;
186
- padding: 15px;
187
- }
188
- }
189
-
190
- /* Question side */
191
- .question-side {
192
- position: relative;
193
- min-height: 200px;
194
- }
195
-
196
- .difficulty-indicator {
197
- position: absolute;
198
- top: 10px;
199
- right: 10px;
200
- width: 10px;
201
- height: 10px;
202
- border-radius: 50%;
203
- }
204
-
205
- .difficulty-indicator.beginner { background: #4ade80; }
206
- .difficulty-indicator.intermediate { background: #fbbf24; }
207
- .difficulty-indicator.advanced { background: #ef4444; }
208
-
209
- .question {
210
- font-size: 1.3em;
211
- font-weight: 600;
212
- color: #2563eb;
213
- margin-bottom: 1.5em;
214
- }
215
-
216
- .prerequisites {
217
- margin-top: 1em;
218
- font-size: 0.9em;
219
- color: #666;
220
- }
221
-
222
- .prerequisites-toggle {
223
- color: #2563eb;
224
- cursor: pointer;
225
- text-decoration: underline;
226
- }
227
-
228
- .prerequisites-content {
229
- display: none;
230
- margin-top: 0.5em;
231
- padding: 0.5em;
232
- background: #f8fafc;
233
- border-radius: 4px;
234
- }
235
-
236
- .prerequisites.show .prerequisites-content {
237
- display: block;
238
- }
239
-
240
- /* Answer side */
241
- .answer-section,
242
- .explanation-section,
243
- .example-section {
244
- margin: 1.5em 0;
245
- padding: 1.2em;
246
- border-radius: 8px;
247
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
248
- }
249
-
250
- .answer-section {
251
- background: #f0f9ff;
252
- border-left: 4px solid #2563eb;
253
- }
254
-
255
- .explanation-section {
256
- background: #f0fdf4;
257
- border-left: 4px solid #4ade80;
258
- }
259
-
260
- .example-section {
261
- background: #fefce8; /* Light yellow */
262
- border-left: 4px solid #facc15; /* Yellow */
263
- }
264
- .example-section pre {
265
- background-color: #2d2d2d; /* Darker background for code blocks */
266
- color: #f8f8f2; /* Light text for contrast */
267
- padding: 1em;
268
- border-radius: 0.3em;
269
- overflow-x: auto; /* Horizontal scroll for long lines */
270
- font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
271
- font-size: 0.9em;
272
- line-height: 1.4;
273
- }
274
-
275
- .example-section code {
276
- font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
277
- }
278
-
279
- .metadata-section {
280
- margin-top: 2em;
281
- padding-top: 1em;
282
- border-top: 1px solid #e5e7eb; /* Light gray border */
283
- font-size: 0.9em;
284
- color: #4b5563; /* Cool gray */
285
- }
286
-
287
- .metadata-section h3 {
288
- font-size: 1em;
289
- color: #1f2937; /* Darker gray for headings */
290
- margin-bottom: 0.5em;
291
- }
292
-
293
- .metadata-section > div {
294
- margin-bottom: 0.8em;
295
- }
296
-
297
- .source-url a {
298
- color: #2563eb;
299
- text-decoration: none;
300
- }
301
- .source-url a:hover {
302
- text-decoration: underline;
303
- }
304
-
305
- /* Styles for cloze deletion cards */
306
- .cloze {
307
- font-weight: bold;
308
- color: blue;
309
- }
310
- .nightMode .cloze {
311
- color: lightblue;
312
- }
313
-
314
- /* General utility */
315
- hr {
316
- border: none;
317
- border-top: 1px dashed #cbd5e1; /* Light dashed line */
318
- margin: 1.5em 0;
319
- }
320
-
321
- /* Rich text field styling (if Anki adds classes for these) */
322
- .field ul, .field ol {
323
- margin-left: 1.5em;
324
- padding-left: 0.5em;
325
- }
326
- .field li {
327
- margin-bottom: 0.3em;
328
- }
329
-
330
- /* Responsive design */
331
- @media (max-width: 640px) {
332
- .answer-section,
333
- .explanation-section,
334
- .example-section {
335
- padding: 1em;
336
- margin: 1em 0;
337
- }
338
- }
339
-
340
- /* Animations */
341
- @keyframes fadeIn {
342
- from { opacity: 0; }
343
- to { opacity: 1; }
344
- }
345
-
346
- .card {
347
- animation: fadeIn 0.3s ease-in-out;
348
- }
349
- """,
350
- # model_type=genanki.Model.BASIC, # This was still incorrect
351
- # No model_type needed, defaults to Basic (0)
352
  )
353
 
354
  CLOZE_MODEL = genanki.Model(
@@ -432,177 +497,8 @@ CLOZE_MODEL = genanki.Model(
432
  """,
433
  }
434
  ],
435
- css="""
436
- /* Base styles */
437
- .card {
438
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
439
- font-size: 16px;
440
- line-height: 1.6;
441
- color: #1a1a1a;
442
- max-width: 800px;
443
- margin: 0 auto;
444
- padding: 20px;
445
- background: #ffffff;
446
- }
447
-
448
- @media (max-width: 768px) {
449
- .card {
450
- font-size: 14px;
451
- padding: 15px;
452
- }
453
- }
454
-
455
- /* Question side */
456
- .question-side {
457
- position: relative;
458
- min-height: 200px;
459
- }
460
-
461
- .difficulty-indicator {
462
- position: absolute;
463
- top: 10px;
464
- right: 10px;
465
- width: 10px;
466
- height: 10px;
467
- border-radius: 50%;
468
- }
469
-
470
- .difficulty-indicator.beginner { background: #4ade80; }
471
- .difficulty-indicator.intermediate { background: #fbbf24; }
472
- .difficulty-indicator.advanced { background: #ef4444; }
473
-
474
- .question {
475
- font-size: 1.3em;
476
- font-weight: 600;
477
- color: #2563eb;
478
- margin-bottom: 1.5em;
479
- }
480
-
481
- .prerequisites {
482
- margin-top: 1em;
483
- font-size: 0.9em;
484
- color: #666;
485
- }
486
-
487
- .prerequisites-toggle {
488
- color: #2563eb;
489
- cursor: pointer;
490
- text-decoration: underline;
491
- }
492
-
493
- .prerequisites-content {
494
- display: none;
495
- margin-top: 0.5em;
496
- padding: 0.5em;
497
- background: #f8fafc;
498
- border-radius: 4px;
499
- }
500
-
501
- .prerequisites.show .prerequisites-content {
502
- display: block;
503
- }
504
-
505
- /* Answer side */
506
- .answer-section,
507
- .explanation-section,
508
- .example-section {
509
- margin: 1.5em 0;
510
- padding: 1.2em;
511
- border-radius: 8px;
512
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
513
- }
514
-
515
- .answer-section { /* Shared with question for cloze, but can be general */
516
- background: #f0f9ff;
517
- border-left: 4px solid #2563eb;
518
- }
519
-
520
- .back-extra-section {
521
- background: #eef2ff; /* A slightly different shade for additional info */
522
- border-left: 4px solid #818cf8; /* Indigo variant */
523
- margin: 1.5em 0;
524
- padding: 1.2em;
525
- border-radius: 8px;
526
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
527
- }
528
-
529
- .explanation-section {
530
- background: #f0fdf4;
531
- border-left: 4px solid #4ade80;
532
- }
533
-
534
- .example-section {
535
- background: #fefce8; /* Light yellow */
536
- border-left: 4px solid #facc15; /* Yellow */
537
- }
538
- .example-section pre {
539
- background-color: #2d2d2d; /* Darker background for code blocks */
540
- color: #f8f8f2; /* Light text for contrast */
541
- padding: 1em;
542
- border-radius: 0.3em;
543
- overflow-x: auto; /* Horizontal scroll for long lines */
544
- font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
545
- font-size: 0.9em;
546
- line-height: 1.4;
547
- }
548
-
549
- .example-section code {
550
- font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
551
- }
552
-
553
- .metadata-section {
554
- margin-top: 2em;
555
- padding-top: 1em;
556
- border-top: 1px solid #e5e7eb; /* Light gray border */
557
- font-size: 0.9em;
558
- color: #4b5563; /* Cool gray */
559
- }
560
-
561
- .metadata-section h3 {
562
- font-size: 1em;
563
- color: #1f2937; /* Darker gray for headings */
564
- margin-bottom: 0.5em;
565
- }
566
-
567
- .metadata-section > div {
568
- margin-bottom: 0.8em;
569
- }
570
-
571
- .source-url a {
572
- color: #2563eb;
573
- text-decoration: none;
574
- }
575
- .source-url a:hover {
576
- text-decoration: underline;
577
- }
578
-
579
- /* Styles for cloze deletion cards */
580
- .cloze {
581
- font-weight: bold;
582
- color: blue;
583
- }
584
- .nightMode .cloze {
585
- color: lightblue;
586
- }
587
-
588
- /* General utility */
589
- hr {
590
- border: none;
591
- border-top: 1px dashed #cbd5e1; /* Light dashed line */
592
- margin: 1.5em 0;
593
- }
594
-
595
- /* Rich text field styling (if Anki adds classes for these) */
596
- .field ul, .field ol {
597
- margin-left: 1.5em;
598
- padding-left: 0.5em;
599
- }
600
- .field li {
601
- margin-bottom: 0.3em;
602
- }
603
- """,
604
- # model_type=genanki.Model.CLOZE, # This was still incorrect
605
- model_type=1, # Corrected to use integer 1 for Cloze
606
  )
607
 
608
 
 
84
  DEFAULT_BASIC_MODEL_ID = random.randrange(1 << 30, 1 << 31)
85
  DEFAULT_CLOZE_MODEL_ID = random.randrange(1 << 30, 1 << 31)
86
 
87
+ # --- Shared CSS with dark mode support ---
88
+ CARD_CSS = """
89
+ /* CSS Variables - Light Mode (default) */
90
+ .card {
91
+ --bg-card: #ffffff;
92
+ --bg-answer: #f0f9ff;
93
+ --bg-explanation: #f0fdf4;
94
+ --bg-example: #fefce8;
95
+ --bg-back-extra: #eef2ff;
96
+ --bg-prereq: #f8fafc;
97
+ --bg-code: #2d2d2d;
98
+
99
+ --text-primary: #1a1a1a;
100
+ --text-secondary: #4b5563;
101
+ --text-muted: #666666;
102
+ --text-heading: #1f2937;
103
+ --text-code: #f8f8f2;
104
+
105
+ --accent-blue: #2563eb;
106
+ --accent-blue-light: #60a5fa;
107
+ --accent-green: #4ade80;
108
+ --accent-yellow: #facc15;
109
+ --accent-indigo: #818cf8;
110
+ --accent-red: #ef4444;
111
+
112
+ --border-light: #e5e7eb;
113
+ --border-dashed: #cbd5e1;
114
+
115
+ --shadow: rgba(0, 0, 0, 0.05);
116
+ }
117
+
118
+ /* Dark Mode Overrides */
119
+ .nightMode .card,
120
+ .night_mode .card {
121
+ --bg-card: #1e1e1e;
122
+ --bg-answer: #1e293b;
123
+ --bg-explanation: #14291a;
124
+ --bg-example: #292518;
125
+ --bg-back-extra: #1e1b2e;
126
+ --bg-prereq: #262626;
127
+ --bg-code: #0d0d0d;
128
+
129
+ --text-primary: #e4e4e7;
130
+ --text-secondary: #a1a1aa;
131
+ --text-muted: #9ca3af;
132
+ --text-heading: #f4f4f5;
133
+ --text-code: #f8f8f2;
134
+
135
+ --accent-blue: #60a5fa;
136
+ --accent-blue-light: #93c5fd;
137
+ --accent-green: #4ade80;
138
+ --accent-yellow: #fde047;
139
+ --accent-indigo: #a5b4fc;
140
+ --accent-red: #f87171;
141
+
142
+ --border-light: #3f3f46;
143
+ --border-dashed: #52525b;
144
+
145
+ --shadow: rgba(0, 0, 0, 0.3);
146
+ }
147
+
148
+ /* Base styles */
149
+ .card {
150
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
151
+ font-size: 16px;
152
+ line-height: 1.6;
153
+ color: var(--text-primary);
154
+ max-width: 800px;
155
+ margin: 0 auto;
156
+ padding: 20px;
157
+ background: var(--bg-card);
158
+ }
159
+
160
+ @media (max-width: 768px) {
161
+ .card {
162
+ font-size: 14px;
163
+ padding: 15px;
164
+ }
165
+ }
166
+
167
+ /* Question side */
168
+ .question-side {
169
+ position: relative;
170
+ min-height: 200px;
171
+ }
172
+
173
+ .difficulty-indicator {
174
+ position: absolute;
175
+ top: 10px;
176
+ right: 10px;
177
+ width: 10px;
178
+ height: 10px;
179
+ border-radius: 50%;
180
+ }
181
+
182
+ .difficulty-indicator.beginner { background: var(--accent-green); }
183
+ .difficulty-indicator.intermediate { background: var(--accent-yellow); }
184
+ .difficulty-indicator.advanced { background: var(--accent-red); }
185
+
186
+ .question {
187
+ font-size: 1.3em;
188
+ font-weight: 600;
189
+ color: var(--accent-blue);
190
+ margin-bottom: 1.5em;
191
+ }
192
+
193
+ .prerequisites {
194
+ margin-top: 1em;
195
+ font-size: 0.9em;
196
+ color: var(--text-muted);
197
+ }
198
+
199
+ .prerequisites-toggle {
200
+ color: var(--accent-blue);
201
+ cursor: pointer;
202
+ text-decoration: underline;
203
+ }
204
+
205
+ .prerequisites-content {
206
+ display: none;
207
+ margin-top: 0.5em;
208
+ padding: 0.5em;
209
+ background: var(--bg-prereq);
210
+ border-radius: 4px;
211
+ }
212
+
213
+ .prerequisites.show .prerequisites-content {
214
+ display: block;
215
+ }
216
+
217
+ /* Answer side sections */
218
+ .answer-section,
219
+ .explanation-section,
220
+ .example-section,
221
+ .back-extra-section {
222
+ margin: 1.5em 0;
223
+ padding: 1.2em;
224
+ border-radius: 8px;
225
+ box-shadow: 0 2px 4px var(--shadow);
226
+ }
227
+
228
+ .answer-section {
229
+ background: var(--bg-answer);
230
+ border-left: 4px solid var(--accent-blue);
231
+ }
232
+
233
+ .back-extra-section {
234
+ background: var(--bg-back-extra);
235
+ border-left: 4px solid var(--accent-indigo);
236
+ }
237
+
238
+ .explanation-section {
239
+ background: var(--bg-explanation);
240
+ border-left: 4px solid var(--accent-green);
241
+ }
242
+
243
+ .example-section {
244
+ background: var(--bg-example);
245
+ border-left: 4px solid var(--accent-yellow);
246
+ }
247
+
248
+ .example-section pre {
249
+ background-color: var(--bg-code);
250
+ color: var(--text-code);
251
+ padding: 1em;
252
+ border-radius: 0.3em;
253
+ overflow-x: auto;
254
+ font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
255
+ font-size: 0.9em;
256
+ line-height: 1.4;
257
+ }
258
+
259
+ .example-section code {
260
+ font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
261
+ }
262
+
263
+ .metadata-section {
264
+ margin-top: 2em;
265
+ padding-top: 1em;
266
+ border-top: 1px solid var(--border-light);
267
+ font-size: 0.9em;
268
+ color: var(--text-secondary);
269
+ }
270
+
271
+ .metadata-section h3 {
272
+ font-size: 1em;
273
+ color: var(--text-heading);
274
+ margin-bottom: 0.5em;
275
+ }
276
+
277
+ .metadata-section > div {
278
+ margin-bottom: 0.8em;
279
+ }
280
+
281
+ .source-url a {
282
+ color: var(--accent-blue);
283
+ text-decoration: none;
284
+ }
285
+ .source-url a:hover {
286
+ text-decoration: underline;
287
+ }
288
+
289
+ /* Cloze deletion styles */
290
+ .cloze {
291
+ font-weight: bold;
292
+ color: var(--accent-blue);
293
+ }
294
+
295
+ /* General utility */
296
+ hr {
297
+ border: none;
298
+ border-top: 1px dashed var(--border-dashed);
299
+ margin: 1.5em 0;
300
+ }
301
+
302
+ /* Rich text field styling */
303
+ .field ul, .field ol {
304
+ margin-left: 1.5em;
305
+ padding-left: 0.5em;
306
+ }
307
+ .field li {
308
+ margin-bottom: 0.3em;
309
+ }
310
+
311
+ /* Responsive design */
312
+ @media (max-width: 640px) {
313
+ .answer-section,
314
+ .explanation-section,
315
+ .example-section,
316
+ .back-extra-section {
317
+ padding: 1em;
318
+ margin: 1em 0;
319
+ }
320
+ }
321
+
322
+ /* Animations */
323
+ @keyframes fadeIn {
324
+ from { opacity: 0; }
325
+ to { opacity: 1; }
326
+ }
327
+
328
+ .card {
329
+ animation: fadeIn 0.3s ease-in-out;
330
+ }
331
+ """
332
+
333
+ # --- Full Model Definitions ---
334
 
335
  BASIC_MODEL = genanki.Model(
336
  DEFAULT_BASIC_MODEL_ID, # Use the generated ID
 
413
  """,
414
  }
415
  ],
416
+ css=CARD_CSS,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  )
418
 
419
  CLOZE_MODEL = genanki.Model(
 
497
  """,
498
  }
499
  ],
500
+ css=CARD_CSS,
501
+ model_type=1, # Cloze model type
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  )
503
 
504
 
ankigen_core/llm_interface.py CHANGED
@@ -1,38 +1,31 @@
1
  # Module for OpenAI client management and API call logic
2
 
 
 
 
 
 
 
3
  from openai import (
 
 
4
  AsyncOpenAI,
5
  OpenAIError,
6
- APIConnectionError, # For more specific retry
7
- RateLimitError, # For more specific retry
8
- APIStatusError, # For retry on 5xx errors
9
- ) # Added OpenAIError for specific exception handling
10
- import json
11
- import time # Added for process_crawled_pages later, but good to have
12
- from typing import List, Optional, Callable # Added List, Optional, Callable
13
  from tenacity import (
14
  retry,
 
15
  stop_after_attempt,
16
  wait_exponential,
17
- retry_if_exception_type,
18
  )
19
- import asyncio # Import asyncio for gather
20
- import tiktoken # Added tiktoken
21
 
22
- # Imports from our new core modules
23
- from ankigen_core.logging import logger # Updated to use the new logger
24
- from ankigen_core.utils import ResponseCache # Removed get_logger
25
- from ankigen_core.models import (
26
- CrawledPage,
27
- Card,
28
- CardFront,
29
- CardBack,
30
- ) # Added CrawledPage, Card, CardFront, CardBack
31
- # We will need Pydantic models if response_format is a Pydantic model,
32
- # but for now, it's a dict like {"type": "json_object"}.
33
- # from ankigen_core.models import ... # Placeholder if needed later
34
 
35
- # logger = get_logger() # Removed, using imported logger
36
 
37
 
38
  class OpenAIClientManager:
@@ -121,106 +114,173 @@ class OpenAIClientManager:
121
  self._client = None
122
 
123
 
124
- # Retry decorator for API calls - kept similar to original
125
- @retry(
126
- stop=stop_after_attempt(3),
127
- wait=wait_exponential(multiplier=1, min=4, max=10),
128
- retry=retry_if_exception_type(
129
- Exception
130
- ), # Consider refining this to specific network/API errors
131
- before_sleep=lambda retry_state: logger.warning(
132
- f"Retrying structured_output_completion (attempt {retry_state.attempt_number}) due to {retry_state.outcome.exception() if retry_state.outcome else 'unknown reason'}"
133
- ),
134
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  async def structured_output_completion(
136
- openai_client: AsyncOpenAI, # Expecting an initialized AsyncOpenAI client instance
137
  model: str,
138
- response_format: dict, # e.g., {"type": "json_object"}
139
  system_prompt: str,
140
  user_prompt: str,
141
- cache: ResponseCache, # Expecting a ResponseCache instance
142
- ):
143
- """Makes an API call to OpenAI with structured output, retry logic, and caching."""
144
-
145
- # Use the passed-in cache instance
146
- cached_response = cache.get(f"{system_prompt}:{user_prompt}", model)
147
- if cached_response is not None:
148
- logger.info(f"Using cached response for model {model}")
149
- return cached_response # Return cached value directly, not as a coroutine
150
 
151
- try:
152
- logger.debug(f"Making API call to OpenAI model {model}")
 
 
153
 
154
- # Ensure system_prompt includes JSON instruction if response_format is json_object
155
- # This was previously done before calling this function, but good to ensure here too.
156
- effective_system_prompt = system_prompt
157
- if (
158
- response_format.get("type") == "json_object"
159
- and "JSON object matching the specified schema" not in system_prompt
160
- ):
161
- effective_system_prompt = f"{system_prompt}\nProvide your response as a JSON object matching the specified schema."
162
 
163
- # Security: Add timeout to prevent indefinite hanging
164
- completion = await openai_client.chat.completions.create(
 
165
  model=model,
166
- messages=[
167
- {"role": "system", "content": effective_system_prompt.strip()},
168
- {"role": "user", "content": user_prompt.strip()},
169
- ],
170
- response_format=response_format, # Pass the dict directly
171
- temperature=0.7, # Consider making this configurable
172
- timeout=120.0, # 120 second timeout
173
  )
174
 
175
- if not hasattr(completion, "choices") or not completion.choices:
176
- logger.warning(
177
- f"No choices returned in OpenAI completion for model {model}."
178
- )
179
- return None # Or raise an error
180
-
181
- first_choice = completion.choices[0]
182
- if (
183
- not hasattr(first_choice, "message")
184
- or first_choice.message is None
185
- or first_choice.message.content is None
186
- ):
187
- logger.warning(
188
- f"No message content in the first choice for OpenAI model {model}."
189
- )
190
- return None # Or raise an error
191
-
192
- # Parse the JSON response
193
- result = json.loads(first_choice.message.content)
194
-
195
- # Cache the successful response using the passed-in cache instance
196
- cache.set(f"{system_prompt}:{user_prompt}", model, result)
197
- logger.debug(f"Successfully received and parsed response from model {model}")
198
  return result
199
 
200
- except OpenAIError as e: # More specific error handling
201
- logger.error(f"OpenAI API call failed for model {model}: {e}", exc_info=True)
202
- raise # Re-raise to be handled by the calling function, potentially as gr.Error
203
- except json.JSONDecodeError as e:
204
- # Accessing first_choice might be an issue if completion itself failed before choices
205
- # However, structure assumes choices are checked before this json.loads typically
206
- # For safety, check if first_choice.message.content is available
207
- response_content_for_log = "<unavailable>"
208
- if (
209
- "first_choice" in locals()
210
- and first_choice.message
211
- and first_choice.message.content
212
- ):
213
- response_content_for_log = first_choice.message.content[:500]
214
- logger.error(
215
- f"Failed to parse JSON response from model {model}: {e}. Response: {response_content_for_log}",
216
- exc_info=True,
217
- )
218
- raise ValueError(
219
- f"Invalid JSON response from AI model {model}."
220
- ) # Raise specific error
221
  except Exception as e:
222
  logger.error(
223
- f"Unexpected error during structured_output_completion for model {model}: {e}",
224
  exc_info=True,
225
  )
226
  raise # Re-raise unexpected errors
@@ -431,29 +491,24 @@ Generate a few high-quality Anki cards from this content.
431
  logger.debug(
432
  f"Attempting to generate cards for {page.url} using model {model}."
433
  )
434
- response_format_param = {"type": "json_object"}
435
- # Security: Add timeout to prevent indefinite hanging
436
- response_data = await openai_client.chat.completions.create(
 
437
  model=model,
438
- messages=[
439
- {"role": "system", "content": system_prompt},
440
- {"role": "user", "content": user_prompt},
441
- ],
442
- response_format=response_format_param,
443
  temperature=0.5,
444
- timeout=120.0, # 120 second timeout
445
  )
446
 
447
- if (
448
- not response_data.choices
449
- or not response_data.choices[0].message
450
- or not response_data.choices[0].message.content
451
- ):
452
- logger.error(f"Invalid or empty response from OpenAI for page {page.url}.")
453
  return []
454
 
455
- cards_json_str = response_data.choices[0].message.content
456
- parsed_cards = json.loads(cards_json_str)
457
 
458
  validated_cards: List[Card] = []
459
 
@@ -471,7 +526,7 @@ Generate a few high-quality Anki cards from this content.
471
  cards_list_from_json = parsed_cards
472
  else:
473
  logger.error(
474
- f"LLM response for {page.url} was not a list or valid dict. Response: {cards_json_str[:200]}..."
475
  )
476
  return []
477
 
@@ -546,34 +601,9 @@ Generate a few high-quality Anki cards from this content.
546
 
547
  return validated_cards
548
 
549
- except json.JSONDecodeError as e:
550
- # cards_json_str might not be defined if json.loads fails early, or if response_data was bad
551
- raw_response_content = "<response_content_unavailable>"
552
- if "cards_json_str" in locals() and cards_json_str:
553
- raw_response_content = cards_json_str[:500]
554
- elif (
555
- "response_data" in locals()
556
- and response_data
557
- and response_data.choices
558
- and len(response_data.choices) > 0
559
- and response_data.choices[0].message
560
- and response_data.choices[0].message.content
561
- ):
562
- raw_response_content = response_data.choices[0].message.content[:500]
563
-
564
- logger.error(
565
- f"Failed to decode JSON response from OpenAI for page {page.url}: {e}. Response: {raw_response_content}...",
566
- exc_info=True,
567
- )
568
- return []
569
- except OpenAIError as e:
570
- logger.error(
571
- f"OpenAI API error while processing page {page.url}: {e}", exc_info=True
572
- )
573
- return []
574
  except Exception as e:
575
  logger.error(
576
- f"Unexpected error processing page {page.url} with LLM: {e}", exc_info=True
577
  )
578
  return []
579
 
 
1
  # Module for OpenAI client management and API call logic
2
 
3
+ import asyncio
4
+ import time
5
+ from typing import Callable, List, Optional, TypeVar
6
+
7
+ import tiktoken
8
+ from agents import Agent, ModelSettings, Runner, set_default_openai_client
9
  from openai import (
10
+ APIConnectionError,
11
+ APIStatusError,
12
  AsyncOpenAI,
13
  OpenAIError,
14
+ RateLimitError,
15
+ )
16
+ from pydantic import BaseModel
 
 
 
 
17
  from tenacity import (
18
  retry,
19
+ retry_if_exception_type,
20
  stop_after_attempt,
21
  wait_exponential,
 
22
  )
 
 
23
 
24
+ from ankigen_core.logging import logger
25
+ from ankigen_core.models import Card, CardBack, CardFront, CrawledPage
26
+ from ankigen_core.utils import ResponseCache
 
 
 
 
 
 
 
 
 
27
 
28
+ T = TypeVar("T", bound=BaseModel)
29
 
30
 
31
  class OpenAIClientManager:
 
114
  self._client = None
115
 
116
 
117
+ # --- Agents SDK Utility ---
118
+
119
+
120
+ async def structured_agent_call(
121
+ openai_client: AsyncOpenAI,
122
+ model: str,
123
+ instructions: str,
124
+ user_input: str,
125
+ output_type: type[T],
126
+ cache: Optional[ResponseCache] = None,
127
+ cache_key: Optional[str] = None,
128
+ temperature: float = 0.7,
129
+ timeout: float = 120.0,
130
+ retry_attempts: int = 3,
131
+ ) -> T:
132
+ """
133
+ Make a single-turn structured output call using the agents SDK.
134
+
135
+ This is a lightweight wrapper for simple structured output calls,
136
+ not intended for complex multi-agent workflows.
137
+
138
+ Args:
139
+ openai_client: AsyncOpenAI client instance
140
+ model: Model name (e.g., "gpt-5.1", "gpt-5.1-chat-latest")
141
+ instructions: System instructions for the agent
142
+ user_input: User prompt/input
143
+ output_type: Pydantic model class for structured output
144
+ cache: Optional ResponseCache instance
145
+ cache_key: Cache key (required if cache is provided)
146
+ temperature: Model temperature (default 0.7)
147
+ timeout: Request timeout in seconds (default 120)
148
+ retry_attempts: Number of retry attempts (default 3)
149
+
150
+ Returns:
151
+ Instance of output_type with the structured response
152
+ """
153
+ # 1. Check cache first
154
+ if cache and cache_key:
155
+ cached = cache.get(cache_key, model)
156
+ if cached is not None:
157
+ logger.info(f"Using cached response for model {model}")
158
+ # Reconstruct Pydantic model from cached dict
159
+ if isinstance(cached, dict):
160
+ return output_type.model_validate(cached)
161
+ return cached
162
+
163
+ # 2. Set up the OpenAI client for agents SDK
164
+ set_default_openai_client(openai_client, use_for_tracing=False)
165
+
166
+ # 3. Build model settings with GPT-5.1 reasoning support
167
+ model_settings_kwargs: dict = {"temperature": temperature}
168
+
169
+ # GPT-5.1 (not chat-latest) supports reasoning_effort
170
+ if model.startswith("gpt-5") and "chat-latest" not in model:
171
+ from openai.types.shared import Reasoning
172
+
173
+ model_settings_kwargs["reasoning"] = Reasoning(effort="none")
174
+
175
+ model_settings = ModelSettings(**model_settings_kwargs)
176
+
177
+ # 4. Create agent with structured output
178
+ agent = Agent(
179
+ name="structured_output_agent",
180
+ instructions=instructions,
181
+ model=model,
182
+ model_settings=model_settings,
183
+ output_type=output_type,
184
+ )
185
+
186
+ # 5. Execute with retry and timeout
187
+ last_error: Optional[Exception] = None
188
+ for attempt in range(retry_attempts):
189
+ try:
190
+ result = await asyncio.wait_for(
191
+ Runner.run(agent, user_input),
192
+ timeout=timeout,
193
+ )
194
+
195
+ # 6. Extract structured output
196
+ output = result.final_output
197
+
198
+ # 7. Cache successful result (as dict for serialization)
199
+ if cache and cache_key and output is not None:
200
+ if isinstance(output, BaseModel):
201
+ cache.set(cache_key, model, output.model_dump())
202
+ else:
203
+ cache.set(cache_key, model, output)
204
+
205
+ logger.debug(f"Successfully received response from model {model}")
206
+ return output
207
+
208
+ except asyncio.TimeoutError as e:
209
+ last_error = e
210
+ if attempt < retry_attempts - 1:
211
+ wait_time = 4 * (2**attempt) # Exponential backoff
212
+ logger.warning(
213
+ f"Agent timed out (attempt {attempt + 1}/{retry_attempts}), "
214
+ f"retrying in {wait_time}s..."
215
+ )
216
+ await asyncio.sleep(wait_time)
217
+ continue
218
+ logger.error(f"Agent timed out after {retry_attempts} attempts")
219
+ raise
220
+ except Exception as e:
221
+ last_error = e
222
+ if attempt < retry_attempts - 1:
223
+ wait_time = 4 * (2**attempt)
224
+ logger.warning(
225
+ f"Agent failed (attempt {attempt + 1}/{retry_attempts}): {e}, "
226
+ f"retrying in {wait_time}s..."
227
+ )
228
+ await asyncio.sleep(wait_time)
229
+ continue
230
+ logger.error(f"Agent failed after {retry_attempts} attempts: {e}")
231
+ raise
232
+
233
+ raise RuntimeError(f"Retry loop exited without result: {last_error}")
234
+
235
+
236
+ # Generic schema for arbitrary JSON structured outputs
237
+ class GenericJsonOutput(BaseModel):
238
+ """Generic container for JSON output - allows any structure."""
239
+
240
+ model_config = {"extra": "allow"} # Allow arbitrary fields
241
+
242
+
243
  async def structured_output_completion(
244
+ openai_client: AsyncOpenAI,
245
  model: str,
246
+ response_format: dict, # Legacy parameter - kept for API compatibility
247
  system_prompt: str,
248
  user_prompt: str,
249
+ cache: ResponseCache,
250
+ ) -> Optional[dict]:
251
+ """
252
+ Makes an API call with structured output using agents SDK.
 
 
 
 
 
253
 
254
+ Note: response_format parameter is ignored - the agents SDK handles
255
+ JSON parsing automatically. For typed outputs, use structured_agent_call() directly.
256
+ """
257
+ cache_key = f"{system_prompt}:{user_prompt}"
258
 
259
+ # Ensure system_prompt includes JSON instruction
260
+ effective_system_prompt = system_prompt
261
+ if "JSON object matching the specified schema" not in system_prompt:
262
+ effective_system_prompt = f"{system_prompt}\nProvide your response as a JSON object matching the specified schema."
 
 
 
 
263
 
264
+ try:
265
+ result = await structured_agent_call(
266
+ openai_client=openai_client,
267
  model=model,
268
+ instructions=effective_system_prompt.strip(),
269
+ user_input=user_prompt.strip(),
270
+ output_type=GenericJsonOutput,
271
+ cache=cache,
272
+ cache_key=cache_key,
273
+ temperature=0.7,
 
274
  )
275
 
276
+ # Convert Pydantic model back to dict for backward compatibility
277
+ if isinstance(result, BaseModel):
278
+ return result.model_dump()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  return result
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  except Exception as e:
282
  logger.error(
283
+ f"structured_output_completion failed for model {model}: {e}",
284
  exc_info=True,
285
  )
286
  raise # Re-raise unexpected errors
 
491
  logger.debug(
492
  f"Attempting to generate cards for {page.url} using model {model}."
493
  )
494
+
495
+ # Use agents SDK for structured output
496
+ result = await structured_agent_call(
497
+ openai_client=openai_client,
498
  model=model,
499
+ instructions=system_prompt,
500
+ user_input=user_prompt,
501
+ output_type=GenericJsonOutput, # Flexible schema for card generation
 
 
502
  temperature=0.5,
503
+ timeout=120.0,
504
  )
505
 
506
+ if result is None:
507
+ logger.error(f"Invalid or empty response from agent for page {page.url}.")
 
 
 
 
508
  return []
509
 
510
+ # Convert Pydantic model to dict for processing
511
+ parsed_cards = result.model_dump() if isinstance(result, BaseModel) else result
512
 
513
  validated_cards: List[Card] = []
514
 
 
526
  cards_list_from_json = parsed_cards
527
  else:
528
  logger.error(
529
+ f"LLM response for {page.url} was not a list or valid dict. Response: {str(parsed_cards)[:200]}..."
530
  )
531
  return []
532
 
 
601
 
602
  return validated_cards
603
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  except Exception as e:
605
  logger.error(
606
+ f"Error processing page {page.url} with agents SDK: {e}", exc_info=True
607
  )
608
  return []
609
 
pyproject.toml CHANGED
@@ -12,12 +12,12 @@ authors = [
12
  readme = "README.md"
13
  requires-python = ">=3.12"
14
  dependencies = [
15
- "openai>=1.109.1",
16
- "openai-agents>=0.3.2",
17
  "gradio>=5.49.1",
18
  "tenacity>=9.1.2",
19
  "genanki>=0.13.1",
20
- "pydantic==2.11.9",
21
  "pandas>=2.3.2",
22
  "beautifulsoup4==4.13.5",
23
  "lxml>=6.0.2",
 
12
  readme = "README.md"
13
  requires-python = ">=3.12"
14
  dependencies = [
15
+ "openai>=2.8.0",
16
+ "openai-agents>=0.6.1",
17
  "gradio>=5.49.1",
18
  "tenacity>=9.1.2",
19
  "genanki>=0.13.1",
20
+ "pydantic>=2.12.3",
21
  "pandas>=2.3.2",
22
  "beautifulsoup4==4.13.5",
23
  "lxml>=6.0.2",
requirements.txt CHANGED
@@ -89,9 +89,9 @@ fsspec==2025.5.1
89
  # huggingface-hub
90
  genanki==0.13.1
91
  # via ankigen (pyproject.toml)
92
- gradio==5.49.1
93
  # via ankigen (pyproject.toml)
94
- gradio-client==1.13.3
95
  # via gradio
96
  griffe==1.7.3
97
  # via openai-agents
@@ -183,11 +183,11 @@ numpy==2.3.1
183
  # via
184
  # gradio
185
  # pandas
186
- openai==1.109.1
187
  # via
188
  # ankigen (pyproject.toml)
189
  # openai-agents
190
- openai-agents==0.3.2
191
  # via ankigen (pyproject.toml)
192
  openapi-core==0.19.5
193
  # via fastmcp
@@ -226,7 +226,7 @@ py-key-value-shared==0.2.8
226
  # via py-key-value-aio
227
  pycparser==2.23
228
  # via cffi
229
- pydantic==2.11.9
230
  # via
231
  # ankigen (pyproject.toml)
232
  # fastapi
@@ -237,7 +237,7 @@ pydantic==2.11.9
237
  # openai-agents
238
  # openapi-pydantic
239
  # pydantic-settings
240
- pydantic-core==2.33.2
241
  # via pydantic
242
  pydantic-settings==2.10.1
243
  # via mcp
@@ -294,9 +294,7 @@ rpds-py==0.26.0
294
  # via
295
  # jsonschema
296
  # referencing
297
- ruff==0.13.1
298
- # via gradio
299
- safehttpx==0.1.6
300
  # via gradio
301
  secretstorage==3.4.0
302
  # via keyring
@@ -355,7 +353,7 @@ typing-extensions==4.15.0
355
  # starlette
356
  # typer
357
  # typing-inspection
358
- typing-inspection==0.4.1
359
  # via
360
  # pydantic
361
  # pydantic-settings
@@ -370,8 +368,6 @@ uvicorn==0.34.3
370
  # gradio
371
  # mcp
372
  websockets==15.0.1
373
- # via
374
- # fastmcp
375
- # gradio-client
376
  werkzeug==3.1.1
377
  # via openapi-core
 
89
  # huggingface-hub
90
  genanki==0.13.1
91
  # via ankigen (pyproject.toml)
92
+ gradio==6.0.2
93
  # via ankigen (pyproject.toml)
94
+ gradio-client==2.0.1
95
  # via gradio
96
  griffe==1.7.3
97
  # via openai-agents
 
183
  # via
184
  # gradio
185
  # pandas
186
+ openai==2.8.1
187
  # via
188
  # ankigen (pyproject.toml)
189
  # openai-agents
190
+ openai-agents==0.6.1
191
  # via ankigen (pyproject.toml)
192
  openapi-core==0.19.5
193
  # via fastmcp
 
226
  # via py-key-value-aio
227
  pycparser==2.23
228
  # via cffi
229
+ pydantic==2.12.4
230
  # via
231
  # ankigen (pyproject.toml)
232
  # fastapi
 
237
  # openai-agents
238
  # openapi-pydantic
239
  # pydantic-settings
240
+ pydantic-core==2.41.5
241
  # via pydantic
242
  pydantic-settings==2.10.1
243
  # via mcp
 
294
  # via
295
  # jsonschema
296
  # referencing
297
+ safehttpx==0.1.7
 
 
298
  # via gradio
299
  secretstorage==3.4.0
300
  # via keyring
 
353
  # starlette
354
  # typer
355
  # typing-inspection
356
+ typing-inspection==0.4.2
357
  # via
358
  # pydantic
359
  # pydantic-settings
 
368
  # gradio
369
  # mcp
370
  websockets==15.0.1
371
+ # via fastmcp
 
 
372
  werkzeug==3.1.1
373
  # via openapi-core
uv.lock CHANGED
@@ -61,11 +61,11 @@ requires-dist = [
61
  { name = "genanki", specifier = ">=0.13.1" },
62
  { name = "gradio", specifier = ">=5.49.1" },
63
  { name = "lxml", specifier = ">=6.0.2" },
64
- { name = "openai", specifier = ">=1.109.1" },
65
- { name = "openai-agents", specifier = ">=0.3.2" },
66
  { name = "pandas", specifier = ">=2.3.2" },
67
  { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
68
- { name = "pydantic", specifier = "==2.11.9" },
69
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },
70
  { name = "pytest-anyio", marker = "extra == 'dev'", specifier = ">=0.0.0" },
71
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
@@ -734,7 +734,7 @@ wheels = [
734
 
735
  [[package]]
736
  name = "gradio"
737
- version = "5.49.1"
738
  source = { registry = "https://pypi.org/simple" }
739
  dependencies = [
740
  { name = "aiofiles" },
@@ -758,7 +758,6 @@ dependencies = [
758
  { name = "pydub" },
759
  { name = "python-multipart" },
760
  { name = "pyyaml" },
761
- { name = "ruff" },
762
  { name = "safehttpx" },
763
  { name = "semantic-version" },
764
  { name = "starlette" },
@@ -767,14 +766,14 @@ dependencies = [
767
  { name = "typing-extensions" },
768
  { name = "uvicorn" },
769
  ]
770
- sdist = { url = "https://files.pythonhosted.org/packages/83/67/17b3969a686f204dfb8f06bd34d1423bcba1df8a2f3674f115ca427188b7/gradio-5.49.1.tar.gz", hash = "sha256:c06faa324ae06c3892c8b4b4e73c706c4520d380f6b9e52a3c02dc53a7627ba9", size = 73784504, upload-time = "2025-10-08T20:18:40.4Z" }
771
  wheels = [
772
- { url = "https://files.pythonhosted.org/packages/8d/95/1c25fbcabfa201ab79b016c8716a4ac0f846121d4bbfd2136ffb6d87f31e/gradio-5.49.1-py3-none-any.whl", hash = "sha256:1b19369387801a26a6ba7fd2f74d46c5b0e2ac9ddef14f24ddc0d11fb19421b7", size = 63523840, upload-time = "2025-10-08T20:18:34.585Z" },
773
  ]
774
 
775
  [[package]]
776
  name = "gradio-client"
777
- version = "1.13.3"
778
  source = { registry = "https://pypi.org/simple" }
779
  dependencies = [
780
  { name = "fsspec" },
@@ -782,11 +781,10 @@ dependencies = [
782
  { name = "huggingface-hub" },
783
  { name = "packaging" },
784
  { name = "typing-extensions" },
785
- { name = "websockets" },
786
  ]
787
- sdist = { url = "https://files.pythonhosted.org/packages/3e/a9/a3beb0ece8c05c33e6376b790fa42e0dd157abca8220cf639b249a597467/gradio_client-1.13.3.tar.gz", hash = "sha256:869b3e67e0f7a0f40df8c48c94de99183265cf4b7b1d9bd4623e336d219ffbe7", size = 323253, upload-time = "2025-09-26T19:51:21.7Z" }
788
  wheels = [
789
- { url = "https://files.pythonhosted.org/packages/6e/0b/337b74504681b5dde39f20d803bb09757f9973ecdc65fd4e819d4b11faf7/gradio_client-1.13.3-py3-none-any.whl", hash = "sha256:3f63e4d33a2899c1a12b10fe3cf77b82a6919ff1a1fb6391f6aa225811aa390c", size = 325350, upload-time = "2025-09-26T19:51:20.288Z" },
790
  ]
791
 
792
  [[package]]
@@ -1351,7 +1349,7 @@ wheels = [
1351
 
1352
  [[package]]
1353
  name = "openai"
1354
- version = "1.109.1"
1355
  source = { registry = "https://pypi.org/simple" }
1356
  dependencies = [
1357
  { name = "anyio" },
@@ -1363,14 +1361,14 @@ dependencies = [
1363
  { name = "tqdm" },
1364
  { name = "typing-extensions" },
1365
  ]
1366
- sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" }
1367
  wheels = [
1368
- { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
1369
  ]
1370
 
1371
  [[package]]
1372
  name = "openai-agents"
1373
- version = "0.3.2"
1374
  source = { registry = "https://pypi.org/simple" }
1375
  dependencies = [
1376
  { name = "griffe" },
@@ -1381,9 +1379,9 @@ dependencies = [
1381
  { name = "types-requests" },
1382
  { name = "typing-extensions" },
1383
  ]
1384
- sdist = { url = "https://files.pythonhosted.org/packages/8c/9f/dafa9f80653778179822e1abf77c7f0d9da5a16806c96b5bb9e0e46bd747/openai_agents-0.3.2.tar.gz", hash = "sha256:b71ac04ee9f502f1bc0f4d142407df4ec69db4442db86c4da252b4558fa90cd5", size = 1727988, upload-time = "2025-09-23T20:37:20.7Z" }
1385
  wheels = [
1386
- { url = "https://files.pythonhosted.org/packages/27/7e/6a8437f9f40937bb473ceb120a65e1b37bc87bcee6da67be4c05b25c6a89/openai_agents-0.3.2-py3-none-any.whl", hash = "sha256:55e02c57f2aaf3170ff0aa0ab7c337c28fd06b43b3bb9edc28b77ffd8142b425", size = 194221, upload-time = "2025-09-23T20:37:19.121Z" },
1387
  ]
1388
 
1389
  [[package]]
@@ -1688,7 +1686,7 @@ wheels = [
1688
 
1689
  [[package]]
1690
  name = "pydantic"
1691
- version = "2.11.9"
1692
  source = { registry = "https://pypi.org/simple" }
1693
  dependencies = [
1694
  { name = "annotated-types" },
@@ -1696,9 +1694,9 @@ dependencies = [
1696
  { name = "typing-extensions" },
1697
  { name = "typing-inspection" },
1698
  ]
1699
- sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
1700
  wheels = [
1701
- { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
1702
  ]
1703
 
1704
  [package.optional-dependencies]
@@ -1708,44 +1706,73 @@ email = [
1708
 
1709
  [[package]]
1710
  name = "pydantic-core"
1711
- version = "2.33.2"
1712
  source = { registry = "https://pypi.org/simple" }
1713
  dependencies = [
1714
  { name = "typing-extensions" },
1715
  ]
1716
- sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
1717
- wheels = [
1718
- { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
1719
- { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
1720
- { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
1721
- { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
1722
- { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
1723
- { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
1724
- { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
1725
- { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
1726
- { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
1727
- { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
1728
- { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
1729
- { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
1730
- { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
1731
- { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
1732
- { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
1733
- { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
1734
- { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
1735
- { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
1736
- { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
1737
- { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
1738
- { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
1739
- { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
1740
- { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
1741
- { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
1742
- { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
1743
- { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
1744
- { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
1745
- { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
1746
- { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
1747
- { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
1748
- { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  ]
1750
 
1751
  [[package]]
@@ -2166,14 +2193,14 @@ wheels = [
2166
 
2167
  [[package]]
2168
  name = "safehttpx"
2169
- version = "0.1.6"
2170
  source = { registry = "https://pypi.org/simple" }
2171
  dependencies = [
2172
  { name = "httpx" },
2173
  ]
2174
- sdist = { url = "https://files.pythonhosted.org/packages/67/4c/19db75e6405692b2a96af8f06d1258f8aa7290bdc35ac966f03e207f6d7f/safehttpx-0.1.6.tar.gz", hash = "sha256:b356bfc82cee3a24c395b94a2dbeabbed60aff1aa5fa3b5fe97c4f2456ebce42", size = 9987, upload-time = "2024-12-02T18:44:10.226Z" }
2175
  wheels = [
2176
- { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692, upload-time = "2024-12-02T18:44:08.555Z" },
2177
  ]
2178
 
2179
  [[package]]
@@ -2351,14 +2378,14 @@ wheels = [
2351
 
2352
  [[package]]
2353
  name = "typing-inspection"
2354
- version = "0.4.1"
2355
  source = { registry = "https://pypi.org/simple" }
2356
  dependencies = [
2357
  { name = "typing-extensions" },
2358
  ]
2359
- sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
2360
  wheels = [
2361
- { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
2362
  ]
2363
 
2364
  [[package]]
 
61
  { name = "genanki", specifier = ">=0.13.1" },
62
  { name = "gradio", specifier = ">=5.49.1" },
63
  { name = "lxml", specifier = ">=6.0.2" },
64
+ { name = "openai", specifier = ">=2.8.0" },
65
+ { name = "openai-agents", specifier = ">=0.6.1" },
66
  { name = "pandas", specifier = ">=2.3.2" },
67
  { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
68
+ { name = "pydantic", specifier = ">=2.12.3" },
69
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },
70
  { name = "pytest-anyio", marker = "extra == 'dev'", specifier = ">=0.0.0" },
71
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
 
734
 
735
  [[package]]
736
  name = "gradio"
737
+ version = "6.0.2"
738
  source = { registry = "https://pypi.org/simple" }
739
  dependencies = [
740
  { name = "aiofiles" },
 
758
  { name = "pydub" },
759
  { name = "python-multipart" },
760
  { name = "pyyaml" },
 
761
  { name = "safehttpx" },
762
  { name = "semantic-version" },
763
  { name = "starlette" },
 
766
  { name = "typing-extensions" },
767
  { name = "uvicorn" },
768
  ]
769
+ sdist = { url = "https://files.pythonhosted.org/packages/b8/e8/66612eabc43a792b09ea8080d5581644128823b4f20185881f86a042e470/gradio-6.0.2.tar.gz", hash = "sha256:e3bb128fd9247a49820048cfb8f5b677b59a0da24f6d81bc990ca1a20eb6d1fb", size = 36452504, upload-time = "2025-12-02T01:57:48.397Z" }
770
  wheels = [
771
+ { url = "https://files.pythonhosted.org/packages/1c/b0/d4bb7a5c9c2e26a952da06f046c6411fb8678003a0b9c7ed379941aff21f/gradio-6.0.2-py3-none-any.whl", hash = "sha256:bcb8b9d147b313c958f811977527415cfd7871ee9547ccd92ef1911970c49a2c", size = 21560841, upload-time = "2025-12-02T01:57:45.471Z" },
772
  ]
773
 
774
  [[package]]
775
  name = "gradio-client"
776
+ version = "2.0.1"
777
  source = { registry = "https://pypi.org/simple" }
778
  dependencies = [
779
  { name = "fsspec" },
 
781
  { name = "huggingface-hub" },
782
  { name = "packaging" },
783
  { name = "typing-extensions" },
 
784
  ]
785
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/cc/b0f04b1c9bf79c7ae9840b9945f5fbd93355719684f83032837695ab1eaf/gradio_client-2.0.1.tar.gz", hash = "sha256:087eb50652370747c0ce66cd0ae79ecb49f9682188d5348e279d44602cbc2814", size = 54792, upload-time = "2025-12-02T01:57:58.685Z" }
786
  wheels = [
787
+ { url = "https://files.pythonhosted.org/packages/1b/11/d680ecf4073bd1cacfe9dea57fa95660e4ea2d1fff3125dbaaa902cc9095/gradio_client-2.0.1-py3-none-any.whl", hash = "sha256:6322eecb5963a07703306c0b048bb98518063d05ca99a65fe384417188af8c63", size = 55439, upload-time = "2025-12-02T01:57:57.551Z" },
788
  ]
789
 
790
  [[package]]
 
1349
 
1350
  [[package]]
1351
  name = "openai"
1352
+ version = "2.8.1"
1353
  source = { registry = "https://pypi.org/simple" }
1354
  dependencies = [
1355
  { name = "anyio" },
 
1361
  { name = "tqdm" },
1362
  { name = "typing-extensions" },
1363
  ]
1364
+ sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" }
1365
  wheels = [
1366
+ { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
1367
  ]
1368
 
1369
  [[package]]
1370
  name = "openai-agents"
1371
+ version = "0.6.1"
1372
  source = { registry = "https://pypi.org/simple" }
1373
  dependencies = [
1374
  { name = "griffe" },
 
1379
  { name = "types-requests" },
1380
  { name = "typing-extensions" },
1381
  ]
1382
+ sdist = { url = "https://files.pythonhosted.org/packages/88/dc/7323c8d96211f252a2ebb288782a8f40bef6b472878eeebb59b3097a5708/openai_agents-0.6.1.tar.gz", hash = "sha256:067d2b66669c390c840effeb02d80939b4ac4a4db53e9735b74895a6d916b840", size = 2011463, upload-time = "2025-11-20T01:17:07.941Z" }
1383
  wheels = [
1384
+ { url = "https://files.pythonhosted.org/packages/11/53/d8076306f324992c79e9b2ee597f2ce863f0ac5d1fd24e6ad88f2a4dcbc0/openai_agents-0.6.1-py3-none-any.whl", hash = "sha256:7bde01c8d2fd723b0c72c9b207dcfeb12a8d211078f5d259945fb163a6f52b89", size = 237609, upload-time = "2025-11-20T01:17:06.454Z" },
1385
  ]
1386
 
1387
  [[package]]
 
1686
 
1687
  [[package]]
1688
  name = "pydantic"
1689
+ version = "2.12.4"
1690
  source = { registry = "https://pypi.org/simple" }
1691
  dependencies = [
1692
  { name = "annotated-types" },
 
1694
  { name = "typing-extensions" },
1695
  { name = "typing-inspection" },
1696
  ]
1697
+ sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
1698
  wheels = [
1699
+ { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
1700
  ]
1701
 
1702
  [package.optional-dependencies]
 
1706
 
1707
  [[package]]
1708
  name = "pydantic-core"
1709
+ version = "2.41.5"
1710
  source = { registry = "https://pypi.org/simple" }
1711
  dependencies = [
1712
  { name = "typing-extensions" },
1713
  ]
1714
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
1715
+ wheels = [
1716
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
1717
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
1718
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
1719
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
1720
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
1721
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
1722
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
1723
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
1724
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
1725
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
1726
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
1727
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
1728
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
1729
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
1730
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
1731
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
1732
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
1733
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
1734
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
1735
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
1736
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
1737
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
1738
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
1739
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
1740
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
1741
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
1742
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
1743
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
1744
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
1745
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
1746
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
1747
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
1748
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
1749
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
1750
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
1751
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
1752
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
1753
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
1754
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
1755
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
1756
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
1757
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
1758
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
1759
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
1760
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
1761
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
1762
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
1763
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
1764
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
1765
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
1766
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
1767
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
1768
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
1769
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
1770
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
1771
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
1772
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
1773
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
1774
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
1775
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
1776
  ]
1777
 
1778
  [[package]]
 
2193
 
2194
  [[package]]
2195
  name = "safehttpx"
2196
+ version = "0.1.7"
2197
  source = { registry = "https://pypi.org/simple" }
2198
  dependencies = [
2199
  { name = "httpx" },
2200
  ]
2201
+ sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" }
2202
  wheels = [
2203
+ { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" },
2204
  ]
2205
 
2206
  [[package]]
 
2378
 
2379
  [[package]]
2380
  name = "typing-inspection"
2381
+ version = "0.4.2"
2382
  source = { registry = "https://pypi.org/simple" }
2383
  dependencies = [
2384
  { name = "typing-extensions" },
2385
  ]
2386
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
2387
  wheels = [
2388
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
2389
  ]
2390
 
2391
  [[package]]