Mohammed Foud
commited on
Commit
·
bef7112
1
Parent(s):
444a04d
Add application file
Browse files- .env +41 -0
- app.py +13 -560
- config.py +35 -0
- d.sh +3 -0
- etc/app.py +584 -0
- a.py → etc/trash/a.py +0 -0
- routes/api.py +453 -0
- routes/views.py +10 -0
- services/document_generator.py +34 -0
- services/model_provider.py +230 -0
- utils/retry_decorator.py +26 -0
.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
|
2 |
-
import
|
3 |
-
import
|
4 |
-
import
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
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 |
-
|
58 |
-
|
59 |
-
|
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 |
-
|
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
|