truerecord-sdk 0.1.2__py3-none-any.whl
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.
- truerecord_sdk/__init__.py +20 -0
- truerecord_sdk/client.py +220 -0
- truerecord_sdk/encryption.py +52 -0
- truerecord_sdk/exceptions.py +27 -0
- truerecord_sdk/py.typed +0 -0
- truerecord_sdk-0.1.2.dist-info/METADATA +132 -0
- truerecord_sdk-0.1.2.dist-info/RECORD +9 -0
- truerecord_sdk-0.1.2.dist-info/WHEEL +4 -0
- truerecord_sdk-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from truerecord_sdk.client import TrueRecordClient, __version__
|
|
2
|
+
from truerecord_sdk.exceptions import (
|
|
3
|
+
APIError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
EncryptionError,
|
|
6
|
+
NetworkError,
|
|
7
|
+
TrueRecordError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"TrueRecordClient",
|
|
13
|
+
"TrueRecordError",
|
|
14
|
+
"APIError",
|
|
15
|
+
"AuthenticationError",
|
|
16
|
+
"EncryptionError",
|
|
17
|
+
"NetworkError",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
truerecord_sdk/client.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import random
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from truerecord_sdk.encryption import Encryptor, load_public_key
|
|
16
|
+
from truerecord_sdk.exceptions import (
|
|
17
|
+
APIError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
NetworkError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = version("truerecord-sdk")
|
|
25
|
+
except PackageNotFoundError: # running from source, not installed
|
|
26
|
+
__version__ = "0.0.0"
|
|
27
|
+
|
|
28
|
+
API_URL = "https://api.truerecord.eu/evaluate"
|
|
29
|
+
|
|
30
|
+
# Responses worth retrying: the API is busy, restarting, or asked us to wait.
|
|
31
|
+
RETRY_STATUSES = frozenset({408, 409, 429, 500, 502, 503, 504})
|
|
32
|
+
TIMEOUT = 15.0 # seconds per attempt
|
|
33
|
+
RETRIES = 2 # attempts after the first
|
|
34
|
+
BACKOFF_BASE = 0.5 # seconds; doubles each retry
|
|
35
|
+
BACKOFF_CAP = 10.0 # seconds
|
|
36
|
+
RETRY_AFTER_CAP = 30.0 # seconds; the longest Retry-After we honour
|
|
37
|
+
DETAIL_MAX = 200 # characters of a response body quoted in an error
|
|
38
|
+
|
|
39
|
+
Origin = Literal["user", "llm", "agent"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TrueRecordClient:
|
|
43
|
+
"""Sends encrypted LLM interaction records to TrueRecord.
|
|
44
|
+
|
|
45
|
+
client = TrueRecordClient(api_key, public_key, application_guid, interaction_guid)
|
|
46
|
+
client.send({"prompt": "hello"}, "user")
|
|
47
|
+
client.send({"text": "hi there"}, "llm")
|
|
48
|
+
|
|
49
|
+
Every method raises only subclasses of `TrueRecordError`.
|
|
50
|
+
|
|
51
|
+
Retries: a send that fails because the API is unreachable, times out, or
|
|
52
|
+
answers 408/409/429/5xx is retried twice with exponential
|
|
53
|
+
backoff and jitter (about 0.5s, then 1s, then 2s...). A `Retry-After`
|
|
54
|
+
header from the API overrides the wait, up to 30s. Other 4xx responses
|
|
55
|
+
are not retried. Redirects are not followed: anything but a 2xx is an error.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
api_key: str,
|
|
61
|
+
public_key: str,
|
|
62
|
+
application_guid: str,
|
|
63
|
+
interaction_guid: str,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Args:
|
|
67
|
+
api_key: Your TrueRecord API key. Sent as a bearer token.
|
|
68
|
+
public_key: Your TrueRecord RSA public key, as a PEM string or
|
|
69
|
+
base64-encoded DER. Messages are encrypted with it before
|
|
70
|
+
leaving your process.
|
|
71
|
+
application_guid: Identifies the application the records belong to.
|
|
72
|
+
interaction_guid: Identifies the specific interaction within the application.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValidationError: An argument is empty.
|
|
76
|
+
EncryptionError: The public key is not a valid RSA key of at least 2048 bits.
|
|
77
|
+
"""
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValidationError("api_key is required")
|
|
80
|
+
if not public_key:
|
|
81
|
+
raise ValidationError("public_key is required")
|
|
82
|
+
if not application_guid:
|
|
83
|
+
raise ValidationError("application_guid is required")
|
|
84
|
+
if not interaction_guid:
|
|
85
|
+
raise ValidationError("interaction_guid is required")
|
|
86
|
+
|
|
87
|
+
self._public_key = load_public_key(public_key)
|
|
88
|
+
self._application_guid = application_guid
|
|
89
|
+
self._interaction_guid = interaction_guid
|
|
90
|
+
self._headers = {
|
|
91
|
+
"Authorization": f"Bearer {api_key}",
|
|
92
|
+
"User-Agent": f"truerecord-sdk/{__version__}",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def send(
|
|
96
|
+
self,
|
|
97
|
+
message: Any,
|
|
98
|
+
origin: Origin,
|
|
99
|
+
message_datetime: str | None = None,
|
|
100
|
+
conversation_ref: str | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Encrypt an LLM message and send it to TrueRecord.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
message: The message content. Any JSON-serialisable value. Encrypted.
|
|
106
|
+
origin: ``"user"`` for a user message, ``"llm"`` for the LLM's
|
|
107
|
+
response, ``"agent"`` for a message from an agent.
|
|
108
|
+
message_datetime: When the message occurred, ISO 8601.
|
|
109
|
+
Defaults to now. Not encrypted.
|
|
110
|
+
conversation_ref: Optional ID linking interactions in one conversation.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The server's JSON response as a dict.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValidationError: A required argument is missing or a value is not JSON-serialisable.
|
|
117
|
+
NetworkError: The server could not be reached or timed out, after all retries.
|
|
118
|
+
AuthenticationError: The API key was rejected.
|
|
119
|
+
APIError: The server returned any other error, after all retries where applicable.
|
|
120
|
+
"""
|
|
121
|
+
body = self._prepare(message, origin, message_datetime, conversation_ref)
|
|
122
|
+
with httpx.Client(headers=self._headers, timeout=TIMEOUT) as http:
|
|
123
|
+
for attempt in range(RETRIES + 1):
|
|
124
|
+
last = attempt == RETRIES
|
|
125
|
+
try:
|
|
126
|
+
result = http.post(API_URL, json=body)
|
|
127
|
+
except httpx.HTTPError as exc:
|
|
128
|
+
if last:
|
|
129
|
+
raise NetworkError(str(exc)) from exc
|
|
130
|
+
time.sleep(_backoff(attempt, None))
|
|
131
|
+
continue
|
|
132
|
+
if result.status_code in RETRY_STATUSES and not last:
|
|
133
|
+
time.sleep(_backoff(attempt, result.headers.get("Retry-After")))
|
|
134
|
+
continue
|
|
135
|
+
return _parse(result.status_code, result.text)
|
|
136
|
+
raise AssertionError("unreachable")
|
|
137
|
+
|
|
138
|
+
async def asend(
|
|
139
|
+
self,
|
|
140
|
+
message: Any,
|
|
141
|
+
origin: Origin,
|
|
142
|
+
message_datetime: str | None = None,
|
|
143
|
+
conversation_ref: str | None = None,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""Async version of `send`. Same arguments, return value, retries and errors."""
|
|
146
|
+
body = self._prepare(message, origin, message_datetime, conversation_ref)
|
|
147
|
+
async with httpx.AsyncClient(headers=self._headers, timeout=TIMEOUT) as http:
|
|
148
|
+
for attempt in range(RETRIES + 1):
|
|
149
|
+
last = attempt == RETRIES
|
|
150
|
+
try:
|
|
151
|
+
result = await http.post(API_URL, json=body)
|
|
152
|
+
except httpx.HTTPError as exc:
|
|
153
|
+
if last:
|
|
154
|
+
raise NetworkError(str(exc)) from exc
|
|
155
|
+
await asyncio.sleep(_backoff(attempt, None))
|
|
156
|
+
continue
|
|
157
|
+
if result.status_code in RETRY_STATUSES and not last:
|
|
158
|
+
await asyncio.sleep(_backoff(attempt, result.headers.get("Retry-After")))
|
|
159
|
+
continue
|
|
160
|
+
return _parse(result.status_code, result.text)
|
|
161
|
+
raise AssertionError("unreachable")
|
|
162
|
+
|
|
163
|
+
def _prepare(
|
|
164
|
+
self,
|
|
165
|
+
message: Any,
|
|
166
|
+
origin: Origin,
|
|
167
|
+
message_datetime: str | None,
|
|
168
|
+
conversation_ref: str | None,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
if message is None:
|
|
171
|
+
raise ValidationError("message is required")
|
|
172
|
+
if origin not in ("user", "llm", "agent"):
|
|
173
|
+
raise ValidationError("origin must be 'user', 'llm' or 'agent'")
|
|
174
|
+
|
|
175
|
+
enc = Encryptor(self._public_key)
|
|
176
|
+
try:
|
|
177
|
+
encrypted = enc.encrypt(message)
|
|
178
|
+
except (TypeError, ValueError) as exc:
|
|
179
|
+
raise ValidationError(f"message is not JSON-serialisable: {exc}") from exc
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"idempotency_key": str(uuid.uuid4()),
|
|
183
|
+
"interaction_guid": self._interaction_guid,
|
|
184
|
+
"application_guid": self._application_guid,
|
|
185
|
+
"conversation_ref": conversation_ref,
|
|
186
|
+
"sent_at_datetime": datetime.now(timezone.utc).isoformat(),
|
|
187
|
+
"record": {
|
|
188
|
+
"encryption_key": enc.wrapped_key,
|
|
189
|
+
"content": encrypted,
|
|
190
|
+
"type": origin,
|
|
191
|
+
"occurred_at": message_datetime or datetime.now(timezone.utc).isoformat(),
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _backoff(attempt: int, retry_after: str | None) -> float:
|
|
197
|
+
if retry_after:
|
|
198
|
+
try:
|
|
199
|
+
seconds = float(retry_after)
|
|
200
|
+
if math.isfinite(seconds):
|
|
201
|
+
return min(max(0.0, seconds), RETRY_AFTER_CAP)
|
|
202
|
+
except ValueError:
|
|
203
|
+
pass
|
|
204
|
+
return min(BACKOFF_BASE * 2**attempt, BACKOFF_CAP) * random.uniform(1.0, 1.5)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse(status_code: int, text: str) -> dict[str, Any]:
|
|
208
|
+
if status_code in (401, 403):
|
|
209
|
+
raise AuthenticationError("API key rejected")
|
|
210
|
+
try:
|
|
211
|
+
body = json.loads(text)
|
|
212
|
+
except (ValueError, RecursionError):
|
|
213
|
+
body = None
|
|
214
|
+
# Only 2xx means the record landed. A 3xx is not followed and not a success.
|
|
215
|
+
if not 200 <= status_code < 300:
|
|
216
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
217
|
+
raise APIError(status_code, str(detail or text or "Unexpected status")[:DETAIL_MAX])
|
|
218
|
+
if not isinstance(body, dict):
|
|
219
|
+
raise APIError(status_code, f"Expected JSON object, got: {text[:DETAIL_MAX]}")
|
|
220
|
+
return body
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
10
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
11
|
+
|
|
12
|
+
from truerecord_sdk.exceptions import EncryptionError
|
|
13
|
+
|
|
14
|
+
_OAEP = padding.OAEP(
|
|
15
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
16
|
+
algorithm=hashes.SHA256(),
|
|
17
|
+
label=None,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_public_key(key_string: str) -> rsa.RSAPublicKey:
|
|
22
|
+
try:
|
|
23
|
+
if "-----BEGIN" in key_string:
|
|
24
|
+
key = serialization.load_pem_public_key(key_string.encode())
|
|
25
|
+
else:
|
|
26
|
+
key = serialization.load_der_public_key(base64.b64decode(key_string))
|
|
27
|
+
except (ValueError, TypeError) as exc:
|
|
28
|
+
raise EncryptionError(f"Invalid public key: {exc}") from exc
|
|
29
|
+
if not isinstance(key, rsa.RSAPublicKey):
|
|
30
|
+
raise EncryptionError(f"Expected an RSA public key, got {type(key).__name__}")
|
|
31
|
+
if key.key_size < 2048:
|
|
32
|
+
raise EncryptionError(f"RSA key is {key.key_size} bits, need at least 2048")
|
|
33
|
+
return key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Encryptor:
|
|
37
|
+
"""One fresh AES-256 key. `wrapped_key` is that key encrypted with the RSA public key."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, public_key: rsa.RSAPublicKey) -> None:
|
|
40
|
+
key = AESGCM.generate_key(bit_length=256)
|
|
41
|
+
self._aes = AESGCM(key)
|
|
42
|
+
self.wrapped_key = _b64(public_key.encrypt(key, _OAEP))
|
|
43
|
+
|
|
44
|
+
def encrypt(self, value: Any) -> str:
|
|
45
|
+
"""JSON-encode `value`, encrypt with AES-GCM. Returns base64(nonce + ciphertext)."""
|
|
46
|
+
nonce = os.urandom(12)
|
|
47
|
+
plaintext = json.dumps(value, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
48
|
+
return _b64(nonce + self._aes.encrypt(nonce, plaintext, None))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _b64(raw: bytes) -> str:
|
|
52
|
+
return base64.b64encode(raw).decode("ascii")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class TrueRecordError(Exception):
|
|
2
|
+
"""Base class for every error raised by this SDK."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(TrueRecordError):
|
|
6
|
+
"""A required argument was missing or invalid. No request was sent."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EncryptionError(TrueRecordError):
|
|
10
|
+
"""The public key could not be loaded or the record could not be encrypted."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetworkError(TrueRecordError):
|
|
14
|
+
"""The server could not be reached, or the request timed out."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthenticationError(TrueRecordError):
|
|
18
|
+
"""The server rejected the API key (HTTP 401 or 403)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class APIError(TrueRecordError):
|
|
22
|
+
"""The server returned an error. Check `status_code` and `detail`."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.detail = detail
|
|
27
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
truerecord_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: truerecord-sdk
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for the TrueRecord LLM Governance Platform
|
|
5
|
+
Author-email: "Exploring Data B.V." <support@truerecord.eu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: cryptography>=41.0.0
|
|
15
|
+
Requires-Dist: httpx>=0.24.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# TrueRecord SDK for Python
|
|
22
|
+
|
|
23
|
+
[TrueRecord](https://truerecord.eu) keeps a record of every interaction with an AI system in your app. This SDK sends those records. Each message is encrypted before it leaves your process, so only TrueRecord can process it. See the [TrueRecord SDK docs](https://truerecord.eu/docs/sdk) for setup and guides.
|
|
24
|
+
|
|
25
|
+
Requires Python 3.9+.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pip install truerecord-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from truerecord_sdk import TrueRecordClient, TrueRecordError
|
|
37
|
+
|
|
38
|
+
client = TrueRecordClient(
|
|
39
|
+
api_key="tr_sk_...",
|
|
40
|
+
public_key=open("truerecord_public.pem").read(),
|
|
41
|
+
application_guid="...",
|
|
42
|
+
interaction_guid="...",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Send the message your user sends to the LLM
|
|
47
|
+
client.send(user_message, "user", conversation_ref="session-42")
|
|
48
|
+
|
|
49
|
+
# Send a message from the LLM to the user
|
|
50
|
+
client.send(llm_message, "llm", conversation_ref="session-42")
|
|
51
|
+
except TrueRecordError as e:
|
|
52
|
+
print(e)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## What to send
|
|
56
|
+
|
|
57
|
+
Each call to `send` creates a record of one message, either what the user sent (`"user"`) or what the LLM sent (`"llm"`). Use the same `conversation_ref` for both so TrueRecord can pair them.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client.send(message, origin, message_datetime=None, conversation_ref=None)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Argument | Required | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `message` | Yes | The message a user sent to the AI or the AI sent to the user. Can be any value that converts to JSON (string, dict, list, etc.). |
|
|
66
|
+
| `origin` | Yes | `"user"` if this is a message sent to the LLM, `"llm"` if it's the LLM's response, `"agent"` if it comes from an agent. |
|
|
67
|
+
| `message_datetime` | No | When the message happened, as an ISO 8601 string (e.g. `"2025-01-15T10:30:00Z"`). When not set defaults to the current time. |
|
|
68
|
+
| `conversation_ref` | No | A reference you choose (like a session ID or chat ID) to link related messages together. Pass the same value for a user message and its AI response so TrueRecord can pair them. |
|
|
69
|
+
|
|
70
|
+
## Implementation details
|
|
71
|
+
|
|
72
|
+
### Non-blocking (recommended)
|
|
73
|
+
|
|
74
|
+
Sending data to TrueRecord runs in the background so the rest of your code continues without waiting for TrueRecord to respond.
|
|
75
|
+
|
|
76
|
+
#### Synchronous apps
|
|
77
|
+
|
|
78
|
+
For apps using Flask, Django or scripts and CLI tools. Use a thread pool to send in the background.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
82
|
+
|
|
83
|
+
executor = ThreadPoolExecutor(max_workers=4)
|
|
84
|
+
|
|
85
|
+
executor.submit(client.send, user_message, "user", conversation_ref="session-42")
|
|
86
|
+
executor.submit(client.send, llm_message, "llm", conversation_ref="session-42")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Asynchronous apps
|
|
90
|
+
|
|
91
|
+
For apps using FastAPI, Starlette, aiohttp or functions defined with `async`. Use `asyncio.create_task` to send in the background.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
|
|
96
|
+
asyncio.create_task(client.asend(user_message, "user", conversation_ref="session-42"))
|
|
97
|
+
asyncio.create_task(client.asend(llm_message, "llm", conversation_ref="session-42"))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Blocking
|
|
101
|
+
|
|
102
|
+
Sending data to TrueRecord waits for TrueRecord to respond before the rest of your code continues.
|
|
103
|
+
|
|
104
|
+
#### Synchronous apps
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
client.send(user_message, "user", conversation_ref="session-42")
|
|
108
|
+
client.send(llm_message, "llm", conversation_ref="session-42")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Asynchronous apps
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
await client.asend(user_message, "user", conversation_ref="session-42")
|
|
115
|
+
await client.asend(llm_message, "llm", conversation_ref="session-42")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
All errors extend `TrueRecordError`, so you can catch them in one place.
|
|
121
|
+
|
|
122
|
+
| Error | When |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `ValidationError` | A required argument is missing or the message can't be serialised to JSON. |
|
|
125
|
+
| `EncryptionError` | The public key isn't a valid RSA key. |
|
|
126
|
+
| `NetworkError` | TrueRecord couldn't be reached after retries. |
|
|
127
|
+
| `AuthenticationError` | The API key was rejected. |
|
|
128
|
+
| `APIError` | Any other server error. Has `status_code` and `detail`. |
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
truerecord_sdk/__init__.py,sha256=P-m8dT9wbsbcPHjSqgtAZbwCC3RaVFsPSwPOvT92vr4,416
|
|
2
|
+
truerecord_sdk/client.py,sha256=HxLIYOnxOV_0ofchOFfLyZYCjkkkIUoTPag4eaSn0F8,8642
|
|
3
|
+
truerecord_sdk/encryption.py,sha256=yF9W_b8onjLA5ftgog3Y3ZfxAe-CJ9Xrzuhs_MyeQzw,1854
|
|
4
|
+
truerecord_sdk/exceptions.py,sha256=ERSoDEcjFGaMIRA_pjaStc-jwnFAa87EnKu-NktW7Kc,847
|
|
5
|
+
truerecord_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
truerecord_sdk-0.1.2.dist-info/METADATA,sha256=DQcd_HrpR3XIP2xBa_Hz_ZRLCSsLL58mm4aCpBlpaDo,4530
|
|
7
|
+
truerecord_sdk-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
truerecord_sdk-0.1.2.dist-info/licenses/LICENSE,sha256=Ctuhvjdl88HD9HbJ5ukS-bPr_bDPqSoEh3uyqJCtApc,1076
|
|
9
|
+
truerecord_sdk-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Exploring Data B.V.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|