Metadata-Version: 2.5
Name: fintom8
Version: 0.1.9
Summary: LiteLLM connector for Gemini, Vertex AI, OpenAI, and Azure — chat, stream, and document extract.
Project-URL: Homepage, https://github.com/NikolaienkoIgor/f8_templates
Project-URL: Documentation, https://github.com/NikolaienkoIgor/f8_templates/tree/main/fintom8
Author: Igor Nikolaienko
Author-email: Harsh Bansal <harshhb6@gmail.com>
License: MIT
Keywords: azure,gemini,litellm,llm,openai,vertex
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.14
Requires-Dist: langgraph>=0.2.74
Requires-Dist: litellm[google]<2.0.0,>=1.94.0
Requires-Dist: openpyxl>=3.1.0
Requires-Dist: pydantic>=2.4.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: tenacity>=8.2.0
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: lxml>=6.1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Provides-Extra: pdf
Requires-Dist: fonttools>=4.0.0; extra == 'pdf'
Requires-Dist: pikepdf>=9.0.0; extra == 'pdf'
Provides-Extra: schematron
Requires-Dist: lxml>=6.1.0; extra == 'schematron'
Requires-Dist: saxonche; extra == 'schematron'
Description-Content-Type: text/markdown

# fintom8

LiteLLM connector for **Gemini / Vertex AI / OpenAI / Azure**. Chat, stream, and document extract. Students install with pip and call a few methods — keys stay in `.env`.

Requires **Python 3.14+**.

### Install from Git (recommended for private use)

GitHub Packages Python upload is currently unreliable (SSL issues). Install directly from this repo instead:

```bash
# HTTPS (use a PAT with repo read access if the repo is private)
pip install "git+https://github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"

# Pin a tag / commit
pip install "git+https://github.com/NikolaienkoIgor/f8_templates.git@fintom8-v0.1.9#subdirectory=fintom8"

# SSH (no token in the URL if your SSH key is set up)
pip install "git+ssh://git@github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"
```

Private HTTPS with an explicit token:

```bash
pip install "git+https://<GITHUB_USERNAME>:<GITHUB_PAT>@github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"
```

### Other install options

```bash
# Public PyPI (when published)
pip install fintom8

# Local editable install from this checkout
pip install -e ./fintom8
# or: pip install -e "./fintom8[dev]"
```

```python
from fintom8 import LLM

llm = LLM()  # reads .env / environment
print(llm.chat("Summarize this invoice").text)
```

## Configuration

Resolution order: **constructor kwargs / `LLMConfig` > environment > defaults**.

Copy [`.env.example`](./.env.example) to `.env` in your project (never commit it).

| Param | Env | Default | When needed |
|-------|-----|---------|-------------|
| `model` | `LLM_MODEL` | `gemini/gemini-3.5-flash` | always |
| `temperature` | `LLM_TEMPERATURE` | unset (Gemini 3+), `1.0` (older Gemini/Vertex), else `0.0` | optional; omitted for Gemini 3+ (deprecated by Google) |
| `num_retries` | — | `3` | optional |
| `api_key` | `GEMINI_API_KEY` / `OPENAI_API_KEY` / `AZURE_API_KEY` (from model prefix) | unset | Gemini / OpenAI / Azure (`azure/` → required) |
| `api_base` | `AZURE_API_BASE` / `OPENAI_API_BASE` | unset | Azure (`azure/` → required) |
| `api_version` | `AZURE_API_VERSION` | unset | Azure (`azure/` → required) |
| `vertex_project` | `VERTEXAI_PROJECT` | unset | Vertex |
| `vertex_location` | `VERTEXAI_LOCATION` | `eu` | Vertex |

For `azure/<deployment>`, missing `api_key`, `api_base`, or `api_version` raises `Fintom8Error` (set via constructor or `AZURE_*` env vars).

```python
from fintom8 import LLM, LLMConfig

llm = LLM()  # env defaults
llm = LLM(model="gpt-4o", api_key="sk-...", temperature=0)
llm = LLM(LLMConfig(
    model="azure/my-deploy",
    api_key="...",
    api_base="https://....openai.azure.com",
    api_version="2024-10-21",
))
```

### Switch provider

| `LLM_MODEL` | Env |
|-------------|-----|
| `gemini/gemini-3.5-flash` | `GEMINI_API_KEY` |
| `vertex_ai/gemini-3.5-flash` | `VERTEXAI_PROJECT` + `VERTEXAI_LOCATION` + ADC (`gcloud auth application-default login`) |
| `gpt-4o` | `OPENAI_API_KEY` |
| `azure/<deployment>` | `AZURE_API_KEY` + `AZURE_API_BASE` + `AZURE_API_VERSION` |

## Contributors

- Igor Nikolaienko
- [Harsh Bansal](https://github.com/harshhb6)

## Usage

```python
from fintom8 import LLM

llm = LLM()

resp = llm.chat("Hello")
print(resp.text, resp.usage)

# Structured output — file only; detect format → LiteLLM SO → cleanse dates
invoice_rf = {
    "type": "json_schema",
    "json_schema": {
        "name": "Invoice",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                "total": {"type": "number"},
                "vendor": {"type": "string"},
                "invoiceDate": {"type": ["date", "null"]},
            },
            "required": ["total", "vendor", "invoiceDate"],
            "additionalProperties": False,
        },
    },
}
data = llm.structured(
    "invoice.pdf",  # also: .png/.jpg, .txt/.csv/.xml/.xlsx, or bytes
    structuredOutput=invoice_rf,
    dateFormat="DD.MM.YYYY",
)
# {"total": 42.5, "vendor": "Acme", "invoiceDate": "08.08.2026"}

# Invoice / EN16931 / chem ping-pong (keyword args)
report = llm.generate_and_validate(name="invoice", source="invoice.pdf")
# {"correct": true, "name": "invoice", "format": "json", "artifact": {…}, …}

en16931 = llm.generate_and_validate(
    name="en16931", source="invoice.pdf", invoice_format="ubl"
)
# en16931["artifact"] is the last UBL XML string; requires fintom8[schematron]

chem = llm.generate_and_validate(name="chemical_composition", source="cert.pdf")
# Prefer a path for source; pass filePath= only when source is raw bytes

# Document → UBL / ZUGFeRD XML (LLM generate only; no Schematron loop)
ubl_xml = llm.convert("invoice.pdf", invoice_format="ubl")
zugferd_xml = llm.convert("invoice.csv", invoice_format="zugferd")
# also: .json / .xml / images / .xlsx — or bytes with mime= / filePath=

# process() is an alias of structured()
data = llm.process(
    "invoice.pdf",
    structuredOutput=invoice_rf,
    dateFormat="DD.MM.YYYY",
    instructions="Extract invoice vendor and total.",
)

for chunk in llm.stream([{"role": "user", "content": "Write a haiku"}]):
    print(chunk, end="", flush=True)

resp = llm.extract("invoice.pdf", response_format=invoice_rf)
```

Async twins: `achat`, `astream`, `aextract`, `astructured`, `aprocess`, `aconvert`, `agenerate_and_validate`, `acompare`.

Optional helpers: `detect_format`, `fields_to_schema`, `compile_fields`, `prepare_response_format`, `apply_cleanse`, `json_schema_response_format`, `structured_output`, `enforce_strict`, `inline_refs`, `compare`, `acompare`.

### Compare two sources

LLM-based dual-source comparison with built-in profiles or a custom schema:

```python
from fintom8 import LLM, compare

llm = LLM()

# PDF/image/document vs invoice XML (payment-critical mismatches)
report = llm.compare("invoice.pdf", "invoice.xml", profile="document_xml")
# {"matches": true, "total_mismatches": 0, "mismatches": []}

# Mapping document vs reference rules text (Peppol gap analysis step 1)
findings = llm.compare(
    "mapping.html",
    "PEPPOL-EN16931-UBL.sch",
    profile="mapping_reference",
    max_findings=5,
    model="gemini/gemini-3.1-pro-preview",
)

# Custom comparison
custom = llm.compare(
    doc_a,
    doc_b,
    profile="custom",
    instructions="Compare these two contracts for clause differences.",
    structuredOutput={"differences": {"type": "array", "items": {"type": "string"}}},
)
```

Register additional profiles with `fintom8.comparison.register_compare_profile()`. See [`examples/compare_document_xml.py`](examples/compare_document_xml.py).

### Bundled templates

```python
from fintom8.templates import invoice
# or: from fintom8 import templates; templates.invoice
# or: from fintom8 import use_template; use_template("invoice")

data = llm.structured(
    "invoice.pdf",
    structuredOutput=invoice["structuredOutput"],
    systemPrompt=invoice["systemPrompt"],
    dateFormat=invoice.get("dateFormat", "YYYY-MM-DD"),
)
```

`list_templates()` lists packaged names (`invoice`, `chemical_composition`, `recipient_statement`). Pass a path or dict to `use_template` for custom templates.

### Validation

One engine, one envelope. Document types differ only by the **pack** registered for that name. ERP check is a second step on the same envelope — not a different API.

```python
from fintom8 import validate, erp_check

report = validate("invoice", payload)
report = validate("chemical_composition", payload)
report = validate("recipient_statement", payload)

# EN16931 Schematron on UBL or CII XML (format is detected; requires fintom8[schematron])
en16931 = validate("en16931", {"xml": ubl_or_cii_xml})
# or: validate("en16931", {"xml_path": "invoice.xml"})

# ERP check is named + injected. qc55 is SAP vs chemical coils only.
qc55 = erp_check("qc55", chem_payload, reference=qc55_rows)  # bundled qc55.csv if omitted
```

Every call returns:

```json
{
  "correct": false,
  "name": "invoice",
  "format": "json",
  "errors": [{"path": "lineItems[0].totalPriceWithTax", "message": "...", "code": "FORMULA"}],
  "warnings": [],
  "artifact": {},
  "units": null,
  "_debug": null
}
```

`artifact` is the document (invoice JSON, chemical extraction, recipient JSON, or EN16931 XML string). `format` is `json`, `ubl`, or `cii`. Chemical and QC55 put per-coil reports in `units` (including a single coil). Invoice rewritten totals live in `artifact` (the caller's payload is not mutated). QC55 match rows live in `units[coil_id].artifact`. EN16931 `format` is `ubl` or `cii`; Schematron rule ids (e.g. `BR-CO-15`) are in `errors[].code` and XPath in `errors[].path`. `_debug` is `{attempts, exhausted, raw}` only when `include_debug=True`.

Install Schematron support with:

```bash
pip install "fintom8[schematron]"
```

PDF/A-3 (embed invoice XML into a PDF) is available via:

```bash
pip install "fintom8[pdf]"
```

```python
from fintom8 import create_pdfa3, extract_xml

pdfa3_bytes = create_pdfa3("invoice.pdf", "invoice.xml")
xml = extract_xml(pdfa3_bytes)  # or extract_xml("invoice_zugferd.pdf")
```

Bundled EN16931 XSLT files are licensed under EUPL 1.2 (CEN). Default `pip install fintom8` does not require Saxon or pikepdf.

Chemical composition **document** validation does not call QC55; sequence them explicitly:

```python
from fintom8 import validate, erp_check

chem = validate("chemical_composition", extracted)
qc55 = erp_check("qc55", extracted)  # or erp_check("qc55", extracted, reference=live_rows)
```

Optional aliases: `validate_invoice`, `validate_recipient_statement`, `validate_en16931`, `erp_check_qc55` — same envelope as the named calls above.

### Generate + validate (ping-pong)

One standard wrapper. ``name`` selects SO (JSON schema) or NON-SO (XML); ``source`` is the document path or bytes. Same Validation Wrapper loop: validate → on error, correction prompt + regenerate → until `correct` or max attempts.

```python
from fintom8 import LLM

llm = LLM()
report = llm.generate_and_validate(name="invoice", source="invoice.pdf", include_debug=True)

report = llm.generate_and_validate(
    name="en16931", source="invoice.pdf", invoice_format="ubl", include_debug=True
)
# report["artifact"], report["_debug"]["attempts"]

report = llm.generate_and_validate(name="chemical_composition", source="cert.pdf")
```

Packs stay pure (no LLM/retry inside `validation/`). Pass ``filePath=`` / ``mime=`` only when ``source`` is raw bytes.

### Convert document → UBL / ZUGFeRD XML (generate only)

Same idea as the platform invoice-agent generate step without the Schematron loop. For generate + validate + fix, use `generate_and_validate(name="en16931", source=…)` instead.

```python
from fintom8 import LLM, convert_to_xml

llm = LLM()
ubl = llm.convert("invoice.pdf", invoice_format="ubl")
cii = llm.convert("invoice.json", invoice_format="zugferd")  # aliases: cii, factur-x
# or: convert_to_xml(llm, "invoice.pdf", invoice_format="ubl")
```

See [`examples/convert_to_xml.py`](./examples/convert_to_xml.py), [`examples/validate_en16931.py`](./examples/validate_en16931.py).

See [`examples/invoice_extraction_fintom8.py`](./examples/invoice_extraction_fintom8.py), [`examples/generate_and_validate.py`](./examples/generate_and_validate.py), [`examples/validate_invoice.py`](./examples/validate_invoice.py), [`examples/validate_chemical_composition.py`](./examples/validate_chemical_composition.py), [`examples/validate_recipient_statement.py`](./examples/validate_recipient_statement.py), [`examples/validate_en16931.py`](./examples/validate_en16931.py), [`examples/erp_check_qc55.py`](./examples/erp_check_qc55.py).

Also exported: `Invoice`, `LineItem`, `ValidationReport`, `FieldError`, `validation_payload_from_llm`. Packs are pure (dict in, report out) — no LLM, HTTP, or retry loops.

Failures raise `Fintom8Error`.

If you see an authentication error (for example missing `GEMINI_API_KEY`, `OPENAI_API_KEY`, or Vertex setup), that means package import and retries are working; configure credentials for the selected `LLM_MODEL`.

See [`examples/chat.py`](./examples/chat.py) and [`examples/extract.py`](./examples/extract.py).

## Publish (maintainers)

1. Install dev extras and run tests:

   ```bash
   cd fintom8
   pip install -e ".[dev]"
   pytest
   python -c "from fintom8 import LLM"
   ```

2. Build:

   ```bash
   python -m build
   ```

3. Upload to TestPyPI first, then PyPI:

   ```bash
   python -m twine upload --repository testpypi dist/*
   python -m twine upload dist/*
   ```

4. Tag for CI Trusted Publishing (OIDC). Create the PyPI project once and add a GitHub environment `pypi` with Trusted Publisher pointing at `.github/workflows/publish-fintom8.yml`. Then:

   ```bash
   git tag fintom8-v0.1.9
   git push origin fintom8-v0.1.9
   ```
