Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/preprocessing/types.py: 67%
132 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""
2Document preprocessing types.
4Contains dataclasses and enums for document preprocessing.
5"""
7from __future__ import annotations
9from dataclasses import dataclass, field
10from datetime import datetime
11from enum import Enum
12from typing import Any
15class DocumentType(str, Enum):
16 """Types of documents that can be preprocessed."""
18 PDF = "pdf"
19 IMAGE = "image"
20 HTML = "html"
21 MARKDOWN = "markdown"
22 TEXT = "text"
23 DOCX = "docx"
24 UNKNOWN = "unknown"
27class TableFormat(str, Enum):
28 """Formats for extracted tables."""
30 MARKDOWN = "markdown"
31 HTML = "html"
32 CSV = "csv"
33 JSON = "json"
36@dataclass
37class ExtractedImage:
38 """Represents an extracted image from a document.
40 Attributes:
41 image_id: Unique identifier for the image.
42 page_number: Page number where image was found.
43 position: Position on page (x, y, width, height).
44 alt_text: Alternative text description.
45 ocr_text: Text extracted from image via OCR.
46 metadata: Additional metadata.
47 """
49 image_id: str
50 page_number: int
51 position: tuple[int, int, int, int] | None = None
52 alt_text: str | None = None
53 ocr_text: str | None = None
54 metadata: dict[str, Any] = field(default_factory=dict)
57@dataclass
58class ExtractedTable:
59 """Represents an extracted table from a document.
61 Attributes:
62 table_id: Unique identifier for the table.
63 page_number: Page number where table was found.
64 headers: Column headers.
65 rows: Table rows (list of lists).
66 caption: Table caption or title.
67 format: Format of the table.
68 metadata: Additional metadata.
69 """
71 table_id: str
72 page_number: int
73 headers: list[str] = field(default_factory=list)
74 rows: list[list[str]] = field(default_factory=list)
75 caption: str | None = None
76 format: TableFormat = TableFormat.MARKDOWN
77 metadata: dict[str, Any] = field(default_factory=dict)
79 def to_markdown(self) -> str:
80 """Convert table to Markdown format."""
81 if not self.headers and not self.rows:
82 return ""
84 lines = []
86 # Add caption
87 if self.caption:
88 lines.append(f"**{self.caption}**\n")
90 # Add headers
91 if self.headers:
92 lines.append("| " + " | ".join(self.headers) + " |")
93 lines.append("|" + "|".join(["---"] * len(self.headers)) + "|")
95 # Add rows
96 for row in self.rows:
97 lines.append("| " + " | ".join(row) + " |")
99 return "\n".join(lines) + "\n"
101 def to_html(self) -> str:
102 """Convert table to HTML format."""
103 if not self.headers and not self.rows:
104 return ""
106 html_parts = []
108 # Add caption
109 if self.caption:
110 html_parts.append(f"<caption>{self.caption}</caption>")
112 # Build table
113 html_parts.append("<table>")
115 # Add headers
116 if self.headers:
117 html_parts.append("<thead><tr>")
118 for header in self.headers:
119 html_parts.append(f"<th>{header}</th>")
120 html_parts.append("</tr></thead>")
122 # Add rows
123 html_parts.append("<tbody>")
124 for row in self.rows:
125 html_parts.append("<tr>")
126 for cell in row:
127 html_parts.append(f"<td>{cell}</td>")
128 html_parts.append("</tr>")
129 html_parts.append("</tbody>")
131 html_parts.append("</table>")
132 return "".join(html_parts)
134 def to_csv(self) -> str:
135 """Convert table to CSV format."""
136 if not self.headers and not self.rows:
137 return ""
139 lines = []
141 # Add headers
142 if self.headers:
143 lines.append(",".join(self.headers))
145 # Add rows
146 for row in self.rows:
147 lines.append(",".join(row))
149 return "\n".join(lines)
151 def to_json(self) -> list[dict[str, Any]]:
152 """Convert table to JSON-compatible list of dicts."""
153 if not self.headers and not self.rows:
154 return []
156 result: list[dict[str, Any]] = []
157 if self.headers:
158 for row in self.rows:
159 result.append(dict(zip(self.headers, row, strict=False)))
160 else:
161 for i, row in enumerate(self.rows):
162 result.append({"row": i, "data": row})
164 return result
167@dataclass
168class DocumentMetadata:
169 """Metadata for a preprocessed document.
171 Attributes:
172 doc_type: Type of document.
173 page_count: Number of pages.
174 title: Document title.
175 author: Document author.
176 created_at: Creation timestamp.
177 updated_at: Last modification timestamp.
178 language: Document language.
179 word_count: Number of words.
180 char_count: Number of characters.
181 images: List of extracted images.
182 tables: List of extracted tables.
183 custom_metadata: Additional custom metadata.
184 """
186 doc_type: DocumentType = DocumentType.UNKNOWN
187 page_count: int = 0
188 title: str | None = None
189 author: str | None = None
190 created_at: datetime | None = None
191 updated_at: datetime | None = None
192 language: str | None = None
193 word_count: int = 0
194 char_count: int = 0
195 keywords: list[str] = field(default_factory=list)
196 summary: str | None = None
197 images: list[ExtractedImage] = field(default_factory=list)
198 tables: list[ExtractedTable] = field(default_factory=list)
199 custom_metadata: dict[str, Any] = field(default_factory=dict)
201 def add_image(self, image: ExtractedImage) -> None:
202 """Add an extracted image."""
203 self.images.append(image)
205 def add_table(self, table: ExtractedTable) -> None:
206 """Add an extracted table."""
207 self.tables.append(table)
209 @property
210 def custom(self) -> dict[str, Any]:
211 """Backwards compatible alias for custom_metadata."""
212 return self.custom_metadata
214 @property
215 def document_type(self) -> DocumentType:
216 """Backwards compatible alias for doc_type."""
217 return self.doc_type
219 def merge(self, other: DocumentMetadata) -> None:
220 """Merge another metadata object into this one."""
221 if other.title and not self.title:
222 self.title = other.title
223 if other.author and not self.author:
224 self.author = other.author
225 if other.language and not self.language:
226 self.language = other.language
227 if other.word_count > 0 and self.word_count == 0:
228 self.word_count = other.word_count
229 if other.char_count > 0 and self.char_count == 0:
230 self.char_count = other.char_count
231 if (
232 other.doc_type != DocumentType.UNKNOWN
233 and self.doc_type == DocumentType.UNKNOWN
234 ):
235 self.doc_type = other.doc_type
237 self.keywords.extend(k for k in other.keywords if k not in self.keywords)
238 self.images.extend(other.images)
239 self.tables.extend(other.tables)
240 self.custom_metadata.update(other.custom_metadata)
241 if other.summary and not self.summary:
242 self.summary = other.summary