ag-docparse 0.5.0__tar.gz

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.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: ag-docparse
3
+ Version: 0.5.0
4
+ Summary: LLM-powered document extraction SDK
5
+ License: MIT
6
+ Author: Adnan Ghribi
7
+ Author-email: aghribi@gmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Provides-Extra: all
18
+ Provides-Extra: anthropic
19
+ Provides-Extra: openai
20
+ Provides-Extra: pdf
21
+ Requires-Dist: anthropic (>=0.25,<0.26) ; extra == "anthropic" or extra == "all"
22
+ Requires-Dist: click (>=8.0,<9.0)
23
+ Requires-Dist: httpx (>=0.27,<1)
24
+ Requires-Dist: openai (>=1.0,<2.0) ; extra == "openai" or extra == "all"
25
+ Requires-Dist: pdfplumber (>=0.11,<0.12) ; extra == "pdf" or extra == "all"
26
+ Requires-Dist: pydantic (>=2.0,<3.0)
27
+ Project-URL: Homepage, https://github.com/aghribi/ag-docparse
28
+ Project-URL: Repository, https://github.com/aghribi/ag-docparse
29
+ Description-Content-Type: text/markdown
30
+
31
+ # ag-docparse
32
+
33
+ LLM-based structured extraction from documents.
34
+
35
+ Loads PDF or text documents and extracts typed fields with an LLM, using built-in schemas (invoice, loan application, W-2, NDA, contract) or a custom JSON schema.
36
+
37
+ ## Install
38
+
39
+ ```sh
40
+ pip install "ag-docparse[all]"
41
+ npm install ag-docparse
42
+ ```
43
+
44
+ Extras: `openai`, `anthropic`, `pdf` (pdfplumber), `all`.
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from ag_docparse import INVOICE_SCHEMA, LLMExtractor, from_text
50
+
51
+ layout = from_text("Invoice INV-001, total due 1,250.00 EUR by 2026-11-30.")
52
+ result = LLMExtractor(model="gpt-4o-mini").extract(layout, INVOICE_SCHEMA) # needs OPENAI_API_KEY
53
+ print(result)
54
+ ```
55
+
56
+ Command line: `ag-docparse extract -f invoice.pdf -s invoice` and `ag-docparse schemas`.
57
+
58
+ The TypeScript package `ag-docparse` is a port of the core classes.
59
+
60
+ ## Hosted API
61
+
62
+ `AsyncJobClient` submits large documents to the hosted API as asynchronous jobs. API keys are issued at https://ai.artifact-network.org. The default endpoint is `https://ai-api.artifact-network.org`; pass a different base URL to use your own deployment.
63
+
64
+ ## Development
65
+
66
+ ```sh
67
+ cd packages/python && pip install -e ".[all]" && pytest
68
+ cd packages/typescript && npm ci && npm test
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT, Copyright (c) 2026 Adnan Ghribi.
74
+
@@ -0,0 +1,43 @@
1
+ # ag-docparse
2
+
3
+ LLM-based structured extraction from documents.
4
+
5
+ Loads PDF or text documents and extracts typed fields with an LLM, using built-in schemas (invoice, loan application, W-2, NDA, contract) or a custom JSON schema.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install "ag-docparse[all]"
11
+ npm install ag-docparse
12
+ ```
13
+
14
+ Extras: `openai`, `anthropic`, `pdf` (pdfplumber), `all`.
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from ag_docparse import INVOICE_SCHEMA, LLMExtractor, from_text
20
+
21
+ layout = from_text("Invoice INV-001, total due 1,250.00 EUR by 2026-11-30.")
22
+ result = LLMExtractor(model="gpt-4o-mini").extract(layout, INVOICE_SCHEMA) # needs OPENAI_API_KEY
23
+ print(result)
24
+ ```
25
+
26
+ Command line: `ag-docparse extract -f invoice.pdf -s invoice` and `ag-docparse schemas`.
27
+
28
+ The TypeScript package `ag-docparse` is a port of the core classes.
29
+
30
+ ## Hosted API
31
+
32
+ `AsyncJobClient` submits large documents to the hosted API as asynchronous jobs. API keys are issued at https://ai.artifact-network.org. The default endpoint is `https://ai-api.artifact-network.org`; pass a different base URL to use your own deployment.
33
+
34
+ ## Development
35
+
36
+ ```sh
37
+ cd packages/python && pip install -e ".[all]" && pytest
38
+ cd packages/typescript && npm ci && npm test
39
+ ```
40
+
41
+ ## License
42
+
43
+ MIT, Copyright (c) 2026 Adnan Ghribi.
@@ -0,0 +1,23 @@
1
+ """docparse — LLM-powered document extraction."""
2
+
3
+ from importlib.metadata import version as _version
4
+
5
+ from .models import (
6
+ FieldSpec, ExtractionSchema, FieldValue, ExtractionResult,
7
+ PageContent, DocumentLayout,
8
+ )
9
+ from .loader import from_text, load
10
+ from .extractor import LLMExtractor
11
+ from .async_client import AsyncJobClient, QuotaExceededError
12
+ from .schemas import REGISTRY, INVOICE_SCHEMA, LOAN_APPLICATION_SCHEMA, W2_SCHEMA, NDA_SCHEMA, CONTRACT_SCHEMA
13
+
14
+ __version__ = _version("ag-docparse")
15
+
16
+ __all__ = [
17
+ "FieldSpec", "ExtractionSchema", "FieldValue", "ExtractionResult",
18
+ "PageContent", "DocumentLayout",
19
+ "from_text", "load",
20
+ "LLMExtractor", "AsyncJobClient", "QuotaExceededError",
21
+ "REGISTRY", "INVOICE_SCHEMA", "LOAN_APPLICATION_SCHEMA",
22
+ "W2_SCHEMA", "NDA_SCHEMA", "CONTRACT_SCHEMA",
23
+ ]
@@ -0,0 +1,98 @@
1
+ """Async job client for large-document extraction via the hosted DocParse API."""
2
+ from __future__ import annotations
3
+ import time
4
+ from typing import Optional
5
+ import httpx
6
+
7
+ _DEFAULT_BASE = "https://ai-api.artifact-network.org"
8
+
9
+
10
+ class QuotaExceededError(Exception):
11
+ """Raised when the hosted API returns HTTP 429 (monthly quota exceeded)."""
12
+ def __init__(self, message: str = "Monthly quota exceeded"):
13
+ super().__init__(message)
14
+
15
+
16
+ def _check(resp: httpx.Response) -> None:
17
+ if resp.status_code == 429:
18
+ try:
19
+ detail = resp.json().get("detail", resp.text)
20
+ except Exception:
21
+ detail = resp.text
22
+ raise QuotaExceededError(detail)
23
+ resp.raise_for_status()
24
+
25
+
26
+ class AsyncJobClient:
27
+ """
28
+ Submit DocParse extraction jobs and poll for results.
29
+ Use this for large PDFs (>1000 words) where the sync /extract endpoint may time out.
30
+
31
+ Example::
32
+
33
+ from ag_docparse import AsyncJobClient
34
+
35
+ client = AsyncJobClient(api_key="ag_live_...")
36
+
37
+ job = client.submit(text=pdf_text, schema_name="contract",
38
+ webhook_url="https://yourapp.com/hooks/ag_docparse")
39
+ print(job["job_id"], job["status"]) # pending
40
+
41
+ # Poll until done (or use webhook)
42
+ result = client.wait(job["job_id"], timeout=120)
43
+ print(result["result"]) # {"parties": {"value": [...], "confidence": 0.97}, ...}
44
+ """
45
+
46
+ def __init__(self, api_key: str, base_url: str = _DEFAULT_BASE, timeout: float = 15.0):
47
+ self._base = base_url.rstrip("/")
48
+ self._headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
49
+ self._timeout = timeout
50
+
51
+ def submit(
52
+ self,
53
+ text: str,
54
+ schema_name: Optional[str] = None,
55
+ fields: Optional[list[dict]] = None,
56
+ webhook_url: Optional[str] = None,
57
+ ) -> dict:
58
+ """Submit a job. Returns immediately with ``{"job_id": ..., "status": "pending"}``."""
59
+ payload: dict = {"text": text}
60
+ if schema_name:
61
+ payload["schema_name"] = schema_name
62
+ if fields:
63
+ payload["fields"] = fields
64
+ if webhook_url:
65
+ payload["webhook_url"] = webhook_url
66
+
67
+ with httpx.Client(base_url=self._base, headers=self._headers, timeout=self._timeout) as c:
68
+ r = c.post("/v1/doc/jobs", json=payload)
69
+ _check(r)
70
+ return r.json()
71
+
72
+ def poll(self, job_id: str) -> dict:
73
+ """Fetch current status of a job."""
74
+ with httpx.Client(base_url=self._base, headers=self._headers, timeout=self._timeout) as c:
75
+ r = c.get(f"/v1/doc/jobs/{job_id}")
76
+ _check(r)
77
+ return r.json()
78
+
79
+ def list_jobs(self) -> list[dict]:
80
+ """Return the 20 most recent jobs for this API key."""
81
+ with httpx.Client(base_url=self._base, headers=self._headers, timeout=self._timeout) as c:
82
+ r = c.get("/v1/doc/jobs")
83
+ _check(r)
84
+ return r.json()
85
+
86
+ def wait(self, job_id: str, timeout: float = 120.0, poll_interval: float = 2.0) -> dict:
87
+ """
88
+ Block until the job is ``completed`` or ``failed``, then return the job dict.
89
+ Raises ``TimeoutError`` if *timeout* seconds elapse first.
90
+ """
91
+ deadline = time.monotonic() + timeout
92
+ while True:
93
+ job = self.poll(job_id)
94
+ if job["status"] in ("completed", "failed"):
95
+ return job
96
+ if time.monotonic() >= deadline:
97
+ raise TimeoutError(f"Job {job_id} not finished within {timeout}s")
98
+ time.sleep(poll_interval)
@@ -0,0 +1,68 @@
1
+ """docparse CLI — extract structured data from documents."""
2
+ from __future__ import annotations
3
+ import json
4
+ import sys
5
+ import click
6
+ from .loader import load
7
+ from .extractor import LLMExtractor
8
+ from .schemas import REGISTRY
9
+ from .models import ExtractionSchema, FieldSpec
10
+
11
+
12
+ @click.group()
13
+ def cli():
14
+ """docparse — LLM-powered document extraction."""
15
+
16
+
17
+ @cli.command()
18
+ @click.option("--file", "-f", required=True, help="Path to document (PDF, TXT)")
19
+ @click.option("--schema", "-s", required=True, help=f"Schema name: {', '.join(REGISTRY)} — or path to JSON schema file")
20
+ @click.option("--model", "-m", default="gpt-4o-mini", help="LLM model (default gpt-4o-mini)")
21
+ @click.option("--provider", "-p", default="openai", help="LLM provider: openai | anthropic")
22
+ @click.option("--output", "-o", default="json", type=click.Choice(["json", "table"]))
23
+ def extract(file, schema, model, provider, output):
24
+ """Extract structured data from a document."""
25
+ layout = load(file)
26
+
27
+ if schema in REGISTRY:
28
+ sch = REGISTRY[schema]
29
+ else:
30
+ try:
31
+ raw = json.loads(open(schema).read())
32
+ sch = ExtractionSchema(
33
+ name=raw.get("name", "custom"),
34
+ fields=[FieldSpec(**f) for f in raw["fields"]],
35
+ )
36
+ except Exception as e:
37
+ raise click.BadParameter(f"Could not load schema: {e}")
38
+
39
+ extractor = LLMExtractor(model=model, provider=provider)
40
+ result = extractor.extract(layout, sch)
41
+
42
+ if output == "json":
43
+ click.echo(json.dumps({
44
+ k: {"value": v.value, "confidence": round(v.confidence, 2)}
45
+ for k, v in result.fields.items()
46
+ }, indent=2))
47
+ else:
48
+ click.echo(f"{'Field':<25} {'Value':<40} {'Confidence':>10}")
49
+ click.echo("-" * 78)
50
+ for k, v in result.fields.items():
51
+ val = str(v.value) if v.value is not None else "(not found)"
52
+ click.echo(f"{k:<25} {val:<40} {v.confidence:>10.0%}")
53
+
54
+ missing = result.missing_required(sch)
55
+ if missing:
56
+ click.echo(f"\n⚠ Missing required fields: {', '.join(missing)}", err=True)
57
+ sys.exit(1)
58
+
59
+
60
+ @cli.command()
61
+ def schemas():
62
+ """List available built-in schemas."""
63
+ for name, sch in REGISTRY.items():
64
+ click.echo(f"{name:<20} ({len(sch.fields)} fields)")
65
+
66
+
67
+ def main():
68
+ cli()
@@ -0,0 +1,118 @@
1
+ """LLM-powered schema-based extraction."""
2
+ from __future__ import annotations
3
+ import json
4
+ import re
5
+ from .models import DocumentLayout, ExtractionSchema, ExtractionResult, FieldValue
6
+
7
+ _EXTRACT_PROMPT = """\
8
+ You are an expert document extraction assistant. Extract structured data from the document text below.
9
+
10
+ DOCUMENT TEXT:
11
+ {text}
12
+
13
+ SCHEMA — extract these fields:
14
+ {fields}
15
+
16
+ Return ONLY valid JSON in exactly this format (no markdown, no explanation):
17
+ {{
18
+ "fields": {{
19
+ "<field_name>": {{
20
+ "value": <extracted_value_or_null>,
21
+ "confidence": <float_0.0_to_1.0>,
22
+ "raw": "<short_text_snippet_from_document>"
23
+ }}
24
+ }}
25
+ }}
26
+
27
+ Rules:
28
+ - confidence 0.9–1.0: value clearly stated in document
29
+ - confidence 0.5–0.9: value inferred or partially stated
30
+ - confidence 0.0–0.5: value guessed or not found (set value to null)
31
+ - For missing fields, use null with confidence 0.0
32
+ - For dates, use ISO 8601 format (YYYY-MM-DD)
33
+ - For numbers, return numeric type (not string)
34
+ - For booleans, return true/false
35
+ """
36
+
37
+ _JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
38
+
39
+
40
+ class LLMExtractor:
41
+ def __init__(
42
+ self,
43
+ model: str = "gpt-4o-mini",
44
+ provider: str = "openai",
45
+ max_chars: int = 12_000, # ~3K tokens — keep cost low for Phase 0
46
+ ):
47
+ self.model = model
48
+ self.provider = provider
49
+ self.max_chars = max_chars
50
+ self._client = None
51
+
52
+ def _get_client(self):
53
+ if self._client:
54
+ return self._client
55
+ if self.provider == "openai":
56
+ from openai import OpenAI
57
+ self._client = OpenAI()
58
+ else:
59
+ from anthropic import Anthropic
60
+ self._client = Anthropic()
61
+ return self._client
62
+
63
+ def _call(self, prompt: str) -> str:
64
+ client = self._get_client()
65
+ if self.provider == "openai":
66
+ resp = client.chat.completions.create(
67
+ model=self.model,
68
+ messages=[{"role": "user", "content": prompt}],
69
+ temperature=0,
70
+ response_format={"type": "json_object"},
71
+ )
72
+ return resp.choices[0].message.content or ""
73
+ else:
74
+ resp = client.messages.create(
75
+ model=self.model,
76
+ max_tokens=2048,
77
+ messages=[{"role": "user", "content": prompt}],
78
+ )
79
+ return resp.content[0].text if resp.content else ""
80
+
81
+ def extract(self, layout: DocumentLayout, schema: ExtractionSchema) -> ExtractionResult:
82
+ text = layout.total_text[: self.max_chars]
83
+ prompt = _EXTRACT_PROMPT.format(text=text, fields=schema.prompt_lines())
84
+ raw = self._call(prompt)
85
+ return self._parse_response(raw, schema, layout.page_count)
86
+
87
+ def _parse_response(self, raw: str, schema: ExtractionSchema, page_count: int = 1) -> ExtractionResult:
88
+ try:
89
+ data = json.loads(raw)
90
+ fields = data.get("fields", {})
91
+ except json.JSONDecodeError:
92
+ m = _JSON_RE.search(raw)
93
+ if m:
94
+ try:
95
+ data = json.loads(m.group(0))
96
+ fields = data.get("fields", {})
97
+ except json.JSONDecodeError:
98
+ fields = {}
99
+ else:
100
+ fields = {}
101
+
102
+ fv_map: dict[str, FieldValue] = {}
103
+ for spec in schema.fields:
104
+ entry = fields.get(spec.name, {})
105
+ if isinstance(entry, dict):
106
+ fv_map[spec.name] = FieldValue(
107
+ value=entry.get("value"),
108
+ confidence=float(entry.get("confidence", 0.0)),
109
+ raw=str(entry.get("raw", "")),
110
+ )
111
+ else:
112
+ fv_map[spec.name] = FieldValue(value=None, confidence=0.0)
113
+
114
+ return ExtractionResult(
115
+ schema_name=schema.name,
116
+ fields=fv_map,
117
+ page_count=page_count,
118
+ )
@@ -0,0 +1,52 @@
1
+ """Document loader — PDF and raw text to DocumentLayout."""
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from .models import DocumentLayout, PageContent
5
+
6
+
7
+ def from_text(text: str, source: str = "text") -> DocumentLayout:
8
+ """Wrap raw text as a single-page DocumentLayout (useful for testing)."""
9
+ return DocumentLayout(
10
+ source=source,
11
+ pages=[PageContent(page_num=1, text=text)],
12
+ )
13
+
14
+
15
+ def from_pdf(path: str | Path) -> DocumentLayout:
16
+ """Parse a PDF file using pdfplumber."""
17
+ try:
18
+ import pdfplumber
19
+ except ImportError:
20
+ raise ImportError("PDF support requires pdfplumber: pip install docparse[pdf]")
21
+
22
+ path = Path(path)
23
+ pages: list[PageContent] = []
24
+
25
+ with pdfplumber.open(path) as pdf:
26
+ for i, page in enumerate(pdf.pages, start=1):
27
+ text = page.extract_text() or ""
28
+ tables = []
29
+ for table in page.extract_tables() or []:
30
+ # Normalise None cells to empty string
31
+ cleaned = [[cell or "" for cell in row] for row in table if row]
32
+ if cleaned:
33
+ tables.append(cleaned)
34
+ pages.append(PageContent(
35
+ page_num=i,
36
+ text=text,
37
+ tables=tables,
38
+ width=float(page.width),
39
+ height=float(page.height),
40
+ ))
41
+
42
+ return DocumentLayout(source=str(path), pages=pages)
43
+
44
+
45
+ def load(path: str | Path) -> DocumentLayout:
46
+ path = Path(path)
47
+ suffix = path.suffix.lower()
48
+ if suffix == ".pdf":
49
+ return from_pdf(path)
50
+ if suffix in (".txt", ".md"):
51
+ return from_text(path.read_text(encoding="utf-8"), source=str(path))
52
+ raise ValueError(f"Unsupported file type: {suffix}. Use .pdf, .txt, or .md")
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Literal
3
+ from pydantic import BaseModel, field_validator
4
+
5
+ FieldType = Literal["string", "number", "date", "boolean", "list"]
6
+
7
+
8
+ class FieldSpec(BaseModel):
9
+ name: str
10
+ description: str
11
+ type: FieldType = "string"
12
+ required: bool = False
13
+ example: str | None = None # helps the LLM understand the expected format
14
+
15
+
16
+ class ExtractionSchema(BaseModel):
17
+ name: str
18
+ fields: list[FieldSpec]
19
+
20
+ def field_names(self) -> list[str]:
21
+ return [f.name for f in self.fields]
22
+
23
+ def prompt_lines(self) -> str:
24
+ lines = []
25
+ for f in self.fields:
26
+ req = " (REQUIRED)" if f.required else ""
27
+ ex = f' e.g. "{f.example}"' if f.example else ""
28
+ lines.append(f"- {f.name} ({f.type}){req}: {f.description}{ex}")
29
+ return "\n".join(lines)
30
+
31
+
32
+ class FieldValue(BaseModel):
33
+ value: Any
34
+ confidence: float = 0.0
35
+ raw: str = ""
36
+
37
+ @field_validator("confidence")
38
+ @classmethod
39
+ def clamp(cls, v: float) -> float:
40
+ return max(0.0, min(1.0, v))
41
+
42
+
43
+ class ExtractionResult(BaseModel):
44
+ schema_name: str
45
+ fields: dict[str, FieldValue]
46
+ page_count: int = 1
47
+
48
+ def get(self, field: str) -> Any:
49
+ fv = self.fields.get(field)
50
+ return fv.value if fv else None
51
+
52
+ def confidence(self, field: str) -> float:
53
+ fv = self.fields.get(field)
54
+ return fv.confidence if fv else 0.0
55
+
56
+ def missing_required(self, schema: ExtractionSchema) -> list[str]:
57
+ return [
58
+ f.name for f in schema.fields
59
+ if f.required and (self.fields.get(f.name) is None or self.fields[f.name].value is None)
60
+ ]
61
+
62
+
63
+ class TableCell(BaseModel):
64
+ text: str
65
+ row: int
66
+ col: int
67
+ rowspan: int = 1
68
+ colspan: int = 1
69
+
70
+
71
+ class PageContent(BaseModel):
72
+ page_num: int
73
+ text: str
74
+ tables: list[list[list[str]]] = [] # tables[table_idx][row][col]
75
+ width: float = 0.0
76
+ height: float = 0.0
77
+
78
+
79
+ class DocumentLayout(BaseModel):
80
+ source: str = "unknown"
81
+ pages: list[PageContent]
82
+ total_text: str = ""
83
+
84
+ def __init__(self, **data):
85
+ super().__init__(**data)
86
+ if not self.total_text:
87
+ self.total_text = "\n\n".join(p.text for p in self.pages)
88
+
89
+ @property
90
+ def page_count(self) -> int:
91
+ return len(self.pages)
92
+
93
+ def text_for_pages(self, start: int = 1, end: int | None = None) -> str:
94
+ pages = [p for p in self.pages if start <= p.page_num <= (end or len(self.pages))]
95
+ return "\n\n".join(p.text for p in pages)
96
+
97
+ def all_tables(self) -> list[list[list[str]]]:
98
+ return [t for p in self.pages for t in p.tables]
@@ -0,0 +1,16 @@
1
+ from .invoice import INVOICE_SCHEMA
2
+ from .mortgage import LOAN_APPLICATION_SCHEMA, W2_SCHEMA
3
+ from .legal import NDA_SCHEMA, CONTRACT_SCHEMA
4
+
5
+ REGISTRY: dict[str, object] = {
6
+ "invoice": INVOICE_SCHEMA,
7
+ "loan_application": LOAN_APPLICATION_SCHEMA,
8
+ "w2": W2_SCHEMA,
9
+ "nda": NDA_SCHEMA,
10
+ "contract": CONTRACT_SCHEMA,
11
+ }
12
+
13
+ __all__ = [
14
+ "INVOICE_SCHEMA", "LOAN_APPLICATION_SCHEMA", "W2_SCHEMA",
15
+ "NDA_SCHEMA", "CONTRACT_SCHEMA", "REGISTRY",
16
+ ]
@@ -0,0 +1,19 @@
1
+ from ..models import ExtractionSchema, FieldSpec
2
+
3
+ INVOICE_SCHEMA = ExtractionSchema(
4
+ name="invoice",
5
+ fields=[
6
+ FieldSpec(name="invoice_number", type="string", required=True, description="Invoice number or ID", example="INV-2024-001"),
7
+ FieldSpec(name="invoice_date", type="date", required=True, description="Date the invoice was issued", example="2024-03-15"),
8
+ FieldSpec(name="due_date", type="date", required=False, description="Payment due date"),
9
+ FieldSpec(name="vendor_name", type="string", required=True, description="Name of the vendor or supplier"),
10
+ FieldSpec(name="vendor_address", type="string", required=False, description="Full address of the vendor"),
11
+ FieldSpec(name="client_name", type="string", required=False, description="Name of the billed client"),
12
+ FieldSpec(name="subtotal", type="number", required=False, description="Subtotal before tax", example="1250.00"),
13
+ FieldSpec(name="tax_amount", type="number", required=False, description="Tax or VAT amount"),
14
+ FieldSpec(name="total_amount", type="number", required=True, description="Total amount due", example="1500.00"),
15
+ FieldSpec(name="currency", type="string", required=False, description="Currency code", example="EUR"),
16
+ FieldSpec(name="payment_terms", type="string", required=False, description="Payment terms", example="Net 30"),
17
+ FieldSpec(name="line_items", type="list", required=False, description="List of line items as strings"),
18
+ ],
19
+ )
@@ -0,0 +1,30 @@
1
+ from ..models import ExtractionSchema, FieldSpec
2
+
3
+ NDA_SCHEMA = ExtractionSchema(
4
+ name="nda",
5
+ fields=[
6
+ FieldSpec(name="party_1", type="string", required=True, description="First party (disclosing party) full legal name"),
7
+ FieldSpec(name="party_2", type="string", required=True, description="Second party (receiving party) full legal name"),
8
+ FieldSpec(name="effective_date", type="date", required=True, description="Date the agreement becomes effective"),
9
+ FieldSpec(name="expiry_date", type="date", required=False, description="Date the agreement expires if stated"),
10
+ FieldSpec(name="confidential_info", type="string", required=False, description="Description of what constitutes confidential information"),
11
+ FieldSpec(name="jurisdiction", type="string", required=False, description="Governing law / jurisdiction", example="State of California"),
12
+ FieldSpec(name="term_years", type="number", required=False, description="Duration of confidentiality obligation in years"),
13
+ FieldSpec(name="mutual", type="boolean", required=False, description="True if mutual NDA, False if one-way"),
14
+ ],
15
+ )
16
+
17
+ CONTRACT_SCHEMA = ExtractionSchema(
18
+ name="contract",
19
+ fields=[
20
+ FieldSpec(name="contract_title", type="string", required=False, description="Title or type of contract"),
21
+ FieldSpec(name="party_1", type="string", required=True, description="First party full legal name"),
22
+ FieldSpec(name="party_2", type="string", required=True, description="Second party full legal name"),
23
+ FieldSpec(name="effective_date", type="date", required=False, description="Effective date of the contract"),
24
+ FieldSpec(name="expiry_date", type="date", required=False, description="Expiry or end date if stated"),
25
+ FieldSpec(name="contract_value", type="number", required=False, description="Total contract value if stated"),
26
+ FieldSpec(name="payment_terms", type="string", required=False, description="Payment schedule or terms"),
27
+ FieldSpec(name="jurisdiction", type="string", required=False, description="Governing law"),
28
+ FieldSpec(name="termination_notice", type="string", required=False, description="Required notice period for termination"),
29
+ ],
30
+ )
@@ -0,0 +1,33 @@
1
+ from ..models import ExtractionSchema, FieldSpec
2
+
3
+ LOAN_APPLICATION_SCHEMA = ExtractionSchema(
4
+ name="loan_application",
5
+ fields=[
6
+ FieldSpec(name="borrower_name", type="string", required=True, description="Full name of the primary borrower"),
7
+ FieldSpec(name="co_borrower_name", type="string", required=False, description="Full name of the co-borrower if any"),
8
+ FieldSpec(name="property_address", type="string", required=True, description="Address of the property being financed"),
9
+ FieldSpec(name="loan_amount", type="number", required=True, description="Requested loan amount in dollars", example="350000"),
10
+ FieldSpec(name="loan_purpose", type="string", required=False, description="Purpose of the loan", example="Purchase"),
11
+ FieldSpec(name="property_type", type="string", required=False, description="Type of property", example="Single Family"),
12
+ FieldSpec(name="annual_income", type="number", required=False, description="Borrower's annual income"),
13
+ FieldSpec(name="employment_status", type="string", required=False, description="Employment status of borrower", example="Employed"),
14
+ FieldSpec(name="employer_name", type="string", required=False, description="Name of borrower's employer"),
15
+ FieldSpec(name="credit_score", type="number", required=False, description="Credit score if stated"),
16
+ FieldSpec(name="application_date", type="date", required=False, description="Date the application was submitted"),
17
+ FieldSpec(name="ssn_last4", type="string", required=False, description="Last 4 digits of SSN if present"),
18
+ ],
19
+ )
20
+
21
+ W2_SCHEMA = ExtractionSchema(
22
+ name="w2",
23
+ fields=[
24
+ FieldSpec(name="employee_name", type="string", required=True, description="Employee's full name"),
25
+ FieldSpec(name="employer_name", type="string", required=True, description="Employer's name"),
26
+ FieldSpec(name="tax_year", type="string", required=True, description="Tax year", example="2023"),
27
+ FieldSpec(name="wages", type="number", required=True, description="Box 1: Wages, tips, other comp"),
28
+ FieldSpec(name="federal_tax_withheld",type="number", required=False, description="Box 2: Federal income tax withheld"),
29
+ FieldSpec(name="social_security_wages",type="number", required=False, description="Box 3: Social security wages"),
30
+ FieldSpec(name="medicare_wages", type="number", required=False, description="Box 5: Medicare wages"),
31
+ FieldSpec(name="state", type="string", required=False, description="State abbreviation", example="CA"),
32
+ ],
33
+ )
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "ag-docparse"
7
+ version = "0.5.0"
8
+ description = "LLM-powered document extraction SDK"
9
+ authors = ["Adnan Ghribi <aghribi@gmail.com>"]
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ homepage = "https://github.com/aghribi/ag-docparse"
13
+ repository = "https://github.com/aghribi/ag-docparse"
14
+ packages = [{ include = "ag_docparse" }]
15
+
16
+ [tool.poetry.scripts]
17
+ ag-docparse = "ag_docparse.cli:main"
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "^3.10"
21
+ pydantic = "^2.0"
22
+ httpx = ">=0.27,<1"
23
+ click = "^8.0"
24
+
25
+ [tool.poetry.extras]
26
+ openai = ["openai"]
27
+ anthropic = ["anthropic"]
28
+ pdf = ["pdfplumber"]
29
+ all = ["openai", "anthropic", "pdfplumber"]
30
+
31
+ [tool.poetry.dependencies.openai]
32
+ version = "^1.0"
33
+ optional = true
34
+
35
+ [tool.poetry.dependencies.anthropic]
36
+ version = "^0.25"
37
+ optional = true
38
+
39
+ [tool.poetry.dependencies.pdfplumber]
40
+ version = "^0.11"
41
+ optional = true
42
+
43
+ [tool.poetry.group.dev.dependencies]
44
+ pytest = "^8.0"
45
+ pytest-asyncio = "^0.23"
46
+ ruff = "^0.4"
47
+ openai = "^1.0"
48
+ pdfplumber = "^0.11"
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+
53
+ [tool.ruff]
54
+ line-length = 100