eurekiusz 0.1.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.
- eurekiusz-0.1.0/PKG-INFO +61 -0
- eurekiusz-0.1.0/README.md +45 -0
- eurekiusz-0.1.0/pyproject.toml +36 -0
- eurekiusz-0.1.0/setup.cfg +4 -0
- eurekiusz-0.1.0/src/eurekiusz/__init__.py +3 -0
- eurekiusz-0.1.0/src/eurekiusz/cli.py +120 -0
- eurekiusz-0.1.0/src/eurekiusz/data/chunks.db +0 -0
- eurekiusz-0.1.0/src/eurekiusz/eureka.py +97 -0
- eurekiusz-0.1.0/src/eurekiusz/gui.py +120 -0
- eurekiusz-0.1.0/src/eurekiusz/law.py +306 -0
- eurekiusz-0.1.0/src/eurekiusz/paths.py +39 -0
- eurekiusz-0.1.0/src/eurekiusz.egg-info/PKG-INFO +61 -0
- eurekiusz-0.1.0/src/eurekiusz.egg-info/SOURCES.txt +14 -0
- eurekiusz-0.1.0/src/eurekiusz.egg-info/dependency_links.txt +1 -0
- eurekiusz-0.1.0/src/eurekiusz.egg-info/entry_points.txt +5 -0
- eurekiusz-0.1.0/src/eurekiusz.egg-info/top_level.txt +1 -0
eurekiusz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eurekiusz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lokalny asystent ustaw podatkowych: ustawa → nowela → KIS. Bez zgadywania kwot.
|
|
5
|
+
Author-email: QA10 <hello@qa10.io>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://qa10.io
|
|
8
|
+
Keywords: vat,podatki,eli,kis,eureka
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
12
|
+
Classifier: Intended Audience :: Legal Industry
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Eurekiusz
|
|
18
|
+
|
|
19
|
+
Lokalny asystent ustaw podatkowych. Trzy warstwy: **ustawa → nowela → KIS**.
|
|
20
|
+
Model nie zgaduje kwot. Pusty kontekst = `Nie znalazlem w bazie.`
|
|
21
|
+
|
|
22
|
+
## Instalacja
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pip install eurekiusz
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Windows (po `winget install Python.Python.3.12` i **nowym** PowerShellu):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
python -m pip install eurekiusz
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Użycie
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
eurekiusz ask "jaki jest limit zwolnienia VAT 2026"
|
|
38
|
+
eurekiusz lookup DU/2025/775 113
|
|
39
|
+
eurekiusz health
|
|
40
|
+
eurekiusz gui
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Albo skrót okna: `eurekiusz-gui`.
|
|
44
|
+
|
|
45
|
+
Bramka: art. 113 TJ `DU/2025/775` = **200 000 zł**. Limit 240 000 zł to nowela `DU/2025/896` od 1.01.2026. Sama kwota 240k przy cytacie 775 = błąd.
|
|
46
|
+
|
|
47
|
+
## Baza
|
|
48
|
+
|
|
49
|
+
Wheel zawiera `chunks.db` (~27 MB). Nadpisanie:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
set EUREKIUSZ_DB=C:\path\chunks.db
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
albo plik `~/.eurekiusz/chunks.db`.
|
|
56
|
+
|
|
57
|
+
## Czego tu nie ma
|
|
58
|
+
|
|
59
|
+
- modelu LLM / GGUF (osobny download na desktop Wails)
|
|
60
|
+
- Qdrant, 8 botów, chmury
|
|
61
|
+
- treści z bazy Optima `prawo_rachunkowe`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Eurekiusz
|
|
2
|
+
|
|
3
|
+
Lokalny asystent ustaw podatkowych. Trzy warstwy: **ustawa → nowela → KIS**.
|
|
4
|
+
Model nie zgaduje kwot. Pusty kontekst = `Nie znalazlem w bazie.`
|
|
5
|
+
|
|
6
|
+
## Instalacja
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install eurekiusz
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Windows (po `winget install Python.Python.3.12` i **nowym** PowerShellu):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
python -m pip install eurekiusz
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Użycie
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
eurekiusz ask "jaki jest limit zwolnienia VAT 2026"
|
|
22
|
+
eurekiusz lookup DU/2025/775 113
|
|
23
|
+
eurekiusz health
|
|
24
|
+
eurekiusz gui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Albo skrót okna: `eurekiusz-gui`.
|
|
28
|
+
|
|
29
|
+
Bramka: art. 113 TJ `DU/2025/775` = **200 000 zł**. Limit 240 000 zł to nowela `DU/2025/896` od 1.01.2026. Sama kwota 240k przy cytacie 775 = błąd.
|
|
30
|
+
|
|
31
|
+
## Baza
|
|
32
|
+
|
|
33
|
+
Wheel zawiera `chunks.db` (~27 MB). Nadpisanie:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
set EUREKIUSZ_DB=C:\path\chunks.db
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
albo plik `~/.eurekiusz/chunks.db`.
|
|
40
|
+
|
|
41
|
+
## Czego tu nie ma
|
|
42
|
+
|
|
43
|
+
- modelu LLM / GGUF (osobny download na desktop Wails)
|
|
44
|
+
- Qdrant, 8 botów, chmury
|
|
45
|
+
- treści z bazy Optima `prawo_rachunkowe`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "eurekiusz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lokalny asystent ustaw podatkowych: ustawa → nowela → KIS. Bez zgadywania kwot."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "QA10", email = "hello@qa10.io" }]
|
|
12
|
+
license = { text = "Proprietary" }
|
|
13
|
+
keywords = ["vat", "podatki", "eli", "kis", "eureka"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Environment :: Win32 (MS Windows)",
|
|
18
|
+
"Intended Audience :: Legal Industry",
|
|
19
|
+
"Topic :: Office/Business :: Financial",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://qa10.io"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
eurekiusz = "eurekiusz.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.gui-scripts]
|
|
30
|
+
eurekiusz-gui = "eurekiusz.gui:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
eurekiusz = ["data/chunks.db"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""CLI: eurekiusz ask|lookup|search|health|gui"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from eurekiusz import __version__
|
|
9
|
+
from eurekiusz.law import ask, health, lookup, search
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _clip(text: str, n: int = 900) -> str:
|
|
13
|
+
text = (text or "").strip()
|
|
14
|
+
if len(text) > n:
|
|
15
|
+
return text[:n] + "…"
|
|
16
|
+
return text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _hit_lines(hit: dict) -> list[str]:
|
|
20
|
+
if not hit:
|
|
21
|
+
return []
|
|
22
|
+
inner = hit.get("article") or hit
|
|
23
|
+
eli = inner.get("eli") or hit.get("eli") or ""
|
|
24
|
+
art = inner.get("art") or hit.get("art") or ""
|
|
25
|
+
text = inner.get("text") or hit.get("text") or hit.get("head") or ""
|
|
26
|
+
lines = [f"{eli} art. {art}".strip()]
|
|
27
|
+
if text:
|
|
28
|
+
lines.append(_clip(text))
|
|
29
|
+
return lines
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def format_ask(out: dict) -> str:
|
|
33
|
+
if not out.get("ok"):
|
|
34
|
+
return (out.get("reason") or "Nie znalazlem w bazie.") + "\n"
|
|
35
|
+
lines: list[str] = []
|
|
36
|
+
mode = out.get("mode")
|
|
37
|
+
if mode == "dual_cite":
|
|
38
|
+
if out.get("ruling"):
|
|
39
|
+
lines.append(out["ruling"])
|
|
40
|
+
lines.append("")
|
|
41
|
+
lines.extend(_hit_lines(out.get("tj") or {}))
|
|
42
|
+
lines.append("")
|
|
43
|
+
nowela = out.get("nowela") or {}
|
|
44
|
+
if nowela.get("ok"):
|
|
45
|
+
lines.append("Nowela DU/2025/896 (wejście 1.01.2026):")
|
|
46
|
+
lines.extend(_hit_lines(nowela))
|
|
47
|
+
lines.append("")
|
|
48
|
+
elif mode == "lookup":
|
|
49
|
+
lines.extend(_hit_lines(out.get("hit") or {}))
|
|
50
|
+
lines.append("")
|
|
51
|
+
elif mode == "disambiguate":
|
|
52
|
+
lines.append(out.get("note") or "Kilka ustaw. Sprecyzuj.")
|
|
53
|
+
for c in out.get("candidates") or []:
|
|
54
|
+
lines.append(f"- {c.get('eli')} art. {c.get('art')} ({c.get('domain')})")
|
|
55
|
+
lines.append("")
|
|
56
|
+
else:
|
|
57
|
+
hits = out.get("hits") or []
|
|
58
|
+
if not hits:
|
|
59
|
+
return "Nie znalazlem w bazie.\n"
|
|
60
|
+
for h in hits[:3]:
|
|
61
|
+
lines.extend(_hit_lines(h))
|
|
62
|
+
lines.append("")
|
|
63
|
+
kis = out.get("kis")
|
|
64
|
+
if kis:
|
|
65
|
+
lines.append(f"KIS {kis.get('sygnatura') or kis.get('id')}: {kis.get('teza') or ''}")
|
|
66
|
+
if kis.get("url"):
|
|
67
|
+
lines.append(kis["url"])
|
|
68
|
+
if kis.get("uwaga"):
|
|
69
|
+
lines.append(kis["uwaga"])
|
|
70
|
+
elif mode != "disambiguate":
|
|
71
|
+
lines.append("KIS: nie znalazłem w bazie interpretacji dla tego artykułu.")
|
|
72
|
+
return "\n".join(lines).strip() + "\n"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> int:
|
|
76
|
+
p = argparse.ArgumentParser(prog="eurekiusz")
|
|
77
|
+
p.add_argument("--version", action="version", version=f"eurekiusz {__version__}")
|
|
78
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
79
|
+
|
|
80
|
+
pa = sub.add_parser("ask", help="pytanie → ustawa + nowela + KIS")
|
|
81
|
+
pa.add_argument("question")
|
|
82
|
+
pa.add_argument("--json", action="store_true")
|
|
83
|
+
|
|
84
|
+
pl = sub.add_parser("lookup")
|
|
85
|
+
pl.add_argument("eli")
|
|
86
|
+
pl.add_argument("art")
|
|
87
|
+
|
|
88
|
+
ps = sub.add_parser("search")
|
|
89
|
+
ps.add_argument("query")
|
|
90
|
+
ps.add_argument("--domain")
|
|
91
|
+
ps.add_argument("--eli")
|
|
92
|
+
ps.add_argument("-k", type=int, default=5)
|
|
93
|
+
|
|
94
|
+
sub.add_parser("health")
|
|
95
|
+
sub.add_parser("gui", help="okno desktopowe")
|
|
96
|
+
|
|
97
|
+
args = p.parse_args()
|
|
98
|
+
if args.cmd == "gui":
|
|
99
|
+
from eurekiusz.gui import run_gui
|
|
100
|
+
|
|
101
|
+
run_gui()
|
|
102
|
+
return 0
|
|
103
|
+
if args.cmd == "lookup":
|
|
104
|
+
out = lookup(args.eli, args.art)
|
|
105
|
+
elif args.cmd == "search":
|
|
106
|
+
out = search(args.query, domain=args.domain, eli=args.eli, k=args.k)
|
|
107
|
+
elif args.cmd == "health":
|
|
108
|
+
out = health()
|
|
109
|
+
else:
|
|
110
|
+
out = ask(args.question)
|
|
111
|
+
if not getattr(args, "json", False) and args.cmd == "ask":
|
|
112
|
+
sys.stdout.write(format_ask(out))
|
|
113
|
+
return 0 if out.get("ok") else 1
|
|
114
|
+
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
|
|
115
|
+
sys.stdout.write("\n")
|
|
116
|
+
return 0 if out.get("ok") else 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
raise SystemExit(main())
|
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""EUREKA (KIS) layer 3. Never overrides statute. Empty is success."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
EUREKA_SEARCH = (
|
|
11
|
+
"https://eureka.mf.gov.pl/api/public/v1/wyszukiwarka/informacje/"
|
|
12
|
+
"?size=12&page=0&sort=parametryPozycjonowania%2Casc"
|
|
13
|
+
)
|
|
14
|
+
UA = "Mozilla/5.0 (compatible; Eurekiusz/1.0; +https://qa10.io)"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _przepisy_blob(pr) -> str:
|
|
18
|
+
if isinstance(pr, list):
|
|
19
|
+
return " ".join(str(x) for x in pr)
|
|
20
|
+
return str(pr or "")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _status_ok(st) -> bool:
|
|
24
|
+
s = st[0] if isinstance(st, list) else st
|
|
25
|
+
return bool(s) and "aktualna" in str(s).lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def art_in_przepisy(pr, art: str) -> bool:
|
|
29
|
+
"""EUREKA encodes 'art. 113-ust. 1'. Substring '113' is a false friend."""
|
|
30
|
+
blob = _przepisy_blob(pr)
|
|
31
|
+
art = art.strip()
|
|
32
|
+
return bool(re.search(rf"art\.\s*{re.escape(art)}-", blob, re.I))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def search_kis(nouns: str, art: str | None = None, timeout: float = 8.0) -> dict | None:
|
|
36
|
+
"""Return at most one aktualna interpretacja, or None.
|
|
37
|
+
|
|
38
|
+
Requires art in PRZEPISY when art is given. Requires at least one
|
|
39
|
+
query token (len>=4) in TEZA. Otherwise skip — ranking is garbage.
|
|
40
|
+
"""
|
|
41
|
+
q = " ".join((nouns or "").split()[:8]).strip()
|
|
42
|
+
if len(q) < 4:
|
|
43
|
+
return None
|
|
44
|
+
body = json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"searchQuery": q,
|
|
47
|
+
"searchInContent": True,
|
|
48
|
+
"searchInSynonyms": True,
|
|
49
|
+
"searchInFullPhrase": False,
|
|
50
|
+
"filter": {},
|
|
51
|
+
"columns": [
|
|
52
|
+
"ID_INFORMACJI",
|
|
53
|
+
"SYG",
|
|
54
|
+
"DT_WYD",
|
|
55
|
+
"TEZA",
|
|
56
|
+
"STATUS_INFORMACJI",
|
|
57
|
+
"PRZEPISY",
|
|
58
|
+
"KATEGORIA_INFORMACJI",
|
|
59
|
+
],
|
|
60
|
+
"warunkiDodatkowe": [],
|
|
61
|
+
}
|
|
62
|
+
).encode()
|
|
63
|
+
req = urllib.request.Request(
|
|
64
|
+
EUREKA_SEARCH,
|
|
65
|
+
data=body,
|
|
66
|
+
headers={
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"Accept": "application/json",
|
|
69
|
+
"User-Agent": UA,
|
|
70
|
+
},
|
|
71
|
+
method="POST",
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
75
|
+
data = json.loads(r.read().decode())
|
|
76
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
|
|
77
|
+
return None
|
|
78
|
+
tokens = [t.lower() for t in re.findall(r"[a-ząćęłńóśźż]{4,}", q, re.I)]
|
|
79
|
+
for item in data.get("results") or []:
|
|
80
|
+
if not _status_ok(item.get("STATUS_INFORMACJI")):
|
|
81
|
+
continue
|
|
82
|
+
if art and not art_in_przepisy(item.get("PRZEPISY"), art):
|
|
83
|
+
continue
|
|
84
|
+
teza = (item.get("TEZA") or "").lower()
|
|
85
|
+
if tokens and not any(t in teza for t in tokens):
|
|
86
|
+
continue
|
|
87
|
+
iid = str(item.get("ID_INFORMACJI") or "")
|
|
88
|
+
return {
|
|
89
|
+
"id": iid,
|
|
90
|
+
"sygnatura": item.get("SYG"),
|
|
91
|
+
"data": item.get("DT_WYD"),
|
|
92
|
+
"teza": (item.get("TEZA") or "")[:400],
|
|
93
|
+
"przepisy": item.get("PRZEPISY") or [],
|
|
94
|
+
"url": f"https://eureka.mf.gov.pl/informacje/podglad/{iid}" if iid else None,
|
|
95
|
+
"uwaga": "Interpretacja KIS nie zmienia ustawy. To wykładnia organu.",
|
|
96
|
+
}
|
|
97
|
+
return None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Desktop window. stdlib tkinter. No LLM. Empty RAG = nie znalazłem."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
from eurekiusz.cli import format_ask
|
|
6
|
+
from eurekiusz.law import ask, health
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_gui() -> None:
|
|
10
|
+
root = tk.Tk()
|
|
11
|
+
root.title("Eurekiusz")
|
|
12
|
+
root.minsize(720, 480)
|
|
13
|
+
root.configure(bg="#1a1412")
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
h = health()
|
|
17
|
+
status = (
|
|
18
|
+
f"akty {h.get('acts')} · artykuły {h.get('articles')} · "
|
|
19
|
+
f"113={'200k' if h.get('vat_113_200k') else 'BŁĄD'}"
|
|
20
|
+
)
|
|
21
|
+
except SystemExit as e:
|
|
22
|
+
status = str(e)
|
|
23
|
+
|
|
24
|
+
title = tk.Label(
|
|
25
|
+
root,
|
|
26
|
+
text="Eurekiusz",
|
|
27
|
+
fg="#E8DDD4",
|
|
28
|
+
bg="#1a1412",
|
|
29
|
+
font=("Segoe UI", 18, "bold"),
|
|
30
|
+
)
|
|
31
|
+
title.pack(anchor="w", padx=16, pady=8)
|
|
32
|
+
|
|
33
|
+
hint = tk.Label(
|
|
34
|
+
root,
|
|
35
|
+
text="Pytanie do ustawy. Model nie zgaduje kwot. Pusty kontekst = nie znalazłem.",
|
|
36
|
+
fg="#93A292",
|
|
37
|
+
bg="#1a1412",
|
|
38
|
+
font=("Segoe UI", 10),
|
|
39
|
+
)
|
|
40
|
+
hint.pack(anchor="w", padx=16)
|
|
41
|
+
|
|
42
|
+
q = tk.StringVar()
|
|
43
|
+
row = tk.Frame(root, bg="#1a1412")
|
|
44
|
+
row.pack(fill="x", padx=16, pady=12)
|
|
45
|
+
entry = tk.Entry(
|
|
46
|
+
row,
|
|
47
|
+
textvariable=q,
|
|
48
|
+
font=("Segoe UI", 12),
|
|
49
|
+
bg="#2a201c",
|
|
50
|
+
fg="#E8DDD4",
|
|
51
|
+
insertbackground="#E8DDD4",
|
|
52
|
+
relief="flat",
|
|
53
|
+
)
|
|
54
|
+
entry.pack(side="left", fill="x", expand=True, ipady=8)
|
|
55
|
+
|
|
56
|
+
body = tk.Text(
|
|
57
|
+
root,
|
|
58
|
+
wrap="word",
|
|
59
|
+
bg="#120e0c",
|
|
60
|
+
fg="#E8DDD4",
|
|
61
|
+
insertbackground="#E8DDD4",
|
|
62
|
+
relief="flat",
|
|
63
|
+
padx=12,
|
|
64
|
+
pady=12,
|
|
65
|
+
font=("Consolas", 11),
|
|
66
|
+
)
|
|
67
|
+
body.pack(fill="both", expand=True, padx=16, pady=(0, 8))
|
|
68
|
+
body.insert("1.0", status + "\n")
|
|
69
|
+
body.configure(state="disabled")
|
|
70
|
+
|
|
71
|
+
foot = tk.Label(
|
|
72
|
+
root,
|
|
73
|
+
text=status,
|
|
74
|
+
fg="#93A292",
|
|
75
|
+
bg="#1a1412",
|
|
76
|
+
font=("Segoe UI", 9),
|
|
77
|
+
)
|
|
78
|
+
foot.pack(anchor="w", padx=16, pady=(0, 12))
|
|
79
|
+
|
|
80
|
+
def run_ask(_event=None) -> None:
|
|
81
|
+
question = q.get().strip()
|
|
82
|
+
if not question:
|
|
83
|
+
return
|
|
84
|
+
body.configure(state="normal")
|
|
85
|
+
body.delete("1.0", "end")
|
|
86
|
+
body.insert("1.0", "Szukam…\n")
|
|
87
|
+
body.update_idletasks()
|
|
88
|
+
try:
|
|
89
|
+
out = ask(question)
|
|
90
|
+
text = format_ask(out)
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
text = f"Błąd: {exc}\n"
|
|
93
|
+
body.delete("1.0", "end")
|
|
94
|
+
body.insert("1.0", text)
|
|
95
|
+
body.configure(state="disabled")
|
|
96
|
+
|
|
97
|
+
btn = tk.Button(
|
|
98
|
+
row,
|
|
99
|
+
text="Pytaj",
|
|
100
|
+
command=run_ask,
|
|
101
|
+
bg="#39703D",
|
|
102
|
+
fg="#E8DDD4",
|
|
103
|
+
activebackground="#75B974",
|
|
104
|
+
relief="flat",
|
|
105
|
+
font=("Segoe UI", 11, "bold"),
|
|
106
|
+
padx=16,
|
|
107
|
+
pady=6,
|
|
108
|
+
)
|
|
109
|
+
btn.pack(side="left", padx=(8, 0))
|
|
110
|
+
entry.bind("<Return>", run_ask)
|
|
111
|
+
entry.focus_set()
|
|
112
|
+
root.mainloop()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main() -> None:
|
|
116
|
+
run_gui()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Swiss-watch query layer over rag/chunks.db. JSON to stdout.
|
|
3
|
+
|
|
4
|
+
python3 indexer/law.py lookup DU/2025/775 113
|
|
5
|
+
python3 indexer/law.py search "zwolnienie podmiotowe" --domain vat
|
|
6
|
+
python3 indexer/law.py ask "jaki jest limit zwolnienia VAT 2026"
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sqlite3
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from eurekiusz.paths import find_db
|
|
18
|
+
|
|
19
|
+
DB = find_db()
|
|
20
|
+
|
|
21
|
+
STOP = {
|
|
22
|
+
"jaki", "jaka", "jakie", "jest", "czy", "dla", "oraz", "lub", "oraz",
|
|
23
|
+
"the", "and", "art", "ust", "pkt", "tej", "tym", "nie", "tak",
|
|
24
|
+
"przez", "oraz", "się", "od", "do", "na", "w", "z", "o", "u", "i", "a",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
CORE_TJ = {
|
|
28
|
+
"vat": "DU/2025/775",
|
|
29
|
+
"pit": "DU/2026/592",
|
|
30
|
+
"cit": "DU/2026/554",
|
|
31
|
+
"ordynacja": "DU/2026/622",
|
|
32
|
+
"ryczalt": "DU/2025/843",
|
|
33
|
+
"zus": "DU/2026/199",
|
|
34
|
+
"nip": "DU/2026/151",
|
|
35
|
+
"akcyza": "DU/2026/412",
|
|
36
|
+
"rolny": "DU/2025/1344",
|
|
37
|
+
"pcc": "DU/2026/191",
|
|
38
|
+
"kks": "DU/2025/633",
|
|
39
|
+
"spadki": "DU/2026/478",
|
|
40
|
+
"lokalne": "DU/2025/707",
|
|
41
|
+
"zdrowotna": "DU/2025/1461",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
DOMAIN_RX = [
|
|
45
|
+
("vat", re.compile(r"\bvat\b|towarów i usług|ksef|faktury ustrukturyzowan|zwolnieni\w+ podmiotow", re.I)),
|
|
46
|
+
("pit", re.compile(r"\bpit\b|osób fizycznych|skala podatk", re.I)),
|
|
47
|
+
("cit", re.compile(r"\bcit\b|osób prawnych", re.I)),
|
|
48
|
+
("ryczalt", re.compile(r"ryczałt|zryczałtowan", re.I)),
|
|
49
|
+
("ordynacja", re.compile(r"ordynacj|przedawnien|zobowiązan\w+ podatkow", re.I)),
|
|
50
|
+
("zus", re.compile(r"\bzus\b|składk|ubezpieczeń społeczn", re.I)),
|
|
51
|
+
("akcyza", re.compile(r"akcyz", re.I)),
|
|
52
|
+
("nip", re.compile(r"\bnip\b|ewidencj\w+ podatnik", re.I)),
|
|
53
|
+
("pcc", re.compile(r"\bpcc\b|czynności cywilnoprawn", re.I)),
|
|
54
|
+
("kks", re.compile(r"\bkks\b|karny skarbowy", re.I)),
|
|
55
|
+
("spadki", re.compile(r"spadk|darowizn", re.I)),
|
|
56
|
+
("lokalne", re.compile(r"nieruchomości|opłatach lokalnych", re.I)),
|
|
57
|
+
("zdrowotna", re.compile(r"składk\w+ zdrowotn|opieki zdrowotnej", re.I)),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def conn() -> sqlite3.Connection:
|
|
62
|
+
if not DB.exists():
|
|
63
|
+
raise SystemExit(f"missing DB {DB}")
|
|
64
|
+
c = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
|
65
|
+
c.row_factory = sqlite3.Row
|
|
66
|
+
return c
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def amendments_for(c: sqlite3.Connection, eli: str, art: str | None = None) -> list[dict]:
|
|
70
|
+
q = "SELECT * FROM amendments WHERE target_eli=?"
|
|
71
|
+
args: list = [eli]
|
|
72
|
+
if art:
|
|
73
|
+
q += " AND (target_art=? OR target_art IS NULL OR target_art='')"
|
|
74
|
+
args.append(art)
|
|
75
|
+
rows = c.execute(q, args).fetchall()
|
|
76
|
+
return [dict(r) for r in rows]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def lookup(eli: str, art: str) -> dict:
|
|
80
|
+
c = conn()
|
|
81
|
+
art = art.lstrip("0") if art else art
|
|
82
|
+
row = c.execute(
|
|
83
|
+
"SELECT a.eli, a.art, a.text, a.char_len, t.title, t.year, t.pos, t.domain, t.source, t.is_tj, t.notes "
|
|
84
|
+
"FROM articles a JOIN acts t ON t.eli=a.eli WHERE a.eli=? AND a.art=?",
|
|
85
|
+
(eli, art),
|
|
86
|
+
).fetchone()
|
|
87
|
+
if not row:
|
|
88
|
+
return {"ok": False, "eli": eli, "art": art, "error": "not found"}
|
|
89
|
+
d = dict(row)
|
|
90
|
+
d["ok"] = True
|
|
91
|
+
d["amendments"] = amendments_for(c, eli, art)
|
|
92
|
+
return d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def fts_query(question: str) -> str:
|
|
96
|
+
words = re.findall(r"[0-9A-Za-zÀ-ÿąćęłńóśźżĄĆĘŁŃÓŚŹŻ]{3,}", question, re.I)
|
|
97
|
+
terms = []
|
|
98
|
+
for w in words:
|
|
99
|
+
lw = w.lower()
|
|
100
|
+
if lw in STOP:
|
|
101
|
+
continue
|
|
102
|
+
if re.fullmatch(r"\d+[a-z]*", lw):
|
|
103
|
+
continue
|
|
104
|
+
terms.append(lw)
|
|
105
|
+
# unique preserve order
|
|
106
|
+
seen = set()
|
|
107
|
+
out = []
|
|
108
|
+
for t in terms:
|
|
109
|
+
if t not in seen:
|
|
110
|
+
seen.add(t)
|
|
111
|
+
out.append(t)
|
|
112
|
+
if not out:
|
|
113
|
+
return ""
|
|
114
|
+
return " AND ".join(out[:8])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def route_domain(question: str) -> str | None:
|
|
118
|
+
for name, rx in DOMAIN_RX:
|
|
119
|
+
if rx.search(question):
|
|
120
|
+
return name
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def search(question: str, domain: str | None = None, eli: str | None = None, k: int = 5) -> dict:
|
|
125
|
+
c = conn()
|
|
126
|
+
if not eli and domain and domain in CORE_TJ:
|
|
127
|
+
eli = CORE_TJ[domain]
|
|
128
|
+
match = fts_query(question)
|
|
129
|
+
if not match:
|
|
130
|
+
return {"ok": False, "error": "empty query", "question": question}
|
|
131
|
+
sql = (
|
|
132
|
+
"SELECT a.eli, a.art, a.char_len, t.title, t.domain, t.is_tj, "
|
|
133
|
+
"snippet(articles_fts, 2, '[', ']', '…', 24) AS snip, "
|
|
134
|
+
"bm25(articles_fts) AS rank "
|
|
135
|
+
"FROM articles_fts "
|
|
136
|
+
"JOIN articles a ON a.id = articles_fts.rowid "
|
|
137
|
+
"JOIN acts t ON t.eli = a.eli "
|
|
138
|
+
"WHERE articles_fts MATCH ? AND t.searchable=1 AND a.char_len>=80"
|
|
139
|
+
)
|
|
140
|
+
args: list = [match]
|
|
141
|
+
if eli:
|
|
142
|
+
sql += " AND a.eli=?"
|
|
143
|
+
args.append(eli)
|
|
144
|
+
elif domain:
|
|
145
|
+
sql += " AND t.domain=?"
|
|
146
|
+
args.append(domain)
|
|
147
|
+
sql += " ORDER BY t.is_tj DESC, rank LIMIT ?"
|
|
148
|
+
args.append(k)
|
|
149
|
+
rows = [dict(r) for r in c.execute(sql, args).fetchall()]
|
|
150
|
+
used = match
|
|
151
|
+
if not rows:
|
|
152
|
+
parts = match.split(" AND ")
|
|
153
|
+
while len(parts) > 1 and not rows:
|
|
154
|
+
parts = parts[:-1]
|
|
155
|
+
used = " AND ".join(parts)
|
|
156
|
+
args2 = [used] + args[1:]
|
|
157
|
+
rows = [dict(r) for r in c.execute(sql, args2).fetchall()]
|
|
158
|
+
return {
|
|
159
|
+
"ok": True,
|
|
160
|
+
"match": used,
|
|
161
|
+
"eli": eli,
|
|
162
|
+
"domain": domain,
|
|
163
|
+
"hits": rows,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def ask(question: str) -> dict:
|
|
168
|
+
domain = route_domain(question)
|
|
169
|
+
# Hard gate: VAT limit 2026
|
|
170
|
+
if re.search(r"limit|zwolnieni", question, re.I) and (domain == "vat" or re.search(r"vat|113", question, re.I)):
|
|
171
|
+
tj = lookup("DU/2025/775", "113")
|
|
172
|
+
nowela = lookup("DU/2025/896", "1")
|
|
173
|
+
return attach_kis(
|
|
174
|
+
{
|
|
175
|
+
"ok": True,
|
|
176
|
+
"mode": "dual_cite",
|
|
177
|
+
"question": question,
|
|
178
|
+
"domain": "vat",
|
|
179
|
+
"ruling": (
|
|
180
|
+
"W TJ DU/2025/775 art. 113 ust. 1 jest 200 000 zł. "
|
|
181
|
+
"Od 1.01.2026 nowela DU/2025/896 podnosi limit do 240 000 zł. "
|
|
182
|
+
"Nie wolno cytować 240 000 przy samej sygnaturze 775."
|
|
183
|
+
),
|
|
184
|
+
"tj": tj,
|
|
185
|
+
"nowela": nowela,
|
|
186
|
+
},
|
|
187
|
+
question,
|
|
188
|
+
"113",
|
|
189
|
+
)
|
|
190
|
+
# explicit art. N
|
|
191
|
+
m = re.search(r"art\.?\s*(\d+[a-z]*)", question, re.I)
|
|
192
|
+
if m and domain and domain in CORE_TJ:
|
|
193
|
+
hit = lookup(CORE_TJ[domain], m.group(1))
|
|
194
|
+
if hit.get("ok"):
|
|
195
|
+
return attach_kis(
|
|
196
|
+
{"ok": True, "mode": "lookup", "question": question, "domain": domain, "hit": hit},
|
|
197
|
+
question,
|
|
198
|
+
m.group(1),
|
|
199
|
+
)
|
|
200
|
+
if m and not domain:
|
|
201
|
+
c = conn()
|
|
202
|
+
art = m.group(1)
|
|
203
|
+
rows = c.execute(
|
|
204
|
+
"SELECT a.eli, a.art, t.domain, t.is_tj, substr(a.text,1,240) AS head "
|
|
205
|
+
"FROM articles a JOIN acts t ON t.eli=a.eli "
|
|
206
|
+
"WHERE a.art=? AND t.searchable=1 AND t.is_tj=1 AND a.char_len>=80 "
|
|
207
|
+
"ORDER BY t.domain",
|
|
208
|
+
(art,),
|
|
209
|
+
).fetchall()
|
|
210
|
+
hits = [dict(r) for r in rows]
|
|
211
|
+
if len(hits) == 1:
|
|
212
|
+
full = lookup(hits[0]["eli"], art)
|
|
213
|
+
return attach_kis(
|
|
214
|
+
{"ok": True, "mode": "lookup", "question": question, "domain": hits[0]["domain"], "hit": full},
|
|
215
|
+
question,
|
|
216
|
+
art,
|
|
217
|
+
)
|
|
218
|
+
if len(hits) > 1:
|
|
219
|
+
return {
|
|
220
|
+
"ok": True,
|
|
221
|
+
"mode": "disambiguate",
|
|
222
|
+
"question": question,
|
|
223
|
+
"art": art,
|
|
224
|
+
"note": "Ten numer artykułu jest w kilku ustawach. Podaj VAT/PIT/CIT/Ordynacja.",
|
|
225
|
+
"candidates": hits,
|
|
226
|
+
}
|
|
227
|
+
s = search(question, domain=domain, k=5)
|
|
228
|
+
s["question"] = question
|
|
229
|
+
s["mode"] = "search"
|
|
230
|
+
art = None
|
|
231
|
+
if s.get("hits"):
|
|
232
|
+
art = s["hits"][0].get("art")
|
|
233
|
+
return attach_kis(s, question, art)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def attach_kis(out: dict, question: str, art: str | None) -> dict:
|
|
237
|
+
try:
|
|
238
|
+
from eurekiusz.eureka import search_kis
|
|
239
|
+
except ImportError:
|
|
240
|
+
from indexer.eureka import search_kis # type: ignore
|
|
241
|
+
nouns = " ".join(re.findall(r"[A-Za-ząćęłńóśźżĄĆĘŁŃÓŚŹŻ]{4,}", question)[:6])
|
|
242
|
+
out["kis"] = search_kis(nouns, art=art)
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def health() -> dict:
|
|
247
|
+
c = conn()
|
|
248
|
+
empty = [r[0] for r in c.execute(
|
|
249
|
+
"SELECT a.eli FROM acts a LEFT JOIN articles j ON j.eli=a.eli GROUP BY a.eli HAVING count(j.id)=0"
|
|
250
|
+
)]
|
|
251
|
+
gold = ["13", "15", "86", "86a", "106ga", "108a", "113"]
|
|
252
|
+
have = {r[0] for r in c.execute("SELECT art FROM articles WHERE eli='DU/2025/775'")}
|
|
253
|
+
row113 = c.execute(
|
|
254
|
+
"SELECT instr(text,'200 000'), instr(text,'240 000'), char_len FROM articles WHERE eli='DU/2025/775' AND art='113'"
|
|
255
|
+
).fetchone()
|
|
256
|
+
amd = c.execute("SELECT replaces_to, entry_into_force FROM amendments WHERE eli='DU/2025/896'").fetchone()
|
|
257
|
+
cores = {}
|
|
258
|
+
for name, eli in CORE_TJ.items():
|
|
259
|
+
n = c.execute("SELECT count(*) FROM articles WHERE eli=?", (eli,)).fetchone()[0]
|
|
260
|
+
cores[name] = {"eli": eli, "articles": n, "ok": n > 0}
|
|
261
|
+
return {
|
|
262
|
+
"ok": True,
|
|
263
|
+
"acts": c.execute("SELECT count(*) FROM acts").fetchone()[0],
|
|
264
|
+
"articles": c.execute("SELECT count(*) FROM articles").fetchone()[0],
|
|
265
|
+
"amendments": c.execute("SELECT count(*) FROM amendments").fetchone()[0],
|
|
266
|
+
"empty_acts": empty,
|
|
267
|
+
"gold_775_missing": [a for a in gold if a not in have],
|
|
268
|
+
"vat_113_200k": bool(row113 and row113[0] > 0),
|
|
269
|
+
"vat_113_no_240k": bool(row113 and row113[1] == 0),
|
|
270
|
+
"vat_113_len": row113[2] if row113 else None,
|
|
271
|
+
"amd_896": dict(zip(("to", "from_date"), amd)) if amd else None,
|
|
272
|
+
"searchable": c.execute("SELECT count(*) FROM acts WHERE searchable=1").fetchone()[0],
|
|
273
|
+
"cores": cores,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main() -> int:
|
|
278
|
+
p = argparse.ArgumentParser()
|
|
279
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
280
|
+
pl = sub.add_parser("lookup")
|
|
281
|
+
pl.add_argument("eli")
|
|
282
|
+
pl.add_argument("art")
|
|
283
|
+
ps = sub.add_parser("search")
|
|
284
|
+
ps.add_argument("query")
|
|
285
|
+
ps.add_argument("--domain")
|
|
286
|
+
ps.add_argument("--eli")
|
|
287
|
+
ps.add_argument("-k", type=int, default=5)
|
|
288
|
+
pa = sub.add_parser("ask")
|
|
289
|
+
pa.add_argument("question")
|
|
290
|
+
sub.add_parser("health")
|
|
291
|
+
args = p.parse_args()
|
|
292
|
+
if args.cmd == "lookup":
|
|
293
|
+
out = lookup(args.eli, args.art)
|
|
294
|
+
elif args.cmd == "search":
|
|
295
|
+
out = search(args.query, domain=args.domain, eli=args.eli, k=args.k)
|
|
296
|
+
elif args.cmd == "health":
|
|
297
|
+
out = health()
|
|
298
|
+
else:
|
|
299
|
+
out = ask(args.question)
|
|
300
|
+
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
|
|
301
|
+
sys.stdout.write("\n")
|
|
302
|
+
return 0 if out.get("ok") else 1
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""DB discovery for pip install (CWD is not the repo)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_db() -> Path:
|
|
9
|
+
env = os.environ.get("EUREKIUSZ_DB")
|
|
10
|
+
if env:
|
|
11
|
+
p = Path(env).expanduser()
|
|
12
|
+
if p.is_file():
|
|
13
|
+
return p
|
|
14
|
+
raise SystemExit(f"EUREKIUSZ_DB nie wskazuje na plik: {p}")
|
|
15
|
+
|
|
16
|
+
home = Path.home() / ".eurekiusz" / "chunks.db"
|
|
17
|
+
if home.is_file():
|
|
18
|
+
return home
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from importlib.resources import files
|
|
22
|
+
|
|
23
|
+
packaged = files("eurekiusz").joinpath("data/chunks.db")
|
|
24
|
+
if packaged.is_file():
|
|
25
|
+
return Path(str(packaged))
|
|
26
|
+
except (ModuleNotFoundError, TypeError, OSError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
here = Path(__file__).resolve()
|
|
30
|
+
for cand in (
|
|
31
|
+
here.parents[2] / "rag" / "chunks.db",
|
|
32
|
+
Path("/root/eurekiusz-go/rag/chunks.db"),
|
|
33
|
+
):
|
|
34
|
+
if cand.is_file():
|
|
35
|
+
return cand
|
|
36
|
+
|
|
37
|
+
raise SystemExit(
|
|
38
|
+
"Brak chunks.db. Ustaw EUREKIUSZ_DB albo skopiuj bazę do ~/.eurekiusz/chunks.db"
|
|
39
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eurekiusz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lokalny asystent ustaw podatkowych: ustawa → nowela → KIS. Bez zgadywania kwot.
|
|
5
|
+
Author-email: QA10 <hello@qa10.io>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://qa10.io
|
|
8
|
+
Keywords: vat,podatki,eli,kis,eureka
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
12
|
+
Classifier: Intended Audience :: Legal Industry
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Eurekiusz
|
|
18
|
+
|
|
19
|
+
Lokalny asystent ustaw podatkowych. Trzy warstwy: **ustawa → nowela → KIS**.
|
|
20
|
+
Model nie zgaduje kwot. Pusty kontekst = `Nie znalazlem w bazie.`
|
|
21
|
+
|
|
22
|
+
## Instalacja
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pip install eurekiusz
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Windows (po `winget install Python.Python.3.12` i **nowym** PowerShellu):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
python -m pip install eurekiusz
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Użycie
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
eurekiusz ask "jaki jest limit zwolnienia VAT 2026"
|
|
38
|
+
eurekiusz lookup DU/2025/775 113
|
|
39
|
+
eurekiusz health
|
|
40
|
+
eurekiusz gui
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Albo skrót okna: `eurekiusz-gui`.
|
|
44
|
+
|
|
45
|
+
Bramka: art. 113 TJ `DU/2025/775` = **200 000 zł**. Limit 240 000 zł to nowela `DU/2025/896` od 1.01.2026. Sama kwota 240k przy cytacie 775 = błąd.
|
|
46
|
+
|
|
47
|
+
## Baza
|
|
48
|
+
|
|
49
|
+
Wheel zawiera `chunks.db` (~27 MB). Nadpisanie:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
set EUREKIUSZ_DB=C:\path\chunks.db
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
albo plik `~/.eurekiusz/chunks.db`.
|
|
56
|
+
|
|
57
|
+
## Czego tu nie ma
|
|
58
|
+
|
|
59
|
+
- modelu LLM / GGUF (osobny download na desktop Wails)
|
|
60
|
+
- Qdrant, 8 botów, chmury
|
|
61
|
+
- treści z bazy Optima `prawo_rachunkowe`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/eurekiusz/__init__.py
|
|
4
|
+
src/eurekiusz/cli.py
|
|
5
|
+
src/eurekiusz/eureka.py
|
|
6
|
+
src/eurekiusz/gui.py
|
|
7
|
+
src/eurekiusz/law.py
|
|
8
|
+
src/eurekiusz/paths.py
|
|
9
|
+
src/eurekiusz.egg-info/PKG-INFO
|
|
10
|
+
src/eurekiusz.egg-info/SOURCES.txt
|
|
11
|
+
src/eurekiusz.egg-info/dependency_links.txt
|
|
12
|
+
src/eurekiusz.egg-info/entry_points.txt
|
|
13
|
+
src/eurekiusz.egg-info/top_level.txt
|
|
14
|
+
src/eurekiusz/data/chunks.db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eurekiusz
|