Mohammed Foud commited on
Commit
bef7112
·
1 Parent(s): 444a04d

Add application file

Browse files
.env ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Application Settings
2
+ FLASK_DEBUG=true
3
+ FLASK_SECRET_KEY=your-secret-key-here
4
+ UPLOAD_FOLDER=output
5
+
6
+ # AI Provider Selection (options: g4f, openai, huggingface, together)
7
+ AI_PROVIDER=openai
8
+
9
+ # OpenAI Configuration
10
+ OPENAI_API_KEY=your-openai-api-key-here
11
+ OPENAI_ORG_ID=your-organization-id-if-applicable
12
+ OPENAI_BASE_URL=https://christian-heidie-randai-0573d5c0.koyeb.app/v1 # Change for Azure/LocalAI/other proxies
13
+ OPENAI_MAX_TOKENS=1000
14
+ OPENAI_TEMPERATURE=0.7
15
+
16
+ # HuggingFace Configuration
17
+ HUGGINGFACE_API_KEY=your-hf-api-key-here
18
+ HUGGINGFACE_API_URL=https://api-inference.huggingface.co/models
19
+ HUGGINGFACE_MAX_TOKENS=1000
20
+ HUGGINGFACE_TEMPERATURE=0.7
21
+
22
+ # Together AI Configuration
23
+ TOGETHER_API_KEY=your-together-api-key-here
24
+ TOGETHER_API_URL=https://api.together.xyz/v1/completions
25
+ TOGETHER_MAX_TOKENS=1000
26
+ TOGETHER_TEMPERATURE=0.7
27
+
28
+ # g4f Configuration (usually doesn't need API keys)
29
+ G4F_PROXY= # Optional proxy URL if needed
30
+
31
+ # Pandoc Configuration (for Word document conversion)
32
+ PANDOC_PATH=pandoc # Path to pandoc if not in system PATH
33
+ REFERENCE_DOCX=reference.docx # Path to custom reference Word template
34
+
35
+ # Rate Limiting (optional)
36
+ MAX_RETRIES=3
37
+ INITIAL_DELAY=1
38
+ BACKOFF_FACTOR=2
39
+
40
+ # Caching Settings
41
+ MODEL_CACHE_TTL=3600 # 1 hour cache for model lists
app.py CHANGED
@@ -1,565 +1,18 @@
1
- from flask import Flask, render_template, request, jsonify, send_from_directory, Response, copy_current_request_context
2
- import g4f
3
- import os
4
- import subprocess
5
- from datetime import datetime
6
- from typing import List, Tuple
7
- import uuid
8
- from werkzeug.utils import secure_filename
9
- import json
10
- import time
11
- from functools import wraps
12
-
13
- app = Flask(__name__)
14
- app.config['UPLOAD_FOLDER'] = 'output'
15
-
16
- from functools import wraps
17
- import time
18
- import random
19
-
20
- def retry(max_retries=3, initial_delay=1, backoff_factor=2):
21
- def decorator(func):
22
- @wraps(func)
23
- def wrapper(*args, **kwargs):
24
- retries = 0
25
- delay = initial_delay
26
-
27
- while retries < max_retries:
28
- try:
29
- return func(*args, **kwargs)
30
- except (SystemExit, KeyboardInterrupt):
31
- raise
32
- except Exception as e:
33
- retries += 1
34
- if retries >= max_retries:
35
- raise # Re-raise the last exception if max retries reached
36
-
37
- # Exponential backoff with some randomness
38
- time.sleep(delay + random.uniform(0, 0.5))
39
- delay *= backoff_factor
40
- return wrapper
41
- return decorator
42
- # Initialize output directory
43
- os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
44
-
45
- def get_available_models() -> List[str]:
46
- """Get list of available models from g4f"""
47
- try:
48
- models = sorted(g4f.models._all_models)
49
- # Ensure gpt-4o is first if available
50
- if 'gpt-4o' in models:
51
- models.remove('gpt-4o')
52
- models.insert(0, 'gpt-4o')
53
- return models
54
- except Exception:
55
- return ['gpt-4o', 'gpt-4', 'gpt-3.5-turbo', 'llama2-70b', 'claude-2']
56
 
57
- def generate_filename() -> Tuple[str, str]:
58
- """Generate filenames with unique ID"""
59
- unique_id = str(uuid.uuid4())[:8]
60
- md_filename = f"research_paper_{unique_id}.md"
61
- docx_filename = f"research_paper_{unique_id}.docx"
62
- return md_filename, docx_filename
63
-
64
- def generate_index_content(model: str, research_subject: str, manual_chapters: List[str] = None) -> str:
65
- """Generate index content for the research paper"""
66
- try:
67
- if manual_chapters:
68
- prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject} with these chapters: " + \
69
- ", ".join(manual_chapters) + ". Include section headings in markdown format."
70
- else:
71
- prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject}. Include chapter titles and section headings in markdown format."
72
-
73
- response = g4f.ChatCompletion.create(
74
- model=model,
75
- messages=[{"role": "user", "content": prompt}],
76
- )
77
- return str(response) if response else "[Empty response from model]"
78
- except Exception as e:
79
- raise Exception(f"Failed to generate index: {str(e)}")
80
-
81
- def extract_chapters(index_content: str) -> List[str]:
82
- """Extract chapter titles from index content"""
83
- chapters = []
84
- for line in index_content.split('\n'):
85
- if line.strip().startswith('## '):
86
- chapter_title = line.strip()[3:].strip()
87
- if chapter_title.lower() not in ['introduction', 'conclusion', 'references']:
88
- chapters.append(chapter_title)
89
- return chapters if chapters else ["Literature Review", "Methodology", "Results and Discussion"]
90
-
91
- def generate_automatic_sections(model: str, research_subject: str) -> List[Tuple[str, str]]:
92
- """Generate sections automatically based on AI-generated index"""
93
- try:
94
- index_content = generate_index_content(model, research_subject)
95
- chapters = extract_chapters(index_content)
96
-
97
- sections = [
98
- ("Index", index_content),
99
- ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}. Include background information, research objectives, and significance of the study.")
100
- ]
101
-
102
- for i, chapter in enumerate(chapters, 1):
103
- sections.append(
104
- (f"Chapter {i}: {chapter}",
105
- f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}. "
106
- f"Provide comprehensive coverage of this aspect, including relevant theories, examples, and analysis.")
107
- )
108
-
109
- sections.append(
110
- ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}. Summarize key findings, discuss implications, and suggest future research directions.")
111
- )
112
-
113
- return sections
114
- except Exception as e:
115
- raise Exception(f"Failed to generate automatic structure: {str(e)}")
116
-
117
- def get_manual_sections(research_subject: str) -> List[Tuple[str, str]]:
118
- """Get predefined manual sections"""
119
- return [
120
- ("Index", "[Index will be generated first]"),
121
- ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}."),
122
- ("Chapter 1: Literature Review", f"Create a detailed literature review chapter about {research_subject}."),
123
- ("Chapter 2: Methodology", f"Describe the research methodology for a study about {research_subject}."),
124
- ("Chapter 3: Results and Discussion", f"Present hypothetical results and discussion for a research paper about {research_subject}. Analyze findings and compare with existing literature."),
125
- ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}.")
126
- ]
127
-
128
- @retry(max_retries=3, initial_delay=1, backoff_factor=2)
129
- def generate_section_content(model: str, prompt: str) -> str:
130
- """Generate content for a single section with retry logic"""
131
- try:
132
- response = g4f.ChatCompletion.create(
133
- model=model,
134
- messages=[{"role": "user", "content": prompt}],
135
- stream=False # Disable streaming to avoid async issues
136
- )
137
- return str(response) if response else "[Empty response from model]"
138
- except Exception as e:
139
- raise Exception(f"Failed to generate section content: {str(e)}")
140
-
141
- def write_research_paper(md_filename: str, research_subject: str, sections: List[Tuple[str, str]], model: str) -> None:
142
- """Write the research paper to a markdown file"""
143
- full_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
144
- with open(full_path, "w", encoding="utf-8") as f:
145
- f.write(f"# Research Paper: {research_subject}\n\n")
146
-
147
- for section_title, prompt in sections:
148
- try:
149
- if isinstance(prompt, str) and (prompt.startswith("##") or prompt.startswith("#")):
150
- content = f"{prompt}\n\n"
151
- else:
152
- response = generate_section_content(model, prompt)
153
- content = f"## {section_title}\n\n{response}\n\n"
154
- f.write(content)
155
- except Exception as e:
156
- f.write(f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n")
157
-
158
- def convert_to_word(md_filename: str, docx_filename: str) -> None:
159
- """Convert markdown file to Word document using Pandoc"""
160
- md_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
161
- docx_path = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)
162
-
163
- command = [
164
- "pandoc", md_path,
165
- "-o", docx_path,
166
- "--standalone",
167
- "--table-of-contents",
168
- "--toc-depth=3"
169
- ]
170
-
171
- if os.path.exists("reference.docx"):
172
- command.extend(["--reference-doc", "reference.docx"])
173
 
174
- subprocess.run(command, check=True)
175
-
176
- @app.route('/')
177
- def index():
178
- models = get_available_models()
179
- return render_template('index.html', models=models)
180
-
181
- def sse_stream_required(f):
182
- """Decorator to ensure SSE stream has request context"""
183
- @wraps(f)
184
- def decorated(*args, **kwargs):
185
- @copy_current_request_context
186
- def generator():
187
- return f(*args, **kwargs)
188
- return generator()
189
- return decorated
190
-
191
- @app.route('/stream')
192
- @sse_stream_required
193
- def stream():
194
- research_subject = request.args.get('subject', '').strip()
195
- selected_model = request.args.get('model', 'gpt-4o')
196
- structure_type = request.args.get('structure', 'automatic')
197
-
198
- def generate():
199
- try:
200
- if not research_subject:
201
- yield "data: " + json.dumps({"error": "Research subject is required"}) + "\n\n"
202
- return
203
-
204
- # Generate filenames
205
- md_filename, docx_filename = generate_filename()
206
-
207
- # Initial steps
208
- steps = [
209
- {"id": 0, "text": "Preparing document structure...", "status": "pending"},
210
- {"id": 1, "text": "Generating index/table of contents...", "status": "pending"},
211
- {"id": 2, "text": "Determining chapters...", "status": "pending"},
212
- {"id": 3, "text": "Writing content...", "status": "pending", "subSteps": []},
213
- {"id": 4, "text": "Finalizing document...", "status": "pending"},
214
- {"id": 5, "text": "Converting to Word format...", "status": "pending"}
215
- ]
216
-
217
- # Initial progress update
218
- yield "data: " + json.dumps({"steps": steps, "progress": 0}) + "\n\n"
219
-
220
- # Step 0: Prepare
221
- steps[0]["status"] = "in-progress"
222
- yield "data: " + json.dumps({
223
- "steps": steps,
224
- "progress": 0,
225
- "current_step": 0
226
- }) + "\n\n"
227
-
228
- sections = []
229
- chapter_steps = []
230
-
231
- if structure_type == 'automatic':
232
- try:
233
- # Step 1: Generate index
234
- steps[1]["status"] = "in-progress"
235
- yield "data: " + json.dumps({
236
- "steps": steps,
237
- "progress": 10,
238
- "current_step": 1
239
- }) + "\n\n"
240
-
241
- index_content = generate_index_content(selected_model, research_subject)
242
- sections.append(("Index", index_content))
243
-
244
- steps[1]["status"] = "complete"
245
- yield "data: " + json.dumps({
246
- "steps": steps,
247
- "progress": 20,
248
- "current_step": 1
249
- }) + "\n\n"
250
-
251
- # Step 2: Determine chapters
252
- steps[2]["status"] = "in-progress"
253
- yield "data: " + json.dumps({
254
- "steps": steps,
255
- "progress": 30,
256
- "current_step": 2
257
- }) + "\n\n"
258
-
259
- chapters = extract_chapters(index_content)
260
-
261
- # Create sub-steps for each chapter with initial timing info
262
- chapter_substeps = [
263
- {
264
- "id": f"chapter_{i}",
265
- "text": chapter,
266
- "status": "pending",
267
- "start_time": None,
268
- "duration": None
269
- }
270
- for i, chapter in enumerate(chapters)
271
- ]
272
-
273
- steps[3]["subSteps"] = chapter_substeps
274
-
275
- steps[2]["status"] = "complete"
276
- yield "data: " + json.dumps({
277
- "steps": steps,
278
- "progress": 40,
279
- "current_step": 2,
280
- "update_steps": True
281
- }) + "\n\n"
282
-
283
- # Add introduction and conclusion
284
- sections.append((
285
- "Introduction",
286
- f"Write a comprehensive introduction for a research paper about {research_subject}."
287
- ))
288
-
289
- for i, chapter in enumerate(chapters, 1):
290
- sections.append((
291
- f"Chapter {i}: {chapter}",
292
- f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
293
- ))
294
-
295
- sections.append((
296
- "Conclusion",
297
- f"Write a conclusion section for a research paper about {research_subject}."
298
- ))
299
-
300
- # Generate content for each chapter with timing
301
- for i, chapter in enumerate(chapters):
302
- # Update chapter start time
303
- steps[3]["subSteps"][i]["start_time"] = time.time()
304
- steps[3]["subSteps"][i]["status"] = "in-progress"
305
-
306
- yield "data: " + json.dumps({
307
- "steps": steps,
308
- "progress": 40 + (i * 50 / len(chapters)),
309
- "current_step": 3,
310
- "chapter_progress": {
311
- "current": i + 1,
312
- "total": len(chapters),
313
- "chapter": chapter,
314
- "percent": ((i + 1) / len(chapters)) * 100
315
- }
316
- }) + "\n\n"
317
-
318
- try:
319
- response = generate_section_content(
320
- selected_model,
321
- f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
322
- )
323
-
324
- # Calculate and store duration
325
- duration = time.time() - steps[3]["subSteps"][i]["start_time"]
326
- steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
327
- steps[3]["subSteps"][i]["status"] = "complete"
328
-
329
- yield "data: " + json.dumps({
330
- "steps": steps,
331
- "progress": 40 + ((i + 1) * 50 / len(chapters)),
332
- "current_step": 3,
333
- "chapter_progress": {
334
- "current": i + 1,
335
- "total": len(chapters),
336
- "chapter": chapter,
337
- "percent": ((i + 1) / len(chapters)) * 100,
338
- "duration": f"{duration:.1f}s"
339
- }
340
- }) + "\n\n"
341
- except Exception as e:
342
- duration = time.time() - steps[3]["subSteps"][i]["start_time"]
343
- steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
344
- steps[3]["subSteps"][i]["status"] = "error"
345
- steps[3]["subSteps"][i]["message"] = str(e)
346
-
347
- yield "data: " + json.dumps({
348
- "steps": steps,
349
- "progress": 40 + ((i + 1) * 50 / len(chapters)),
350
- "current_step": 3,
351
- "warning": f"Failed to generate chapter {i+1} after retries",
352
- "chapter_progress": {
353
- "current": i + 1,
354
- "total": len(chapters),
355
- "chapter": chapter,
356
- "percent": ((i + 1) / len(chapters)) * 100,
357
- "error": str(e)
358
- }
359
- }) + "\n\n"
360
-
361
- steps[3]["status"] = "complete"
362
- yield "data: " + json.dumps({
363
- "steps": steps,
364
- "progress": 90,
365
- "current_step": 3,
366
- "chapter_progress": {
367
- "complete": True,
368
- "total_chapters": len(chapters)
369
- }
370
- }) + "\n\n"
371
-
372
- except Exception as e:
373
- steps[1]["status"] = "error"
374
- steps[1]["message"] = str(e)
375
- yield "data: " + json.dumps({
376
- "steps": steps,
377
- "progress": 20,
378
- "current_step": 1
379
- }) + "\n\n"
380
-
381
- # Fallback to manual structure
382
- sections = get_manual_sections(research_subject)
383
- steps[1]["message"] = "Falling back to manual structure"
384
- yield "data: " + json.dumps({
385
- "steps": steps,
386
- "progress": 20,
387
- "current_step": 1
388
- }) + "\n\n"
389
-
390
- try:
391
- index_content = generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
392
- sections[0] = ("Index", index_content)
393
-
394
- steps[1]["status"] = "complete"
395
- yield "data: " + json.dumps({
396
- "steps": steps,
397
- "progress": 25,
398
- "current_step": 1
399
- }) + "\n\n"
400
- except Exception as e:
401
- steps[1]["status"] = "error"
402
- steps[1]["message"] = str(e)
403
- yield "data: " + json.dumps({
404
- "steps": steps,
405
- "progress": 20,
406
- "current_step": 1,
407
- "error": "Failed to generate even fallback content"
408
- }) + "\n\n"
409
- return
410
- else:
411
- sections = get_manual_sections(research_subject)
412
- steps[1]["status"] = "in-progress"
413
- yield "data: " + json.dumps({
414
- "steps": steps,
415
- "progress": 10,
416
- "current_step": 1
417
- }) + "\n\n"
418
-
419
- try:
420
- index_content = generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
421
- sections[0] = ("Index", index_content)
422
-
423
- steps[1]["status"] = "complete"
424
- yield "data: " + json.dumps({
425
- "steps": steps,
426
- "progress": 20,
427
- "current_step": 1
428
- }) + "\n\n"
429
- except Exception as e:
430
- steps[1]["status"] = "error"
431
- steps[1]["message"] = str(e)
432
- yield "data: " + json.dumps({
433
- "steps": steps,
434
- "progress": 20,
435
- "current_step": 1,
436
- "error": "Failed to generate manual index"
437
- }) + "\n\n"
438
- return
439
-
440
- # Write introduction
441
- steps[3]["status"] = "in-progress"
442
- yield "data: " + json.dumps({
443
- "steps": steps,
444
- "progress": 40,
445
- "current_step": 3
446
- }) + "\n\n"
447
-
448
- try:
449
- introduction_content = generate_section_content(
450
- selected_model,
451
- f"Write a comprehensive introduction for a research paper about {research_subject}."
452
- )
453
-
454
- steps[3]["status"] = "complete"
455
- yield "data: " + json.dumps({
456
- "steps": steps,
457
- "progress": 60,
458
- "current_step": 3
459
- }) + "\n\n"
460
- except Exception as e:
461
- steps[3]["status"] = "error"
462
- steps[3]["message"] = str(e)
463
- yield "data: " + json.dumps({
464
- "steps": steps,
465
- "progress": 60,
466
- "current_step": 3,
467
- "warning": "Failed to generate introduction after retries"
468
- }) + "\n\n"
469
-
470
- # Write conclusion
471
- steps[4]["status"] = "in-progress"
472
- yield "data: " + json.dumps({
473
- "steps": steps,
474
- "progress": 80,
475
- "current_step": 4
476
- }) + "\n\n"
477
-
478
- try:
479
- conclusion_content = generate_section_content(
480
- selected_model,
481
- f"Write a conclusion section for a research paper about {research_subject}."
482
- )
483
-
484
- steps[4]["status"] = "complete"
485
- yield "data: " + json.dumps({
486
- "steps": steps,
487
- "progress": 90,
488
- "current_step": 4
489
- }) + "\n\n"
490
- except Exception as e:
491
- steps[4]["status"] = "error"
492
- steps[4]["message"] = str(e)
493
- yield "data: " + json.dumps({
494
- "steps": steps,
495
- "progress": 90,
496
- "current_step": 4,
497
- "warning": "Failed to generate conclusion after retries"
498
- }) + "\n\n"
499
-
500
- # Write the complete paper
501
- full_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
502
- with open(full_path, "w", encoding="utf-8") as f:
503
- f.write(f"# Research Paper: {research_subject}\n\n")
504
-
505
- for section_title, prompt in sections:
506
- try:
507
- if isinstance(prompt, str) and (prompt.startswith("##") or prompt.startswith("#")):
508
- content = f"{prompt}\n\n"
509
- else:
510
- try:
511
- response = generate_section_content(selected_model, prompt)
512
- content = f"## {section_title}\n\n{response}\n\n"
513
- except Exception as e:
514
- content = f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n"
515
- f.write(content)
516
- except Exception as e:
517
- f.write(f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n")
518
-
519
- # Convert to Word
520
- steps[5]["status"] = "in-progress"
521
- yield "data: " + json.dumps({
522
- "steps": steps,
523
- "progress": 95,
524
- "current_step": 5
525
- }) + "\n\n"
526
-
527
- try:
528
- convert_to_word(md_filename, docx_filename)
529
- steps[5]["status"] = "complete"
530
- yield "data: " + json.dumps({
531
- "steps": steps,
532
- "progress": 100,
533
- "current_step": 5,
534
- "status": "complete",
535
- "docx_file": docx_filename,
536
- "md_file": md_filename
537
- }) + "\n\n"
538
- except Exception as e:
539
- steps[5]["status"] = "error"
540
- steps[5]["message"] = str(e)
541
- yield "data: " + json.dumps({
542
- "steps": steps,
543
- "progress": 100,
544
- "current_step": 5,
545
- "status": "partial_success",
546
- "message": f'Paper generated but Word conversion failed: {str(e)}',
547
- "md_file": md_filename
548
- }) + "\n\n"
549
-
550
- except Exception as e:
551
- yield "data: " + json.dumps({"error": f"Failed to generate paper: {str(e)}"}) + "\n\n"
552
-
553
- return Response(generate(), mimetype="text/event-stream")
554
-
555
- @app.route('/download/<filename>')
556
- def download(filename):
557
- safe_filename = secure_filename(filename)
558
- return send_from_directory(
559
- app.config['UPLOAD_FOLDER'],
560
- safe_filename,
561
- as_attachment=True
562
- )
563
 
564
  if __name__ == '__main__':
 
565
  app.run(debug=True)
 
1
+ from flask import Flask
2
+ from config import Config
3
+ from routes.api import api_bp
4
+ from routes.views import views_bp
5
+
6
+ def create_app():
7
+ app = Flask(__name__)
8
+ app.config.from_object(Config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Register blueprints
11
+ app.register_blueprint(views_bp)
12
+ app.register_blueprint(api_bp, url_prefix='/api')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  if __name__ == '__main__':
17
+ app = create_app()
18
  app.run(debug=True)
config.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ class Config:
4
+ UPLOAD_FOLDER = 'output'
5
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-123'
6
+ MAX_RETRIES = 3
7
+ INITIAL_DELAY = 1
8
+ BACKOFF_FACTOR = 2
9
+ AI_PROVIDER = os.getenv('AI_PROVIDER', 'g4f') # Options: g4f, huggingface, together, openai
10
+
11
+ AI_PROVIDER_CONFIG = {
12
+ 'g4f': {
13
+ # g4f specific configuration
14
+ },
15
+ 'huggingface': {
16
+ 'api_key': os.getenv('HUGGINGFACE_API_KEY'),
17
+ 'max_tokens': 1000,
18
+ 'temperature': 0.7
19
+ },
20
+ 'together': {
21
+ 'api_key': os.getenv('TOGETHER_API_KEY'),
22
+ 'max_tokens': 1000,
23
+ 'temperature': 0.7
24
+ },
25
+ 'openai': {
26
+ 'api_key': os.getenv('OPENAI_API_KEY'),
27
+ 'organization': os.getenv('OPENAI_ORG_ID'),
28
+ 'base_url': os.getenv('OPENAI_BASE_URL', "https://api.openai.com/v1"), # Default OpenAI endpoint
29
+ 'max_tokens': 1000,
30
+ 'temperature': 0.7,
31
+ 'top_p': 0.9,
32
+ 'frequency_penalty': 0,
33
+ 'presence_penalty': 0
34
+ }
35
+ }
d.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ git add .
2
+ git commit -m "Add application file"
3
+ git push
etc/app.py ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from config import Config
3
+ from routes.api import api_bp
4
+ from routes.views import views_bp
5
+
6
+ def create_app():
7
+ app = Flask(__name__)
8
+ app.config.from_object(Config)
9
+
10
+ # Register blueprints
11
+ app.register_blueprint(views_bp)
12
+ app.register_blueprint(api_bp, url_prefix='/api')
13
+
14
+ return app
15
+
16
+ if __name__ == '__main__':
17
+ app = create_app()
18
+ app.run(debug=True)
19
+
20
+ from flask import Flask, render_template, request, jsonify, send_from_directory, Response, copy_current_request_context
21
+ import g4f
22
+ import os
23
+ import subprocess
24
+ from datetime import datetime
25
+ from typing import List, Tuple
26
+ import uuid
27
+ from werkzeug.utils import secure_filename
28
+ import json
29
+ import time
30
+ from functools import wraps
31
+
32
+ app = Flask(__name__)
33
+ app.config['UPLOAD_FOLDER'] = 'output'
34
+
35
+ from functools import wraps
36
+ import time
37
+ import random
38
+
39
+ def retry(max_retries=3, initial_delay=1, backoff_factor=2):
40
+ def decorator(func):
41
+ @wraps(func)
42
+ def wrapper(*args, **kwargs):
43
+ retries = 0
44
+ delay = initial_delay
45
+
46
+ while retries < max_retries:
47
+ try:
48
+ return func(*args, **kwargs)
49
+ except (SystemExit, KeyboardInterrupt):
50
+ raise
51
+ except Exception as e:
52
+ retries += 1
53
+ if retries >= max_retries:
54
+ raise # Re-raise the last exception if max retries reached
55
+
56
+ # Exponential backoff with some randomness
57
+ time.sleep(delay + random.uniform(0, 0.5))
58
+ delay *= backoff_factor
59
+ return wrapper
60
+ return decorator
61
+ # Initialize output directory
62
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
63
+
64
+ def get_available_models() -> List[str]:
65
+ """Get list of available models from g4f"""
66
+ try:
67
+ models = sorted(g4f.models._all_models)
68
+ # Ensure gpt-4o is first if available
69
+ if 'gpt-4o' in models:
70
+ models.remove('gpt-4o')
71
+ models.insert(0, 'gpt-4o')
72
+ return models
73
+ except Exception:
74
+ return ['gpt-4o', 'gpt-4', 'gpt-3.5-turbo', 'llama2-70b', 'claude-2']
75
+
76
+ def generate_filename() -> Tuple[str, str]:
77
+ """Generate filenames with unique ID"""
78
+ unique_id = str(uuid.uuid4())[:8]
79
+ md_filename = f"research_paper_{unique_id}.md"
80
+ docx_filename = f"research_paper_{unique_id}.docx"
81
+ return md_filename, docx_filename
82
+
83
+ def generate_index_content(model: str, research_subject: str, manual_chapters: List[str] = None) -> str:
84
+ """Generate index content for the research paper"""
85
+ try:
86
+ if manual_chapters:
87
+ prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject} with these chapters: " + \
88
+ ", ".join(manual_chapters) + ". Include section headings in markdown format."
89
+ else:
90
+ prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject}. Include chapter titles and section headings in markdown format."
91
+
92
+ response = g4f.ChatCompletion.create(
93
+ model=model,
94
+ messages=[{"role": "user", "content": prompt}],
95
+ )
96
+ return str(response) if response else "[Empty response from model]"
97
+ except Exception as e:
98
+ raise Exception(f"Failed to generate index: {str(e)}")
99
+
100
+ def extract_chapters(index_content: str) -> List[str]:
101
+ """Extract chapter titles from index content"""
102
+ chapters = []
103
+ for line in index_content.split('\n'):
104
+ if line.strip().startswith('## '):
105
+ chapter_title = line.strip()[3:].strip()
106
+ if chapter_title.lower() not in ['introduction', 'conclusion', 'references']:
107
+ chapters.append(chapter_title)
108
+ return chapters if chapters else ["Literature Review", "Methodology", "Results and Discussion"]
109
+
110
+ def generate_automatic_sections(model: str, research_subject: str) -> List[Tuple[str, str]]:
111
+ """Generate sections automatically based on AI-generated index"""
112
+ try:
113
+ index_content = generate_index_content(model, research_subject)
114
+ chapters = extract_chapters(index_content)
115
+
116
+ sections = [
117
+ ("Index", index_content),
118
+ ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}. Include background information, research objectives, and significance of the study.")
119
+ ]
120
+
121
+ for i, chapter in enumerate(chapters, 1):
122
+ sections.append(
123
+ (f"Chapter {i}: {chapter}",
124
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}. "
125
+ f"Provide comprehensive coverage of this aspect, including relevant theories, examples, and analysis.")
126
+ )
127
+
128
+ sections.append(
129
+ ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}. Summarize key findings, discuss implications, and suggest future research directions.")
130
+ )
131
+
132
+ return sections
133
+ except Exception as e:
134
+ raise Exception(f"Failed to generate automatic structure: {str(e)}")
135
+
136
+ def get_manual_sections(research_subject: str) -> List[Tuple[str, str]]:
137
+ """Get predefined manual sections"""
138
+ return [
139
+ ("Index", "[Index will be generated first]"),
140
+ ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}."),
141
+ ("Chapter 1: Literature Review", f"Create a detailed literature review chapter about {research_subject}."),
142
+ ("Chapter 2: Methodology", f"Describe the research methodology for a study about {research_subject}."),
143
+ ("Chapter 3: Results and Discussion", f"Present hypothetical results and discussion for a research paper about {research_subject}. Analyze findings and compare with existing literature."),
144
+ ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}.")
145
+ ]
146
+
147
+ @retry(max_retries=3, initial_delay=1, backoff_factor=2)
148
+ def generate_section_content(model: str, prompt: str) -> str:
149
+ """Generate content for a single section with retry logic"""
150
+ try:
151
+ response = g4f.ChatCompletion.create(
152
+ model=model,
153
+ messages=[{"role": "user", "content": prompt}],
154
+ stream=False # Disable streaming to avoid async issues
155
+ )
156
+ return str(response) if response else "[Empty response from model]"
157
+ except Exception as e:
158
+ raise Exception(f"Failed to generate section content: {str(e)}")
159
+
160
+ def write_research_paper(md_filename: str, research_subject: str, sections: List[Tuple[str, str]], model: str) -> None:
161
+ """Write the research paper to a markdown file"""
162
+ full_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
163
+ with open(full_path, "w", encoding="utf-8") as f:
164
+ f.write(f"# Research Paper: {research_subject}\n\n")
165
+
166
+ for section_title, prompt in sections:
167
+ try:
168
+ if isinstance(prompt, str) and (prompt.startswith("##") or prompt.startswith("#")):
169
+ content = f"{prompt}\n\n"
170
+ else:
171
+ response = generate_section_content(model, prompt)
172
+ content = f"## {section_title}\n\n{response}\n\n"
173
+ f.write(content)
174
+ except Exception as e:
175
+ f.write(f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n")
176
+
177
+ def convert_to_word(md_filename: str, docx_filename: str) -> None:
178
+ """Convert markdown file to Word document using Pandoc"""
179
+ md_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
180
+ docx_path = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)
181
+
182
+ command = [
183
+ "pandoc", md_path,
184
+ "-o", docx_path,
185
+ "--standalone",
186
+ "--table-of-contents",
187
+ "--toc-depth=3"
188
+ ]
189
+
190
+ if os.path.exists("reference.docx"):
191
+ command.extend(["--reference-doc", "reference.docx"])
192
+
193
+ subprocess.run(command, check=True)
194
+
195
+ @app.route('/')
196
+ def index():
197
+ models = get_available_models()
198
+ return render_template('index.html', models=models)
199
+
200
+ def sse_stream_required(f):
201
+ """Decorator to ensure SSE stream has request context"""
202
+ @wraps(f)
203
+ def decorated(*args, **kwargs):
204
+ @copy_current_request_context
205
+ def generator():
206
+ return f(*args, **kwargs)
207
+ return generator()
208
+ return decorated
209
+
210
+ @app.route('/stream')
211
+ @sse_stream_required
212
+ def stream():
213
+ research_subject = request.args.get('subject', '').strip()
214
+ selected_model = request.args.get('model', 'gpt-4o')
215
+ structure_type = request.args.get('structure', 'automatic')
216
+
217
+ def generate():
218
+ try:
219
+ if not research_subject:
220
+ yield "data: " + json.dumps({"error": "Research subject is required"}) + "\n\n"
221
+ return
222
+
223
+ # Generate filenames
224
+ md_filename, docx_filename = generate_filename()
225
+
226
+ # Initial steps
227
+ steps = [
228
+ {"id": 0, "text": "Preparing document structure...", "status": "pending"},
229
+ {"id": 1, "text": "Generating index/table of contents...", "status": "pending"},
230
+ {"id": 2, "text": "Determining chapters...", "status": "pending"},
231
+ {"id": 3, "text": "Writing content...", "status": "pending", "subSteps": []},
232
+ {"id": 4, "text": "Finalizing document...", "status": "pending"},
233
+ {"id": 5, "text": "Converting to Word format...", "status": "pending"}
234
+ ]
235
+
236
+ # Initial progress update
237
+ yield "data: " + json.dumps({"steps": steps, "progress": 0}) + "\n\n"
238
+
239
+ # Step 0: Prepare
240
+ steps[0]["status"] = "in-progress"
241
+ yield "data: " + json.dumps({
242
+ "steps": steps,
243
+ "progress": 0,
244
+ "current_step": 0
245
+ }) + "\n\n"
246
+
247
+ sections = []
248
+ chapter_steps = []
249
+
250
+ if structure_type == 'automatic':
251
+ try:
252
+ # Step 1: Generate index
253
+ steps[1]["status"] = "in-progress"
254
+ yield "data: " + json.dumps({
255
+ "steps": steps,
256
+ "progress": 10,
257
+ "current_step": 1
258
+ }) + "\n\n"
259
+
260
+ index_content = generate_index_content(selected_model, research_subject)
261
+ sections.append(("Index", index_content))
262
+
263
+ steps[1]["status"] = "complete"
264
+ yield "data: " + json.dumps({
265
+ "steps": steps,
266
+ "progress": 20,
267
+ "current_step": 1
268
+ }) + "\n\n"
269
+
270
+ # Step 2: Determine chapters
271
+ steps[2]["status"] = "in-progress"
272
+ yield "data: " + json.dumps({
273
+ "steps": steps,
274
+ "progress": 30,
275
+ "current_step": 2
276
+ }) + "\n\n"
277
+
278
+ chapters = extract_chapters(index_content)
279
+
280
+ # Create sub-steps for each chapter with initial timing info
281
+ chapter_substeps = [
282
+ {
283
+ "id": f"chapter_{i}",
284
+ "text": chapter,
285
+ "status": "pending",
286
+ "start_time": None,
287
+ "duration": None
288
+ }
289
+ for i, chapter in enumerate(chapters)
290
+ ]
291
+
292
+ steps[3]["subSteps"] = chapter_substeps
293
+
294
+ steps[2]["status"] = "complete"
295
+ yield "data: " + json.dumps({
296
+ "steps": steps,
297
+ "progress": 40,
298
+ "current_step": 2,
299
+ "update_steps": True
300
+ }) + "\n\n"
301
+
302
+ # Add introduction and conclusion
303
+ sections.append((
304
+ "Introduction",
305
+ f"Write a comprehensive introduction for a research paper about {research_subject}."
306
+ ))
307
+
308
+ for i, chapter in enumerate(chapters, 1):
309
+ sections.append((
310
+ f"Chapter {i}: {chapter}",
311
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
312
+ ))
313
+
314
+ sections.append((
315
+ "Conclusion",
316
+ f"Write a conclusion section for a research paper about {research_subject}."
317
+ ))
318
+
319
+ # Generate content for each chapter with timing
320
+ for i, chapter in enumerate(chapters):
321
+ # Update chapter start time
322
+ steps[3]["subSteps"][i]["start_time"] = time.time()
323
+ steps[3]["subSteps"][i]["status"] = "in-progress"
324
+
325
+ yield "data: " + json.dumps({
326
+ "steps": steps,
327
+ "progress": 40 + (i * 50 / len(chapters)),
328
+ "current_step": 3,
329
+ "chapter_progress": {
330
+ "current": i + 1,
331
+ "total": len(chapters),
332
+ "chapter": chapter,
333
+ "percent": ((i + 1) / len(chapters)) * 100
334
+ }
335
+ }) + "\n\n"
336
+
337
+ try:
338
+ response = generate_section_content(
339
+ selected_model,
340
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
341
+ )
342
+
343
+ # Calculate and store duration
344
+ duration = time.time() - steps[3]["subSteps"][i]["start_time"]
345
+ steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
346
+ steps[3]["subSteps"][i]["status"] = "complete"
347
+
348
+ yield "data: " + json.dumps({
349
+ "steps": steps,
350
+ "progress": 40 + ((i + 1) * 50 / len(chapters)),
351
+ "current_step": 3,
352
+ "chapter_progress": {
353
+ "current": i + 1,
354
+ "total": len(chapters),
355
+ "chapter": chapter,
356
+ "percent": ((i + 1) / len(chapters)) * 100,
357
+ "duration": f"{duration:.1f}s"
358
+ }
359
+ }) + "\n\n"
360
+ except Exception as e:
361
+ duration = time.time() - steps[3]["subSteps"][i]["start_time"]
362
+ steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
363
+ steps[3]["subSteps"][i]["status"] = "error"
364
+ steps[3]["subSteps"][i]["message"] = str(e)
365
+
366
+ yield "data: " + json.dumps({
367
+ "steps": steps,
368
+ "progress": 40 + ((i + 1) * 50 / len(chapters)),
369
+ "current_step": 3,
370
+ "warning": f"Failed to generate chapter {i+1} after retries",
371
+ "chapter_progress": {
372
+ "current": i + 1,
373
+ "total": len(chapters),
374
+ "chapter": chapter,
375
+ "percent": ((i + 1) / len(chapters)) * 100,
376
+ "error": str(e)
377
+ }
378
+ }) + "\n\n"
379
+
380
+ steps[3]["status"] = "complete"
381
+ yield "data: " + json.dumps({
382
+ "steps": steps,
383
+ "progress": 90,
384
+ "current_step": 3,
385
+ "chapter_progress": {
386
+ "complete": True,
387
+ "total_chapters": len(chapters)
388
+ }
389
+ }) + "\n\n"
390
+
391
+ except Exception as e:
392
+ steps[1]["status"] = "error"
393
+ steps[1]["message"] = str(e)
394
+ yield "data: " + json.dumps({
395
+ "steps": steps,
396
+ "progress": 20,
397
+ "current_step": 1
398
+ }) + "\n\n"
399
+
400
+ # Fallback to manual structure
401
+ sections = get_manual_sections(research_subject)
402
+ steps[1]["message"] = "Falling back to manual structure"
403
+ yield "data: " + json.dumps({
404
+ "steps": steps,
405
+ "progress": 20,
406
+ "current_step": 1
407
+ }) + "\n\n"
408
+
409
+ try:
410
+ index_content = generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
411
+ sections[0] = ("Index", index_content)
412
+
413
+ steps[1]["status"] = "complete"
414
+ yield "data: " + json.dumps({
415
+ "steps": steps,
416
+ "progress": 25,
417
+ "current_step": 1
418
+ }) + "\n\n"
419
+ except Exception as e:
420
+ steps[1]["status"] = "error"
421
+ steps[1]["message"] = str(e)
422
+ yield "data: " + json.dumps({
423
+ "steps": steps,
424
+ "progress": 20,
425
+ "current_step": 1,
426
+ "error": "Failed to generate even fallback content"
427
+ }) + "\n\n"
428
+ return
429
+ else:
430
+ sections = get_manual_sections(research_subject)
431
+ steps[1]["status"] = "in-progress"
432
+ yield "data: " + json.dumps({
433
+ "steps": steps,
434
+ "progress": 10,
435
+ "current_step": 1
436
+ }) + "\n\n"
437
+
438
+ try:
439
+ index_content = generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
440
+ sections[0] = ("Index", index_content)
441
+
442
+ steps[1]["status"] = "complete"
443
+ yield "data: " + json.dumps({
444
+ "steps": steps,
445
+ "progress": 20,
446
+ "current_step": 1
447
+ }) + "\n\n"
448
+ except Exception as e:
449
+ steps[1]["status"] = "error"
450
+ steps[1]["message"] = str(e)
451
+ yield "data: " + json.dumps({
452
+ "steps": steps,
453
+ "progress": 20,
454
+ "current_step": 1,
455
+ "error": "Failed to generate manual index"
456
+ }) + "\n\n"
457
+ return
458
+
459
+ # Write introduction
460
+ steps[3]["status"] = "in-progress"
461
+ yield "data: " + json.dumps({
462
+ "steps": steps,
463
+ "progress": 40,
464
+ "current_step": 3
465
+ }) + "\n\n"
466
+
467
+ try:
468
+ introduction_content = generate_section_content(
469
+ selected_model,
470
+ f"Write a comprehensive introduction for a research paper about {research_subject}."
471
+ )
472
+
473
+ steps[3]["status"] = "complete"
474
+ yield "data: " + json.dumps({
475
+ "steps": steps,
476
+ "progress": 60,
477
+ "current_step": 3
478
+ }) + "\n\n"
479
+ except Exception as e:
480
+ steps[3]["status"] = "error"
481
+ steps[3]["message"] = str(e)
482
+ yield "data: " + json.dumps({
483
+ "steps": steps,
484
+ "progress": 60,
485
+ "current_step": 3,
486
+ "warning": "Failed to generate introduction after retries"
487
+ }) + "\n\n"
488
+
489
+ # Write conclusion
490
+ steps[4]["status"] = "in-progress"
491
+ yield "data: " + json.dumps({
492
+ "steps": steps,
493
+ "progress": 80,
494
+ "current_step": 4
495
+ }) + "\n\n"
496
+
497
+ try:
498
+ conclusion_content = generate_section_content(
499
+ selected_model,
500
+ f"Write a conclusion section for a research paper about {research_subject}."
501
+ )
502
+
503
+ steps[4]["status"] = "complete"
504
+ yield "data: " + json.dumps({
505
+ "steps": steps,
506
+ "progress": 90,
507
+ "current_step": 4
508
+ }) + "\n\n"
509
+ except Exception as e:
510
+ steps[4]["status"] = "error"
511
+ steps[4]["message"] = str(e)
512
+ yield "data: " + json.dumps({
513
+ "steps": steps,
514
+ "progress": 90,
515
+ "current_step": 4,
516
+ "warning": "Failed to generate conclusion after retries"
517
+ }) + "\n\n"
518
+
519
+ # Write the complete paper
520
+ full_path = os.path.join(app.config['UPLOAD_FOLDER'], md_filename)
521
+ with open(full_path, "w", encoding="utf-8") as f:
522
+ f.write(f"# Research Paper: {research_subject}\n\n")
523
+
524
+ for section_title, prompt in sections:
525
+ try:
526
+ if isinstance(prompt, str) and (prompt.startswith("##") or prompt.startswith("#")):
527
+ content = f"{prompt}\n\n"
528
+ else:
529
+ try:
530
+ response = generate_section_content(selected_model, prompt)
531
+ content = f"## {section_title}\n\n{response}\n\n"
532
+ except Exception as e:
533
+ content = f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n"
534
+ f.write(content)
535
+ except Exception as e:
536
+ f.write(f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n")
537
+
538
+ # Convert to Word
539
+ steps[5]["status"] = "in-progress"
540
+ yield "data: " + json.dumps({
541
+ "steps": steps,
542
+ "progress": 95,
543
+ "current_step": 5
544
+ }) + "\n\n"
545
+
546
+ try:
547
+ convert_to_word(md_filename, docx_filename)
548
+ steps[5]["status"] = "complete"
549
+ yield "data: " + json.dumps({
550
+ "steps": steps,
551
+ "progress": 100,
552
+ "current_step": 5,
553
+ "status": "complete",
554
+ "docx_file": docx_filename,
555
+ "md_file": md_filename
556
+ }) + "\n\n"
557
+ except Exception as e:
558
+ steps[5]["status"] = "error"
559
+ steps[5]["message"] = str(e)
560
+ yield "data: " + json.dumps({
561
+ "steps": steps,
562
+ "progress": 100,
563
+ "current_step": 5,
564
+ "status": "partial_success",
565
+ "message": f'Paper generated but Word conversion failed: {str(e)}',
566
+ "md_file": md_filename
567
+ }) + "\n\n"
568
+
569
+ except Exception as e:
570
+ yield "data: " + json.dumps({"error": f"Failed to generate paper: {str(e)}"}) + "\n\n"
571
+
572
+ return Response(generate(), mimetype="text/event-stream")
573
+
574
+ @app.route('/download/<filename>')
575
+ def download(filename):
576
+ safe_filename = secure_filename(filename)
577
+ return send_from_directory(
578
+ app.config['UPLOAD_FOLDER'],
579
+ safe_filename,
580
+ as_attachment=True
581
+ )
582
+
583
+ if __name__ == '__main__':
584
+ app.run(debug=True)
a.py → etc/trash/a.py RENAMED
File without changes
routes/api.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+ from flask import Blueprint, jsonify, request, Response, send_from_directory, copy_current_request_context
3
+ from functools import wraps
4
+ import time
5
+ import json
6
+ import os
7
+ import uuid
8
+ import random
9
+ from werkzeug.utils import secure_filename
10
+ from services.model_provider import ModelProvider
11
+ from services.document_generator import DocumentGenerator
12
+ from config import Config
13
+ from utils.retry_decorator import retry
14
+
15
+ api_bp = Blueprint('api', __name__)
16
+ model_provider = ModelProvider()
17
+ doc_generator = DocumentGenerator(Config.UPLOAD_FOLDER)
18
+
19
+ def sse_stream_required(f):
20
+ """Decorator to ensure SSE stream has request context"""
21
+ @wraps(f)
22
+ def decorated(*args, **kwargs):
23
+ @copy_current_request_context
24
+ def generator():
25
+ return f(*args, **kwargs)
26
+ return generator()
27
+ return decorated
28
+
29
+ def extract_chapters(index_content: str) -> List[str]:
30
+ """Extract chapter titles from index content"""
31
+ chapters = []
32
+ for line in index_content.split('\n'):
33
+ if line.strip().startswith('## '):
34
+ chapter_title = line.strip()[3:].strip()
35
+ if chapter_title.lower() not in ['introduction', 'conclusion', 'references']:
36
+ chapters.append(chapter_title)
37
+ return chapters if chapters else ["Literature Review", "Methodology", "Results and Discussion"]
38
+
39
+ def generate_automatic_sections(model: str, research_subject: str) -> List[Tuple[str, str]]:
40
+ """Generate sections automatically based on AI-generated index"""
41
+ try:
42
+ index_content = model_provider.generate_index_content(model, research_subject)
43
+ chapters = extract_chapters(index_content)
44
+
45
+ sections = [
46
+ ("Index", index_content),
47
+ ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}. Include background information, research objectives, and significance of the study.")
48
+ ]
49
+
50
+ for i, chapter in enumerate(chapters, 1):
51
+ sections.append(
52
+ (f"Chapter {i}: {chapter}",
53
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}. "
54
+ f"Provide comprehensive coverage of this aspect, including relevant theories, examples, and analysis.")
55
+ )
56
+
57
+ sections.append(
58
+ ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}. Summarize key findings, discuss implications, and suggest future research directions.")
59
+ )
60
+
61
+ return sections
62
+ except Exception as e:
63
+ raise Exception(f"Failed to generate automatic structure: {str(e)}")
64
+
65
+ def get_manual_sections(research_subject: str) -> List[Tuple[str, str]]:
66
+ """Get predefined manual sections"""
67
+ return [
68
+ ("Index", "[Index will be generated first]"),
69
+ ("Introduction", f"Write a comprehensive introduction for a research paper about {research_subject}."),
70
+ ("Chapter 1: Literature Review", f"Create a detailed literature review chapter about {research_subject}."),
71
+ ("Chapter 2: Methodology", f"Describe the research methodology for a study about {research_subject}."),
72
+ ("Chapter 3: Results and Discussion", f"Present hypothetical results and discussion for a research paper about {research_subject}. Analyze findings and compare with existing literature."),
73
+ ("Conclusion", f"Write a conclusion section for a research paper about {research_subject}.")
74
+ ]
75
+
76
+ def write_research_paper(md_filename: str, research_subject: str, sections: List[Tuple[str, str]], model: str) -> None:
77
+ """Write the research paper to a markdown file"""
78
+ full_path = os.path.join(Config.UPLOAD_FOLDER, md_filename)
79
+ with open(full_path, "w", encoding="utf-8") as f:
80
+ f.write(f"# Research Paper: {research_subject}\n\n")
81
+
82
+ for section_title, prompt in sections:
83
+ try:
84
+ if isinstance(prompt, str) and (prompt.startswith("##") or prompt.startswith("#")):
85
+ content = f"{prompt}\n\n"
86
+ else:
87
+ response = model_provider.generate_content(model, prompt)
88
+ content = f"## {section_title}\n\n{response}\n\n"
89
+ f.write(content)
90
+ except Exception as e:
91
+ f.write(f"## {section_title}\n\n[Error generating this section: {str(e)}]\n\n")
92
+
93
+ @api_bp.route('/models')
94
+ def get_models():
95
+ models = model_provider.get_available_models()
96
+ return jsonify(models)
97
+
98
+ @api_bp.route('/stream')
99
+ @sse_stream_required
100
+ def stream():
101
+ research_subject = request.args.get('subject', '').strip()
102
+ selected_model = request.args.get('model', 'gpt-4o')
103
+ structure_type = request.args.get('structure', 'automatic')
104
+
105
+ def generate():
106
+ try:
107
+ if not research_subject:
108
+ yield "data: " + json.dumps({"error": "Research subject is required"}) + "\n\n"
109
+ return
110
+
111
+ # Generate filenames
112
+ md_filename, docx_filename = doc_generator.generate_filename()
113
+
114
+ # Initial steps
115
+ steps = [
116
+ {"id": 0, "text": "Preparing document structure...", "status": "pending"},
117
+ {"id": 1, "text": "Generating index/table of contents...", "status": "pending"},
118
+ {"id": 2, "text": "Determining chapters...", "status": "pending"},
119
+ {"id": 3, "text": "Writing content...", "status": "pending", "subSteps": []},
120
+ {"id": 4, "text": "Finalizing document...", "status": "pending"},
121
+ {"id": 5, "text": "Converting to Word format...", "status": "pending"}
122
+ ]
123
+
124
+ # Initial progress update
125
+ yield "data: " + json.dumps({"steps": steps, "progress": 0}) + "\n\n"
126
+
127
+ # Step 0: Prepare
128
+ steps[0]["status"] = "in-progress"
129
+ yield "data: " + json.dumps({
130
+ "steps": steps,
131
+ "progress": 0,
132
+ "current_step": 0
133
+ }) + "\n\n"
134
+
135
+ sections = []
136
+ chapter_steps = []
137
+
138
+ if structure_type == 'automatic':
139
+ try:
140
+ # Step 1: Generate index
141
+ steps[1]["status"] = "in-progress"
142
+ yield "data: " + json.dumps({
143
+ "steps": steps,
144
+ "progress": 10,
145
+ "current_step": 1
146
+ }) + "\n\n"
147
+
148
+ index_content = model_provider.generate_index_content(selected_model, research_subject)
149
+ sections.append(("Index", index_content))
150
+
151
+ steps[1]["status"] = "complete"
152
+ yield "data: " + json.dumps({
153
+ "steps": steps,
154
+ "progress": 20,
155
+ "current_step": 1
156
+ }) + "\n\n"
157
+
158
+ # Step 2: Determine chapters
159
+ steps[2]["status"] = "in-progress"
160
+ yield "data: " + json.dumps({
161
+ "steps": steps,
162
+ "progress": 30,
163
+ "current_step": 2
164
+ }) + "\n\n"
165
+
166
+ chapters = extract_chapters(index_content)
167
+
168
+ # Create sub-steps for each chapter with initial timing info
169
+ chapter_substeps = [
170
+ {
171
+ "id": f"chapter_{i}",
172
+ "text": chapter,
173
+ "status": "pending",
174
+ "start_time": None,
175
+ "duration": None
176
+ }
177
+ for i, chapter in enumerate(chapters)
178
+ ]
179
+
180
+ steps[3]["subSteps"] = chapter_substeps
181
+
182
+ steps[2]["status"] = "complete"
183
+ yield "data: " + json.dumps({
184
+ "steps": steps,
185
+ "progress": 40,
186
+ "current_step": 2,
187
+ "update_steps": True
188
+ }) + "\n\n"
189
+
190
+ # Add introduction and conclusion
191
+ sections.append((
192
+ "Introduction",
193
+ f"Write a comprehensive introduction for a research paper about {research_subject}."
194
+ ))
195
+
196
+ for i, chapter in enumerate(chapters, 1):
197
+ sections.append((
198
+ f"Chapter {i}: {chapter}",
199
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
200
+ ))
201
+
202
+ sections.append((
203
+ "Conclusion",
204
+ f"Write a conclusion section for a research paper about {research_subject}."
205
+ ))
206
+
207
+ # Generate content for each chapter with timing
208
+ for i, chapter in enumerate(chapters):
209
+ # Update chapter start time
210
+ steps[3]["subSteps"][i]["start_time"] = time.time()
211
+ steps[3]["subSteps"][i]["status"] = "in-progress"
212
+
213
+ yield "data: " + json.dumps({
214
+ "steps": steps,
215
+ "progress": 40 + (i * 50 / len(chapters)),
216
+ "current_step": 3,
217
+ "chapter_progress": {
218
+ "current": i + 1,
219
+ "total": len(chapters),
220
+ "chapter": chapter,
221
+ "percent": ((i + 1) / len(chapters)) * 100
222
+ }
223
+ }) + "\n\n"
224
+
225
+ try:
226
+ response = model_provider.generate_content(
227
+ selected_model,
228
+ f"Write a detailed chapter about '{chapter}' for a research paper about {research_subject}."
229
+ )
230
+
231
+ # Calculate and store duration
232
+ duration = time.time() - steps[3]["subSteps"][i]["start_time"]
233
+ steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
234
+ steps[3]["subSteps"][i]["status"] = "complete"
235
+
236
+ yield "data: " + json.dumps({
237
+ "steps": steps,
238
+ "progress": 40 + ((i + 1) * 50 / len(chapters)),
239
+ "current_step": 3,
240
+ "chapter_progress": {
241
+ "current": i + 1,
242
+ "total": len(chapters),
243
+ "chapter": chapter,
244
+ "percent": ((i + 1) / len(chapters)) * 100,
245
+ "duration": f"{duration:.1f}s"
246
+ }
247
+ }) + "\n\n"
248
+ except Exception as e:
249
+ duration = time.time() - steps[3]["subSteps"][i]["start_time"]
250
+ steps[3]["subSteps"][i]["duration"] = f"{duration:.1f}s"
251
+ steps[3]["subSteps"][i]["status"] = "error"
252
+ steps[3]["subSteps"][i]["message"] = str(e)
253
+
254
+ yield "data: " + json.dumps({
255
+ "steps": steps,
256
+ "progress": 40 + ((i + 1) * 50 / len(chapters)),
257
+ "current_step": 3,
258
+ "warning": f"Failed to generate chapter {i+1} after retries",
259
+ "chapter_progress": {
260
+ "current": i + 1,
261
+ "total": len(chapters),
262
+ "chapter": chapter,
263
+ "percent": ((i + 1) / len(chapters)) * 100,
264
+ "error": str(e)
265
+ }
266
+ }) + "\n\n"
267
+
268
+ steps[3]["status"] = "complete"
269
+ yield "data: " + json.dumps({
270
+ "steps": steps,
271
+ "progress": 90,
272
+ "current_step": 3,
273
+ "chapter_progress": {
274
+ "complete": True,
275
+ "total_chapters": len(chapters)
276
+ }
277
+ }) + "\n\n"
278
+
279
+ except Exception as e:
280
+ steps[1]["status"] = "error"
281
+ steps[1]["message"] = str(e)
282
+ yield "data: " + json.dumps({
283
+ "steps": steps,
284
+ "progress": 20,
285
+ "current_step": 1
286
+ }) + "\n\n"
287
+
288
+ # Fallback to manual structure
289
+ sections = get_manual_sections(research_subject)
290
+ steps[1]["message"] = "Falling back to manual structure"
291
+ yield "data: " + json.dumps({
292
+ "steps": steps,
293
+ "progress": 20,
294
+ "current_step": 1
295
+ }) + "\n\n"
296
+
297
+ try:
298
+ index_content = model_provider.generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
299
+ sections[0] = ("Index", index_content)
300
+
301
+ steps[1]["status"] = "complete"
302
+ yield "data: " + json.dumps({
303
+ "steps": steps,
304
+ "progress": 25,
305
+ "current_step": 1
306
+ }) + "\n\n"
307
+ except Exception as e:
308
+ steps[1]["status"] = "error"
309
+ steps[1]["message"] = str(e)
310
+ yield "data: " + json.dumps({
311
+ "steps": steps,
312
+ "progress": 20,
313
+ "current_step": 1,
314
+ "error": "Failed to generate even fallback content"
315
+ }) + "\n\n"
316
+ return
317
+ else:
318
+ sections = get_manual_sections(research_subject)
319
+ steps[1]["status"] = "in-progress"
320
+ yield "data: " + json.dumps({
321
+ "steps": steps,
322
+ "progress": 10,
323
+ "current_step": 1
324
+ }) + "\n\n"
325
+
326
+ try:
327
+ index_content = model_provider.generate_index_content(selected_model, research_subject, [s[0] for s in sections[1:]])
328
+ sections[0] = ("Index", index_content)
329
+
330
+ steps[1]["status"] = "complete"
331
+ yield "data: " + json.dumps({
332
+ "steps": steps,
333
+ "progress": 20,
334
+ "current_step": 1
335
+ }) + "\n\n"
336
+ except Exception as e:
337
+ steps[1]["status"] = "error"
338
+ steps[1]["message"] = str(e)
339
+ yield "data: " + json.dumps({
340
+ "steps": steps,
341
+ "progress": 20,
342
+ "current_step": 1,
343
+ "error": "Failed to generate manual index"
344
+ }) + "\n\n"
345
+ return
346
+
347
+ # Write introduction
348
+ steps[3]["status"] = "in-progress"
349
+ yield "data: " + json.dumps({
350
+ "steps": steps,
351
+ "progress": 40,
352
+ "current_step": 3
353
+ }) + "\n\n"
354
+
355
+ try:
356
+ introduction_content = model_provider.generate_content(
357
+ selected_model,
358
+ f"Write a comprehensive introduction for a research paper about {research_subject}."
359
+ )
360
+
361
+ steps[3]["status"] = "complete"
362
+ yield "data: " + json.dumps({
363
+ "steps": steps,
364
+ "progress": 60,
365
+ "current_step": 3
366
+ }) + "\n\n"
367
+ except Exception as e:
368
+ steps[3]["status"] = "error"
369
+ steps[3]["message"] = str(e)
370
+ yield "data: " + json.dumps({
371
+ "steps": steps,
372
+ "progress": 60,
373
+ "current_step": 3,
374
+ "warning": "Failed to generate introduction after retries"
375
+ }) + "\n\n"
376
+
377
+ # Write conclusion
378
+ steps[4]["status"] = "in-progress"
379
+ yield "data: " + json.dumps({
380
+ "steps": steps,
381
+ "progress": 80,
382
+ "current_step": 4
383
+ }) + "\n\n"
384
+
385
+ try:
386
+ conclusion_content = model_provider.generate_content(
387
+ selected_model,
388
+ f"Write a conclusion section for a research paper about {research_subject}."
389
+ )
390
+
391
+ steps[4]["status"] = "complete"
392
+ yield "data: " + json.dumps({
393
+ "steps": steps,
394
+ "progress": 90,
395
+ "current_step": 4
396
+ }) + "\n\n"
397
+ except Exception as e:
398
+ steps[4]["status"] = "error"
399
+ steps[4]["message"] = str(e)
400
+ yield "data: " + json.dumps({
401
+ "steps": steps,
402
+ "progress": 90,
403
+ "current_step": 4,
404
+ "warning": "Failed to generate conclusion after retries"
405
+ }) + "\n\n"
406
+
407
+ # Write the complete paper
408
+ write_research_paper(md_filename, research_subject, sections, selected_model)
409
+
410
+ # Convert to Word
411
+ steps[5]["status"] = "in-progress"
412
+ yield "data: " + json.dumps({
413
+ "steps": steps,
414
+ "progress": 95,
415
+ "current_step": 5
416
+ }) + "\n\n"
417
+
418
+ try:
419
+ doc_generator.convert_to_word(md_filename, docx_filename)
420
+ steps[5]["status"] = "complete"
421
+ yield "data: " + json.dumps({
422
+ "steps": steps,
423
+ "progress": 100,
424
+ "current_step": 5,
425
+ "status": "complete",
426
+ "docx_file": docx_filename,
427
+ "md_file": md_filename
428
+ }) + "\n\n"
429
+ except Exception as e:
430
+ steps[5]["status"] = "error"
431
+ steps[5]["message"] = str(e)
432
+ yield "data: " + json.dumps({
433
+ "steps": steps,
434
+ "progress": 100,
435
+ "current_step": 5,
436
+ "status": "partial_success",
437
+ "message": f'Paper generated but Word conversion failed: {str(e)}',
438
+ "md_file": md_filename
439
+ }) + "\n\n"
440
+
441
+ except Exception as e:
442
+ yield "data: " + json.dumps({"error": f"Failed to generate paper: {str(e)}"}) + "\n\n"
443
+
444
+ return Response(generate(), mimetype="text/event-stream")
445
+
446
+ @api_bp.route('/download/<filename>')
447
+ def download(filename):
448
+ safe_filename = secure_filename(filename)
449
+ return send_from_directory(
450
+ Config.UPLOAD_FOLDER,
451
+ safe_filename,
452
+ as_attachment=True
453
+ )
routes/views.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template
2
+ from services.model_provider import ModelProvider
3
+
4
+ views_bp = Blueprint('views', __name__)
5
+ model_provider = ModelProvider()
6
+
7
+ @views_bp.route('/')
8
+ def index():
9
+ models = model_provider.get_available_models()
10
+ return render_template('index.html', models=models)
services/document_generator.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import subprocess
4
+ from typing import List, Tuple
5
+
6
+ class DocumentGenerator:
7
+ def __init__(self, upload_folder):
8
+ self.upload_folder = upload_folder
9
+ os.makedirs(self.upload_folder, exist_ok=True)
10
+
11
+ def generate_filename(self) -> Tuple[str, str]:
12
+ """Generate filenames with unique ID"""
13
+ unique_id = str(uuid.uuid4())[:8]
14
+ md_filename = f"research_paper_{unique_id}.md"
15
+ docx_filename = f"research_paper_{unique_id}.docx"
16
+ return md_filename, docx_filename
17
+
18
+ def convert_to_word(self, md_filename: str, docx_filename: str) -> None:
19
+ """Convert markdown file to Word document using Pandoc"""
20
+ md_path = os.path.join(self.upload_folder, md_filename)
21
+ docx_path = os.path.join(self.upload_folder, docx_filename)
22
+
23
+ command = [
24
+ "pandoc", md_path,
25
+ "-o", docx_path,
26
+ "--standalone",
27
+ "--table-of-contents",
28
+ "--toc-depth=3"
29
+ ]
30
+
31
+ if os.path.exists("reference.docx"):
32
+ command.extend(["--reference-doc", "reference.docx"])
33
+
34
+ subprocess.run(command, check=True)
services/model_provider.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import g4f
3
+ import requests
4
+ from typing import List, Dict, Optional
5
+ from config import Config
6
+ from utils.retry_decorator import retry
7
+ import logging
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class BaseAIService:
13
+ """Base class for AI services"""
14
+ def __init__(self, config: Dict):
15
+ self.config = config
16
+
17
+ def generate_content(self, model: str, prompt: str) -> str:
18
+ raise NotImplementedError
19
+
20
+ def get_available_models(self) -> List[str]:
21
+ raise NotImplementedError
22
+
23
+ class G4FService(BaseAIService):
24
+ """Service for g4f provider"""
25
+ def generate_content(self, model: str, prompt: str) -> str:
26
+ try:
27
+ response = g4f.ChatCompletion.create(
28
+ model=model,
29
+ messages=[{"role": "user", "content": prompt}],
30
+ stream=False
31
+ )
32
+ return str(response) if response else "[Empty response from model]"
33
+ except Exception as e:
34
+ logger.error(f"G4FService error: {str(e)}")
35
+ raise
36
+
37
+ def get_available_models(self) -> List[str]:
38
+ try:
39
+ models = sorted(g4f.models._all_models)
40
+ if 'gpt-4o' in models:
41
+ models.remove('gpt-4o')
42
+ models.insert(0, 'gpt-4o')
43
+ return models
44
+ except Exception as e:
45
+ logger.error(f"Failed to get G4F models: {str(e)}")
46
+ return ['gpt-4o', 'gpt-4', 'gpt-3.5-turbo', 'llama2-70b', 'claude-2']
47
+
48
+ class HuggingFaceService(BaseAIService):
49
+ """Service for HuggingFace Inference API"""
50
+ def __init__(self, config: Dict):
51
+ super().__init__(config)
52
+ self.api_key = self.config.get('api_key', os.getenv('HUGGINGFACE_API_KEY'))
53
+ self.api_url = self.config.get('api_url', "https://api-inference.huggingface.co/models")
54
+
55
+ def generate_content(self, model: str, prompt: str) -> str:
56
+ headers = {
57
+ "Authorization": f"Bearer {self.api_key}",
58
+ "Content-Type": "application/json"
59
+ }
60
+
61
+ payload = {
62
+ "inputs": prompt,
63
+ "parameters": {
64
+ "max_new_tokens": self.config.get('max_tokens', 1000),
65
+ "temperature": self.config.get('temperature', 0.7)
66
+ }
67
+ }
68
+
69
+ try:
70
+ response = requests.post(
71
+ f"{self.api_url}/{model}",
72
+ headers=headers,
73
+ json=payload
74
+ )
75
+ response.raise_for_status()
76
+ return response.json()[0]['generated_text']
77
+ except Exception as e:
78
+ logger.error(f"HuggingFace API error: {str(e)}")
79
+ raise
80
+
81
+ def get_available_models(self) -> List[str]:
82
+ # Note: HuggingFace doesn't provide a simple way to list all available models
83
+ # You would need to maintain your own list or use their API with pagination
84
+ return [
85
+ "meta-llama/Llama-2-70b-chat-hf",
86
+ "mistralai/Mixtral-8x7B-Instruct-v0.1",
87
+ "google/gemma-7b-it"
88
+ ]
89
+
90
+ class TogetherAIService(BaseAIService):
91
+ """Service for Together AI API"""
92
+ def __init__(self, config: Dict):
93
+ super().__init__(config)
94
+ self.api_key = self.config.get('api_key', os.getenv('TOGETHER_API_KEY'))
95
+ self.api_url = self.config.get('api_url', "https://api.together.xyz/v1/completions")
96
+
97
+ def generate_content(self, model: str, prompt: str) -> str:
98
+ headers = {
99
+ "Authorization": f"Bearer {self.api_key}",
100
+ "Content-Type": "application/json"
101
+ }
102
+
103
+ payload = {
104
+ "model": model,
105
+ "prompt": prompt,
106
+ "max_tokens": self.config.get('max_tokens', 1000),
107
+ "temperature": self.config.get('temperature', 0.7),
108
+ "top_p": self.config.get('top_p', 0.9),
109
+ "stop": self.config.get('stop_sequences', ["</s>"])
110
+ }
111
+
112
+ try:
113
+ response = requests.post(
114
+ self.api_url,
115
+ headers=headers,
116
+ json=payload
117
+ )
118
+ response.raise_for_status()
119
+ return response.json()['choices'][0]['text']
120
+ except Exception as e:
121
+ logger.error(f"Together AI API error: {str(e)}")
122
+ raise
123
+
124
+ def get_available_models(self) -> List[str]:
125
+ return [
126
+ "togethercomputer/llama-2-70b-chat",
127
+ "mistralai/Mixtral-8x7B-Instruct-v0.1",
128
+ "togethercomputer/CodeLlama-34b-Instruct"
129
+ ]
130
+
131
+ import openai
132
+ from typing import List, Dict
133
+ from datetime import datetime
134
+
135
+ class OpenAIService(BaseAIService):
136
+ """Service for OpenAI API with custom base URL support"""
137
+ def __init__(self, config: Dict):
138
+ super().__init__(config)
139
+ self.api_key = self.config.get('api_key', os.getenv('OPENAI_API_KEY'))
140
+ self.organization = self.config.get('organization', os.getenv('OPENAI_ORG_ID'))
141
+ self.base_url = self.config.get('base_url', "https://api.openai.com/v1")
142
+
143
+ # Initialize the OpenAI client with custom configuration
144
+ self.client = openai.OpenAI(
145
+ api_key=self.api_key,
146
+ organization=self.organization,
147
+ base_url=self.base_url
148
+ )
149
+
150
+ def generate_content(self, model: str, prompt: str) -> str:
151
+ try:
152
+ response = self.client.chat.completions.create(
153
+ model=model,
154
+ messages=[{"role": "user", "content": prompt}],
155
+ temperature=self.config.get('temperature', 0.7),
156
+ max_tokens=self.config.get('max_tokens', 1000),
157
+ top_p=self.config.get('top_p', 0.9),
158
+ frequency_penalty=self.config.get('frequency_penalty', 0),
159
+ presence_penalty=self.config.get('presence_penalty', 0)
160
+ )
161
+ return response.choices[0].message.content
162
+ except Exception as e:
163
+ logger.error(f"OpenAI API error: {str(e)}")
164
+ raise
165
+
166
+ def get_available_models(self) -> List[str]:
167
+ try:
168
+ # Cache model list for 1 hour to avoid frequent API calls
169
+ if hasattr(self, '_cached_models') and \
170
+ (datetime.now() - self._cache_time).seconds < 3600:
171
+ return self._cached_models
172
+
173
+ models = self.client.models.list()
174
+ self._cached_models = sorted([m.id for m in models.data
175
+ if m.id.startswith('gpt-')])
176
+ self._cache_time = datetime.now()
177
+ return self._cached_models
178
+ except Exception as e:
179
+ logger.error(f"Failed to get OpenAI models: {str(e)}")
180
+ # Fallback to known models if API fails
181
+ return [
182
+ "gpt-4-turbo-preview",
183
+ "gpt-4",
184
+ "gpt-3.5-turbo",
185
+ "gpt-4-32k",
186
+ "gpt-4-vision-preview"
187
+ ]
188
+
189
+ class ModelProvider:
190
+ """Main provider class that routes requests to the configured service"""
191
+ def __init__(self):
192
+ self.service = self._initialize_service()
193
+
194
+ def _initialize_service(self) -> BaseAIService:
195
+ """Initialize the appropriate AI service based on config"""
196
+ provider = Config.AI_PROVIDER.lower()
197
+ provider_config = Config.AI_PROVIDER_CONFIG.get(provider, {})
198
+
199
+ if provider == 'g4f':
200
+ return G4FService(provider_config)
201
+ elif provider == 'huggingface':
202
+ return HuggingFaceService(provider_config)
203
+ elif provider == 'together':
204
+ return TogetherAIService(provider_config)
205
+ elif provider == 'openai':
206
+ return OpenAIService(provider_config)
207
+ else:
208
+ raise ValueError(f"Unsupported AI provider: {provider}")
209
+
210
+ @retry()
211
+ def generate_content(self, model: str, prompt: str) -> str:
212
+ """Generate content using the configured service"""
213
+ return self.service.generate_content(model, prompt)
214
+
215
+ def generate_index_content(self, model: str, research_subject: str, manual_chapters: List[str] = None) -> str:
216
+ """Generate index content for the research paper"""
217
+ try:
218
+ if manual_chapters:
219
+ prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject} with these chapters: " + \
220
+ ", ".join(manual_chapters) + ". Include section headings in markdown format."
221
+ else:
222
+ prompt = f"Generate a detailed index/table of contents for a research paper about {research_subject}. Include chapter titles and section headings in markdown format."
223
+
224
+ return self.generate_content(model, prompt)
225
+ except Exception as e:
226
+ raise Exception(f"Failed to generate index: {str(e)}")
227
+
228
+ def get_available_models(self) -> List[str]:
229
+ """Get available models from the configured service"""
230
+ return self.service.get_available_models()
utils/retry_decorator.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import random
3
+ from functools import wraps
4
+
5
+ def retry(max_retries=3, initial_delay=1, backoff_factor=2):
6
+ def decorator(func):
7
+ @wraps(func)
8
+ def wrapper(*args, **kwargs):
9
+ retries = 0
10
+ delay = initial_delay
11
+
12
+ while retries < max_retries:
13
+ try:
14
+ return func(*args, **kwargs)
15
+ except (SystemExit, KeyboardInterrupt):
16
+ raise
17
+ except Exception as e:
18
+ retries += 1
19
+ if retries >= max_retries:
20
+ raise # Re-raise the last exception if max retries reached
21
+
22
+ # Exponential backoff with some randomness
23
+ time.sleep(delay + random.uniform(0, 0.5))
24
+ delay *= backoff_factor
25
+ return wrapper
26
+ return decorator