eurekiusz 0.4.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,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: eurekiusz
3
+ Version: 0.4.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
+ Provides-Extra: gui
17
+ Requires-Dist: pywebview>=5; extra == "gui"
18
+
19
+ # Eurekiusz
20
+
21
+ Lokalny asystent ustaw podatkowych. Trzy warstwy: ustawa → nowela → KIS.
22
+ Model nie zgaduje kwot. Pusty kontekst = `Nie znalazlem w bazie.`
23
+
24
+ ## Instalacja
25
+
26
+ ```
27
+ python -m pip install -U eurekiusz
28
+ eurekiusz gui
29
+ ```
30
+
31
+ Windows: `winget install Python.Python.3.12`, nowy PowerShell, potem powyższe.
32
+ Okno natywne: `python -m pip install pywebview` (WebView2). Bez tego workbench wstaje na 127.0.0.1.
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
+ Bramka: art. 113 TJ DU/2025/775 = 200 000 zł. Limit 240 000 zł to nowela DU/2025/896 od 1.01.2026.
44
+
45
+ ## Czego tu nie ma
46
+
47
+ - modelu LLM / GGUF
48
+ - `eurekiusz.exe` z tego builda (Wails tylko na Windows)
49
+ - Qdrant, 8 botów, treści Optima `prawo_rachunkowe`
@@ -0,0 +1,31 @@
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
+ python -m pip install -U eurekiusz
10
+ eurekiusz gui
11
+ ```
12
+
13
+ Windows: `winget install Python.Python.3.12`, nowy PowerShell, potem powyższe.
14
+ Okno natywne: `python -m pip install pywebview` (WebView2). Bez tego workbench wstaje na 127.0.0.1.
15
+
16
+ ## Użycie
17
+
18
+ ```
19
+ eurekiusz ask "jaki jest limit zwolnienia VAT 2026"
20
+ eurekiusz lookup DU/2025/775 113
21
+ eurekiusz health
22
+ eurekiusz gui
23
+ ```
24
+
25
+ Bramka: art. 113 TJ DU/2025/775 = 200 000 zł. Limit 240 000 zł to nowela DU/2025/896 od 1.01.2026.
26
+
27
+ ## Czego tu nie ma
28
+
29
+ - modelu LLM / GGUF
30
+ - `eurekiusz.exe` z tego builda (Wails tylko na Windows)
31
+ - Qdrant, 8 botów, treści Optima `prawo_rachunkowe`
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "eurekiusz"
7
+ version = "0.4.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.optional-dependencies]
24
+ gui = ["pywebview>=5"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://qa10.io"
28
+
29
+ [project.scripts]
30
+ eurekiusz = "eurekiusz.cli:main"
31
+
32
+ [project.gui-scripts]
33
+ eurekiusz-gui = "eurekiusz.gui:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ eurekiusz = ["data/chunks.db", "static/index.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Eurekiusz — lokalny asystent ustaw podatkowych (ustawa → nowela → KIS)."""
2
+
3
+ __version__ = "0.4.0"
@@ -0,0 +1,167 @@
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 format_ask_md(out: dict) -> str:
76
+ """Markdown for the desktop chat. Never invents amounts."""
77
+ if not out.get("ok"):
78
+ return out.get("reason") or "Nie znalazlem w bazie."
79
+ parts: list[str] = []
80
+ mode = out.get("mode")
81
+ if mode == "dual_cite":
82
+ parts.append("## Rozstrzygnięcie")
83
+ parts.append(out.get("ruling") or "")
84
+ parts.append("## Ustawa")
85
+ parts.extend(_hit_lines(out.get("tj") or {}))
86
+ nowela = out.get("nowela") or {}
87
+ if nowela.get("ok"):
88
+ parts.append("## Nowela")
89
+ parts.append("DU/2025/896, wejście **1.01.2026**.")
90
+ parts.extend(_hit_lines(nowela))
91
+ elif mode == "lookup":
92
+ parts.append("## Ustawa")
93
+ parts.extend(_hit_lines(out.get("hit") or {}))
94
+ elif mode == "disambiguate":
95
+ parts.append("## Sprecyzuj")
96
+ parts.append(out.get("note") or "Ten numer artykułu jest w kilku ustawach.")
97
+ for c in out.get("candidates") or []:
98
+ parts.append(f"- {c.get('eli')} art. {c.get('art')} ({c.get('domain')})")
99
+ return "\n\n".join(p for p in parts if p).strip()
100
+ else:
101
+ hits = out.get("hits") or []
102
+ if not hits:
103
+ return "Nie znalazlem w bazie."
104
+ parts.append("## Ustawa")
105
+ for h in hits[:3]:
106
+ parts.extend(_hit_lines(h))
107
+ parts.append("")
108
+ kis = out.get("kis")
109
+ parts.append("## KIS")
110
+ if kis:
111
+ syg = kis.get("sygnatura") or kis.get("id") or ""
112
+ parts.append(f"{syg}: {kis.get('teza') or ''}".strip())
113
+ if kis.get("url"):
114
+ parts.append(f"[Źródło EUREKA]({kis['url']})")
115
+ if kis.get("uwaga"):
116
+ parts.append(kis["uwaga"])
117
+ else:
118
+ parts.append("Nie znalazlem w bazie interpretacji dla tego artykułu.")
119
+ return "\n\n".join(p for p in parts if p is not None).strip()
120
+
121
+
122
+ def main() -> int:
123
+ p = argparse.ArgumentParser(prog="eurekiusz")
124
+ p.add_argument("--version", action="version", version=f"eurekiusz {__version__}")
125
+ sub = p.add_subparsers(dest="cmd", required=True)
126
+
127
+ pa = sub.add_parser("ask", help="pytanie → ustawa + nowela + KIS")
128
+ pa.add_argument("question")
129
+ pa.add_argument("--json", action="store_true")
130
+
131
+ pl = sub.add_parser("lookup")
132
+ pl.add_argument("eli")
133
+ pl.add_argument("art")
134
+
135
+ ps = sub.add_parser("search")
136
+ ps.add_argument("query")
137
+ ps.add_argument("--domain")
138
+ ps.add_argument("--eli")
139
+ ps.add_argument("-k", type=int, default=5)
140
+
141
+ sub.add_parser("health")
142
+ sub.add_parser("gui", help="okno desktopowe")
143
+
144
+ args = p.parse_args()
145
+ if args.cmd == "gui":
146
+ from eurekiusz.gui import run_gui
147
+
148
+ run_gui()
149
+ return 0
150
+ if args.cmd == "lookup":
151
+ out = lookup(args.eli, args.art)
152
+ elif args.cmd == "search":
153
+ out = search(args.query, domain=args.domain, eli=args.eli, k=args.k)
154
+ elif args.cmd == "health":
155
+ out = health()
156
+ else:
157
+ out = ask(args.question)
158
+ if not getattr(args, "json", False) and args.cmd == "ask":
159
+ sys.stdout.write(format_ask(out))
160
+ return 0 if out.get("ok") else 1
161
+ json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
162
+ sys.stdout.write("\n")
163
+ return 0 if out.get("ok") else 1
164
+
165
+
166
+ if __name__ == "__main__":
167
+ raise SystemExit(main())
@@ -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,44 @@
1
+ """Desktop chat window: local HTML UI + 127.0.0.1 API."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ import time
6
+ import webbrowser
7
+
8
+ from eurekiusz.server import make_server
9
+
10
+
11
+ def run_gui() -> None:
12
+ httpd = make_server("127.0.0.1", 0)
13
+ host, port = httpd.server_address[:2]
14
+ url = f"http://{host}:{port}/"
15
+ worker = threading.Thread(target=httpd.serve_forever, daemon=True)
16
+ worker.start()
17
+
18
+ opened = False
19
+ try:
20
+ import webview # type: ignore
21
+
22
+ webview.create_window("Eurekiusz", url, width=1120, height=760)
23
+ webview.start()
24
+ opened = True
25
+ except Exception:
26
+ opened = False
27
+
28
+ if not opened:
29
+ webbrowser.open(url)
30
+ print(f"Eurekiusz okno nie wstało (brak pywebview). Workbench: {url}", flush=True)
31
+ try:
32
+ while worker.is_alive():
33
+ time.sleep(0.4)
34
+ except KeyboardInterrupt:
35
+ pass
36
+ httpd.shutdown()
37
+
38
+
39
+ def main() -> None:
40
+ run_gui()
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()