-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add file attachment support for chat agent with document parsing #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,147 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| File Parser Service for extracting text content from documents. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Handles: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - PDF text extraction using PyMuPDF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - DOCX text extraction using python-docx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - PPTX text extraction using python-pptx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from io import BytesIO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class FileParserService: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Service for parsing and extracting text from various document formats.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_pdf(self, file_bytes: bytes, filename: str = "document.pdf") -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Extract text content from a PDF file.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fitz # PyMuPDF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc = fitz.open(stream=file_bytes, filetype="pdf") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pages = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_text = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for page_num, page in enumerate(doc, start=1): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = page.get_text("text") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pages.append({"page_number": page_num, "text": text.strip()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_text.append(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata = doc.metadata or {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "success": True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "filename": filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "file_type": "application/pdf", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "page_count": len(pages), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pages": pages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "full_text": "\n\n".join(full_text).strip(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "metadata": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "title": metadata.get("title", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "author": metadata.get("author", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "subject": metadata.get("subject", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Error parsing PDF: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"success": False, "filename": filename, "file_type": "application/pdf", "error": str(e)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -path "*crawler*" -name "*.py" | grep -i parser | head -10Repository: tale-project/tale Length of output: 178 🏁 Script executed: find . -type f -name "file_parser_service.py"Repository: tale-project/tale Length of output: 106 🏁 Script executed: git ls-files | grep -i "file_parser"Repository: tale-project/tale Length of output: 104 🏁 Script executed: cat -n services/crawler/app/file_parser_service.py | head -100Repository: tale-project/tale Length of output: 4484 🏁 Script executed: cat -n services/crawler/app/file_parser_service.py | sed -n '54-110p'Repository: tale-project/tale Length of output: 112 🏁 Script executed: tail -60 services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 2992 🏁 Script executed: find . -name "pyproject.toml" -o -name "setup.py" -o -name "setup.cfg" | head -3 | xargs grep -l "python" 2>/dev/null | head -1Repository: tale-project/tale Length of output: 147 Ensure PDF resources close on all paths and use The try:
- doc = fitz.open(stream=file_bytes, filetype="pdf")
- pages = []
- full_text = []
+ doc = fitz.open(stream=file_bytes, filetype="pdf")
+ try:
+ pages: list[dict[str, Any]] = []
+ full_text: list[str] = []
- for page_num, page in enumerate(doc, start=1):
- text = page.get_text("text")
- pages.append({"page_number": page_num, "text": text.strip()})
- full_text.append(text)
+ for page_num, page in enumerate(doc, start=1):
+ text = page.get_text("text")
+ pages.append({"page_number": page_num, "text": text.strip()})
+ full_text.append(text)
- metadata = doc.metadata or {}
- doc.close()
+ metadata = doc.metadata or {}
+ finally:
+ doc.close()
return {
"success": True,
"filename": filename,
"file_type": "application/pdf",
"page_count": len(pages),
"pages": pages,
"full_text": "\n\n".join(full_text).strip(),
"metadata": {
"title": metadata.get("title", ""),
"author": metadata.get("author", ""),
"subject": metadata.get("subject", ""),
},
}
- except Exception as e:
- logger.error(f"Error parsing PDF: {e}")
- return {"success": False, "filename": filename, "file_type": "application/pdf", "error": str(e)}
+ except Exception as e:
+ logger.exception("Error parsing PDF")
+ return {
+ "success": False,
+ "filename": filename,
+ "file_type": "application/pdf",
+ "error": str(e),
+ }🧰 Tools🪛 Ruff (0.14.8)50-50: Do not catch blind exception: (BLE001) 51-51: Use Replace with (TRY400) 51-51: Logging statement uses f-string (G004) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_docx(self, file_bytes: bytes, filename: str = "document.docx") -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Extract text content from a DOCX file.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from docx import Document | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| doc = Document(BytesIO(file_bytes)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| paragraphs = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for para in doc.paragraphs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = para.text.strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| paragraphs.append({"text": text, "style": para.style.name if para.style else None}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tables = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for table in doc.tables: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table_data = [[cell.text.strip() for cell in row.cells] for row in table.rows] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if table_data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tables.append(table_data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_text = "\n".join(p["text"] for p in paragraphs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| core_props = doc.core_properties | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "success": True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "filename": filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "file_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "paragraph_count": len(paragraphs), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "table_count": len(tables), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "paragraphs": paragraphs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "tables": tables, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "full_text": full_text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "metadata": {"title": core_props.title or "", "author": core_props.author or ""}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Error parsing DOCX: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"success": False, "filename": filename, "error": str(e)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "file_parser_service.py" -type fRepository: tale-project/tale Length of output: 106 🏁 Script executed: git ls-files | grep -i "file_parser" | head -20Repository: tale-project/tale Length of output: 104 🏁 Script executed: find . -path "*/crawler/app/*" -name "*.py" | head -20Repository: tale-project/tale Length of output: 445 🏁 Script executed: git ls-files "**/*file_parser*" || rg -l "file_parser_service" -t py || find . -name "*file_parser*" -type fRepository: tale-project/tale Length of output: 104 🏁 Script executed: find . -type f -name "*.py" | xargs grep -l "def parse_docx" 2>/dev/null | head -5Repository: tale-project/tale Length of output: 106 🏁 Script executed: rg "def parse_docx" -A 20 -t pyRepository: tale-project/tale Length of output: 1849 🏁 Script executed: rg "def parse_pdf" -A 20 -t pyRepository: tale-project/tale Length of output: 1754 🏁 Script executed: rg "parse_docx\|parse_pdf" -t py | head -30Repository: tale-project/tale Length of output: 43 🏁 Script executed: find . -type d -name "crawler" 2>/dev/nullRepository: tale-project/tale Length of output: 191 🏁 Script executed: ls -laRepository: tale-project/tale Length of output: 1145 🏁 Script executed: find . -type f -name "*.py" | wc -l && find . -type f -name "*.py" | head -20Repository: tale-project/tale Length of output: 781 🏁 Script executed: rg "application/vnd.openxmlformats-officedocument.wordprocessingml" -t pyRepository: tale-project/tale Length of output: 208 🏁 Script executed: git log --oneline -5 2>/dev/null || echo "Not a git repo"Repository: tale-project/tale Length of output: 139 🏁 Script executed: rg "Extract text content from a DOCX file" -t pyRepository: tale-project/tale Length of output: 157 🏁 Script executed: rg "logger.exception\|logger.error" -t py -A 2 | head -40Repository: tale-project/tale Length of output: 43 🏁 Script executed: rg "success.*False" -t py | head -20Repository: tale-project/tale Length of output: 1029 🌐 Web query:
💡 Result: Short answer
Key differences / gotchas
Minimal examples
Sources 🌐 Web query:
💡 Result: Here are concise, practical guidelines for consistent API responses, error handling, and payload fields in Python APIs — plus a minimal example. Key rules
Minimal JSON patterns
Python implementation notes
Mapping examples (brief)
References
If you want, I can:
🏁 Script executed: cat -n services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 7240 🏁 Script executed: rg "def parse_docx" -B 2 -A 40 services/crawler/app/file_parser_service.py | head -50Repository: tale-project/tale Length of output: 1922 🏁 Script executed: sed -n '54,90p' services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 1707 🏁 Script executed: rg "def parse_pdf" -B 2 -A 45 services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 1973 🏁 Script executed: grep -n "except Exception" services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 163 🏁 Script executed: sed -n '96,145p' services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 2534 🏁 Script executed: rg "except Exception as e:" services/crawler/app/file_parser_service.py -A 3Repository: tale-project/tale Length of output: 583 🏁 Script executed: rg "parse_docx\|parse_pdf" services/crawler/app/main.py -B 3 -A 3Repository: tale-project/tale Length of output: 43 🏁 Script executed: head -20 services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 569 Standardize DOCX failure response to include
- except Exception as e:
- logger.error(f"Error parsing DOCX: {e}")
- return {"success": False, "filename": filename, "error": str(e)}
+ except Exception as e:
+ logger.exception("Error parsing DOCX")
+ return {
+ "success": False,
+ "filename": filename,
+ "file_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "error": str(e),
+ }📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.14.8)86-86: Do not catch blind exception: (BLE001) 87-87: Use Replace with (TRY400) 87-87: Logging statement uses f-string (G004) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_pptx(self, file_bytes: bytes, filename: str = "presentation.pptx") -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Extract text content from a PPTX file.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pptx import Presentation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prs = Presentation(BytesIO(file_bytes)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slides = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_text_parts = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for slide_num, slide in enumerate(prs.slides, start=1): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slide_text = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for shape in slide.shapes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if shape.has_text_frame: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for paragraph in shape.text_frame.paragraphs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = paragraph.text.strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slide_text.append(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if shape.has_table: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for row in shape.table.rows: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for cell in row.cells: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = cell.text.strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slide_text.append(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slides.append({"slide_number": slide_num, "text_content": slide_text, "full_text": "\n".join(slide_text)}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_text_parts.extend(slide_text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| core_props = prs.core_properties | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "success": True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "filename": filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "file_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "slide_count": len(slides), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "slides": slides, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "full_text": "\n\n".join(full_text_parts), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "metadata": {"title": core_props.title or "", "author": core_props.author or ""}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Error parsing PPTX: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {"success": False, "filename": filename, "error": str(e)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cd /tmp && find . -name "file_parser_service.py" -type f 2>/dev/nullRepository: tale-project/tale Length of output: 43 🏁 Script executed: cd /tmp && git ls-files | grep -i "file_parser"Repository: tale-project/tale Length of output: 129 🏁 Script executed: cd /tmp && git ls-files | head -20Repository: tale-project/tale Length of output: 129 🏁 Script executed: find . -name "file_parser_service.py" -type fRepository: tale-project/tale Length of output: 106 🏁 Script executed: find . -type f -name "*.py" | grep -i parser | head -20Repository: tale-project/tale Length of output: 106 🏁 Script executed: wc -l ./services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 110 🏁 Script executed: cat -n ./services/crawler/app/file_parser_service.pyRepository: tale-project/tale Length of output: 7240 Standardize PPTX failure payload shape ( PPTX failure response omits except Exception as e:
- logger.error(f"Error parsing PPTX: {e}")
- return {"success": False, "filename": filename, "error": str(e)}
+ logger.exception("Error parsing PPTX")
+ return {
+ "success": False,
+ "filename": filename,
+ "file_type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "error": str(e),
+ }🧰 Tools🪛 Ruff (0.14.8)127-127: Do not catch blind exception: (BLE001) 128-128: Use Replace with (TRY400) 128-128: Logging statement uses f-string (G004) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_file(self, file_bytes: bytes, filename: str, content_type: str = "") -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Parse a file based on its content type or filename extension.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filename_lower = filename.lower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content_type_lower = content_type.lower() if content_type else "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if filename_lower.endswith(".pdf") or "pdf" in content_type_lower: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.parse_pdf(file_bytes, filename) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif filename_lower.endswith(".docx") or "wordprocessingml" in content_type_lower: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.parse_docx(file_bytes, filename) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif filename_lower.endswith(".pptx") or "presentationml" in content_type_lower: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.parse_pptx(file_bytes, filename) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "success": False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "filename": filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error": f"Unsupported file type: {filename} ({content_type}). Supported: PDF, DOCX, PPTX.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add guardrails for untrusted uploads (size limits / zip-bomb risk) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,10 +37,24 @@ | |
| GeneratePptxResponse, | ||
| GenerateDocxRequest, | ||
| GenerateDocxResponse, | ||
| # File parsing models | ||
| ParseFileResponse, | ||
| ) | ||
| from app.crawler_service import get_crawler_service | ||
| from app.converter_service import get_converter_service | ||
| from app.template_service import get_template_service | ||
| from app.file_parser_service import FileParserService | ||
|
|
||
| # Global file parser service instance | ||
| _file_parser_service: FileParserService | None = None | ||
|
|
||
|
|
||
| def get_file_parser_service() -> FileParserService: | ||
| """Get or create the file parser service instance.""" | ||
| global _file_parser_service | ||
| if _file_parser_service is None: | ||
| _file_parser_service = FileParserService() | ||
| return _file_parser_service | ||
|
|
||
|
|
||
| # Configure logging | ||
|
|
@@ -886,6 +900,53 @@ async def generate_docx_from_template( | |
| ) | ||
|
|
||
|
|
||
| # ==================== File Parsing Endpoints ==================== | ||
|
|
||
|
|
||
| @app.post("/api/v1/parse/file", response_model=ParseFileResponse) | ||
| async def parse_file_upload( | ||
| file: UploadFile = File(..., description="File to parse (PDF, DOCX, or PPTX)"), | ||
| ): | ||
| """ | ||
| Parse a document file and extract its text content. | ||
|
|
||
| Supports PDF, DOCX, and PPTX files. Returns the extracted text content | ||
| along with metadata like page count, paragraph count, or slide count. | ||
|
|
||
| Args: | ||
| file: The document file to parse | ||
|
|
||
| Returns: | ||
| Parsed content including full text and metadata | ||
| """ | ||
| try: | ||
| file_bytes = await file.read() | ||
|
|
||
| if not file_bytes: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail="Empty file uploaded", | ||
| ) | ||
|
|
||
| filename = file.filename or "unknown" | ||
| content_type = file.content_type or "" | ||
|
|
||
| parser = get_file_parser_service() | ||
| result = parser.parse_file(file_bytes, filename, content_type) | ||
|
|
||
| return ParseFileResponse(**result) | ||
|
|
||
| except HTTPException: | ||
| raise | ||
| except Exception as e: | ||
| logger.error(f"Error parsing file: {e}") | ||
| return ParseFileResponse( | ||
| success=False, | ||
| filename=file.filename or "unknown", | ||
| error=f"Failed to parse file: {str(e)}", | ||
| ) | ||
|
Comment on lines
+906
to
+947
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider using explicit f-string conversion flag. The endpoint implementation is well-structured and follows the existing patterns. One minor improvement based on static analysis: Line 946 uses - error=f"Failed to parse file: {str(e)}",
+ error=f"Failed to parse file: {e!s}",🧰 Tools🪛 Ruff (0.14.8)908-908: Do not perform function call (B008) 926-929: Abstract (TRY301) 941-941: Do not catch blind exception: (BLE001) 946-946: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import uvicorn | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,4 +20,5 @@ markdown==3.7 | |
| # Office document generation | ||
| python-pptx==1.0.2 # PPTX parsing and generation | ||
| python-docx==1.1.2 # DOCX generation | ||
| pymupdf==1.25.5 # PDF parsing (PyMuPDF/fitz) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: PyMuPDF 1.25.5 was released on March 31, 2025. [1][2] Security/bug notes: the release bundles MuPDF‑1.25.6 (fixing crashes on macOS with particular fonts and other robustness fixes) and fixes several crash/behavior bugs; public vulnerability databases list no known security advisories for PyMuPDF 1.25.5. [2][3] Sources: 🌐 Web query:
💡 Result: Summary for pymupdf (PyMuPDF) version 1.25.5
Sources: 🌐 Web query:
💡 Result: Short answer: Yes — PyMuPDF 1.25.5 has been affected by publicly disclosed CVEs reported after its 2025‑03‑31 release. Notable CVEs:
Recommendation: upgrade to the patched PyMuPDF/MuPDF release (1.26.x or later) or the latest release from the project. See the vendor release notes and CVE pages for exact affected-version ranges and fixes. [2][4][1] Sources:
PyMuPDF 1.25.5 has known security vulnerabilities — upgrade to 1.26.0 or later. Version 1.25.5 (released 2025-03-31) is affected by:
Upgrade to PyMuPDF 1.26.x or the latest stable release to resolve these security issues. 🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
|
|
||
| import { Textarea } from '@/components/ui/textarea'; | ||
| import { ComponentPropsWithoutRef, useRef, useState } from 'react'; | ||
| import { X } from 'lucide-react'; | ||
| import { X, Paperclip } from 'lucide-react'; | ||
|
Comment on lines
4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Prevent default paste + memoize handler to avoid double-paste and unstable refs. -import { ComponentPropsWithoutRef, useRef, useState } from 'react';
+import { ComponentPropsWithoutRef, useCallback, useRef, useState } from 'react';
- const handlePaste = (e: React.ClipboardEvent) => {
+ const handlePaste = useCallback((e: React.ClipboardEvent) => {
const items = e.clipboardData?.items;
if (!items) return;
const imageFiles: File[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
// Create a meaningful filename with timestamp
const extension = item.type.split('/')[1] || 'png';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const renamedFile = new File([file], `pasted-image-${timestamp}.${extension}`, {
type: file.type,
});
imageFiles.push(renamedFile);
}
}
}
if (imageFiles.length > 0) {
+ e.preventDefault();
// Create a DataTransfer to get a FileList
const dataTransfer = new DataTransfer();
imageFiles.forEach((file) => dataTransfer.items.add(file));
uploadFiles(dataTransfer.files);
}
- };
+ }, [uploadFiles]);Based on learnings, use Also applies to: 167-195, 344-345 🤖 Prompt for AI Agents |
||
| import { useMutation } from 'convex/react'; | ||
| import { api } from '@/convex/_generated/api'; | ||
| import { toast } from '@/hooks/use-toast'; | ||
|
|
@@ -164,6 +164,36 @@ export default function ChatInput({ | |
| } | ||
| }; | ||
|
|
||
| // Handle paste event for images | ||
| const handlePaste = (e: React.ClipboardEvent) => { | ||
| const items = e.clipboardData?.items; | ||
| if (!items) return; | ||
|
|
||
| const imageFiles: File[] = []; | ||
| for (let i = 0; i < items.length; i++) { | ||
| const item = items[i]; | ||
| if (item.type.startsWith('image/')) { | ||
| const file = item.getAsFile(); | ||
| if (file) { | ||
| // Create a meaningful filename with timestamp | ||
| const extension = item.type.split('/')[1] || 'png'; | ||
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | ||
| const renamedFile = new File([file], `pasted-image-${timestamp}.${extension}`, { | ||
| type: file.type, | ||
| }); | ||
| imageFiles.push(renamedFile); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (imageFiles.length > 0) { | ||
| // Create a DataTransfer to get a FileList | ||
| const dataTransfer = new DataTransfer(); | ||
| imageFiles.forEach((file) => dataTransfer.items.add(file)); | ||
| uploadFiles(dataTransfer.files); | ||
| } | ||
| }; | ||
|
|
||
| const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const files = e.target.files; | ||
| if (files && files.length > 0) { | ||
|
|
@@ -311,6 +341,7 @@ export default function ChatInput({ | |
| value={value} | ||
| onChange={(e) => handleInputChange(e.target.value)} | ||
| onKeyDown={handleKeyDown} | ||
| onPaste={handlePaste} | ||
| className="min-h-[100px] relative border-0 shadow-none resize-none focus-visible:ring-0 focus-visible:ring-offset-0 text-foreground px-0 py-0 bg-transparent placeholder:text-muted-foreground" | ||
| disabled={isLoading} | ||
| placeholder="" | ||
|
|
@@ -335,10 +366,25 @@ export default function ChatInput({ | |
| </svg> | ||
| </span> | ||
| </div> | ||
| to send or drag files here. | ||
| to send | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Action buttons row */} | ||
| <div className="flex items-center pb-3"> | ||
| {/* Attachment button */} | ||
| <button | ||
| type="button" | ||
| onClick={() => fileInputRef.current?.click()} | ||
| disabled={isLoading} | ||
| className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | ||
| title="Attach files" | ||
| > | ||
| <Paperclip className="size-4" /> | ||
| <span className="text-xs">Attach</span> | ||
| </button> | ||
| </div> | ||
|
Comment on lines
+374
to
+387
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider 🤖 Prompt for AI Agents |
||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switch
Dict[...]todict[...](and standardize response typing)Ruff is right: prefer built-in generics for py3.9+.
🧰 Tools
🪛 Ruff (0.14.8)
12-12:
typing.Dictis deprecated, usedictinstead(UP035)
🤖 Prompt for AI Agents