codex-agent-framework 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_agent/__init__.py +38 -0
- codex_agent/__main__.py +18 -0
- codex_agent/agent.py +1179 -0
- codex_agent/ai.py +494 -0
- codex_agent/builtin_commands.py +136 -0
- codex_agent/builtin_providers.py +42 -0
- codex_agent/builtin_tools.py +418 -0
- codex_agent/chat.py +139 -0
- codex_agent/command.py +19 -0
- codex_agent/event.py +136 -0
- codex_agent/get_text/__init__.py +20 -0
- codex_agent/get_text/default_gitignore +110 -0
- codex_agent/get_text/get_text.py +1240 -0
- codex_agent/get_text/simpler_get_text.py +184 -0
- codex_agent/get_webdriver.py +44 -0
- codex_agent/image.py +304 -0
- codex_agent/latex.py +165 -0
- codex_agent/memory.py +318 -0
- codex_agent/message.py +423 -0
- codex_agent/prompts/image_generation_system_prompt.txt +13 -0
- codex_agent/prompts/system_prompt.txt +88 -0
- codex_agent/provider.py +19 -0
- codex_agent/stream_utils.py +645 -0
- codex_agent/tool.py +235 -0
- codex_agent/utils.py +374 -0
- codex_agent/voice.py +353 -0
- codex_agent/worker.py +190 -0
- codex_agent_framework-0.1.1.dist-info/METADATA +361 -0
- codex_agent_framework-0.1.1.dist-info/RECORD +33 -0
- codex_agent_framework-0.1.1.dist-info/WHEEL +5 -0
- codex_agent_framework-0.1.1.dist-info/entry_points.txt +2 -0
- codex_agent_framework-0.1.1.dist-info/licenses/LICENSE +21 -0
- codex_agent_framework-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
"""Text extraction utilities for various sources.
|
|
2
|
+
|
|
3
|
+
Supports extraction from:
|
|
4
|
+
- Files (PDF, DOCX, ODT, HTML, text files, CSV, JSON, XML, YAML, TOML, INI, XLSX, ODS)
|
|
5
|
+
- Archives (ZIP, TAR, TAR.GZ, TAR.BZ2)
|
|
6
|
+
- URLs (web pages, documents)
|
|
7
|
+
- Python objects (classes, functions, modules, instances)
|
|
8
|
+
- File system (directory listings)
|
|
9
|
+
- Email files (EML)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional, Dict, List
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import inspect
|
|
16
|
+
import re
|
|
17
|
+
from io import BytesIO, StringIO, TextIOWrapper
|
|
18
|
+
import csv
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
import configparser
|
|
21
|
+
import zipfile
|
|
22
|
+
import tarfile
|
|
23
|
+
import email
|
|
24
|
+
from email import policy
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# For gitignore handling
|
|
28
|
+
try:
|
|
29
|
+
import pathspec
|
|
30
|
+
HAS_PATHSPEC = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_PATHSPEC = False
|
|
33
|
+
|
|
34
|
+
# Try to import optional stdlib modules
|
|
35
|
+
try:
|
|
36
|
+
import tomllib # Python 3.11+
|
|
37
|
+
except ImportError:
|
|
38
|
+
try:
|
|
39
|
+
import tomli as tomllib # Fallback for older Python
|
|
40
|
+
except ImportError:
|
|
41
|
+
tomllib = None
|
|
42
|
+
|
|
43
|
+
import requests
|
|
44
|
+
from bs4 import BeautifulSoup
|
|
45
|
+
import PyPDF2
|
|
46
|
+
import docx
|
|
47
|
+
from odf import opendocument, text, teletype
|
|
48
|
+
import yaml
|
|
49
|
+
|
|
50
|
+
# Optional imports for extended functionality
|
|
51
|
+
try:
|
|
52
|
+
import openpyxl
|
|
53
|
+
HAS_OPENPYXL = True
|
|
54
|
+
except ImportError:
|
|
55
|
+
HAS_OPENPYXL = False
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
import trafilatura
|
|
59
|
+
HAS_TRAFILATURA = True
|
|
60
|
+
except ImportError:
|
|
61
|
+
HAS_TRAFILATURA = False
|
|
62
|
+
|
|
63
|
+
from ..get_webdriver import get_webdriver
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Text Processing Utilities
|
|
68
|
+
# ============================================================================
|
|
69
|
+
|
|
70
|
+
def strip_newlines(string: str) -> str:
|
|
71
|
+
"""Remove leading/trailing newlines and collapse multiple newlines.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
string: Input text to clean
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Cleaned text with normalized newlines
|
|
78
|
+
"""
|
|
79
|
+
string = string.strip('\n')
|
|
80
|
+
return re.sub(r'\n{2,}', '\n', string)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# Document Processing
|
|
85
|
+
# ============================================================================
|
|
86
|
+
|
|
87
|
+
def _extract_pdf_text(file_obj) -> str:
|
|
88
|
+
"""Extract text from PDF file object.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
file_obj: File-like object containing PDF data
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Extracted text from all pages
|
|
95
|
+
"""
|
|
96
|
+
reader = PyPDF2.PdfReader(file_obj)
|
|
97
|
+
return "".join(page.extract_text() for page in reader.pages)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract_docx_text(file_obj) -> str:
|
|
101
|
+
"""Extract text from DOCX file object.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_obj: File-like object or path to DOCX file
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Extracted text from all paragraphs
|
|
108
|
+
"""
|
|
109
|
+
doc = docx.Document(file_obj)
|
|
110
|
+
return "\n".join(para.text for para in doc.paragraphs)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_odt_text(file_obj) -> str:
|
|
114
|
+
"""Extract text from ODT file object.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file_obj: File-like object or path to ODT file
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Extracted text from all paragraphs
|
|
121
|
+
"""
|
|
122
|
+
doc = opendocument.load(file_obj)
|
|
123
|
+
paragraphs = doc.getElementsByType(text.P)
|
|
124
|
+
return '\n'.join(teletype.extractText(para) for para in paragraphs)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_html_text(file_obj) -> str:
|
|
128
|
+
"""Extract text from HTML file object.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
file_obj: File-like object containing HTML
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Extracted text content
|
|
135
|
+
"""
|
|
136
|
+
soup = BeautifulSoup(file_obj, 'html.parser')
|
|
137
|
+
return soup.get_text()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _extract_csv_text(file_obj) -> str:
|
|
141
|
+
"""Extract text from CSV file object.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
file_obj: File-like object containing CSV data
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Formatted CSV content as text
|
|
148
|
+
"""
|
|
149
|
+
content = file_obj.read()
|
|
150
|
+
if isinstance(content, bytes):
|
|
151
|
+
content = content.decode('utf-8')
|
|
152
|
+
|
|
153
|
+
reader = csv.reader(StringIO(content))
|
|
154
|
+
lines = []
|
|
155
|
+
for row in reader:
|
|
156
|
+
lines.append(' | '.join(row))
|
|
157
|
+
return '\n'.join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _extract_json_text(file_obj) -> str:
|
|
161
|
+
"""Extract text from JSON file object.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
file_obj: File-like object containing JSON data
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Pretty-printed JSON content
|
|
168
|
+
"""
|
|
169
|
+
content = file_obj.read()
|
|
170
|
+
if isinstance(content, bytes):
|
|
171
|
+
content = content.decode('utf-8')
|
|
172
|
+
|
|
173
|
+
data = json.loads(content)
|
|
174
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_xml_text(file_obj) -> str:
|
|
178
|
+
"""Extract text from XML file object.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_obj: File-like object containing XML data
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
XML content as formatted text
|
|
185
|
+
"""
|
|
186
|
+
content = file_obj.read()
|
|
187
|
+
if isinstance(content, bytes):
|
|
188
|
+
content = content.decode('utf-8')
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
tree = ET.ElementTree(ET.fromstring(content))
|
|
192
|
+
root = tree.getroot()
|
|
193
|
+
|
|
194
|
+
def extract_element(elem, level=0):
|
|
195
|
+
"""Recursively extract text from XML elements."""
|
|
196
|
+
indent = ' ' * level
|
|
197
|
+
text_parts = []
|
|
198
|
+
|
|
199
|
+
# Add element tag
|
|
200
|
+
text_parts.append(f"{indent}<{elem.tag}>")
|
|
201
|
+
|
|
202
|
+
# Add element text if present
|
|
203
|
+
if elem.text and elem.text.strip():
|
|
204
|
+
text_parts.append(f"{indent} {elem.text.strip()}")
|
|
205
|
+
|
|
206
|
+
# Recurse into children
|
|
207
|
+
for child in elem:
|
|
208
|
+
text_parts.append(extract_element(child, level + 1))
|
|
209
|
+
|
|
210
|
+
return '\n'.join(text_parts)
|
|
211
|
+
|
|
212
|
+
return extract_element(root)
|
|
213
|
+
except ET.ParseError:
|
|
214
|
+
# If parsing fails, return raw content
|
|
215
|
+
return content
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extract_yaml_text(file_obj) -> str:
|
|
219
|
+
"""Extract text from YAML file object.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
file_obj: File-like object containing YAML data
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Pretty-printed YAML content
|
|
226
|
+
"""
|
|
227
|
+
content = file_obj.read()
|
|
228
|
+
if isinstance(content, bytes):
|
|
229
|
+
content = content.decode('utf-8')
|
|
230
|
+
|
|
231
|
+
data = yaml.safe_load(content)
|
|
232
|
+
return yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _extract_toml_text(file_obj) -> str:
|
|
236
|
+
"""Extract text from TOML file object.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
file_obj: File-like object containing TOML data
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
TOML content as text (raw if tomllib not available)
|
|
243
|
+
"""
|
|
244
|
+
content = file_obj.read()
|
|
245
|
+
if isinstance(content, bytes):
|
|
246
|
+
content = content.decode('utf-8')
|
|
247
|
+
|
|
248
|
+
if tomllib is None:
|
|
249
|
+
# If tomllib not available, return raw content
|
|
250
|
+
return content
|
|
251
|
+
|
|
252
|
+
data = tomllib.loads(content)
|
|
253
|
+
# Since there's no standard toml dump in stdlib, return pretty JSON representation
|
|
254
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _extract_ini_text(file_obj) -> str:
|
|
258
|
+
"""Extract text from INI/CFG file object.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
file_obj: File-like object containing INI data
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Formatted INI content as text
|
|
265
|
+
"""
|
|
266
|
+
content = file_obj.read()
|
|
267
|
+
if isinstance(content, bytes):
|
|
268
|
+
content = content.decode('utf-8')
|
|
269
|
+
|
|
270
|
+
config = configparser.ConfigParser()
|
|
271
|
+
config.read_string(content)
|
|
272
|
+
|
|
273
|
+
lines = []
|
|
274
|
+
for section in config.sections():
|
|
275
|
+
lines.append(f"[{section}]")
|
|
276
|
+
for key, value in config.items(section):
|
|
277
|
+
lines.append(f"{key} = {value}")
|
|
278
|
+
lines.append("")
|
|
279
|
+
|
|
280
|
+
return '\n'.join(lines)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _extract_xlsx_text(file_obj) -> str:
|
|
284
|
+
"""Extract text from XLSX file object.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
file_obj: File-like object or path to XLSX file
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Formatted spreadsheet content as text
|
|
291
|
+
"""
|
|
292
|
+
if not HAS_OPENPYXL:
|
|
293
|
+
return "Error: openpyxl not installed. Install with: pip install openpyxl"
|
|
294
|
+
|
|
295
|
+
wb = openpyxl.load_workbook(file_obj, data_only=True)
|
|
296
|
+
lines = []
|
|
297
|
+
|
|
298
|
+
for sheet_name in wb.sheetnames:
|
|
299
|
+
sheet = wb[sheet_name]
|
|
300
|
+
lines.append(f"=== Sheet: {sheet_name} ===\n")
|
|
301
|
+
|
|
302
|
+
for row in sheet.iter_rows(values_only=True):
|
|
303
|
+
row_text = ' | '.join(str(cell) if cell is not None else '' for cell in row)
|
|
304
|
+
if row_text.strip():
|
|
305
|
+
lines.append(row_text)
|
|
306
|
+
|
|
307
|
+
lines.append("")
|
|
308
|
+
|
|
309
|
+
return '\n'.join(lines)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _extract_ods_text(file_obj) -> str:
|
|
313
|
+
"""Extract text from ODS spreadsheet file object.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
file_obj: File-like object or path to ODS file
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Formatted spreadsheet content as text
|
|
320
|
+
"""
|
|
321
|
+
doc = opendocument.load(file_obj)
|
|
322
|
+
from odf.table import Table, TableRow, TableCell
|
|
323
|
+
|
|
324
|
+
lines = []
|
|
325
|
+
tables = doc.getElementsByType(Table)
|
|
326
|
+
|
|
327
|
+
for table in tables:
|
|
328
|
+
table_name = table.getAttribute('name')
|
|
329
|
+
if table_name:
|
|
330
|
+
lines.append(f"=== Sheet: {table_name} ===\n")
|
|
331
|
+
|
|
332
|
+
rows = table.getElementsByType(TableRow)
|
|
333
|
+
for row in rows:
|
|
334
|
+
cells = row.getElementsByType(TableCell)
|
|
335
|
+
row_data = []
|
|
336
|
+
for cell in cells:
|
|
337
|
+
cell_text = teletype.extractText(cell)
|
|
338
|
+
row_data.append(cell_text)
|
|
339
|
+
|
|
340
|
+
row_text = ' | '.join(row_data)
|
|
341
|
+
if row_text.strip():
|
|
342
|
+
lines.append(row_text)
|
|
343
|
+
|
|
344
|
+
lines.append("")
|
|
345
|
+
|
|
346
|
+
return '\n'.join(lines)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _extract_eml_text(file_obj) -> str:
|
|
350
|
+
"""Extract text from email file object.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
file_obj: File-like object containing email data
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Formatted email content as text
|
|
357
|
+
"""
|
|
358
|
+
content = file_obj.read()
|
|
359
|
+
if isinstance(content, bytes):
|
|
360
|
+
msg = email.message_from_bytes(content, policy=policy.default)
|
|
361
|
+
else:
|
|
362
|
+
msg = email.message_from_string(content, policy=policy.default)
|
|
363
|
+
|
|
364
|
+
lines = []
|
|
365
|
+
|
|
366
|
+
# Extract headers
|
|
367
|
+
lines.append("=== Email Headers ===")
|
|
368
|
+
lines.append(f"From: {msg.get('From', 'Unknown')}")
|
|
369
|
+
lines.append(f"To: {msg.get('To', 'Unknown')}")
|
|
370
|
+
lines.append(f"Subject: {msg.get('Subject', 'No Subject')}")
|
|
371
|
+
lines.append(f"Date: {msg.get('Date', 'Unknown')}")
|
|
372
|
+
lines.append("")
|
|
373
|
+
|
|
374
|
+
# Extract body
|
|
375
|
+
lines.append("=== Email Body ===")
|
|
376
|
+
if msg.is_multipart():
|
|
377
|
+
for part in msg.walk():
|
|
378
|
+
content_type = part.get_content_type()
|
|
379
|
+
if content_type == 'text/plain':
|
|
380
|
+
lines.append(part.get_content())
|
|
381
|
+
else:
|
|
382
|
+
lines.append(msg.get_content())
|
|
383
|
+
|
|
384
|
+
return '\n'.join(lines)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ============================================================================
|
|
388
|
+
# Archive Handlers
|
|
389
|
+
# ============================================================================
|
|
390
|
+
|
|
391
|
+
def _extract_zip_text(file_obj) -> str:
|
|
392
|
+
"""Extract file listing from ZIP archive.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
file_obj: File-like object or path to ZIP file
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
List of files in the archive with sizes
|
|
399
|
+
"""
|
|
400
|
+
if isinstance(file_obj, str):
|
|
401
|
+
zf = zipfile.ZipFile(file_obj, 'r')
|
|
402
|
+
else:
|
|
403
|
+
zf = zipfile.ZipFile(file_obj)
|
|
404
|
+
|
|
405
|
+
lines = ["=== ZIP Archive Contents ===\n"]
|
|
406
|
+
|
|
407
|
+
for info in zf.infolist():
|
|
408
|
+
size_str = f"{info.file_size:,} bytes"
|
|
409
|
+
lines.append(f"{info.filename:<50} {size_str:>15}")
|
|
410
|
+
|
|
411
|
+
lines.append(f"\nTotal files: {len(zf.filelist)}")
|
|
412
|
+
|
|
413
|
+
return '\n'.join(lines)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _extract_tar_text(file_obj, mode='r') -> str:
|
|
417
|
+
"""Extract file listing from TAR archive.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
file_obj: File-like object or path to TAR file
|
|
421
|
+
mode: Open mode ('r', 'r:gz', 'r:bz2', etc.)
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
List of files in the archive with sizes
|
|
425
|
+
"""
|
|
426
|
+
if isinstance(file_obj, str):
|
|
427
|
+
tf = tarfile.open(file_obj, mode)
|
|
428
|
+
else:
|
|
429
|
+
tf = tarfile.open(fileobj=file_obj, mode=mode)
|
|
430
|
+
|
|
431
|
+
lines = ["=== TAR Archive Contents ===\n"]
|
|
432
|
+
|
|
433
|
+
for member in tf.getmembers():
|
|
434
|
+
size_str = f"{member.size:,} bytes"
|
|
435
|
+
type_char = 'd' if member.isdir() else 'f'
|
|
436
|
+
lines.append(f"{type_char} {member.name:<50} {size_str:>15}")
|
|
437
|
+
|
|
438
|
+
lines.append(f"\nTotal entries: {len(tf.getmembers())}")
|
|
439
|
+
|
|
440
|
+
return '\n'.join(lines)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ============================================================================
|
|
444
|
+
# File System Handlers
|
|
445
|
+
# ============================================================================
|
|
446
|
+
|
|
447
|
+
def _load_gitignore_patterns(directory: str) -> Optional['pathspec.PathSpec']:
|
|
448
|
+
"""Load gitignore patterns from default and local .gitignore files.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
directory: Directory to scan for local .gitignore
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
PathSpec object with combined patterns, or None if pathspec not available
|
|
455
|
+
"""
|
|
456
|
+
if not HAS_PATHSPEC:
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
patterns = []
|
|
460
|
+
|
|
461
|
+
# Load default gitignore patterns bundled with get_text
|
|
462
|
+
default_gitignore = Path(__file__).parent / 'default_gitignore'
|
|
463
|
+
if default_gitignore.exists():
|
|
464
|
+
with open(default_gitignore, 'r', encoding='utf-8') as f:
|
|
465
|
+
patterns.extend(f.readlines())
|
|
466
|
+
|
|
467
|
+
# Load local .gitignore if present
|
|
468
|
+
local_gitignore = Path(directory) / '.gitignore'
|
|
469
|
+
if local_gitignore.exists():
|
|
470
|
+
with open(local_gitignore, 'r', encoding='utf-8') as f:
|
|
471
|
+
patterns.extend(f.readlines())
|
|
472
|
+
|
|
473
|
+
# Create PathSpec from patterns
|
|
474
|
+
if patterns:
|
|
475
|
+
return pathspec.PathSpec.from_lines('gitwildmatch', patterns)
|
|
476
|
+
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def handle_directory(path: str, max_depth: int = 3) -> str:
|
|
481
|
+
"""Generate a tree-like representation of directory structure.
|
|
482
|
+
|
|
483
|
+
Automatically applies sensible exclusions from:
|
|
484
|
+
- Default gitignore patterns (bundled with get_text)
|
|
485
|
+
- Local .gitignore file (if present)
|
|
486
|
+
|
|
487
|
+
This filters out common junk directories like .git, __pycache__, node_modules, etc.
|
|
488
|
+
Recursion is limited to max_depth levels to avoid overwhelming output.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
path: Path to directory
|
|
492
|
+
max_depth: Maximum recursion depth (default: 3)
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Tree structure as text, or error message
|
|
496
|
+
"""
|
|
497
|
+
if not os.path.exists(path):
|
|
498
|
+
return f'The path {path} does not exist.'
|
|
499
|
+
|
|
500
|
+
if not os.path.isdir(path):
|
|
501
|
+
return f'The path {path} is not a valid directory.'
|
|
502
|
+
|
|
503
|
+
# Load gitignore patterns
|
|
504
|
+
spec = _load_gitignore_patterns(path)
|
|
505
|
+
|
|
506
|
+
# Track directories that weren't fully explored
|
|
507
|
+
unexplored_dirs = []
|
|
508
|
+
|
|
509
|
+
def should_ignore(item_path: str, base_path: str) -> bool:
|
|
510
|
+
"""Check if path should be ignored based on gitignore patterns."""
|
|
511
|
+
if spec is None:
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
# Get relative path from base
|
|
515
|
+
try:
|
|
516
|
+
rel_path = os.path.relpath(item_path, base_path)
|
|
517
|
+
# pathspec expects forward slashes
|
|
518
|
+
rel_path = rel_path.replace(os.sep, '/')
|
|
519
|
+
# Check if it's a directory
|
|
520
|
+
if os.path.isdir(item_path):
|
|
521
|
+
rel_path += '/'
|
|
522
|
+
return spec.match_file(rel_path)
|
|
523
|
+
except ValueError:
|
|
524
|
+
# Handles case where paths are on different drives on Windows
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
def has_contents(folder: str) -> bool:
|
|
528
|
+
"""Check if a directory has non-ignored contents."""
|
|
529
|
+
try:
|
|
530
|
+
contents = os.listdir(folder)
|
|
531
|
+
for item in contents:
|
|
532
|
+
item_path = os.path.join(folder, item)
|
|
533
|
+
if not should_ignore(item_path, path):
|
|
534
|
+
return True
|
|
535
|
+
return False
|
|
536
|
+
except PermissionError:
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
def recurse_folder(folder: str, prefix: str = '', base_path: str = None, depth: int = 0) -> str:
|
|
540
|
+
"""Recursively build directory tree with gitignore filtering and depth limit."""
|
|
541
|
+
if base_path is None:
|
|
542
|
+
base_path = folder
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
contents = os.listdir(folder)
|
|
546
|
+
except PermissionError:
|
|
547
|
+
return prefix + '├── [Permission Denied]\n'
|
|
548
|
+
|
|
549
|
+
# Filter out ignored items
|
|
550
|
+
filtered_contents = []
|
|
551
|
+
for item in contents:
|
|
552
|
+
item_path = os.path.join(folder, item)
|
|
553
|
+
if not should_ignore(item_path, base_path):
|
|
554
|
+
filtered_contents.append(item)
|
|
555
|
+
|
|
556
|
+
output = ''
|
|
557
|
+
for i, item in enumerate(filtered_contents):
|
|
558
|
+
item_path = os.path.join(folder, item)
|
|
559
|
+
is_dir = os.path.isdir(item_path)
|
|
560
|
+
|
|
561
|
+
# Check if we've reached max depth for this directory
|
|
562
|
+
depth_limit_reached = is_dir and depth >= max_depth
|
|
563
|
+
|
|
564
|
+
# Check if directory has unexplored contents
|
|
565
|
+
has_more = is_dir and depth_limit_reached and has_contents(item_path)
|
|
566
|
+
|
|
567
|
+
# Format the item name with appropriate indicators
|
|
568
|
+
if has_more:
|
|
569
|
+
item_display = f"{item}/ [...explore further]"
|
|
570
|
+
unexplored_dirs.append(item_path)
|
|
571
|
+
elif is_dir and depth < max_depth:
|
|
572
|
+
# Check if empty after filtering
|
|
573
|
+
if not has_contents(item_path):
|
|
574
|
+
item_display = f"{item}/ [empty]"
|
|
575
|
+
else:
|
|
576
|
+
item_display = f"{item}/"
|
|
577
|
+
elif is_dir:
|
|
578
|
+
item_display = f"{item}/"
|
|
579
|
+
else:
|
|
580
|
+
item_display = item
|
|
581
|
+
|
|
582
|
+
output += prefix + '├── ' + item_display + '\n'
|
|
583
|
+
|
|
584
|
+
# Recurse into subdirectories if under depth limit
|
|
585
|
+
if is_dir and depth < max_depth:
|
|
586
|
+
new_prefix = prefix + (' ' if i == len(filtered_contents) - 1 else '│ ')
|
|
587
|
+
output += recurse_folder(item_path, new_prefix, base_path, depth + 1)
|
|
588
|
+
|
|
589
|
+
return output
|
|
590
|
+
|
|
591
|
+
result = path + '\n' + recurse_folder(path, depth=0)
|
|
592
|
+
|
|
593
|
+
# Add notes about filtering and depth limit
|
|
594
|
+
notes = []
|
|
595
|
+
|
|
596
|
+
if spec is not None:
|
|
597
|
+
notes.append("Common junk directories (.git, __pycache__, node_modules, etc.) are automatically filtered")
|
|
598
|
+
elif HAS_PATHSPEC:
|
|
599
|
+
notes.append("No gitignore patterns found, showing all files")
|
|
600
|
+
else:
|
|
601
|
+
notes.append("Warning: pathspec not installed. Install with 'pip install pathspec' for gitignore filtering")
|
|
602
|
+
|
|
603
|
+
notes.append(f"Recursion limited to {max_depth} levels deep")
|
|
604
|
+
|
|
605
|
+
if unexplored_dirs:
|
|
606
|
+
notes.append(f"Directories with unexplored contents ({len(unexplored_dirs)} total):")
|
|
607
|
+
for unexplored_dir in unexplored_dirs[:10]: # Show max 10 examples
|
|
608
|
+
notes.append(f" - {unexplored_dir}")
|
|
609
|
+
if len(unexplored_dirs) > 10:
|
|
610
|
+
notes.append(f" ... and {len(unexplored_dirs) - 10} more")
|
|
611
|
+
notes.append("Tip: Call get_text() on specific subdirectories to explore them further")
|
|
612
|
+
|
|
613
|
+
result += '\n\n[' + '\n '.join(notes) + ']'
|
|
614
|
+
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def handle_file(source: str) -> str:
|
|
619
|
+
"""Extract text from a file based on its extension.
|
|
620
|
+
|
|
621
|
+
Supported formats:
|
|
622
|
+
- Documents: PDF, DOCX, ODT, HTML, TXT
|
|
623
|
+
- Data: CSV, JSON, XML, YAML, TOML, INI, CFG
|
|
624
|
+
- Spreadsheets: XLSX, ODS
|
|
625
|
+
- Archives: ZIP, TAR, TAR.GZ, TAR.BZ2, TGZ, TBZ2
|
|
626
|
+
- Email: EML
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
source: Path to file
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Extracted text content, or error message
|
|
633
|
+
"""
|
|
634
|
+
ext = os.path.splitext(source)[1].lower()
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
# Document formats
|
|
638
|
+
if ext == '.pdf':
|
|
639
|
+
with open(source, 'rb') as file:
|
|
640
|
+
return _extract_pdf_text(file)
|
|
641
|
+
|
|
642
|
+
elif ext == '.docx':
|
|
643
|
+
return _extract_docx_text(source)
|
|
644
|
+
|
|
645
|
+
elif ext == '.odt':
|
|
646
|
+
return _extract_odt_text(source)
|
|
647
|
+
|
|
648
|
+
elif ext in ('.html', '.htm'):
|
|
649
|
+
with open(source, 'r', encoding='utf-8') as f:
|
|
650
|
+
return _extract_html_text(f)
|
|
651
|
+
|
|
652
|
+
# Data formats
|
|
653
|
+
elif ext == '.csv':
|
|
654
|
+
with open(source, 'rb') as f:
|
|
655
|
+
return _extract_csv_text(f)
|
|
656
|
+
|
|
657
|
+
elif ext == '.json':
|
|
658
|
+
with open(source, 'rb') as f:
|
|
659
|
+
return _extract_json_text(f)
|
|
660
|
+
|
|
661
|
+
elif ext == '.xml':
|
|
662
|
+
with open(source, 'rb') as f:
|
|
663
|
+
return _extract_xml_text(f)
|
|
664
|
+
|
|
665
|
+
elif ext in ('.yaml', '.yml'):
|
|
666
|
+
with open(source, 'rb') as f:
|
|
667
|
+
return _extract_yaml_text(f)
|
|
668
|
+
|
|
669
|
+
elif ext == '.toml':
|
|
670
|
+
with open(source, 'rb') as f:
|
|
671
|
+
return _extract_toml_text(f)
|
|
672
|
+
|
|
673
|
+
elif ext in ('.ini', '.cfg', '.conf'):
|
|
674
|
+
with open(source, 'rb') as f:
|
|
675
|
+
return _extract_ini_text(f)
|
|
676
|
+
|
|
677
|
+
# Spreadsheet formats
|
|
678
|
+
elif ext == '.xlsx':
|
|
679
|
+
return _extract_xlsx_text(source)
|
|
680
|
+
|
|
681
|
+
elif ext == '.ods':
|
|
682
|
+
return _extract_ods_text(source)
|
|
683
|
+
|
|
684
|
+
# Archive formats
|
|
685
|
+
elif ext == '.zip':
|
|
686
|
+
return _extract_zip_text(source)
|
|
687
|
+
|
|
688
|
+
elif ext in ('.tar', '.tar.gz', '.tgz'):
|
|
689
|
+
mode = 'r:gz' if ext in ('.tar.gz', '.tgz') else 'r'
|
|
690
|
+
with open(source, 'rb') as f:
|
|
691
|
+
return _extract_tar_text(f, mode=mode)
|
|
692
|
+
|
|
693
|
+
elif ext in ('.tar.bz2', '.tbz2'):
|
|
694
|
+
with open(source, 'rb') as f:
|
|
695
|
+
return _extract_tar_text(f, mode='r:bz2')
|
|
696
|
+
|
|
697
|
+
# Email format
|
|
698
|
+
elif ext == '.eml':
|
|
699
|
+
with open(source, 'rb') as f:
|
|
700
|
+
return _extract_eml_text(f)
|
|
701
|
+
|
|
702
|
+
else:
|
|
703
|
+
# Plain text file
|
|
704
|
+
with open(source, 'r', encoding='utf-8') as f:
|
|
705
|
+
return f.read()
|
|
706
|
+
|
|
707
|
+
except FileNotFoundError:
|
|
708
|
+
return f"Unable to process file. File not found: {source}"
|
|
709
|
+
except PermissionError:
|
|
710
|
+
return f"Unable to process file. Permission denied: {source}"
|
|
711
|
+
except Exception as e:
|
|
712
|
+
return f"Error while attempting to read file: {e}"
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# ============================================================================
|
|
716
|
+
# Web Handlers
|
|
717
|
+
# ============================================================================
|
|
718
|
+
|
|
719
|
+
def _extract_with_beautifulsoup(html_content: str) -> str:
|
|
720
|
+
"""Extract text from HTML using BeautifulSoup with smart filtering.
|
|
721
|
+
|
|
722
|
+
Removes common non-content elements like scripts, styles, navigation, etc.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
html_content: HTML content as string
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
Extracted text content
|
|
729
|
+
"""
|
|
730
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
731
|
+
|
|
732
|
+
# Remove non-content elements
|
|
733
|
+
for element in soup(['script', 'style', 'nav', 'header', 'footer',
|
|
734
|
+
'aside', 'iframe', 'noscript']):
|
|
735
|
+
element.decompose()
|
|
736
|
+
|
|
737
|
+
# Try to find main content area first
|
|
738
|
+
main_content = soup.find('main') or soup.find('article') or soup.find('body')
|
|
739
|
+
|
|
740
|
+
if main_content:
|
|
741
|
+
text = main_content.get_text(separator='\n', strip=True)
|
|
742
|
+
else:
|
|
743
|
+
text = soup.get_text(separator='\n', strip=True)
|
|
744
|
+
|
|
745
|
+
return strip_newlines(text)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _is_content_sufficient(text: str, min_length: int = 100) -> bool:
|
|
749
|
+
"""Check if extracted content seems sufficient (not empty or too short).
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
text: Extracted text content
|
|
753
|
+
min_length: Minimum acceptable length in characters
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
True if content seems sufficient, False otherwise
|
|
757
|
+
"""
|
|
758
|
+
if not text or len(text.strip()) < min_length:
|
|
759
|
+
return False
|
|
760
|
+
|
|
761
|
+
# Check if content is not just boilerplate
|
|
762
|
+
words = text.split()
|
|
763
|
+
if len(words) < 20: # Less than 20 words is suspicious
|
|
764
|
+
return False
|
|
765
|
+
|
|
766
|
+
return True
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def extract_webpage_content(url: str, force_selenium: bool = False) -> str:
|
|
770
|
+
"""Extract text content from a web page using a hybrid approach.
|
|
771
|
+
|
|
772
|
+
Strategy:
|
|
773
|
+
1. If force_selenium=False:
|
|
774
|
+
- Try trafilatura first (if available) - fast and smart
|
|
775
|
+
- If trafilatura unavailable or content insufficient, try BeautifulSoup
|
|
776
|
+
- If still insufficient, fallback to Selenium (handles JavaScript)
|
|
777
|
+
2. If force_selenium=True:
|
|
778
|
+
- Use Selenium directly (for known dynamic pages)
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
url: URL to fetch
|
|
782
|
+
force_selenium: If True, skip lightweight methods and use Selenium directly
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
Extracted text content from the page
|
|
786
|
+
"""
|
|
787
|
+
# Force Selenium path
|
|
788
|
+
if force_selenium:
|
|
789
|
+
driver = None
|
|
790
|
+
try:
|
|
791
|
+
driver = get_webdriver()
|
|
792
|
+
driver.get(url)
|
|
793
|
+
page_source = driver.page_source
|
|
794
|
+
return _extract_with_beautifulsoup(page_source)
|
|
795
|
+
finally:
|
|
796
|
+
if driver:
|
|
797
|
+
driver.quit()
|
|
798
|
+
|
|
799
|
+
# Try lightweight methods first
|
|
800
|
+
try:
|
|
801
|
+
# Fetch page content
|
|
802
|
+
response = requests.get(url, timeout=30)
|
|
803
|
+
response.raise_for_status()
|
|
804
|
+
html_content = response.text
|
|
805
|
+
|
|
806
|
+
# Method 1: Try trafilatura if available (best for articles/blogs)
|
|
807
|
+
if HAS_TRAFILATURA:
|
|
808
|
+
extracted = trafilatura.extract(html_content, include_comments=False,
|
|
809
|
+
include_tables=True)
|
|
810
|
+
if extracted and _is_content_sufficient(extracted):
|
|
811
|
+
return strip_newlines(extracted)
|
|
812
|
+
|
|
813
|
+
# Method 2: Try BeautifulSoup with smart filtering
|
|
814
|
+
text = _extract_with_beautifulsoup(html_content)
|
|
815
|
+
if _is_content_sufficient(text):
|
|
816
|
+
return text
|
|
817
|
+
|
|
818
|
+
# Method 3: Content seems insufficient, might need JavaScript
|
|
819
|
+
# Fallback to Selenium
|
|
820
|
+
driver = None
|
|
821
|
+
try:
|
|
822
|
+
driver = get_webdriver()
|
|
823
|
+
driver.get(url)
|
|
824
|
+
page_source = driver.page_source
|
|
825
|
+
return _extract_with_beautifulsoup(page_source)
|
|
826
|
+
finally:
|
|
827
|
+
if driver:
|
|
828
|
+
driver.quit()
|
|
829
|
+
|
|
830
|
+
except requests.exceptions.RequestException:
|
|
831
|
+
# If requests fails, try Selenium as last resort
|
|
832
|
+
driver = None
|
|
833
|
+
try:
|
|
834
|
+
driver = get_webdriver()
|
|
835
|
+
driver.get(url)
|
|
836
|
+
page_source = driver.page_source
|
|
837
|
+
return _extract_with_beautifulsoup(page_source)
|
|
838
|
+
finally:
|
|
839
|
+
if driver:
|
|
840
|
+
driver.quit()
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def handle_url(source: str) -> str:
|
|
844
|
+
"""Extract text from a URL (document or web page).
|
|
845
|
+
|
|
846
|
+
Automatically handles various document types served over HTTP based on
|
|
847
|
+
content-type or URL extension, falling back to web scraping for HTML pages.
|
|
848
|
+
|
|
849
|
+
Supported formats:
|
|
850
|
+
- Documents: PDF, DOCX, ODT
|
|
851
|
+
- Data: CSV, JSON, XML, YAML
|
|
852
|
+
- Spreadsheets: XLSX, ODS
|
|
853
|
+
- Archives: ZIP
|
|
854
|
+
- Web pages: HTML (via Selenium)
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
source: URL to fetch
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
Extracted text content, or error message
|
|
861
|
+
"""
|
|
862
|
+
try:
|
|
863
|
+
response = requests.get(source, timeout=30)
|
|
864
|
+
response.raise_for_status()
|
|
865
|
+
|
|
866
|
+
content_type = response.headers.get('content-type', '').lower()
|
|
867
|
+
|
|
868
|
+
# Handle PDF documents
|
|
869
|
+
if 'application/pdf' in content_type or source.endswith('.pdf'):
|
|
870
|
+
with BytesIO(response.content) as file:
|
|
871
|
+
return _extract_pdf_text(file)
|
|
872
|
+
|
|
873
|
+
# Handle DOCX documents
|
|
874
|
+
elif 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' in content_type \
|
|
875
|
+
or source.endswith('.docx'):
|
|
876
|
+
with BytesIO(response.content) as file:
|
|
877
|
+
return _extract_docx_text(file)
|
|
878
|
+
|
|
879
|
+
# Handle ODT documents
|
|
880
|
+
elif 'application/vnd.oasis.opendocument.text' in content_type \
|
|
881
|
+
or source.endswith('.odt'):
|
|
882
|
+
with BytesIO(response.content) as file:
|
|
883
|
+
return _extract_odt_text(file)
|
|
884
|
+
|
|
885
|
+
# Handle XLSX spreadsheets
|
|
886
|
+
elif 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' in content_type \
|
|
887
|
+
or source.endswith('.xlsx'):
|
|
888
|
+
with BytesIO(response.content) as file:
|
|
889
|
+
return _extract_xlsx_text(file)
|
|
890
|
+
|
|
891
|
+
# Handle ODS spreadsheets
|
|
892
|
+
elif 'application/vnd.oasis.opendocument.spreadsheet' in content_type \
|
|
893
|
+
or source.endswith('.ods'):
|
|
894
|
+
with BytesIO(response.content) as file:
|
|
895
|
+
return _extract_ods_text(file)
|
|
896
|
+
|
|
897
|
+
# Handle CSV
|
|
898
|
+
elif 'text/csv' in content_type or source.endswith('.csv'):
|
|
899
|
+
with BytesIO(response.content) as file:
|
|
900
|
+
return _extract_csv_text(file)
|
|
901
|
+
|
|
902
|
+
# Handle JSON
|
|
903
|
+
elif 'application/json' in content_type or source.endswith('.json'):
|
|
904
|
+
with BytesIO(response.content) as file:
|
|
905
|
+
return _extract_json_text(file)
|
|
906
|
+
|
|
907
|
+
# Handle XML
|
|
908
|
+
elif 'application/xml' in content_type or 'text/xml' in content_type \
|
|
909
|
+
or source.endswith('.xml'):
|
|
910
|
+
with BytesIO(response.content) as file:
|
|
911
|
+
return _extract_xml_text(file)
|
|
912
|
+
|
|
913
|
+
# Handle YAML
|
|
914
|
+
elif 'application/x-yaml' in content_type or source.endswith(('.yaml', '.yml')):
|
|
915
|
+
with BytesIO(response.content) as file:
|
|
916
|
+
return _extract_yaml_text(file)
|
|
917
|
+
|
|
918
|
+
# Handle ZIP archives
|
|
919
|
+
elif 'application/zip' in content_type or source.endswith('.zip'):
|
|
920
|
+
with BytesIO(response.content) as file:
|
|
921
|
+
return _extract_zip_text(file)
|
|
922
|
+
|
|
923
|
+
# Default to web scraping
|
|
924
|
+
else:
|
|
925
|
+
return extract_webpage_content(source)
|
|
926
|
+
|
|
927
|
+
except requests.exceptions.Timeout:
|
|
928
|
+
return f"Unable to process URL. Request timed out: {source}"
|
|
929
|
+
except requests.exceptions.RequestException as e:
|
|
930
|
+
return f"Unable to process URL. Connection error: {e}"
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# ============================================================================
|
|
934
|
+
# Python Object Inspection
|
|
935
|
+
# ============================================================================
|
|
936
|
+
|
|
937
|
+
def handle_class(source: type) -> str:
|
|
938
|
+
"""Generate detailed JSON representation of a class.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
source: Class object to inspect
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
JSON string with class metadata
|
|
945
|
+
"""
|
|
946
|
+
class_info = {
|
|
947
|
+
"type": "class",
|
|
948
|
+
"name": source.__name__,
|
|
949
|
+
"doc": inspect.getdoc(source) or "No documentation available.",
|
|
950
|
+
"base_classes": [base.__name__ for base in inspect.getmro(source)[1:]],
|
|
951
|
+
"methods": {},
|
|
952
|
+
"class_methods": {},
|
|
953
|
+
"static_methods": {},
|
|
954
|
+
"properties": {},
|
|
955
|
+
"attributes": {},
|
|
956
|
+
"string_repr": repr(source)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
# Process methods, class methods, static methods, and properties
|
|
960
|
+
for name, member in inspect.getmembers(source):
|
|
961
|
+
if inspect.isfunction(member):
|
|
962
|
+
class_info["methods"][name] = {
|
|
963
|
+
"doc": inspect.getdoc(member) or "No documentation available.",
|
|
964
|
+
"signature": str(inspect.signature(member))
|
|
965
|
+
}
|
|
966
|
+
elif isinstance(member, classmethod):
|
|
967
|
+
class_info["class_methods"][name] = {
|
|
968
|
+
"doc": inspect.getdoc(member) or "No documentation available.",
|
|
969
|
+
"signature": str(inspect.signature(member.__func__))
|
|
970
|
+
}
|
|
971
|
+
elif isinstance(member, staticmethod):
|
|
972
|
+
class_info["static_methods"][name] = {
|
|
973
|
+
"doc": inspect.getdoc(member) or "No documentation available."
|
|
974
|
+
}
|
|
975
|
+
elif isinstance(member, property):
|
|
976
|
+
class_info["properties"][name] = {
|
|
977
|
+
"doc": inspect.getdoc(member) or "No documentation available."
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
# Process attributes
|
|
981
|
+
for name in dir(source):
|
|
982
|
+
if not name.startswith('__') and not inspect.isroutine(getattr(source, name)):
|
|
983
|
+
try:
|
|
984
|
+
attribute = inspect.getattr_static(source, name)
|
|
985
|
+
serialized = json.dumps(attribute)
|
|
986
|
+
except:
|
|
987
|
+
serialized = repr(attribute)
|
|
988
|
+
|
|
989
|
+
class_info["attributes"][name] = {
|
|
990
|
+
"value": serialized,
|
|
991
|
+
"type": type(attribute).__name__
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return json.dumps(class_info, indent=4)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def handle_function(source: Any) -> str:
|
|
998
|
+
"""Generate detailed JSON representation of a function or callable.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
source: Function, method, or callable object to inspect
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
JSON string with callable metadata
|
|
1005
|
+
"""
|
|
1006
|
+
# Determine the specific type of callable
|
|
1007
|
+
if inspect.isfunction(source):
|
|
1008
|
+
callable_type = "function"
|
|
1009
|
+
elif inspect.ismethod(source):
|
|
1010
|
+
callable_type = "instance method"
|
|
1011
|
+
elif inspect.isbuiltin(source):
|
|
1012
|
+
callable_type = "built-in"
|
|
1013
|
+
elif inspect.ismethoddescriptor(source):
|
|
1014
|
+
callable_type = "method descriptor"
|
|
1015
|
+
else:
|
|
1016
|
+
callable_type = "other callable"
|
|
1017
|
+
|
|
1018
|
+
callable_info = {
|
|
1019
|
+
"type": callable_type,
|
|
1020
|
+
"name": getattr(source, '__name__', 'Unnamed'),
|
|
1021
|
+
"doc": inspect.getdoc(source) or "No documentation available.",
|
|
1022
|
+
"module": getattr(source, '__module__', None),
|
|
1023
|
+
"string_repr": repr(source),
|
|
1024
|
+
"signature": None,
|
|
1025
|
+
"source": None
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
# Attempt to get the signature
|
|
1029
|
+
try:
|
|
1030
|
+
signature = inspect.signature(source)
|
|
1031
|
+
callable_info["signature"] = str(signature)
|
|
1032
|
+
except (TypeError, ValueError):
|
|
1033
|
+
callable_info["signature"] = "Not available"
|
|
1034
|
+
|
|
1035
|
+
# Attempt to get the source code if it's a regular function
|
|
1036
|
+
if callable_type == "function":
|
|
1037
|
+
try:
|
|
1038
|
+
callable_info["source"] = inspect.getsource(source)
|
|
1039
|
+
except Exception:
|
|
1040
|
+
callable_info["source"] = "Not available"
|
|
1041
|
+
|
|
1042
|
+
return json.dumps(callable_info, indent=4)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def handle_module(source: Any) -> str:
|
|
1046
|
+
"""Generate detailed JSON representation of a module.
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
source: Module object to inspect
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
JSON string with module metadata
|
|
1053
|
+
"""
|
|
1054
|
+
module_info = {
|
|
1055
|
+
"type": "module",
|
|
1056
|
+
"name": source.__name__,
|
|
1057
|
+
"doc": inspect.getdoc(source) or "No documentation available.",
|
|
1058
|
+
"functions": {},
|
|
1059
|
+
"classes": {},
|
|
1060
|
+
"variables": {},
|
|
1061
|
+
"submodules": {},
|
|
1062
|
+
"string_repr": repr(source)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
for name, member in inspect.getmembers(source):
|
|
1066
|
+
if inspect.isfunction(member) and not name.startswith('_'):
|
|
1067
|
+
module_info["functions"][name] = {
|
|
1068
|
+
"doc": inspect.getdoc(member) or "No documentation available.",
|
|
1069
|
+
"signature": str(inspect.signature(member))
|
|
1070
|
+
}
|
|
1071
|
+
elif inspect.isclass(member) and not name.startswith('_'):
|
|
1072
|
+
module_info["classes"][name] = {
|
|
1073
|
+
"doc": inspect.getdoc(member) or "No documentation available."
|
|
1074
|
+
}
|
|
1075
|
+
elif inspect.ismodule(member) and not name.startswith('_'):
|
|
1076
|
+
module_info["submodules"][name] = {
|
|
1077
|
+
"doc": inspect.getdoc(member) or "No documentation available."
|
|
1078
|
+
}
|
|
1079
|
+
elif not (inspect.isroutine(member) or inspect.isclass(member) or inspect.ismodule(member)) and not name.startswith('_'):
|
|
1080
|
+
try:
|
|
1081
|
+
value = json.dumps(member)
|
|
1082
|
+
except:
|
|
1083
|
+
value = repr(member)
|
|
1084
|
+
|
|
1085
|
+
module_info["variables"][name] = {
|
|
1086
|
+
"value": value,
|
|
1087
|
+
"type": type(member).__name__
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return json.dumps(module_info, indent=4)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def handle_object(source: Any) -> str:
|
|
1094
|
+
"""Generate detailed JSON representation of a generic object instance.
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
source: Object instance to inspect
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
JSON string with instance metadata
|
|
1101
|
+
"""
|
|
1102
|
+
# Try to serialize the object
|
|
1103
|
+
serialized = repr(source)
|
|
1104
|
+
if hasattr(source, 'dumps') and callable(getattr(source, 'dumps')):
|
|
1105
|
+
try:
|
|
1106
|
+
serialized = source.dumps()
|
|
1107
|
+
except:
|
|
1108
|
+
pass
|
|
1109
|
+
else:
|
|
1110
|
+
try:
|
|
1111
|
+
serialized = json.dumps(source)
|
|
1112
|
+
except:
|
|
1113
|
+
pass
|
|
1114
|
+
|
|
1115
|
+
instance_info = {
|
|
1116
|
+
"type": "class_instance",
|
|
1117
|
+
"class": source.__class__.__name__,
|
|
1118
|
+
"doc": inspect.getdoc(source) or "No documentation available.",
|
|
1119
|
+
"attributes": {},
|
|
1120
|
+
"methods": {},
|
|
1121
|
+
"properties": {},
|
|
1122
|
+
"callable": callable(source),
|
|
1123
|
+
"call_signature": None,
|
|
1124
|
+
"string_repr": serialized
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if instance_info["callable"]:
|
|
1128
|
+
try:
|
|
1129
|
+
instance_info["call_signature"] = str(inspect.signature(source.__call__))
|
|
1130
|
+
except (TypeError, ValueError):
|
|
1131
|
+
instance_info["call_signature"] = "Not available"
|
|
1132
|
+
|
|
1133
|
+
# Use inspect.getmembers to safely retrieve object members
|
|
1134
|
+
members = inspect.getmembers(source, lambda a: not inspect.isroutine(a))
|
|
1135
|
+
methods = inspect.getmembers(source, inspect.isroutine)
|
|
1136
|
+
properties = [m for m in members if isinstance(m[1], property)]
|
|
1137
|
+
|
|
1138
|
+
# Filter attributes, methods, and properties
|
|
1139
|
+
for name, member in members:
|
|
1140
|
+
if not name.startswith('__'):
|
|
1141
|
+
try:
|
|
1142
|
+
value = json.dumps(member)
|
|
1143
|
+
except:
|
|
1144
|
+
value = repr(member)
|
|
1145
|
+
|
|
1146
|
+
instance_info["attributes"][name] = {
|
|
1147
|
+
"value": value,
|
|
1148
|
+
"type": type(member).__name__,
|
|
1149
|
+
"string_repr": repr(member)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
for name, method in methods:
|
|
1153
|
+
if not name.startswith('__'):
|
|
1154
|
+
try:
|
|
1155
|
+
signature = str(inspect.signature(method))
|
|
1156
|
+
except (TypeError, ValueError):
|
|
1157
|
+
signature = "Not available"
|
|
1158
|
+
|
|
1159
|
+
instance_info["methods"][name] = {
|
|
1160
|
+
"doc": inspect.getdoc(method) or "No documentation available.",
|
|
1161
|
+
"signature": signature
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
for name, prop in properties:
|
|
1165
|
+
instance_info["properties"][name] = {
|
|
1166
|
+
"doc": inspect.getdoc(prop) or "No documentation available."
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return json.dumps(instance_info, indent=4)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
# ============================================================================
|
|
1173
|
+
# Main Entry Point
|
|
1174
|
+
# ============================================================================
|
|
1175
|
+
|
|
1176
|
+
def get_text(source: Any) -> str:
|
|
1177
|
+
"""Extract text from various sources.
|
|
1178
|
+
|
|
1179
|
+
This is the main entry point that dispatches to appropriate handlers
|
|
1180
|
+
based on the type of source.
|
|
1181
|
+
|
|
1182
|
+
Supported sources:
|
|
1183
|
+
- Python objects (classes, functions, modules, instances)
|
|
1184
|
+
- URLs (web pages, documents)
|
|
1185
|
+
- File paths (PDF, DOCX, ODT, HTML, text files)
|
|
1186
|
+
- Directory paths (generates tree structure)
|
|
1187
|
+
- Primitive types (lists, dicts, etc.)
|
|
1188
|
+
- Plain strings (returned as-is)
|
|
1189
|
+
|
|
1190
|
+
Args:
|
|
1191
|
+
source: The source to extract text from
|
|
1192
|
+
|
|
1193
|
+
Returns:
|
|
1194
|
+
Extracted text, cleaned of excessive newlines
|
|
1195
|
+
"""
|
|
1196
|
+
# Handle non-string objects (Python objects)
|
|
1197
|
+
if not isinstance(source, str):
|
|
1198
|
+
# Classes
|
|
1199
|
+
if inspect.isclass(source):
|
|
1200
|
+
text = handle_class(source)
|
|
1201
|
+
|
|
1202
|
+
# Callables (functions, methods, builtins)
|
|
1203
|
+
elif inspect.isfunction(source) or inspect.ismethod(source) or \
|
|
1204
|
+
inspect.isbuiltin(source) or inspect.ismethoddescriptor(source):
|
|
1205
|
+
text = handle_function(source)
|
|
1206
|
+
|
|
1207
|
+
# Modules
|
|
1208
|
+
elif inspect.ismodule(source):
|
|
1209
|
+
text = handle_module(source)
|
|
1210
|
+
|
|
1211
|
+
# Built-in types
|
|
1212
|
+
elif isinstance(source, (list, dict, tuple, set, int, float, bool)):
|
|
1213
|
+
try:
|
|
1214
|
+
text = json.dumps(source, indent=2, ensure_ascii=False)
|
|
1215
|
+
except:
|
|
1216
|
+
text = repr(source)
|
|
1217
|
+
|
|
1218
|
+
# Generic objects
|
|
1219
|
+
elif isinstance(source, object):
|
|
1220
|
+
text = handle_object(source)
|
|
1221
|
+
|
|
1222
|
+
# Fallback
|
|
1223
|
+
else:
|
|
1224
|
+
text = repr(source)
|
|
1225
|
+
|
|
1226
|
+
# Handle strings (URLs, file paths, or plain text)
|
|
1227
|
+
elif source.startswith('http://') or source.startswith('https://'):
|
|
1228
|
+
text = handle_url(source)
|
|
1229
|
+
|
|
1230
|
+
elif os.path.isfile(source):
|
|
1231
|
+
text = handle_file(source)
|
|
1232
|
+
|
|
1233
|
+
elif os.path.isdir(source):
|
|
1234
|
+
text = handle_directory(source)
|
|
1235
|
+
|
|
1236
|
+
else:
|
|
1237
|
+
# Plain string
|
|
1238
|
+
text = source
|
|
1239
|
+
|
|
1240
|
+
return strip_newlines(text)
|