-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
352 lines (290 loc) · 13.3 KB
/
main.py
File metadata and controls
352 lines (290 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""
Main CLI application for Flashcard Generation Agent.
Orchestrates the workflow and handles command-line argument parsing.
"""
import os
import argparse
import logging
import json
from pathlib import Path
from datetime import datetime
# Import models
from models import FlashcardSet
# Import functions from modular files
from openai_client import (
prepare_input,
generate_flashcards,
critique_flashcards,
revise_flashcards,
analyze_knowledge_gaps,
cleanup_file,
)
from anki_exporter import export_to_anki, save_flashcards_text
from study_session import (
conduct_study_session,
adaptive_update_flashcards,
print_adaptive_summary,
)
# Set up logging
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)
# Set up evaluation data directory
EVAL_DATA_DIR = Path("evaluation_data")
EVAL_DATA_DIR.mkdir(exist_ok=True)
# Create log filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = LOG_DIR / f"flashcard_generation_{timestamp}.log"
# Create timestamped evaluation data directory
eval_data_subdir = EVAL_DATA_DIR / timestamp
eval_data_subdir.mkdir(exist_ok=True)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
def create_flashcards(
file_path: str,
deck_name: str = "Generated Flashcards",
model: str = "gpt-4o",
max_iterations: int = 2,
keep_file: bool = False,
enable_study_session: bool = False
):
"""
Main workflow for creating flashcards from a PDF or text file.
Args:
file_path: Path to the PDF or text file
deck_name: Name for the Anki deck
model: OpenAI model to use
max_iterations: Maximum critique/revision iterations
keep_file: Whether to keep uploaded file on OpenAI servers (only for PDFs)
enable_study_session: Whether to enable interactive study session
"""
print(f"\n{'='*60}")
print(f"Starting flashcard generation for: {file_path}")
print(f"Using model: {model}")
print(f"Max iterations: {max_iterations}")
print(f"{'='*60}\n")
logging.info(f"Starting flashcard generation for: {file_path}")
logging.info(f"Using model: {model}, max_iterations: {max_iterations}")
file_id = None
text_content = None
try:
# Prepare input (upload PDF or read text file)
file_id, text_content = prepare_input(file_path)
# Save evaluation metadata
metadata = {
"source_file": file_path,
"deck_name": deck_name,
"model": model,
"max_iterations": max_iterations,
"timestamp": timestamp,
"file_id": file_id if file_id else None,
"has_text_content": text_content is not None,
"study_session_enabled": enable_study_session
}
metadata_path = eval_data_subdir / "evaluation_metadata.json"
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, indent=2)
logging.info(f"Saved evaluation metadata to {metadata_path}")
if text_content:
# Save text content for evaluation
text_content_path = eval_data_subdir / "source_text.txt"
with open(text_content_path, "w", encoding="utf-8") as f:
f.write(text_content)
logging.info(f"Saved source text to {text_content_path}")
flashcards = generate_flashcards(file_id=file_id, text_content=text_content, model=model)
print(f"✓ Generated {len(flashcards.flashcards)} flashcards\n")
logging.info(f"Generated {len(flashcards.flashcards)} initial flashcards")
# Save initial flashcards for evaluation
initial_flashcards_path = eval_data_subdir / "flashcards_initial.json"
with open(initial_flashcards_path, "w", encoding="utf-8") as f:
json.dump(flashcards.model_dump(), f, indent=2)
logging.info(f"Saved initial flashcards to {initial_flashcards_path}")
# Log initial flashcards
logging.debug("Initial flashcards:")
for i, fc in enumerate(flashcards.flashcards):
logging.debug(f" {i+1}. Q: {fc.question} | A: {fc.answer}")
# Critique and revise loop
for i in range(max_iterations):
print(f"Iteration {i+1}/{max_iterations}:")
logging.info(f"Iteration {i+1}/{max_iterations}")
critique = critique_flashcards(flashcards, model)
if critique.is_acceptable:
print("✓ Flashcards approved!\n")
logging.info("Flashcards approved - no revision needed")
break
print(f"⚠ Issues found: {', '.join(critique.issues)}")
logging.warning(f"Issues found: {', '.join(critique.issues)}")
logging.info(f"Critique feedback: {critique.feedback}")
flashcards = revise_flashcards(flashcards, critique, model)
# Log revised flashcards
logging.info(f"Revised to {len(flashcards.flashcards)} flashcards")
logging.debug("Revised flashcards:")
for j, fc in enumerate(flashcards.flashcards):
logging.debug(f" {j+1}. Q: {fc.question} | A: {fc.answer}")
print()
# Save revised flashcards for evaluation
revised_flashcards_path = eval_data_subdir / "flashcards_revised.json"
with open(revised_flashcards_path, "w", encoding="utf-8") as f:
json.dump(flashcards.model_dump(), f, indent=2)
logging.info(f"Saved revised flashcards to {revised_flashcards_path}")
# Store original flashcards
original_flashcards = flashcards
# Only export directly if study session not enabled
if not enable_study_session:
output_file = "output.apkg"
export_to_anki(flashcards, deck_name, output_file)
print(f"\nTo import into Anki:")
print(f"1. Open Anki")
print(f"2. File → Import")
print(f"3. Select {output_file}")
# Also save as text file
save_flashcards_text(flashcards, "flashcards.txt")
print(f"\n{'='*60}")
print(f"Done! Created {len(flashcards.flashcards)} flashcards")
print(f"{'='*60}\n")
logging.info(f"Completed! Created {len(flashcards.flashcards)} flashcards")
logging.info(f"Log file saved to: {log_file}")
print(f"✓ Evaluation data saved to: {eval_data_subdir}")
else:
# Study session mode - ask user
print("\n" + "="*60)
response = input("Would you like to start a study session? (y/n): ").lower()
if response == 'y':
# Conduct session
session = conduct_study_session(original_flashcards)
logging.info(f"Study session completed: {len(session.ratings)} ratings collected")
# Save study session for evaluation
study_session_path = eval_data_subdir / "study_session.json"
with open(study_session_path, "w", encoding="utf-8") as f:
json.dump(session.model_dump(), f, indent=2)
logging.info(f"Saved study session to {study_session_path}")
# Analyze gaps
gaps = analyze_knowledge_gaps(session, file_id=file_id, text_content=text_content, model=model)
logging.info(f"Knowledge gaps analyzed: {len(gaps.weak_areas)} weak areas identified")
# Save knowledge gaps for evaluation
knowledge_gaps_path = eval_data_subdir / "knowledge_gaps.json"
with open(knowledge_gaps_path, "w", encoding="utf-8") as f:
json.dump(gaps.model_dump(), f, indent=2)
logging.info(f"Saved knowledge gaps to {knowledge_gaps_path}")
# Adaptive update
adaptive_result = adaptive_update_flashcards(
original_flashcards, session, gaps, file_id, text_content, model
)
# Save adapted flashcards for evaluation
adapted_flashcards_path = eval_data_subdir / "flashcards_adapted.json"
with open(adapted_flashcards_path, "w", encoding="utf-8") as f:
json.dump(adaptive_result.final_flashcards.model_dump(), f, indent=2)
logging.info(f"Saved adapted flashcards to {adapted_flashcards_path}")
# Save adaptive update for evaluation
adaptive_update_path = eval_data_subdir / "adaptive_update.json"
with open(adaptive_update_path, "w", encoding="utf-8") as f:
json.dump(adaptive_result.model_dump(), f, indent=2)
logging.info(f"Saved adaptive update to {adaptive_update_path}")
# Export adaptive deck
export_to_anki(
adaptive_result.final_flashcards,
deck_name + " (Adaptive)",
"output.apkg"
)
# Save gap report
with open("knowledge_gaps_report.txt", "w", encoding="utf-8") as f:
f.write(adaptive_result.gap_report)
# Display summary
print_adaptive_summary(adaptive_result)
# Also save adaptive deck as text
save_flashcards_text(adaptive_result.final_flashcards, "flashcards.txt")
print(f"✓ Gap report saved to: knowledge_gaps_report.txt")
print(f"\n{'='*60}")
print(f"Done! Created adaptive deck with {len(adaptive_result.final_flashcards.flashcards)} flashcards")
print(f"{'='*60}\n")
logging.info(f"Adaptive deck created: {len(adaptive_result.final_flashcards.flashcards)} cards")
logging.info(f"Log file saved to: {log_file}")
print(f"✓ Evaluation data saved to: {eval_data_subdir}")
return adaptive_result.final_flashcards
else:
# User declined, export original
output_file = "output.apkg"
export_to_anki(original_flashcards, deck_name, output_file)
save_flashcards_text(original_flashcards, "flashcards.txt")
print(f"✓ Exported {len(original_flashcards.flashcards)} flashcards to {output_file}")
logging.info(f"Log file saved to: {log_file}")
return flashcards
finally:
# Clean up uploaded file unless user wants to keep it
if file_id and not keep_file:
cleanup_file(file_id)
# CLI entry point
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate Anki flashcards from PDF or text documents using AI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python main.py lecture_notes.pdf
python main.py lecture_notes.txt
python main.py lecture_notes.pdf --deck "Biology 101"
python main.py transcript.txt --model gpt-4o-mini --iterations 3
python main.py lecture_notes.pdf --verbose
python main.py 03-GNN1.pdf --deck "Graph Neural Networks" --model gpt-4o --iterations 1
"""
)
parser.add_argument(
"input_file",
help="Path to the PDF or text file (.pdf, .txt, .text) to generate flashcards from"
)
parser.add_argument(
"--deck",
default="Generated Flashcards",
help="Name of the Anki deck (default: Generated Flashcards)"
)
parser.add_argument(
"--model",
default="gpt-4o",
choices=["gpt-4o", "gpt-4o-mini", "o1"],
help="OpenAI model to use (default: gpt-4o)"
)
parser.add_argument(
"--iterations",
type=int,
default=1,
help="Maximum number of critique/revision iterations (default: 1)"
)
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose logging (show all flashcards in log)"
)
parser.add_argument(
"--keep-file",
action="store_true",
help="Keep uploaded file on OpenAI servers (default: delete after use)"
)
parser.add_argument(
"--study-session",
action="store_true",
help="Enable interactive study session with adaptive learning"
)
args = parser.parse_args()
# Set logging level based on verbose flag
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
logging.info("Verbose mode enabled - all flashcards will be logged")
if not os.path.exists(args.input_file):
print(f"Error: File not found: {args.input_file}")
logging.error(f"File not found: {args.input_file}")
exit(1)
print(f"Log file: {log_file}")
create_flashcards(
args.input_file,
args.deck,
args.model,
args.iterations,
args.keep_file,
args.study_session
)