Spaces:
Sleeping
Sleeping
added pdf download report analysis
Browse files
app.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
import google.generativeai as genai
|
3 |
import os
|
@@ -13,6 +15,9 @@ import tempfile
|
|
13 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
14 |
import time
|
15 |
import os
|
|
|
|
|
|
|
16 |
|
17 |
|
18 |
|
@@ -258,12 +263,175 @@ class RepositoryAnalyzer:
|
|
258 |
"total_contributors": len(contributor_data),
|
259 |
"contributors": contributor_data
|
260 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
@retry(
|
263 |
retry=retry_if_exception_type(Exception),
|
264 |
stop=stop_after_attempt(3),
|
265 |
wait=wait_exponential(multiplier=1, min=4, max=10)
|
266 |
)
|
|
|
|
|
267 |
def analyze_repository(repo_url: str, progress=gr.Progress()) -> Tuple[str, str, str]:
|
268 |
"""Analyze repository and generate LLM summary with rate limit handling"""
|
269 |
try:
|
@@ -363,12 +531,31 @@ Please provide detailed analysis for each section while maintaining the formatti
|
|
363 |
json.dump(analysis_data, f, indent=2)
|
364 |
analysis_file = f.name
|
365 |
|
366 |
-
progress(
|
367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
|
369 |
except Exception as e:
|
370 |
error_message = f"β Error analyzing repository: {str(e)}"
|
371 |
-
return "", "", error_message
|
372 |
|
373 |
def create_chat_session() -> Any:
|
374 |
"""Create a new chat session for follow-up questions"""
|
@@ -426,6 +613,7 @@ def ask_question(question: str, analysis_file: str, chat_history: List[Tuple[str
|
|
426 |
|
427 |
|
428 |
|
|
|
429 |
# Create Gradio interface
|
430 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
431 |
gr.Markdown("""
|
@@ -435,8 +623,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
435 |
1. π Analyze repository structure and patterns
|
436 |
2. π‘ Generate insights about development practices
|
437 |
3. π Allow you to ask follow-up questions about the analysis
|
438 |
-
|
439 |
-
Enter a GitHub repository URL (e.g., `https://github.com/owner/repo`)
|
440 |
""")
|
441 |
|
442 |
with gr.Row():
|
@@ -445,16 +632,24 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
445 |
placeholder="https://github.com/owner/repo",
|
446 |
scale=4
|
447 |
)
|
448 |
-
analyze_btn = gr.Button("π Analyze", variant="primary", scale=1)
|
449 |
|
450 |
-
|
|
|
|
|
|
|
|
|
|
|
451 |
status_msg = gr.Markdown("", elem_id="status_message")
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
with gr.Row():
|
454 |
-
|
455 |
-
summary = gr.Markdown(
|
456 |
-
label="Analysis Summary",
|
457 |
-
)
|
458 |
|
459 |
with gr.Row():
|
460 |
chatbot = gr.Chatbot(
|
@@ -472,13 +667,32 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
472 |
ask_btn = gr.Button("π Ask", variant="primary", scale=1)
|
473 |
clear_btn = gr.Button("ποΈ Clear Chat", variant="secondary", scale=1)
|
474 |
|
475 |
-
# Hidden
|
476 |
analysis_file = gr.State("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
477 |
|
478 |
def clear_outputs():
|
479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
480 |
|
481 |
-
#
|
482 |
analyze_btn.click(
|
483 |
fn=lambda: "β³ Analysis in progress...",
|
484 |
inputs=None,
|
@@ -487,15 +701,30 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
487 |
).then(
|
488 |
analyze_repository,
|
489 |
inputs=[repo_url],
|
490 |
-
outputs=[summary, analysis_file, status_msg]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
491 |
)
|
492 |
|
493 |
ask_btn.click(
|
494 |
ask_question,
|
495 |
inputs=[question, analysis_file, chatbot],
|
496 |
-
outputs=[chatbot]
|
497 |
).then(
|
498 |
-
lambda: "",
|
499 |
None,
|
500 |
question,
|
501 |
queue=False
|
@@ -504,10 +733,11 @@ with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
504 |
clear_btn.click(
|
505 |
clear_outputs,
|
506 |
inputs=None,
|
507 |
-
outputs=[summary, chatbot, question, status_msg]
|
508 |
-
queue=False
|
509 |
)
|
510 |
|
|
|
|
|
511 |
# Launch the app
|
512 |
if __name__ == "__main__":
|
513 |
app.launch(
|
|
|
1 |
+
!pip install requests
|
2 |
+
|
3 |
import gradio as gr
|
4 |
import google.generativeai as genai
|
5 |
import os
|
|
|
15 |
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
16 |
import time
|
17 |
import os
|
18 |
+
import re
|
19 |
+
from fpdf import FPDF
|
20 |
+
import requests
|
21 |
|
22 |
|
23 |
|
|
|
263 |
"total_contributors": len(contributor_data),
|
264 |
"contributors": contributor_data
|
265 |
}
|
266 |
+
def create_pdf_from_markdown(markdown_text: str, filename: str) -> str:
|
267 |
+
"""Convert markdown text to PDF"""
|
268 |
+
class PDF(FPDF):
|
269 |
+
def header(self):
|
270 |
+
self.set_font('Arial', 'B', 12)
|
271 |
+
self.cell(0, 10, 'Repository Analysis Report', 0, 1, 'C')
|
272 |
+
self.ln(10)
|
273 |
+
|
274 |
+
def footer(self):
|
275 |
+
self.set_y(-15)
|
276 |
+
self.set_font('Arial', 'I', 8)
|
277 |
+
self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
|
278 |
+
|
279 |
+
pdf = PDF()
|
280 |
+
pdf.add_page()
|
281 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
282 |
+
|
283 |
+
# Process markdown sections
|
284 |
+
sections = markdown_text.split('\n## ')
|
285 |
+
|
286 |
+
# Handle main title
|
287 |
+
if sections[0].startswith('# '):
|
288 |
+
title = sections[0].split('\n')[0].replace('# ', '')
|
289 |
+
pdf.set_font('Arial', 'B', 16)
|
290 |
+
pdf.cell(0, 10, title, 0, 1, 'C')
|
291 |
+
pdf.ln(5)
|
292 |
+
content = '\n'.join(sections[0].split('\n')[1:])
|
293 |
+
sections[0] = content
|
294 |
+
|
295 |
+
for section in sections:
|
296 |
+
if section:
|
297 |
+
# Extract section title and content
|
298 |
+
lines = section.split('\n')
|
299 |
+
if section == sections[0]: # First section (after main title)
|
300 |
+
section_title = ''
|
301 |
+
content = lines
|
302 |
+
else:
|
303 |
+
section_title = lines[0]
|
304 |
+
content = lines[1:]
|
305 |
+
|
306 |
+
# Add section title
|
307 |
+
if section_title:
|
308 |
+
pdf.set_font('Arial', 'B', 14)
|
309 |
+
# Remove emojis from section titles
|
310 |
+
clean_title = re.sub(r'[^\x00-\x7F]+', '', section_title)
|
311 |
+
pdf.cell(0, 10, clean_title.strip(), 0, 1, 'L')
|
312 |
+
pdf.ln(5)
|
313 |
+
|
314 |
+
# Add content
|
315 |
+
pdf.set_font('Arial', '', 11)
|
316 |
+
for line in content:
|
317 |
+
if line.strip():
|
318 |
+
# Remove markdown formatting and emojis
|
319 |
+
clean_line = re.sub(r'[\*\[\]]', '', line)
|
320 |
+
clean_line = re.sub(r'[^\x00-\x7F]+', '', clean_line)
|
321 |
+
|
322 |
+
if line.startswith('- '):
|
323 |
+
pdf.cell(10, 5, '', 0, 0)
|
324 |
+
pdf.multi_cell(0, 5, clean_line[2:])
|
325 |
+
else:
|
326 |
+
pdf.multi_cell(0, 5, clean_line)
|
327 |
+
pdf.ln(5)
|
328 |
+
|
329 |
+
# Save PDF
|
330 |
+
pdf_path = f"{filename}.pdf"
|
331 |
+
pdf.output(pdf_path)
|
332 |
+
return pdf_path
|
333 |
+
|
334 |
+
def download_noto_font():
|
335 |
+
"""Download Google's Noto Color Emoji font if not already present"""
|
336 |
+
font_path = "NotoColorEmoji.ttf"
|
337 |
+
if not os.path.exists(font_path):
|
338 |
+
url = "https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf"
|
339 |
+
response = requests.get(url)
|
340 |
+
with open(font_path, "wb") as f:
|
341 |
+
f.write(response.content)
|
342 |
+
return font_path
|
343 |
+
|
344 |
+
class PDFWithEmoji(FPDF):
|
345 |
+
def __init__(self):
|
346 |
+
super().__init__()
|
347 |
+
self.add_font('DejaVu', '', 'DejaVuSansCondensed.ttf', uni=True)
|
348 |
+
self.add_font('Noto', '', 'NotoColorEmoji.ttf', uni=True)
|
349 |
+
|
350 |
+
def header(self):
|
351 |
+
self.set_font('DejaVu', '', 12)
|
352 |
+
self.cell(0, 10, 'π Repository Analysis Report', 0, 1, 'C')
|
353 |
+
self.ln(10)
|
354 |
+
|
355 |
+
def footer(self):
|
356 |
+
self.set_y(-15)
|
357 |
+
self.set_font('DejaVu', '', 8)
|
358 |
+
self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
|
359 |
+
|
360 |
+
def write_with_emoji(self, text):
|
361 |
+
"""Write text with proper emoji support"""
|
362 |
+
self.set_font('DejaVu', '', 11)
|
363 |
+
self.multi_cell(0, 5, text)
|
364 |
+
|
365 |
+
def create_pdf_report(markdown_text: str) -> str:
|
366 |
+
"""Create a PDF report with full emoji and Unicode support"""
|
367 |
+
# Ensure required fonts are available
|
368 |
+
if not os.path.exists("DejaVuSansCondensed.ttf"):
|
369 |
+
url = "https://github.com/dejavu-fonts/dejavu-fonts/raw/master/ttf/DejaVuSansCondensed.ttf"
|
370 |
+
response = requests.get(url)
|
371 |
+
with open("DejaVuSansCondensed.ttf", "wb") as f:
|
372 |
+
f.write(response.content)
|
373 |
+
|
374 |
+
download_noto_font()
|
375 |
+
|
376 |
+
# Create PDF
|
377 |
+
pdf = PDFWithEmoji()
|
378 |
+
pdf.add_page()
|
379 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
380 |
+
|
381 |
+
# Process markdown text
|
382 |
+
sections = markdown_text.split('\n## ')
|
383 |
+
|
384 |
+
# Handle main title
|
385 |
+
if sections[0].startswith('# '):
|
386 |
+
title = sections[0].split('\n')[0].replace('# ', '')
|
387 |
+
pdf.set_font('DejaVu', '', 16)
|
388 |
+
pdf.cell(0, 10, title, 0, 1, 'C')
|
389 |
+
pdf.ln(5)
|
390 |
+
content = '\n'.join(sections[0].split('\n')[1:])
|
391 |
+
sections[0] = content
|
392 |
+
|
393 |
+
# Process each section
|
394 |
+
for section in sections:
|
395 |
+
if section:
|
396 |
+
lines = section.split('\n')
|
397 |
+
if section == sections[0]: # First section
|
398 |
+
section_title = ''
|
399 |
+
content = lines
|
400 |
+
else:
|
401 |
+
section_title = lines[0]
|
402 |
+
content = lines[1:]
|
403 |
+
|
404 |
+
# Add section title
|
405 |
+
if section_title:
|
406 |
+
pdf.set_font('DejaVu', '', 14)
|
407 |
+
pdf.cell(0, 10, section_title, 0, 1, 'L')
|
408 |
+
pdf.ln(5)
|
409 |
+
|
410 |
+
# Add content
|
411 |
+
pdf.set_font('DejaVu', '', 11)
|
412 |
+
for line in content:
|
413 |
+
if line.strip():
|
414 |
+
if line.strip().startswith('- '):
|
415 |
+
pdf.cell(10, 5, 'β’', 0, 0)
|
416 |
+
pdf.write_with_emoji(line.strip()[2:])
|
417 |
+
pdf.ln()
|
418 |
+
else:
|
419 |
+
pdf.write_with_emoji(line.strip())
|
420 |
+
pdf.ln()
|
421 |
+
|
422 |
+
# Save PDF
|
423 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
424 |
+
pdf_path = f"repo_analysis_{timestamp}.pdf"
|
425 |
+
pdf.output(pdf_path)
|
426 |
+
return pdf_path
|
427 |
|
428 |
@retry(
|
429 |
retry=retry_if_exception_type(Exception),
|
430 |
stop=stop_after_attempt(3),
|
431 |
wait=wait_exponential(multiplier=1, min=4, max=10)
|
432 |
)
|
433 |
+
|
434 |
+
|
435 |
def analyze_repository(repo_url: str, progress=gr.Progress()) -> Tuple[str, str, str]:
|
436 |
"""Analyze repository and generate LLM summary with rate limit handling"""
|
437 |
try:
|
|
|
531 |
json.dump(analysis_data, f, indent=2)
|
532 |
analysis_file = f.name
|
533 |
|
534 |
+
progress(0.9, desc="Generating PDF report...")
|
535 |
+
try:
|
536 |
+
pdf_path = create_pdf_report(response.text)
|
537 |
+
except Exception as pdf_error:
|
538 |
+
print(f"PDF generation error: {str(pdf_error)}")
|
539 |
+
pdf_path = ""
|
540 |
+
|
541 |
+
|
542 |
+
|
543 |
+
# Generate PDF
|
544 |
+
pdf_path = create_pdf_from_markdown(response.text, f"analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
545 |
+
|
546 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
|
547 |
+
json.dump(analysis_data, f, indent=2)
|
548 |
+
analysis_file = f.name
|
549 |
+
|
550 |
+
|
551 |
+
|
552 |
+
|
553 |
+
progress(1.0, desc="β¨ Analysis complete!")
|
554 |
+
return response.text, analysis_file, pdf_path, "β
Analysis completed successfully!"
|
555 |
|
556 |
except Exception as e:
|
557 |
error_message = f"β Error analyzing repository: {str(e)}"
|
558 |
+
return "", "", "", error_message # Return 4 empty values when there's an error
|
559 |
|
560 |
def create_chat_session() -> Any:
|
561 |
"""Create a new chat session for follow-up questions"""
|
|
|
613 |
|
614 |
|
615 |
|
616 |
+
# Create Gradio interface
|
617 |
# Create Gradio interface
|
618 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
619 |
gr.Markdown("""
|
|
|
623 |
1. π Analyze repository structure and patterns
|
624 |
2. π‘ Generate insights about development practices
|
625 |
3. π Allow you to ask follow-up questions about the analysis
|
626 |
+
4. π Generate a downloadable PDF report
|
|
|
627 |
""")
|
628 |
|
629 |
with gr.Row():
|
|
|
632 |
placeholder="https://github.com/owner/repo",
|
633 |
scale=4
|
634 |
)
|
|
|
635 |
|
636 |
+
with gr.Row():
|
637 |
+
with gr.Column(scale=1):
|
638 |
+
analyze_btn = gr.Button("π Analyze", variant="primary")
|
639 |
+
download_pdf_btn = gr.Button("π Download PDF", variant="secondary")
|
640 |
+
|
641 |
+
# Status message
|
642 |
status_msg = gr.Markdown("", elem_id="status_message")
|
643 |
+
|
644 |
+
# File output for PDF
|
645 |
+
pdf_output = gr.File(
|
646 |
+
label="Analysis Report",
|
647 |
+
visible=False,
|
648 |
+
interactive=True
|
649 |
+
)
|
650 |
+
|
651 |
with gr.Row():
|
652 |
+
summary = gr.Markdown(label="Analysis Summary")
|
|
|
|
|
|
|
653 |
|
654 |
with gr.Row():
|
655 |
chatbot = gr.Chatbot(
|
|
|
667 |
ask_btn = gr.Button("π Ask", variant="primary", scale=1)
|
668 |
clear_btn = gr.Button("ποΈ Clear Chat", variant="secondary", scale=1)
|
669 |
|
670 |
+
# Hidden states
|
671 |
analysis_file = gr.State("")
|
672 |
+
current_pdf_path = gr.State("")
|
673 |
+
|
674 |
+
def handle_pdf_download(pdf_path):
|
675 |
+
"""Handle PDF download when button is clicked"""
|
676 |
+
if pdf_path and os.path.exists(pdf_path):
|
677 |
+
return {
|
678 |
+
pdf_output: pdf_path
|
679 |
+
}
|
680 |
+
return {
|
681 |
+
pdf_output: None
|
682 |
+
}
|
683 |
|
684 |
def clear_outputs():
|
685 |
+
"""Clear all outputs"""
|
686 |
+
return {
|
687 |
+
summary: "",
|
688 |
+
chatbot: [],
|
689 |
+
question: "",
|
690 |
+
status_msg: "",
|
691 |
+
pdf_output: None,
|
692 |
+
current_pdf_path: ""
|
693 |
+
}
|
694 |
|
695 |
+
# Event handlers
|
696 |
analyze_btn.click(
|
697 |
fn=lambda: "β³ Analysis in progress...",
|
698 |
inputs=None,
|
|
|
701 |
).then(
|
702 |
analyze_repository,
|
703 |
inputs=[repo_url],
|
704 |
+
outputs=[summary, analysis_file, current_pdf_path, status_msg]
|
705 |
+
).then(
|
706 |
+
lambda: gr.update(visible=True),
|
707 |
+
None,
|
708 |
+
download_pdf_btn
|
709 |
+
)
|
710 |
+
|
711 |
+
# PDF download handler
|
712 |
+
download_pdf_btn.click(
|
713 |
+
handle_pdf_download,
|
714 |
+
inputs=[current_pdf_path],
|
715 |
+
outputs=pdf_output
|
716 |
+
).then(
|
717 |
+
lambda: gr.update(visible=True),
|
718 |
+
None,
|
719 |
+
pdf_output
|
720 |
)
|
721 |
|
722 |
ask_btn.click(
|
723 |
ask_question,
|
724 |
inputs=[question, analysis_file, chatbot],
|
725 |
+
outputs=[chatbot]
|
726 |
).then(
|
727 |
+
lambda: "",
|
728 |
None,
|
729 |
question,
|
730 |
queue=False
|
|
|
733 |
clear_btn.click(
|
734 |
clear_outputs,
|
735 |
inputs=None,
|
736 |
+
outputs=[summary, chatbot, question, status_msg, pdf_output, current_pdf_path]
|
|
|
737 |
)
|
738 |
|
739 |
+
|
740 |
+
|
741 |
# Launch the app
|
742 |
if __name__ == "__main__":
|
743 |
app.launch(
|