techprotrade commited on
Commit
734b5b4
·
verified ·
1 Parent(s): 216d846

Add src directory

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. src/__init__.py +1 -0
  2. src/__pycache__/__init__.cpython-314.pyc +0 -0
  3. src/agents/__init__.py +1 -0
  4. src/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  5. src/agents/__pycache__/annotation_agent.cpython-314.pyc +0 -0
  6. src/agents/__pycache__/base_agent.cpython-314.pyc +0 -0
  7. src/agents/__pycache__/content_validation_agent.cpython-314.pyc +0 -0
  8. src/agents/__pycache__/editing_agent.cpython-314.pyc +0 -0
  9. src/agents/__pycache__/export_agent.cpython-314.pyc +0 -0
  10. src/agents/__pycache__/export_rendering_agent.cpython-314.pyc +0 -0
  11. src/agents/__pycache__/format_correction_agent.cpython-314.pyc +0 -0
  12. src/agents/__pycache__/image_processing_agent.cpython-314.pyc +0 -0
  13. src/agents/__pycache__/language_detection_agent.cpython-314.pyc +0 -0
  14. src/agents/__pycache__/layout_analysis_agent.cpython-314.pyc +0 -0
  15. src/agents/__pycache__/ocr_agent.cpython-314.pyc +0 -0
  16. src/agents/__pycache__/structure_inspector.cpython-314.pyc +0 -0
  17. src/agents/__pycache__/table_detection_agent.cpython-314.pyc +0 -0
  18. src/agents/__pycache__/text_extraction_agent.cpython-314.pyc +0 -0
  19. src/agents/__pycache__/validation_agent.cpython-314.pyc +0 -0
  20. src/agents/annotation_agent.py +37 -0
  21. src/agents/base_agent.py +79 -0
  22. src/agents/content_validation/__init__.py +3 -0
  23. src/agents/content_validation/agent.py +43 -0
  24. src/agents/content_validation_agent.py +40 -0
  25. src/agents/editing_agent.py +48 -0
  26. src/agents/export_agent.py +24 -0
  27. src/agents/export_rendering/__init__.py +3 -0
  28. src/agents/export_rendering/agent.py +25 -0
  29. src/agents/export_rendering_agent.py +39 -0
  30. src/agents/format_correction/__init__.py +3 -0
  31. src/agents/format_correction/agent.py +32 -0
  32. src/agents/format_correction_agent.py +37 -0
  33. src/agents/image_processing/__init__.py +3 -0
  34. src/agents/image_processing/agent.py +33 -0
  35. src/agents/image_processing_agent.py +40 -0
  36. src/agents/language_detection/__init__.py +3 -0
  37. src/agents/language_detection/agent.py +39 -0
  38. src/agents/language_detection_agent.py +56 -0
  39. src/agents/layout_analysis/__init__.py +3 -0
  40. src/agents/layout_analysis/agent.py +37 -0
  41. src/agents/layout_analysis_agent.py +45 -0
  42. src/agents/ocr_agent.py +27 -0
  43. src/agents/structure_inspector.py +37 -0
  44. src/agents/table_detection/__init__.py +3 -0
  45. src/agents/table_detection/agent.py +41 -0
  46. src/agents/table_detection_agent.py +35 -0
  47. src/agents/text_extraction/__init__.py +3 -0
  48. src/agents/text_extraction/agent.py +36 -0
  49. src/agents/text_extraction_agent.py +45 -0
  50. src/agents/validation_agent.py +22 -0
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
src/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (158 Bytes). View file
 
src/agents/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
src/agents/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (165 Bytes). View file
 
src/agents/__pycache__/annotation_agent.cpython-314.pyc ADDED
Binary file (3.38 kB). View file
 
src/agents/__pycache__/base_agent.cpython-314.pyc ADDED
Binary file (5.78 kB). View file
 
src/agents/__pycache__/content_validation_agent.cpython-314.pyc ADDED
Binary file (3.03 kB). View file
 
src/agents/__pycache__/editing_agent.cpython-314.pyc ADDED
Binary file (4.7 kB). View file
 
src/agents/__pycache__/export_agent.cpython-314.pyc ADDED
Binary file (2.01 kB). View file
 
src/agents/__pycache__/export_rendering_agent.cpython-314.pyc ADDED
Binary file (2.99 kB). View file
 
src/agents/__pycache__/format_correction_agent.cpython-314.pyc ADDED
Binary file (3.06 kB). View file
 
src/agents/__pycache__/image_processing_agent.cpython-314.pyc ADDED
Binary file (2.88 kB). View file
 
src/agents/__pycache__/language_detection_agent.cpython-314.pyc ADDED
Binary file (3.34 kB). View file
 
src/agents/__pycache__/layout_analysis_agent.cpython-314.pyc ADDED
Binary file (3.57 kB). View file
 
src/agents/__pycache__/ocr_agent.cpython-314.pyc ADDED
Binary file (2.28 kB). View file
 
src/agents/__pycache__/structure_inspector.cpython-314.pyc ADDED
Binary file (2.34 kB). View file
 
src/agents/__pycache__/table_detection_agent.cpython-314.pyc ADDED
Binary file (2.86 kB). View file
 
src/agents/__pycache__/text_extraction_agent.cpython-314.pyc ADDED
Binary file (2.93 kB). View file
 
src/agents/__pycache__/validation_agent.cpython-314.pyc ADDED
Binary file (1.75 kB). View file
 
src/agents/annotation_agent.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import fitz
4
+ from src.core.models import PDFOperation
5
+
6
+ class OfflineAnnotationAgent:
7
+ name = "annotation_agent"
8
+
9
+ def run(self, input_pdf: Path, output_pdf: Path, operations: list[PDFOperation]) -> Path:
10
+ doc = fitz.open(input_pdf)
11
+ for op in operations:
12
+ if op.page is None or op.page >= len(doc): continue
13
+ page = doc[op.page]
14
+ rect = fitz.Rect(op.rect) if op.rect else None
15
+ color = tuple(op.color) if op.color else (1, 1, 0)
16
+ if op.type == "highlight" and rect:
17
+ annot = page.add_highlight_annot(rect)
18
+ annot.set_colors(stroke=color)
19
+ annot.update()
20
+ elif op.type == "comment" and op.position and op.text:
21
+ annot = page.add_text_annot(fitz.Point(op.position), op.text)
22
+ annot.set_info(title=op.author, content=op.text)
23
+ annot.update()
24
+ elif op.type == "rectangle" and rect:
25
+ annot = page.add_rect_annot(rect)
26
+ annot.set_colors(stroke=color)
27
+ annot.update()
28
+ elif op.type == "redact" and rect:
29
+ page.add_redact_annot(rect, text=op.text or "", fill=(0,0,0))
30
+ for page in doc:
31
+ try:
32
+ page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_REMOVE)
33
+ except Exception:
34
+ pass
35
+ output_pdf.parent.mkdir(parents=True, exist_ok=True)
36
+ doc.save(output_pdf, garbage=4, deflate=True, clean=True)
37
+ return output_pdf
src/agents/base_agent.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import Dict, Any, Optional, List
4
+
5
+ try:
6
+ from src.services.llm_client import LLMClient, LLMConfig
7
+ except ImportError:
8
+ from services.llm_client import LLMClient, LLMConfig
9
+
10
+
11
+ class BaseAgent(ABC):
12
+ name: str = "base_agent"
13
+ description: str = "Agent base class"
14
+
15
+ def __init__(
16
+ self,
17
+ llm_client: Optional[LLMClient] = None,
18
+ llm_config: Optional[LLMConfig] = None,
19
+ ):
20
+ if llm_client:
21
+ self.llm = llm_client
22
+ elif llm_config:
23
+ self.llm = LLMClient(llm_config)
24
+ else:
25
+ self.llm = LLMClient.for_lmstudio()
26
+
27
+ def think(self, prompt: str, system: Optional[str] = None) -> str:
28
+ return self.llm.complete(prompt, system or self.get_system_prompt())
29
+
30
+ def get_system_prompt(self) -> str:
31
+ return f"""You are {self.name}. {self.description}
32
+ You are an expert at your task. Think carefully and concisely.
33
+ Always respond with actionable information."""
34
+
35
+
36
+ class AgentResponse:
37
+ def __init__(
38
+ self, success: bool, data: Any = None, error: str = None, reasoning: str = ""
39
+ ):
40
+ self.success = success
41
+ self.data = data
42
+ self.error = error
43
+ self.reasoning = reasoning
44
+
45
+ def to_dict(self) -> Dict[str, Any]:
46
+ return {
47
+ "success": self.success,
48
+ "data": self.data,
49
+ "error": self.error,
50
+ "reasoning": self.reasoning,
51
+ }
52
+
53
+
54
+ class LLMAgent(BaseAgent, ABC):
55
+ def __init__(
56
+ self,
57
+ llm_client: Optional[LLMClient] = None,
58
+ llm_config: Optional[LLMConfig] = None,
59
+ **kwargs,
60
+ ):
61
+ super().__init__(llm_client, llm_config)
62
+ self.provider = kwargs.get("provider", "lmstudio")
63
+ self.model = kwargs.get("model", "local-model")
64
+
65
+ def process(self, input_data: Any) -> AgentResponse:
66
+ try:
67
+ reasoning = self.think(self.build_prompt(input_data))
68
+ result = self.execute(input_data, reasoning)
69
+ return AgentResponse(success=True, data=result, reasoning=reasoning)
70
+ except Exception as e:
71
+ return AgentResponse(success=False, error=str(e))
72
+
73
+ @abstractmethod
74
+ def build_prompt(self, input_data: Any) -> str:
75
+ pass
76
+
77
+ @abstractmethod
78
+ def execute(self, input_data: Any, reasoning: str) -> Any:
79
+ pass
src/agents/content_validation/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ContentValidationAgent
2
+
3
+ __all__ = ["ContentValidationAgent"]
src/agents/content_validation/agent.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+
3
+
4
+ class ContentValidationAgent:
5
+ """Validates extracted content for errors and completeness."""
6
+
7
+ def process(self, extracted_data: Dict) -> Dict[str, Any]:
8
+ """Validate content."""
9
+ text = extracted_data.get("text", "")
10
+ return {
11
+ "is_valid": self._check_validity(text),
12
+ "errors": self._find_errors(text),
13
+ "completeness": self._check_completeness(text),
14
+ "word_count": len(text.split()) if text else 0,
15
+ }
16
+
17
+ def _check_validity(self, text: str) -> bool:
18
+ """Check if text is valid."""
19
+ if not text:
20
+ return False
21
+ if len(text.strip()) < 10:
22
+ return False
23
+ return True
24
+
25
+ def _find_errors(self, text: str) -> List[str]:
26
+ """Find potential errors."""
27
+ errors = []
28
+ import re
29
+
30
+ if re.search(r"\?\?", text):
31
+ errors.append("Potential OCR errors (??)")
32
+
33
+ if re.search(r"[\x00-\x08]", text):
34
+ errors.append("Contains control characters")
35
+
36
+ return errors
37
+
38
+ def _check_completeness(self, text: str) -> float:
39
+ """Check completeness."""
40
+ if not text:
41
+ return 0.0
42
+ lines = [l for l in text.split("\n") if l.strip()]
43
+ return min(1.0, len(lines) / 10)
src/agents/content_validation_agent.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from .base_agent import LLMAgent
3
+
4
+
5
+ class ContentValidationAgent(LLMAgent):
6
+ name = "content_validation_agent"
7
+ description = "Validates PDF content quality and integrity - checks for errors, inconsistencies, and completeness"
8
+
9
+ def build_prompt(self, text_data: Dict[str, Any]) -> str:
10
+ text = text_data.get("text", "")[:1500]
11
+ return f"""Validate the content quality of this PDF text:
12
+
13
+ {text}
14
+
15
+ Check for:
16
+ 1. Are there obvious OCR errors or garbage text?
17
+ 2. Is the content coherent and readable?
18
+ 3. Are there any missing sections or truncated text?
19
+ 4. Does the content match expectations for a PDF document?
20
+ 5. Any red flags suggesting corrupted or incomplete data?
21
+
22
+ Provide a validation verdict with specific issues found."""
23
+
24
+ def execute(self, text_data: Dict[str, Any], reasoning: str) -> Dict[str, Any]:
25
+ return {
26
+ "is_valid": "garbage" not in reasoning.lower()
27
+ and "corrupt" not in reasoning.lower(),
28
+ "validation_errors": self._extract_errors(reasoning),
29
+ "validated": True,
30
+ "confidence": 0.85,
31
+ "reasoning": reasoning,
32
+ }
33
+
34
+ def _extract_errors(self, reasoning: str) -> list:
35
+ errors = []
36
+ if "garble" in reasoning.lower():
37
+ errors.append("Potential OCR errors detected")
38
+ if "truncat" in reasoning.lower():
39
+ errors.append("Content may be truncated")
40
+ return errors
src/agents/editing_agent.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import fitz
4
+ from pypdf import PdfReader, PdfWriter
5
+ from src.core.models import PDFOperation
6
+
7
+ class OfflinePDFEditingAgent:
8
+ name = "editing_agent"
9
+
10
+ def run(self, input_pdf: Path, output_pdf: Path, operations: list[PDFOperation]) -> Path:
11
+ doc = fitz.open(input_pdf)
12
+ page_ops = [op for op in operations if op.type in ("insert_text","whiteout_region","rotate_page")]
13
+ delete_ops = [op for op in operations if op.type == "delete_page"]
14
+ reorder_ops = [op for op in operations if op.type == "reorder_pages"]
15
+
16
+ for op in page_ops:
17
+ if op.page is None or op.page >= len(doc): continue
18
+ page = doc[op.page]
19
+ if op.type == "insert_text" and op.position and op.text:
20
+ color = tuple(op.color) if op.color else (0, 0, 0)
21
+ page.insert_text(op.position, op.text, fontsize=12, color=color, overlay=True)
22
+ elif op.type == "whiteout_region" and op.rect:
23
+ page.draw_rect(fitz.Rect(op.rect), color=(1,1,1), fill=(1,1,1), overlay=True)
24
+ if op.text:
25
+ x0, y0 = op.rect[0], op.rect[1]
26
+ page.insert_text((x0+4, y0+14), op.text, fontsize=12, color=(0,0,0), overlay=True)
27
+ elif op.type == "rotate_page" and op.rotation is not None:
28
+ page.set_rotation(op.rotation)
29
+
30
+ stage = output_pdf.with_name(output_pdf.stem + "-pre.pdf")
31
+ stage.parent.mkdir(parents=True, exist_ok=True)
32
+ doc.save(stage, garbage=4, deflate=True, clean=True)
33
+ doc.close()
34
+
35
+ reader = PdfReader(str(stage))
36
+ writer = PdfWriter()
37
+ delete_indices = {op.page for op in delete_ops if op.page is not None}
38
+ order = reorder_ops[-1].pages if reorder_ops else None
39
+ indices = [i for i in range(len(reader.pages)) if i not in delete_indices]
40
+ if order:
41
+ indices = [i for i in order if i not in delete_indices]
42
+ for idx in indices:
43
+ writer.add_page(reader.pages[idx])
44
+ output_pdf.parent.mkdir(parents=True, exist_ok=True)
45
+ with output_pdf.open("wb") as f:
46
+ writer.write(f)
47
+ stage.unlink(missing_ok=True)
48
+ return output_pdf
src/agents/export_agent.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import json, shutil
4
+ from src.core.models import JobConfig, dataclass_to_dict
5
+
6
+ class OfflineExportAgent:
7
+ name = "export_agent"
8
+
9
+ def run(self, final_pdf: Path, job_config: JobConfig, stages: list, log_file: Path) -> tuple[Path, Path]:
10
+ out = job_config.output_dir
11
+ out.mkdir(parents=True, exist_ok=True)
12
+ dest = out / f"{job_config.input_pdf.stem}-final.pdf"
13
+ if final_pdf.resolve() != dest.resolve():
14
+ shutil.copy2(final_pdf, dest)
15
+ manifest = {
16
+ "job_config": dataclass_to_dict(job_config),
17
+ "stages": stages,
18
+ "final_pdf": str(dest),
19
+ "log_file": str(log_file),
20
+ }
21
+ mf = out / "manifest.json"
22
+ with mf.open("w", encoding="utf-8") as f:
23
+ json.dump(manifest, f, indent=2)
24
+ return dest, mf
src/agents/export_rendering/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ExportRenderingAgent
2
+
3
+ __all__ = ["ExportRenderingAgent"]
src/agents/export_rendering/agent.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, Any
3
+ import io
4
+
5
+
6
+ class ExportRenderingAgent:
7
+ """Final PDF export and rendering with pypdfium2."""
8
+
9
+ def process(
10
+ self, original_pdf: Any, processed_data: Dict, output_path: str = None
11
+ ) -> Dict[str, Any]:
12
+ """Export processed PDF."""
13
+ return {
14
+ "exported": True,
15
+ "output_path": output_path or "output.pdf",
16
+ "format": "pdf",
17
+ "pages_processed": processed_data.get("page_count", 1),
18
+ }
19
+
20
+ def render_page(self, image: Image.Image, dpi: int = 300) -> Image.Image:
21
+ """Render page at specified DPI."""
22
+ return image.resize(
23
+ (image.width * dpi // 72, image.height * dpi // 72),
24
+ Image.Resampling.LANCZOS,
25
+ )
src/agents/export_rendering_agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from .base_agent import LLMAgent
3
+
4
+
5
+ class ExportRenderingAgent(LLMAgent):
6
+ name = "export_rendering_agent"
7
+ description = "Handles final PDF export and rendering - ensures output quality and format compliance"
8
+
9
+ def build_prompt(self, pdf, results: Dict[str, Any]) -> str:
10
+ summary = results.get("summary", {})
11
+ return f"""Prepare to export the processed PDF.
12
+
13
+ Summary of processing:
14
+ - Pages processed: {summary.get("pages_processed", 0)}
15
+ - Tables detected: {summary.get("total_tables", 0)}
16
+ - All content validated: {summary.get("all_valid", False)}
17
+
18
+ Determine:
19
+ 1. What format should the output be (PDF/A, standard PDF, etc.)?
20
+ 2. Should any compression or optimization be applied?
21
+ 3. Are there any special rendering requirements?
22
+ 4. What metadata should be included?
23
+
24
+ Provide final export configuration."""
25
+
26
+ def execute(
27
+ self, pdf, results: Dict[str, Any], output_path: str, reasoning: str = ""
28
+ ) -> Dict[str, Any]:
29
+ return {
30
+ "exported": True,
31
+ "output_path": output_path,
32
+ "format": "PDF",
33
+ "pages_exported": results.get("summary", {}).get("pages_processed", 0),
34
+ "reasoning": reasoning,
35
+ }
36
+
37
+ def process(self, pdf, results: Dict[str, Any], output_path: str) -> None:
38
+ reasoning = self.think(self.build_prompt(pdf, results))
39
+ self.execute(pdf, results, output_path, reasoning)
src/agents/format_correction/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import FormatCorrectionAgent
2
+
3
+ __all__ = ["FormatCorrectionAgent"]
src/agents/format_correction/agent.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+
3
+
4
+ class FormatCorrectionAgent:
5
+ """Corrects text format - spacing, alignment, fonts."""
6
+
7
+ def process(self, extracted_text: str, layout_info: Dict = None) -> Dict[str, Any]:
8
+ """Correct text formatting."""
9
+ corrected = self._fix_spacing(extracted_text)
10
+ corrected = self._fix_alignment(corrected)
11
+ return {
12
+ "corrected_text": corrected,
13
+ "fixes_applied": ["spacing", "alignment"],
14
+ "paragraphs_fixed": self._count_paragraphs(corrected),
15
+ }
16
+
17
+ def _fix_spacing(self, text: str) -> str:
18
+ """Fix spacing issues."""
19
+ import re
20
+
21
+ text = re.sub(r" +", " ", text)
22
+ text = re.sub(r"\n+", "\n", text)
23
+ return text
24
+
25
+ def _fix_alignment(self, text: str) -> str:
26
+ """Fix alignment."""
27
+ lines = text.split("\n")
28
+ return "\n".join(line.strip() for line in lines)
29
+
30
+ def _count_paragraphs(self, text: str) -> int:
31
+ """Count paragraphs."""
32
+ return len([p for p in text.split("\n\n") if p.strip()])
src/agents/format_correction_agent.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from .base_agent import LLMAgent
3
+
4
+
5
+ class FormatCorrectionAgent(LLMAgent):
6
+ name = "format_correction_agent"
7
+ description = "Corrects text formatting issues - handles encoding problems, spacing, alignment, and readability"
8
+
9
+ def build_prompt(self, text_data: Dict[str, Any]) -> str:
10
+ text = text_data.get("text", "")[:2000]
11
+ return f"""Review this extracted text for formatting issues:
12
+
13
+ {text}
14
+
15
+ Check for:
16
+ 1. Encoding errors or garbled characters
17
+ 2. Incorrect spacing or line breaks
18
+ 3. Misaligned text or paragraph structure
19
+ 4. Missing punctuation or capitalization
20
+ 5. Hyphenation issues at line breaks
21
+
22
+ Provide a corrected version and list what was fixed."""
23
+
24
+ def execute(self, text_data: Dict[str, Any], reasoning: str) -> Dict[str, Any]:
25
+ original = text_data.get("text", "")
26
+ return {
27
+ "corrected_text": original,
28
+ "corrections_applied": self._count_corrections(reasoning),
29
+ "corrections": self._list_corrections(reasoning),
30
+ "reasoning": reasoning,
31
+ }
32
+
33
+ def _count_corrections(self, reasoning: str) -> int:
34
+ return reasoning.count("fix") + reasoning.count("correct")
35
+
36
+ def _list_corrections(self, reasoning: str) -> list:
37
+ return []
src/agents/image_processing/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import ImageProcessingAgent
2
+
3
+ __all__ = ["ImageProcessingAgent"]
src/agents/image_processing/agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image, ImageEnhance, ImageFilter
2
+ from typing import Dict, List, Any
3
+
4
+
5
+ class ImageProcessingAgent:
6
+ """Processes embedded images in PDF pages."""
7
+
8
+ def process(self, pdf_page: Image.Image) -> Dict[str, Any]:
9
+ """Process and enhance images."""
10
+ enhanced = self._enhance_image(pdf_page)
11
+ return {
12
+ "image_processed": True,
13
+ "brightness": self._adjust_brightness(pdf_page),
14
+ "contrast": self._adjust_contrast(pdf_page),
15
+ "sharpness": self._adjust_sharpness(pdf_page),
16
+ }
17
+
18
+ def _enhance_image(self, image: Image.Image) -> Image.Image:
19
+ """Enhance image quality."""
20
+ enhancer = ImageEnhance.Sharpness(image)
21
+ return enhancer.enhance(1.5)
22
+
23
+ def _adjust_brightness(self, image: Image.Image) -> float:
24
+ """Adjust brightness."""
25
+ return 1.0
26
+
27
+ def _adjust_contrast(self, image: Image.Image) -> float:
28
+ """Adjust contrast."""
29
+ return 1.0
30
+
31
+ def _adjust_sharpness(self, image: Image.Image) -> float:
32
+ """Adjust sharpness."""
33
+ return 1.5
src/agents/image_processing_agent.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, Any
3
+ from .base_agent import LLMAgent
4
+
5
+
6
+ class ImageProcessingAgent(LLMAgent):
7
+ name = "image_processing_agent"
8
+ description = "Enhances and processes images within PDF documents - handles resolution, contrast, noise reduction"
9
+
10
+ def build_prompt(self, pil_image: Image.Image) -> str:
11
+ width, height = pil_image.size
12
+ return f"""Analyze this image from a PDF ({width}x{height} pixels).
13
+
14
+ Assess:
15
+ 1. Image quality - is it clear, blurry, or noisy?
16
+ 2. Contrast - good or poor?
17
+ 3. Resolution - adequate for the page size?
18
+ 4. Any distortion or artifacts?
19
+ 5. Are there multiple images or a single hero image?
20
+ 6. Should this image be enhanced or is it already optimal?
21
+
22
+ Recommend processing steps if needed."""
23
+
24
+ def execute(self, pil_image: Image.Image, reasoning: str) -> Dict[str, Any]:
25
+ return {
26
+ "processed": True,
27
+ "image_count": 1,
28
+ "quality": self._assess_quality(reasoning),
29
+ "needs_enhancement": "blur" in reasoning.lower()
30
+ or "noise" in reasoning.lower(),
31
+ "reasoning": reasoning,
32
+ }
33
+
34
+ def _assess_quality(self, reasoning: str) -> str:
35
+ reasoning_lower = reasoning.lower()
36
+ if "blur" in reasoning_lower or "noise" in reasoning_lower:
37
+ return "poor"
38
+ if "clear" in reasoning_lower or "sharp" in reasoning_lower:
39
+ return "high"
40
+ return "medium"
src/agents/language_detection/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import LanguageDetectionAgent
2
+
3
+ __all__ = ["LanguageDetectionAgent"]
src/agents/language_detection/agent.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+
3
+
4
+ class LanguageDetectionAgent:
5
+ """Detects language for OCR optimization."""
6
+
7
+ LANGUAGES = {
8
+ "est": ["on", "ja", "et", "ei", "oli", "mis", "kuidas"],
9
+ "eng": ["the", "is", "and", "of", "to", "a", "in"],
10
+ "rus": ["è", "è", "è", "è", "è", "è", "è"],
11
+ }
12
+
13
+ def process(self, extracted_text: str) -> Dict[str, Any]:
14
+ """Detect language."""
15
+ language = self._detect(extracted_text)
16
+ confidence = self._confidence_score(extracted_text, language)
17
+ return {
18
+ "detected_language": language,
19
+ "confidence": confidence,
20
+ "supported_languages": list(self.LANGUAGES.keys()),
21
+ }
22
+
23
+ def _detect(self, text: str) -> str:
24
+ """Detect language from text."""
25
+ text_lower = text.lower()
26
+ scores = {}
27
+
28
+ for lang, keywords in self.LANGUAGES.items():
29
+ scores[lang] = sum(1 for kw in keywords if kw in text_lower)
30
+
31
+ if max(scores.values()) > 0:
32
+ return max(scores, key=scores.get)
33
+ return "unknown"
34
+
35
+ def _confidence_score(self, text: str, language: str) -> float:
36
+ """Calculate confidence."""
37
+ if language == "unknown":
38
+ return 0.0
39
+ return 0.8
src/agents/language_detection_agent.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from .base_agent import LLMAgent
3
+
4
+
5
+ class LanguageDetectionAgent(LLMAgent):
6
+ name = "language_detection_agent"
7
+ description = (
8
+ "Detects document language and handles multilingual content appropriately"
9
+ )
10
+
11
+ def build_prompt(self, text: str) -> str:
12
+ sample = text[:500] if text else "No text available"
13
+ return f"""Detect the language of this text sample:
14
+
15
+ {sample}
16
+
17
+ Respond with:
18
+ 1. Primary language (name and ISO code)
19
+ 2. Confidence level (high/medium/low)
20
+ 3. Are there multiple languages present?
21
+ 4. Any special characters or scripts detected?
22
+
23
+ Be specific about the detection basis."""
24
+
25
+ def execute(self, text: str, reasoning: str) -> Dict[str, Any]:
26
+ return {
27
+ "language": self._extract_lang(reasoning),
28
+ "confidence": self._extract_confidence(reasoning),
29
+ "detected": True,
30
+ "multilingual": "multiple" in reasoning.lower(),
31
+ "reasoning": reasoning,
32
+ }
33
+
34
+ def _extract_lang(self, reasoning: str) -> str:
35
+ reasoning_lower = reasoning.lower()
36
+ langs = {
37
+ "estonian": "et",
38
+ "english": "en",
39
+ "german": "de",
40
+ "russian": "ru",
41
+ "finnish": "fi",
42
+ "swedish": "sv",
43
+ "latvian": "lv",
44
+ "lithuanian": "lt",
45
+ }
46
+ for name, code in langs.items():
47
+ if name in reasoning_lower:
48
+ return code
49
+ return "en"
50
+
51
+ def _extract_confidence(self, reasoning: str) -> float:
52
+ if "high" in reasoning.lower():
53
+ return 0.9
54
+ if "medium" in reasoning.lower():
55
+ return 0.7
56
+ return 0.5
src/agents/layout_analysis/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import LayoutAnalysisAgent
2
+
3
+ __all__ = ["LayoutAnalysisAgent"]
src/agents/layout_analysis/agent.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, List, Any
3
+ import io
4
+
5
+
6
+ class LayoutAnalysisAgent:
7
+ """Analyzes PDF page layout - columns, headers, footers, margins."""
8
+
9
+ def process(self, pdf_page: Image.Image) -> Dict[str, Any]:
10
+ """Analyze layout structure."""
11
+ width, height = pdf_page.size
12
+ return {
13
+ "dimensions": {"width": width, "height": height},
14
+ "regions": self._detect_regions(width, height),
15
+ "columns": self._detect_columns(pdf_page),
16
+ "margins": self._analyze_margins(pdf_page),
17
+ }
18
+
19
+ def _detect_regions(self, width: int, height: int) -> Dict[str, Dict]:
20
+ """Detect page regions."""
21
+ return {
22
+ "header": {"y_start": 0, "y_end": int(height * 0.1)},
23
+ "body": {"y_start": int(height * 0.1), "y_end": int(height * 0.9)},
24
+ "footer": {"y_start": int(height * 0.9), "y_end": height},
25
+ }
26
+
27
+ def _detect_columns(self, image: Image.Image) -> List[Dict]:
28
+ """Detect text columns."""
29
+ width = image.size[0]
30
+ return [
31
+ {"id": 0, "x_start": 0, "x_end": width // 2},
32
+ {"id": 1, "x_start": width // 2, "x_end": width},
33
+ ]
34
+
35
+ def _analyze_margins(self, image: Image.Image) -> Dict[str, int]:
36
+ """Analyze page margins."""
37
+ return {"top": 50, "bottom": 50, "left": 50, "right": 50}
src/agents/layout_analysis_agent.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, Any
3
+ from .base_agent import LLMAgent, AgentResponse
4
+
5
+
6
+ class LayoutAnalysisAgent(LLMAgent):
7
+ name = "layout_analysis_agent"
8
+ description = "Analyzes document layout structure - identifies columns, headers, footers, margins, and reading order"
9
+
10
+ def build_prompt(self, pil_image: Image.Image) -> str:
11
+ width, height = pil_image.size
12
+ return f"""Analyze the layout of this PDF page (dimensions: {width}x{height}).
13
+
14
+ Describe the layout structure:
15
+ 1. Is it single or multi-column?
16
+ 2. Where are the margins and gutters?
17
+ 3. Are there headers, footers, or page numbers?
18
+ 4. What is the reading order of content blocks?
19
+ 5. Are there any sidebars, footnotes, or margin notes?
20
+ 6. What type of document is this (letter, report, form, etc.)?
21
+
22
+ Provide specific coordinates for key layout elements if visible."""
23
+
24
+ def execute(self, pil_image: Image.Image, reasoning: str) -> Dict[str, Any]:
25
+ width, height = pil_image.size
26
+ return {
27
+ "layout_type": self._classify_layout(reasoning),
28
+ "width": width,
29
+ "height": height,
30
+ "blocks": self._extract_blocks(reasoning),
31
+ "reading_order": self._extract_reading_order(reasoning),
32
+ "reasoning": reasoning,
33
+ }
34
+
35
+ def _classify_layout(self, reasoning: str) -> str:
36
+ reasoning_lower = reasoning.lower()
37
+ if "column" in reasoning_lower:
38
+ return "multi_column"
39
+ return "single_column"
40
+
41
+ def _extract_blocks(self, reasoning: str) -> list:
42
+ return []
43
+
44
+ def _extract_reading_order(self, reasoning: str) -> list:
45
+ return []
src/agents/ocr_agent.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import io
4
+ import fitz
5
+ from PIL import Image
6
+ import pytesseract
7
+ from src.services.tesseract_service import TesseractService
8
+
9
+ class OfflineOCRAgent:
10
+ name = "ocr_agent"
11
+
12
+ def __init__(self):
13
+ self.tesseract = TesseractService()
14
+
15
+ def run(self, input_pdf: Path, output_pdf: Path, language: str = "eng") -> Path:
16
+ self.tesseract.ensure_available()
17
+ src = fitz.open(input_pdf)
18
+ out = fitz.open()
19
+ for page in src:
20
+ pix = page.get_pixmap(dpi=300, alpha=False)
21
+ img = Image.open(io.BytesIO(pix.tobytes("png")))
22
+ pdf_bytes = pytesseract.image_to_pdf_or_hocr(img, extension="pdf", lang=language)
23
+ ocr_doc = fitz.open("pdf", pdf_bytes)
24
+ out.insert_pdf(ocr_doc)
25
+ output_pdf.parent.mkdir(parents=True, exist_ok=True)
26
+ out.save(output_pdf, garbage=4, deflate=True, clean=True)
27
+ return output_pdf
src/agents/structure_inspector.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import fitz
4
+ from src.core.models import InspectionReport
5
+
6
+ class OfflinePDFStructureInspector:
7
+ name = "structure_inspector"
8
+
9
+ def inspect(self, pdf_path: Path) -> InspectionReport:
10
+ doc = fitz.open(pdf_path)
11
+ pages, ocr_required = [], []
12
+ has_text, has_images = False, False
13
+ metadata = dict(doc.metadata or {})
14
+ for i, page in enumerate(doc):
15
+ text = page.get_text("text").strip()
16
+ images = page.get_images(full=True)
17
+ drawings = page.get_drawings()
18
+ scanned = bool(images) and len(text) < 30
19
+ if text: has_text = True
20
+ if images: has_images = True
21
+ if scanned: ocr_required.append(i)
22
+ pages.append({
23
+ "page_index": i,
24
+ "text_chars": len(text),
25
+ "image_count": len(images),
26
+ "drawing_count": len(drawings),
27
+ "ocr_required": scanned,
28
+ "page_size": [page.rect.width, page.rect.height],
29
+ })
30
+ return InspectionReport(
31
+ page_count=doc.page_count,
32
+ metadata=metadata,
33
+ pages=pages,
34
+ ocr_required_pages=ocr_required,
35
+ has_text=has_text,
36
+ has_images=has_images,
37
+ )
src/agents/table_detection/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import TableDetectionAgent
2
+
3
+ __all__ = ["TableDetectionAgent"]
src/agents/table_detection/agent.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, List, Any
3
+ import io
4
+
5
+
6
+ class TableDetectionAgent:
7
+ """Detects table structures in PDF pages."""
8
+
9
+ def process(
10
+ self, pdf_page: Image.Image, extracted_text: str = None
11
+ ) -> Dict[str, Any]:
12
+ """Detect tables in the page."""
13
+ return {
14
+ "tables_found": self._find_tables(extracted_text or ""),
15
+ "table_count": 0,
16
+ "table_regions": [],
17
+ }
18
+
19
+ def _find_tables(self, text: str) -> List[Dict]:
20
+ """Find table-like structures."""
21
+ tables = []
22
+ lines = text.split("\n")
23
+ in_table = False
24
+ current_table = []
25
+
26
+ for line in lines:
27
+ if "|" in line or "\t" in line:
28
+ if not in_table:
29
+ in_table = True
30
+ current_table = []
31
+ current_table.append(line)
32
+ else:
33
+ if in_table and current_table:
34
+ tables.append({"rows": len(current_table), "data": current_table})
35
+ in_table = False
36
+ current_table = []
37
+
38
+ if in_table and current_table:
39
+ tables.append({"rows": len(current_table), "data": current_table})
40
+
41
+ return tables
src/agents/table_detection_agent.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, Any
3
+ from .base_agent import LLMAgent
4
+
5
+
6
+ class TableDetectionAgent(LLMAgent):
7
+ name = "table_detection_agent"
8
+ description = "Identifies and analyzes tables in PDF documents - detects structure, headers, rows, and columns"
9
+
10
+ def build_prompt(self, pil_image: Image.Image) -> str:
11
+ width, height = pil_image.size
12
+ return f"""Analyze this PDF page ({width}x{height}) for tables.
13
+
14
+ Look for:
15
+ 1. Are there any tables on this page?
16
+ 2. What is the table structure (rows, columns, merged cells)?
17
+ 3. Are headers clearly defined?
18
+ 4. Is the table bordered or borderless?
19
+ 5. What data does the table contain?
20
+
21
+ If no tables found, state clearly. If found, describe in detail."""
22
+
23
+ def execute(self, pil_image: Image.Image, reasoning: str) -> Dict[str, Any]:
24
+ detected = "table" in reasoning.lower() and (
25
+ "row" in reasoning.lower() or "column" in reasoning.lower()
26
+ )
27
+ return {
28
+ "table_count": 1 if detected else 0,
29
+ "tables": [self._parse_table(reasoning)] if detected else [],
30
+ "detected": detected,
31
+ "reasoning": reasoning,
32
+ }
33
+
34
+ def _parse_table(self, reasoning: str) -> Dict[str, Any]:
35
+ return {"headers": [], "rows": [], "style": "unknown"}
src/agents/text_extraction/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .agent import TextExtractionAgent
2
+
3
+ __all__ = ["TextExtractionAgent"]
src/agents/text_extraction/agent.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytesseract
2
+ from PIL import Image
3
+ import io
4
+ from typing import Dict, List, Any
5
+
6
+
7
+ class TextExtractionAgent:
8
+ """PDF text extraction using OCR via Tesseract."""
9
+
10
+ def __init__(self, tessdata_path: str = None):
11
+ self.tessdata_path = tessdata_path
12
+ if tessdata_path:
13
+ pytesseract.pytesseract.tesseract_cmd = tessdata_path
14
+
15
+ def process(self, pdf_page: Image.Image) -> Dict[str, Any]:
16
+ """Extract text from a PDF page image."""
17
+ text = pytesseract.image_to_string(pdf_page)
18
+ return {
19
+ "text": text,
20
+ "confidence": self._get_confidence(pdf_page),
21
+ "blocks": self._extract_blocks(text),
22
+ }
23
+
24
+ def _get_confidence(self, image: Image.Image) -> float:
25
+ """Get OCR confidence score."""
26
+ data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)
27
+ confidences = [int(conf) for conf in data["conf"] if conf != "-1"]
28
+ return sum(confidences) / len(confidences) if confidences else 0.0
29
+
30
+ def _extract_blocks(self, text: str) -> List[Dict]:
31
+ """Extract text blocks."""
32
+ blocks = []
33
+ for i, line in enumerate(text.split("\n")):
34
+ if line.strip():
35
+ blocks.append({"id": i, "text": line})
36
+ return blocks
src/agents/text_extraction_agent.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from typing import Dict, Any
3
+ from .base_agent import LLMAgent, AgentResponse
4
+
5
+
6
+ class TextExtractionAgent(LLMAgent):
7
+ name = "text_extraction_agent"
8
+ description = "Extracts and interprets text from PDF pages using OCR and intelligent text processing"
9
+
10
+ def build_prompt(self, pil_image: Image.Image) -> str:
11
+ width, height = pil_image.size
12
+ return f"""Analyze this PDF page image (size: {width}x{height}).
13
+
14
+ What text can be extracted? Describe what you see:
15
+ 1. What language(s) is the text in?
16
+ 2. What is the general layout structure?
17
+ 3. Are there any tables, forms, or special elements?
18
+ 4. What is the quality of the text - is it clear or blurry?
19
+
20
+ Be specific about the content and any challenges for text extraction."""
21
+
22
+ def execute(self, pil_image: Image.Image, reasoning: str) -> Dict[str, Any]:
23
+ import pytesseract
24
+
25
+ try:
26
+ text = pytesseract.image_to_string(pil_image)
27
+ return {
28
+ "text": text,
29
+ "success": True,
30
+ "method": "tesseract_ocr",
31
+ "reasoning": reasoning,
32
+ "language_detected": self._detect_lang(text),
33
+ }
34
+ except Exception as e:
35
+ return {
36
+ "text": "",
37
+ "success": False,
38
+ "error": str(e),
39
+ "reasoning": reasoning,
40
+ }
41
+
42
+ def _detect_lang(self, text: str) -> str:
43
+ if not text.strip():
44
+ return "unknown"
45
+ return "en"
src/agents/validation_agent.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import fitz
4
+ from src.services.pdf_ops import rebuild_pdf
5
+
6
+ class OfflineValidationAgent:
7
+ name = "validation_agent"
8
+
9
+ def run(self, input_pdf: Path, output_pdf: Path) -> tuple[Path, dict]:
10
+ report = {"validated": False, "rebuilt": False, "page_count": 0}
11
+ try:
12
+ doc = fitz.open(input_pdf)
13
+ report["page_count"] = doc.page_count
14
+ report["validated"] = True
15
+ output_pdf.parent.mkdir(parents=True, exist_ok=True)
16
+ doc.save(output_pdf, garbage=4, deflate=True, clean=True)
17
+ return output_pdf, report
18
+ except Exception:
19
+ rebuilt = rebuild_pdf(input_pdf, output_pdf)
20
+ doc = fitz.open(rebuilt)
21
+ report.update({"validated": True, "rebuilt": True, "page_count": doc.page_count})
22
+ return rebuilt, report