Realistvik0 commited on
Commit
46c0259
·
verified ·
1 Parent(s): 9dbe8fc

Create Attachment reader.py

Browse files
Files changed (1) hide show
  1. tools/Attachment reader.py +158 -0
tools/Attachment reader.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tools/attachment_reader.py
3
+
4
+ Tool that lets the agent read a task's attached file from a local path,
5
+ plus a helper that downloads the attachment from the scoring API.
6
+
7
+ Optional dependencies (only needed for the matching file types):
8
+ pip install openpyxl # .xlsx
9
+ pip install pypdf # .pdf
10
+ pip install python-docx # .docx
11
+ """
12
+
13
+ import os
14
+ import re
15
+ import mimetypes
16
+
17
+ import requests
18
+ import pandas as pd
19
+ from smolagents import Tool
20
+
21
+ ATTACHMENTS_DIR = "attachments"
22
+ MAX_CHARS = 20000
23
+
24
+ TEXT_EXTENSIONS = {".txt", ".md", ".json", ".py", ".html", ".xml", ".yaml", ".yml", ".log", ".jsonl"}
25
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
26
+ AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".flac", ".ogg"}
27
+
28
+
29
+ # =========================================================
30
+ # Download helper
31
+ # =========================================================
32
+
33
+ def _filename_from_response(response: requests.Response, task_id: str) -> str:
34
+ """Work out a sensible filename from the response headers."""
35
+ disposition = response.headers.get("Content-Disposition", "")
36
+ match = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^";]+)"?', disposition)
37
+ if match:
38
+ return os.path.basename(match.group(1).strip())
39
+
40
+ content_type = response.headers.get("Content-Type", "").split(";")[0].strip()
41
+ extension = mimetypes.guess_extension(content_type) or ""
42
+ return f"{task_id}{extension}"
43
+
44
+
45
+ def download_attachment(api_url: str, task_id: str, timeout: int = 30) -> str | None:
46
+ """
47
+ Downloads the attachment for a task, if there is one.
48
+ Returns the local file path, or None if the task has no attachment
49
+ or the download failed. Never raises.
50
+ """
51
+ url = f"{api_url}/files/{task_id}"
52
+ try:
53
+ response = requests.get(url, timeout=timeout)
54
+ if response.status_code == 404:
55
+ return None # task has no attachment
56
+ response.raise_for_status()
57
+ if not response.content:
58
+ return None
59
+
60
+ task_dir = os.path.join(ATTACHMENTS_DIR, str(task_id))
61
+ os.makedirs(task_dir, exist_ok=True)
62
+ path = os.path.join(task_dir, _filename_from_response(response, task_id))
63
+
64
+ with open(path, "wb") as f:
65
+ f.write(response.content)
66
+
67
+ print(f"Downloaded attachment for task {task_id}: {path}")
68
+ return path
69
+ except Exception as e:
70
+ print(f"Could not download attachment for task {task_id}: {e}")
71
+ return None
72
+
73
+
74
+ # =========================================================
75
+ # Reader tool
76
+ # =========================================================
77
+
78
+ def _truncate(text: str) -> str:
79
+ if len(text) <= MAX_CHARS:
80
+ return text
81
+ return text[:MAX_CHARS] + f"\n\n[... truncated, {len(text) - MAX_CHARS} more characters ...]"
82
+
83
+
84
+ def _read_spreadsheet(path: str, extension: str) -> str:
85
+ if extension == ".csv":
86
+ sheets = {"csv": pd.read_csv(path)}
87
+ else:
88
+ sheets = pd.read_excel(path, sheet_name=None)
89
+
90
+ parts = []
91
+ for name, df in sheets.items():
92
+ parts.append(f"=== Sheet: {name} ({df.shape[0]} rows x {df.shape[1]} columns) ===")
93
+ parts.append(df.to_csv(index=False))
94
+ return "\n".join(parts)
95
+
96
+
97
+ def _read_pdf(path: str) -> str:
98
+ from pypdf import PdfReader
99
+
100
+ reader = PdfReader(path)
101
+ return "\n\n".join(
102
+ f"--- Page {i + 1} ---\n{page.extract_text() or ''}"
103
+ for i, page in enumerate(reader.pages)
104
+ )
105
+
106
+
107
+ def _read_docx(path: str) -> str:
108
+ import docx
109
+
110
+ document = docx.Document(path)
111
+ return "\n".join(p.text for p in document.paragraphs)
112
+
113
+
114
+ class AttachmentReaderTool(Tool):
115
+ name = "read_attachment"
116
+
117
+ description = (
118
+ "Reads a file attached to the current task from a local path and returns "
119
+ "its contents as text. Supports spreadsheets (.xlsx, .xls, .csv), PDF, "
120
+ "Word (.docx) and plain text files. Use this when the question mentions "
121
+ "an attached file and a local path is provided."
122
+ )
123
+
124
+ inputs = {
125
+ "file_path": {
126
+ "type": "string",
127
+ "description": "Local path of the attached file, as given in the question.",
128
+ }
129
+ }
130
+
131
+ output_type = "string"
132
+
133
+ def forward(self, file_path: str) -> str:
134
+ if not os.path.exists(file_path):
135
+ return f"ERROR: file not found: {file_path}"
136
+
137
+ extension = os.path.splitext(file_path)[1].lower()
138
+
139
+ try:
140
+ if extension in {".xlsx", ".xlsm", ".xls", ".csv"}:
141
+ return _truncate(_read_spreadsheet(file_path, extension))
142
+ if extension == ".pdf":
143
+ return _truncate(_read_pdf(file_path))
144
+ if extension == ".docx":
145
+ return _truncate(_read_docx(file_path))
146
+ if extension in TEXT_EXTENSIONS:
147
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
148
+ return _truncate(f.read())
149
+ if extension in IMAGE_EXTENSIONS:
150
+ return "ERROR: image files cannot be read as text by this tool."
151
+ if extension in AUDIO_EXTENSIONS:
152
+ return "ERROR: audio files cannot be read as text by this tool."
153
+
154
+ # Unknown extension: try as text.
155
+ with open(file_path, "r", encoding="utf-8") as f:
156
+ return _truncate(f.read())
157
+ except Exception as e:
158
+ return f"ERROR: could not read {file_path}: {e}"