Metadata-Version: 2.3
Name: contract-to-tree
Version: 0.5.0
Summary: 
Author: Jarosław Jezierczak
Author-email: jezierczak@gmail.com
Requires-Python: >=3.10,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: odfpy (>=1.4,<2.0)
Requires-Dist: openai (>=1.0,<3.0)
Requires-Dist: openpyxl (>=3.1,<4.0)
Requires-Dist: pdfplumber (>=0.11,<1.0)
Requires-Dist: pydantic (>=2.10,<3.0)
Requires-Dist: python-dotenv (>=1.0,<2.0)
Requires-Dist: xlrd (>=2.0,<3.0)
Description-Content-Type: text/markdown

# contract-to-tree

Biblioteka do zamiany kosztorysów budowlanych i instalacyjnych na ustandaryzowane drzewo JSON.

## Stan projektu

Obecnie dostępny jest bazowy parser regułowy dla plików `.xlsx`, `.xls` i `.ods` oraz pierwszy parser PDF-ów tekstowych. Obsługiwane są:

- arkusze z nagłówkami w różnych wierszach;
- grupy i pozycje kosztorysowe;
- ilość, jednostka, cena jednostkowa i cena końcowa;
- składniki kosztu: materiał, robocizna i sprzęt;
- źródło pozycji: plik, arkusz i numer wiersza;
- ostrzeżenia, poziom pewności i wartości wyliczone;
- płaski wynik oraz drzewo o konfigurowalnej głębokości.

## Uruchomienie

Opcjonalna konfiguracja LLM:

```bash
Copy-Item .env.example .env
```

Następnie wpisz lokalnie `OPENAI_API_KEY`. Domyślny model to `gpt-4o-mini`. Klucz nie powinien być wpisywany do kodu ani commitowany do repozytorium.

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse assets\budowlanka_oferty.xlsx --max-depth 2
```

Zapis do pliku:

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse assets\budowlanka_oferty.xlsx --output wynik.json
```

PDF tekstowy (domyślnie strategia `verified-pdf`; poprzedni parser: `--strategy pdf-rules`):

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\kosztorys-oferta_bez_podpisu.pdf" --output wynik.json
```

PDF jest zamieniany na wiersze i kolumny (`pdf_table.read_pdf_rows`): słowa ze współrzędnymi → linie → komórki → kolumny wspólne dla dokumentu (liczby po krawędzi wyrównania, tekst po lewej krawędzi, krótkie etykiety jak jednostki po zakresie). Nagłówki i stopki stron są usuwane, a tabela o innym układzie kolumn staje się osobnym segmentem. Dalej działa ten sam mechanizm hipotez i weryfikacji co dla arkuszy; kwoty zapisane z „zł” są traktowane jako sumy kontrolne. Pozycja w wyniku ma `source.page`. Skan bez warstwy tekstowej daje `SCANNED_PDF` i `needs_review` (OCR jeszcze nie jest obsługiwany).

Raport zbiorczy dla katalogu próbek:

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli benchmark assets --output benchmark.json
```

Jawne użycie strategii OpenAI:

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\Przedmiar elektryka.xlsx" --strategy llm-openai --output wynik-llm.json
```

Ta strategia wykonuje zewnętrzne zapytanie API i nie jest uruchamiana przez zwykły benchmark.

Strategia odkrywania schematu nagłówka:

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\budowlanka_oferty.xlsx" --strategy schema-llm --output wynik-schema-llm.json
```

W tym wariancie LLM interpretuje tylko nagłówek i kilka przykładowych wierszy, a cały dokument parsują następnie reguły.

## Strategie arkuszowe

| strategia | CLI | opis |
|---|---|---|
| `rules-spreadsheet` | `--strategy rules` | mapowanie kolumn po nagłówkach |
| `infer-spreadsheet` | `--strategy infer` | role kolumn z treści: słownik jednostek, kody katalogowe, numeracja, arytmetyka ilość × cena ≈ wartość |
| `verified-spreadsheet` | `--strategy verified` (domyślna od 0.2.0) | kilka hipotez układu na arkusz, wybór tej, którą potwierdzają sumy zapisane w dokumencie |
| `hybrid-structured` | `--strategy hybrid` | parser strukturalny: heurystyki + opcjonalny doradca LLM rozpoznają strukturę, kod kopiuje wartości z komórek |

### Strategia `hybrid-structured`

LLM nie przepisuje dokumentu. Odpowiada tylko na małe pytania o strukturę — `classify_row()`, `discover_schema()`, `classify_ambiguous_row()` — i dostaje wyłącznie wiersze, których heurystyki nie rozstrzygnęły (budżet wywołań: `HybridOptions.max_llm_calls`). Wartości kopiuje kod.

```text
arkusz → normalizacja (scalenia, pogrubienia) → cechy wierszy → kandydaci na nagłówki → doradca
       → segmenty tabel z własnym schematem kolumn → deterministyczne czytanie wierszy
       → walidacja (q × p ≈ v, sumy dokumentu) → StructuredEstimate
```

- Każdy segment tabeli ma własny schemat. Kandydaci: LLM, etykiety nagłówka, treść komórek, arytmetyka. Wygrywa ten, który najlepiej pasuje do danych segmentu, więc błędny schemat z LLM przegrywa z danymi.
- Typy wierszy: `document_title`, `metadata`, `section_header`, `table_header`, `item`, `item_detail` (obmiar, zawinięty tekst, „RAZEM:” pozycji), `subtotal`, `section_total`, `grand_total`, `note`, `blank`, `unknown`.
- Każda wartość ma źródło (`sheet`, `row`, `column`, `raw_value`). Komórki bez roli trafiają do `extra_cells`, a wiersze nierozpoznane — do `unclassified_rows`. `validation.rows_accounted == rows_non_blank` oznacza, że żaden niepusty wiersz nie zginął.
- Confidence jest liczone na poziomach: dokument, sekcja, schemat, wiersz, pole. Łączy heurystykę, odpowiedź LLM, spójność schematu i arytmetykę.

```bash
# CostEstimate (wspólny kontrakt); --advisor auto = OpenAI, gdy ustawiono OPENAI_API_KEY
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\budowlanka_oferty.xlsx" --strategy hybrid --output wynik.json
# pełny wynik strukturalny (sekcje, schematy, sumy, pochodzenie pól), same heurystyki
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\budowlanka_oferty.xlsx" --strategy hybrid --advisor none --structure --output struktura.json
```

```python
from contract_to_tree.structured import HybridStructuredParser, LlmStructureAdvisor, OpenAIClient

parser = HybridStructuredParser(LlmStructureAdvisor(OpenAIClient()))  # albo HybridStructuredParser() bez LLM
estimate, structure = parser.parse_with_structure("kosztorys.xlsx")
```

Innego dostawcę (Claude, model lokalny) podłącza się, implementując `LLMClient.complete(system, user, response_model)`. Parser się nie zmienia.

Strategia `verified` porównuje pozycje z sumami kontrolnymi dokumentu: „Razem dział: …”, „RAZEM 1.2”, „SUMA”, „RAZEM kosztorys”, wartości w wierszach rozdziałów, sumy w wierszu nagłówka lub numeracji kolumn. Jeśli wynik nie zgadza się z dokumentem albo układ jest niejednoznaczny, ustawia `quality.needs_review = true` i dodaje ostrzeżenie (`RECONCILIATION_MISMATCH`, `LAYOUT_AMBIGUOUS`, `NO_ITEMS`). Szczegóły (hipotezy, oceny, uzgodnienia) są w `quality.diagnostics["verification"]`.

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli parse "assets\P.R.Zakopane.xlsx_XDOC0000291878.xlsx" --strategy verified --output wynik.json
```

Parametr `--pricing` / `pricing=` mówi, czego się spodziewamy: `blind` (kosztorys ślepy), `priced` (wypełniony) albo `auto` (domyślnie, wykrywanie). Niezgodność z wykrytym `quality.pricing_mode` daje `PRICING_MODE_CONFLICT` i `needs_review`; przy `priced` pozycje bez ceny dostają `MISSING_PRICE`.

Benchmark z sumami referencyjnymi (`tests/golden/expected_totals.json`, sumy deklarowane przez same dokumenty):

```bash
.venv\Scripts\python.exe -m contract_to_tree.cli benchmark assets --strategy verified --expected tests\golden\expected_totals.json
```

## Testy

```bash
.venv\Scripts\python.exe -m unittest discover -s tests -v
```

Opis modelu danych i dalszych wariantów znajduje się w [USTALENIA.md](USTALENIA.md).

## Stabilny interfejs biblioteczny

Najprostsze użycie w innym projekcie:

```python
from contract_to_tree import parse_document

estimate = parse_document("kosztorys.xlsx", max_depth=2)
payload = estimate.model_dump(mode="json")
```

Od 0.2.0 arkusze domyślnie parsuje strategia `verified-spreadsheet`. Deklaracja wyceny i sprawdzenie wyniku:

```python
estimate = parse_document("kosztorys.xlsx", max_depth=2, pricing="priced")
if estimate.quality.needs_review:
    ...  # patrz estimate.quality.warnings
```

Poprzednie zachowanie (strategia `rules`):

```python
from contract_to_tree import StrategyRegistry
from contract_to_tree.strategies import PdfRulesStrategy, SpreadsheetRulesStrategy

estimate = parse_document("kosztorys.xlsx", registry=StrategyRegistry([SpreadsheetRulesStrategy(), PdfRulesStrategy()]))
```

`parse_document()` oraz model `CostEstimate` są głównym interfejsem publicznym.
Nowe strategie powinny nadal zwracać `CostEstimate` w schemacie `1.0`.
Mapowania LLM są lokalnie walidowane pod kątem opisów, ilości i cen; błędny
kandydat jest zastępowany najlepszym kandydatem regułowym.

