trooth 0.3.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.3.0 → trooth-0.4.0}/PKG-INFO +4 -1
- {trooth-0.3.0 → trooth-0.4.0}/README.md +3 -0
- {trooth-0.3.0 → trooth-0.4.0}/pyproject.toml +1 -1
- {trooth-0.3.0 → trooth-0.4.0}/trooth/__init__.py +1 -1
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/PKG-INFO +4 -1
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/__init__.py +1 -1
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/source.py +59 -1
- {trooth-0.3.0 → trooth-0.4.0}/LICENSE +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/setup.cfg +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/__init__.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/langchain.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/llama_index.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/openai_agents.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/SOURCES.txt +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/dependency_links.txt +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/entry_points.txt +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/requires.txt +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/top_level.txt +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/__main__.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/answer.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/canonical.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/cli.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/client.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/httpsig.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/jws.py +0 -0
- {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/merkle.py +0 -0
- {trooth-0.3.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
|
|
@@ -97,6 +97,9 @@ print(a["status"], a["text"])
|
|
|
97
97
|
|
|
98
98
|
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
99
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
|
|
100
103
|
```
|
|
101
104
|
|
|
102
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.
|
|
@@ -67,6 +67,9 @@ print(a["status"], a["text"])
|
|
|
67
67
|
|
|
68
68
|
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
69
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
|
|
70
73
|
```
|
|
71
74
|
|
|
72
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
|
|
@@ -97,6 +97,9 @@ print(a["status"], a["text"])
|
|
|
97
97
|
|
|
98
98
|
r = acme.reply(question=user_question, draft=model_text) # built by code; the draft is discarded
|
|
99
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
|
|
100
103
|
```
|
|
101
104
|
|
|
102
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.
|
|
@@ -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 assemble_reply, 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}
|
|
@@ -156,3 +162,55 @@ class SemantoSource:
|
|
|
156
162
|
def reply(self, fact_id: Optional[str] = None, question: Optional[str] = None, draft: Optional[str] = None) -> dict:
|
|
157
163
|
"""The reply a chatbot shows for a question: built by code from the signed record, with the model's draft discarded."""
|
|
158
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
|
|
File without changes
|