trooth 0.2.0__tar.gz → 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.
- {trooth-0.2.0 → trooth-0.4.0}/PKG-INFO +7 -1
- {trooth-0.2.0 → trooth-0.4.0}/README.md +6 -0
- {trooth-0.2.0 → trooth-0.4.0}/pyproject.toml +1 -1
- {trooth-0.2.0 → trooth-0.4.0}/trooth/__init__.py +1 -1
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/PKG-INFO +7 -1
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/__init__.py +1 -1
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/answer.py +17 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/source.py +63 -1
- {trooth-0.2.0 → trooth-0.4.0}/LICENSE +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/setup.cfg +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth/integrations/__init__.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth/integrations/langchain.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth/integrations/llama_index.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth/integrations/openai_agents.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/SOURCES.txt +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/dependency_links.txt +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/entry_points.txt +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/requires.txt +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth.egg-info/top_level.txt +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/__main__.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/canonical.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/cli.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/client.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/httpsig.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/jws.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/merkle.py +0 -0
- {trooth-0.2.0 → trooth-0.4.0}/trooth_semanto_verify/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trooth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Read a company's Semanto signed copy and check it: signatures, keys, DNS binding, page proofs and the public log.
|
|
5
5
|
Author: Trooth, LLC
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -94,6 +94,12 @@ from trooth import SemantoSource
|
|
|
94
94
|
acme = SemantoSource("acme.com", on_event=lambda e: print(e["event"], e["detail"]))
|
|
95
95
|
a = acme.answer(question="How much is the Starter plan?")
|
|
96
96
|
print(a["status"], a["text"])
|
|
97
|
+
|
|
98
|
+
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
99
|
+
show(r["text"])
|
|
100
|
+
|
|
101
|
+
r = acme.respond(question=user_question, draft=model_text) # follows the company's answer mode
|
|
102
|
+
show(r["text"]) # Audit mode also returns r["audit"]: supported, differs, marked, not_in_record or not_checked
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
It loads `/.well-known/trooth.semanto` and the company's keys from the company's site, checks the file once for each new body, reads the site again at most every 15 minutes, and answers fact IDs and plain questions in process with the same matcher as Trooth's service (`trooth_semanto_verify.answer`, compared case by case with `verifier/test-vectors/answer-v1`). When Trooth's API cannot be reached it keeps answering from the last version that checked out and logs `trooth_api_unreachable`; a new version that does not check out is refused and the last good one stays in use. A fact marked changed since signing or expired returns its mark and no value.
|
|
@@ -64,6 +64,12 @@ from trooth import SemantoSource
|
|
|
64
64
|
acme = SemantoSource("acme.com", on_event=lambda e: print(e["event"], e["detail"]))
|
|
65
65
|
a = acme.answer(question="How much is the Starter plan?")
|
|
66
66
|
print(a["status"], a["text"])
|
|
67
|
+
|
|
68
|
+
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
69
|
+
show(r["text"])
|
|
70
|
+
|
|
71
|
+
r = acme.respond(question=user_question, draft=model_text) # follows the company's answer mode
|
|
72
|
+
show(r["text"]) # Audit mode also returns r["audit"]: supported, differs, marked, not_in_record or not_checked
|
|
67
73
|
```
|
|
68
74
|
|
|
69
75
|
It loads `/.well-known/trooth.semanto` and the company's keys from the company's site, checks the file once for each new body, reads the site again at most every 15 minutes, and answers fact IDs and plain questions in process with the same matcher as Trooth's service (`trooth_semanto_verify.answer`, compared case by case with `verifier/test-vectors/answer-v1`). When Trooth's API cannot be reached it keeps answering from the last version that checked out and logs `trooth_api_unreachable`; a new version that does not check out is refused and the last good one stays in use. A fact marked changed since signing or expired returns its mark and no value.
|
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "trooth"
|
|
15
|
-
version = "0.
|
|
15
|
+
version = "0.4.0"
|
|
16
16
|
description = "Read a company's Semanto signed copy and check it: signatures, keys, DNS binding, page proofs and the public log."
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
requires-python = ">=3.11"
|
|
@@ -14,5 +14,5 @@ kits). Framework integrations are optional extras: trooth[langchain], trooth[lla
|
|
|
14
14
|
from trooth_semanto_verify.client import SemantoClient
|
|
15
15
|
from trooth_semanto_verify.source import SemantoSource
|
|
16
16
|
|
|
17
|
-
__version__ = "0.
|
|
17
|
+
__version__ = "0.4.0"
|
|
18
18
|
__all__ = ["SemantoClient", "SemantoSource", "__version__"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trooth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Read a company's Semanto signed copy and check it: signatures, keys, DNS binding, page proofs and the public log.
|
|
5
5
|
Author: Trooth, LLC
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -94,6 +94,12 @@ from trooth import SemantoSource
|
|
|
94
94
|
acme = SemantoSource("acme.com", on_event=lambda e: print(e["event"], e["detail"]))
|
|
95
95
|
a = acme.answer(question="How much is the Starter plan?")
|
|
96
96
|
print(a["status"], a["text"])
|
|
97
|
+
|
|
98
|
+
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
99
|
+
show(r["text"])
|
|
100
|
+
|
|
101
|
+
r = acme.respond(question=user_question, draft=model_text) # follows the company's answer mode
|
|
102
|
+
show(r["text"]) # Audit mode also returns r["audit"]: supported, differs, marked, not_in_record or not_checked
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
It loads `/.well-known/trooth.semanto` and the company's keys from the company's site, checks the file once for each new body, reads the site again at most every 15 minutes, and answers fact IDs and plain questions in process with the same matcher as Trooth's service (`trooth_semanto_verify.answer`, compared case by case with `verifier/test-vectors/answer-v1`). When Trooth's API cannot be reached it keeps answering from the last version that checked out and logs `trooth_api_unreachable`; a new version that does not check out is refused and the last good one stays in use. A fact marked changed since signing or expired returns its mark and no value.
|
|
@@ -170,3 +170,20 @@ def answer(file: dict, company: dict, fact_id: Optional[str] = None, question: O
|
|
|
170
170
|
lines = [MARK_WORDS[mark]] if mark else [f["statement"], *([f"Conditions: {'; '.join(f['conditions'])}."] if f.get("conditions") else [])]
|
|
171
171
|
lines.append(f"Check: {check}")
|
|
172
172
|
return {**base, "status": "answer", "matched_by": by, "facts": [_as_answered(f, mark)], "text": "\n".join(lines[:3])}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def assemble_reply(a: dict, draft: Optional[str] = None) -> dict:
|
|
176
|
+
"""What the customer is shown, built by code from an answer; a model's own wording is never part of it.
|
|
177
|
+
|
|
178
|
+
For a value answer the first line is the signed sentence exactly as signed, then its conditions if any, then the day
|
|
179
|
+
it was signed and the check link. A marked fact shows its mark instead of a value. Candidates, the not-published
|
|
180
|
+
reply and not_read show the answer's own fixed line. `draft` (what a model wrote) is accepted and discarded:
|
|
181
|
+
`discarded` says whether one was given."""
|
|
182
|
+
discarded = isinstance(draft, str) and len(draft) > 0
|
|
183
|
+
signed_on = str(a["signed_at"])[:10] if a.get("signed_at") else None
|
|
184
|
+
f = a["facts"][0] if a["status"] == "answer" else None
|
|
185
|
+
if not f:
|
|
186
|
+
return {"status": a["status"], "fact_id": None, "statement": None, "signed_on": signed_on, "check": a.get("check"), "text": a["text"], "discarded": discarded}
|
|
187
|
+
tail = f"{f'Signed on {signed_on}. ' if signed_on else ''}Check: {a['check']}"
|
|
188
|
+
lines = [MARK_WORDS[f["mark"]], tail] if f["mark"] else [f["statement"], *([f"Conditions: {'; '.join(f['conditions'])}."] if f["conditions"] else []), tail]
|
|
189
|
+
return {"status": a["status"], "fact_id": f["fact_id"], "statement": f["statement"], "signed_on": signed_on, "check": a["check"], "text": "\n".join(lines), "discarded": discarded}
|
|
@@ -22,18 +22,21 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
24
|
import threading
|
|
25
|
+
import unicodedata
|
|
25
26
|
import time
|
|
26
27
|
import urllib.parse
|
|
27
28
|
from datetime import datetime, timezone
|
|
28
29
|
from typing import Any, Callable, Dict, List, Optional
|
|
29
30
|
|
|
30
31
|
from .answer import answer as _answer
|
|
31
|
-
from .answer import fact_marks
|
|
32
|
+
from .answer import MARK_WORDS, assemble_reply, fact_marks
|
|
32
33
|
from .client import Fetch, urllib_fetch
|
|
33
34
|
from .verify import verify_semanto
|
|
34
35
|
|
|
35
36
|
TROOTH_API = "https://api.trooth.co"
|
|
36
37
|
MAX_REFRESH_SECONDS = 900
|
|
38
|
+
# The answer modes a company chooses on trooth.co.
|
|
39
|
+
ANSWER_MODES = ("checked", "source_only", "audit")
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
class SemantoSource:
|
|
@@ -58,6 +61,9 @@ class SemantoSource:
|
|
|
58
61
|
self.checks = 0
|
|
59
62
|
self._body: Optional[str] = None
|
|
60
63
|
self._lock = threading.Lock()
|
|
64
|
+
self._mode: Optional[dict] = None
|
|
65
|
+
self._mode_read_at: Optional[float] = None
|
|
66
|
+
self._mode_fresh = False
|
|
61
67
|
|
|
62
68
|
def _log(self, event: str, detail: str) -> None:
|
|
63
69
|
e = {"at": datetime.fromtimestamp(self.now(), timezone.utc).isoformat().replace("+00:00", "Z"), "event": event, "detail": detail}
|
|
@@ -152,3 +158,59 @@ class SemantoSource:
|
|
|
152
158
|
return {"object": "semanto.answer", "status": "not_read", "matched_by": None, "company": {"trooth_id": None, "name": None, "domain": self.domain},
|
|
153
159
|
"version": None, "facts": [], "text": f"No signed record of {self.domain} could be read and checked yet."}
|
|
154
160
|
return _answer(self.file, self.company, fact_id=fact_id, question=question, marks=self.marks(), check_base=f"{self.api_base}/public/semanto")
|
|
161
|
+
|
|
162
|
+
def reply(self, fact_id: Optional[str] = None, question: Optional[str] = None, draft: Optional[str] = None) -> dict:
|
|
163
|
+
"""The reply a chatbot shows for a question: built by code from the signed record, with the model's draft discarded."""
|
|
164
|
+
return assemble_reply(self.answer(fact_id=fact_id, question=question), draft)
|
|
165
|
+
|
|
166
|
+
def answer_mode(self) -> dict:
|
|
167
|
+
"""The company's answer mode, read from Trooth and kept between reads. When it has never been read (Trooth's API
|
|
168
|
+
unreachable from the start), Checked is used: the strictest mode, so nothing unchecked is shown."""
|
|
169
|
+
self.ensure_fresh()
|
|
170
|
+
if self.file and (self._mode_read_at is None or self.now() - self._mode_read_at >= self.refresh_seconds):
|
|
171
|
+
tid = urllib.parse.quote(self.file["header"]["trooth_id"], safe="")
|
|
172
|
+
try:
|
|
173
|
+
m = self._json(f"{self.api_base}/public/semanto/{tid}/answer-mode")
|
|
174
|
+
if m.get("mode") in ANSWER_MODES:
|
|
175
|
+
self._mode = {"mode": m["mode"], "generated_sentences": m.get("generated_sentences") is True}
|
|
176
|
+
self._mode_fresh = True
|
|
177
|
+
except Exception as e: # noqa: BLE001
|
|
178
|
+
self._mode_fresh = False
|
|
179
|
+
self._log("trooth_api_unreachable", f"The answer mode could not be read ({e}); {self._mode['mode'] + ' stays in force' if self._mode else 'Checked is used until it can be read'}.")
|
|
180
|
+
self._mode_read_at = self.now()
|
|
181
|
+
if self._mode:
|
|
182
|
+
return {**self._mode, "source": "trooth" if self._mode_fresh else "last_read"}
|
|
183
|
+
return {"mode": "checked", "generated_sentences": False, "source": "fail_closed"}
|
|
184
|
+
|
|
185
|
+
def respond(self, question: Optional[str] = None, draft: str = "", fact_id: Optional[str] = None, mode: Optional[str] = None) -> dict:
|
|
186
|
+
"""The reply a chatbot sends, in the company's answer mode (or `mode` when given). checked: built by code from the
|
|
187
|
+
signed record, the draft discarded. source_only: the draft as written, nothing checked. audit: the draft as
|
|
188
|
+
written, checked right after; the result is in "audit" and in an "audit" event."""
|
|
189
|
+
m = mode if mode in ANSWER_MODES else self.answer_mode()["mode"]
|
|
190
|
+
if m == "checked":
|
|
191
|
+
return {"mode": m, **self.reply(fact_id=fact_id, question=question, draft=draft)}
|
|
192
|
+
if m == "source_only":
|
|
193
|
+
return {"mode": m, "status": "unchecked", "text": draft}
|
|
194
|
+
result = audit_draft(self.answer(fact_id=fact_id, question=question), draft, self.file)
|
|
195
|
+
self._log("audit", f"{result['result']}: {question or fact_id or ''}".strip())
|
|
196
|
+
return {"mode": m, "status": "unchecked", "text": draft, "audit": result}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def audit_draft(a: dict, draft: str, file: Optional[dict] = None) -> dict:
|
|
200
|
+
"""Audit mode's check of a sent draft against the answer for the same question: "supported" when the draft carries the
|
|
201
|
+
signed sentence or its value, "differs" when a single signed fact answers and the draft carries neither (the signed
|
|
202
|
+
sentence comes with it), "marked" for a fact marked changed or expired, "not_in_record" when the record holds nothing
|
|
203
|
+
for it, and "not_checked" for candidates or no version."""
|
|
204
|
+
if a["status"] != "answer":
|
|
205
|
+
return {"result": "not_in_record" if a["status"] == "not_published" else "not_checked", "fact_id": None, "statement": None}
|
|
206
|
+
f = a["facts"][0]
|
|
207
|
+
if f["mark"]:
|
|
208
|
+
return {"result": "marked", "fact_id": f["fact_id"], "statement": None, "words": MARK_WORDS[f["mark"]]}
|
|
209
|
+
norm = lambda s: " ".join(unicodedata.normalize("NFKC", str(s).lower()).split()) # noqa: E731
|
|
210
|
+
d = norm(draft)
|
|
211
|
+
raw = next((x for x in (file or {}).get("facts", []) if x["id"] == f["fact_id"]), None) or {}
|
|
212
|
+
typed = f.get("typed") or raw.get("typed") or {}
|
|
213
|
+
value = typed.get("value") if typed.get("value") is not None else raw.get("value")
|
|
214
|
+
values = [str(x) for x in (value if isinstance(value, list) else [] if value is None else [value])]
|
|
215
|
+
supported = norm(f["statement"]) in d or (len(values) > 0 and all(norm(v) in d for v in values))
|
|
216
|
+
return {"result": "supported" if supported else "differs", "fact_id": f["fact_id"], "statement": f["statement"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|