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.
Files changed (27) hide show
  1. {trooth-0.3.0 → trooth-0.4.0}/PKG-INFO +4 -1
  2. {trooth-0.3.0 → trooth-0.4.0}/README.md +3 -0
  3. {trooth-0.3.0 → trooth-0.4.0}/pyproject.toml +1 -1
  4. {trooth-0.3.0 → trooth-0.4.0}/trooth/__init__.py +1 -1
  5. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/PKG-INFO +4 -1
  6. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/__init__.py +1 -1
  7. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/source.py +59 -1
  8. {trooth-0.3.0 → trooth-0.4.0}/LICENSE +0 -0
  9. {trooth-0.3.0 → trooth-0.4.0}/setup.cfg +0 -0
  10. {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/__init__.py +0 -0
  11. {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/langchain.py +0 -0
  12. {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/llama_index.py +0 -0
  13. {trooth-0.3.0 → trooth-0.4.0}/trooth/integrations/openai_agents.py +0 -0
  14. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/SOURCES.txt +0 -0
  15. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/dependency_links.txt +0 -0
  16. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/entry_points.txt +0 -0
  17. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/requires.txt +0 -0
  18. {trooth-0.3.0 → trooth-0.4.0}/trooth.egg-info/top_level.txt +0 -0
  19. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/__main__.py +0 -0
  20. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/answer.py +0 -0
  21. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/canonical.py +0 -0
  22. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/cli.py +0 -0
  23. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/client.py +0 -0
  24. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/httpsig.py +0 -0
  25. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/jws.py +0 -0
  26. {trooth-0.3.0 → trooth-0.4.0}/trooth_semanto_verify/merkle.py +0 -0
  27. {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.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.3.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.3.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.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.
@@ -91,4 +91,4 @@ __all__ = [
91
91
  "version_id",
92
92
  ]
93
93
 
94
- __version__ = "0.3.0"
94
+ __version__ = "0.4.0"
@@ -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