xproof 0.1.0__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.
- xproof/__init__.py +44 -0
- xproof/client.py +384 -0
- xproof/exceptions.py +54 -0
- xproof/integrations/__init__.py +13 -0
- xproof/integrations/crewai.py +225 -0
- xproof/integrations/langchain.py +248 -0
- xproof/models.py +173 -0
- xproof/py.typed +0 -0
- xproof/utils.py +43 -0
- xproof-0.1.0.dist-info/METADATA +228 -0
- xproof-0.1.0.dist-info/RECORD +14 -0
- xproof-0.1.0.dist-info/WHEEL +5 -0
- xproof-0.1.0.dist-info/licenses/LICENSE +21 -0
- xproof-0.1.0.dist-info/top_level.txt +1 -0
xproof/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""xProof Python SDK — blockchain proof-of-existence on MultiversX."""
|
|
2
|
+
|
|
3
|
+
from .client import XProofClient
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
ConflictError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServerError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
XProofError,
|
|
12
|
+
)
|
|
13
|
+
from .models import (
|
|
14
|
+
BatchResult,
|
|
15
|
+
BatchResultSummary,
|
|
16
|
+
Certification,
|
|
17
|
+
PricingInfo,
|
|
18
|
+
PricingTier,
|
|
19
|
+
RegistrationResult,
|
|
20
|
+
TrialInfo,
|
|
21
|
+
)
|
|
22
|
+
from .utils import hash_bytes, hash_file
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"XProofClient",
|
|
28
|
+
"hash_file",
|
|
29
|
+
"hash_bytes",
|
|
30
|
+
"Certification",
|
|
31
|
+
"BatchResult",
|
|
32
|
+
"BatchResultSummary",
|
|
33
|
+
"PricingInfo",
|
|
34
|
+
"PricingTier",
|
|
35
|
+
"RegistrationResult",
|
|
36
|
+
"TrialInfo",
|
|
37
|
+
"XProofError",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"ConflictError",
|
|
40
|
+
"NotFoundError",
|
|
41
|
+
"RateLimitError",
|
|
42
|
+
"ServerError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
]
|
xproof/client.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Main client for the xProof API."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
ConflictError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
XProofError,
|
|
16
|
+
)
|
|
17
|
+
from .models import BatchResult, Certification, PricingInfo, RegistrationResult
|
|
18
|
+
from .utils import hash_file
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://xproof.app"
|
|
23
|
+
DEFAULT_TIMEOUT = 30
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class XProofClient:
|
|
27
|
+
"""Client for interacting with the xProof API.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Your xProof API key (starts with ``pm_``).
|
|
31
|
+
Pass an empty string or omit if you plan to call
|
|
32
|
+
:meth:`register` first.
|
|
33
|
+
base_url: Override the API base URL (default: ``https://xproof.app``).
|
|
34
|
+
timeout: Request timeout in seconds (default: 30).
|
|
35
|
+
|
|
36
|
+
Example::
|
|
37
|
+
|
|
38
|
+
from xproof import XProofClient
|
|
39
|
+
|
|
40
|
+
client = XProofClient(api_key="pm_...")
|
|
41
|
+
cert = client.certify("report.pdf", author="Alice")
|
|
42
|
+
print(cert.transaction_url)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
api_key: str = "",
|
|
48
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
49
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.api_key = api_key
|
|
52
|
+
self.base_url = base_url.rstrip("/")
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
|
|
55
|
+
self._session = requests.Session()
|
|
56
|
+
self._session.headers.update(
|
|
57
|
+
{
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"User-Agent": f"xproof-python/{__version__}",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
if api_key:
|
|
63
|
+
self._session.headers["Authorization"] = f"Bearer {api_key}"
|
|
64
|
+
|
|
65
|
+
def _request(
|
|
66
|
+
self,
|
|
67
|
+
method: str,
|
|
68
|
+
path: str,
|
|
69
|
+
json: Optional[Dict[str, Any]] = None,
|
|
70
|
+
params: Optional[Dict[str, Any]] = None,
|
|
71
|
+
auth_required: bool = True,
|
|
72
|
+
) -> Dict[str, Any]:
|
|
73
|
+
"""Send an HTTP request to the xProof API and return parsed JSON."""
|
|
74
|
+
url = f"{self.base_url}{path}"
|
|
75
|
+
|
|
76
|
+
headers: Optional[Dict[str, str]] = None
|
|
77
|
+
if not auth_required:
|
|
78
|
+
headers = {"Authorization": ""}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
resp = self._session.request(
|
|
82
|
+
method,
|
|
83
|
+
url,
|
|
84
|
+
json=json,
|
|
85
|
+
params=params,
|
|
86
|
+
timeout=self.timeout,
|
|
87
|
+
headers=headers,
|
|
88
|
+
)
|
|
89
|
+
except requests.RequestException as exc:
|
|
90
|
+
raise XProofError(f"Request failed: {exc}") from exc
|
|
91
|
+
|
|
92
|
+
if resp.status_code in (200, 201):
|
|
93
|
+
try:
|
|
94
|
+
return resp.json() # type: ignore[no-any-return]
|
|
95
|
+
except ValueError as exc:
|
|
96
|
+
raise XProofError(
|
|
97
|
+
f"Unexpected non-JSON response from {method} {url}: "
|
|
98
|
+
f"{resp.text[:200]}"
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
self._handle_error(resp)
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _handle_error(resp: requests.Response) -> None:
|
|
106
|
+
"""Raise an appropriate exception based on the HTTP response."""
|
|
107
|
+
try:
|
|
108
|
+
body = resp.json()
|
|
109
|
+
except ValueError:
|
|
110
|
+
body = {"message": resp.text}
|
|
111
|
+
|
|
112
|
+
message = body.get("message", body.get("error", resp.text))
|
|
113
|
+
status = resp.status_code
|
|
114
|
+
|
|
115
|
+
if status == 400:
|
|
116
|
+
raise ValidationError(message, response=body)
|
|
117
|
+
if status in (401, 403):
|
|
118
|
+
raise AuthenticationError(message, response=body)
|
|
119
|
+
if status == 404:
|
|
120
|
+
raise NotFoundError(message, response=body)
|
|
121
|
+
if status == 409:
|
|
122
|
+
raise ConflictError(
|
|
123
|
+
message,
|
|
124
|
+
certification_id=body.get("certificationId", ""),
|
|
125
|
+
response=body,
|
|
126
|
+
)
|
|
127
|
+
if status == 429:
|
|
128
|
+
raise RateLimitError(message, response=body)
|
|
129
|
+
if status >= 500:
|
|
130
|
+
raise ServerError(message, status_code=status, response=body)
|
|
131
|
+
|
|
132
|
+
raise XProofError(message, status_code=status, response=body)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def register(
|
|
136
|
+
cls,
|
|
137
|
+
agent_name: str,
|
|
138
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
139
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
140
|
+
) -> "XProofClient":
|
|
141
|
+
"""Register a new agent and return a configured client.
|
|
142
|
+
|
|
143
|
+
This is the zero-friction entry point: no wallet, no payment, no
|
|
144
|
+
browser. The returned client is already authenticated with the
|
|
145
|
+
trial API key and ready to certify.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
agent_name: A human-readable name for the agent.
|
|
149
|
+
base_url: Override the API base URL.
|
|
150
|
+
timeout: Request timeout in seconds.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A new :class:`XProofClient` already configured with the
|
|
154
|
+
trial API key. Access registration details via
|
|
155
|
+
``client.registration``.
|
|
156
|
+
|
|
157
|
+
Example::
|
|
158
|
+
|
|
159
|
+
client = XProofClient.register("my-research-agent")
|
|
160
|
+
print(client.registration.trial.remaining) # 10
|
|
161
|
+
cert = client.certify_hash(...)
|
|
162
|
+
"""
|
|
163
|
+
temp = cls(api_key="", base_url=base_url, timeout=timeout)
|
|
164
|
+
data = temp._request(
|
|
165
|
+
"POST",
|
|
166
|
+
"/api/agent/register",
|
|
167
|
+
json={"agent_name": agent_name},
|
|
168
|
+
auth_required=False,
|
|
169
|
+
)
|
|
170
|
+
result = RegistrationResult.from_dict(data)
|
|
171
|
+
new_client = cls(api_key=result.api_key, base_url=base_url, timeout=timeout)
|
|
172
|
+
new_client.registration = result
|
|
173
|
+
return new_client
|
|
174
|
+
|
|
175
|
+
registration: Optional[RegistrationResult] = None
|
|
176
|
+
|
|
177
|
+
def certify(
|
|
178
|
+
self,
|
|
179
|
+
path: Union[str, Path],
|
|
180
|
+
author: str,
|
|
181
|
+
file_name: Optional[str] = None,
|
|
182
|
+
*,
|
|
183
|
+
who: Optional[str] = None,
|
|
184
|
+
what: Optional[str] = None,
|
|
185
|
+
when: Optional[str] = None,
|
|
186
|
+
why: Optional[str] = None,
|
|
187
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
188
|
+
) -> Certification:
|
|
189
|
+
"""Certify a file by path. The file is hashed locally (SHA-256).
|
|
190
|
+
|
|
191
|
+
Supports the xProof 4W framework via optional keyword arguments.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
path: Path to the file to certify.
|
|
195
|
+
author: Author / owner name for the certification.
|
|
196
|
+
file_name: Override the file name recorded on-chain
|
|
197
|
+
(defaults to the basename of *path*).
|
|
198
|
+
who: 4W — agent identity.
|
|
199
|
+
what: 4W — action hash or description.
|
|
200
|
+
when: 4W — ISO-8601 timestamp.
|
|
201
|
+
why: 4W — instruction or reason.
|
|
202
|
+
metadata: Arbitrary key-value metadata stored alongside the proof.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
A :class:`Certification` with the on-chain proof details.
|
|
206
|
+
"""
|
|
207
|
+
if not self.api_key:
|
|
208
|
+
raise ValueError("api_key is required — call register() or pass an api_key")
|
|
209
|
+
|
|
210
|
+
p = Path(path)
|
|
211
|
+
file_hash = hash_file(p)
|
|
212
|
+
resolved_name = file_name or p.name
|
|
213
|
+
|
|
214
|
+
return self.certify_hash(
|
|
215
|
+
file_hash=file_hash,
|
|
216
|
+
file_name=resolved_name,
|
|
217
|
+
author=author,
|
|
218
|
+
who=who,
|
|
219
|
+
what=what,
|
|
220
|
+
when=when,
|
|
221
|
+
why=why,
|
|
222
|
+
metadata=metadata,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def certify_hash(
|
|
226
|
+
self,
|
|
227
|
+
file_hash: str,
|
|
228
|
+
file_name: str,
|
|
229
|
+
author: str,
|
|
230
|
+
*,
|
|
231
|
+
who: Optional[str] = None,
|
|
232
|
+
what: Optional[str] = None,
|
|
233
|
+
when: Optional[str] = None,
|
|
234
|
+
why: Optional[str] = None,
|
|
235
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
236
|
+
) -> Certification:
|
|
237
|
+
"""Certify a file using a pre-computed SHA-256 hash.
|
|
238
|
+
|
|
239
|
+
Supports the xProof 4W framework via optional keyword arguments:
|
|
240
|
+
|
|
241
|
+
- **who**: Agent identity (wallet, name, or ID)
|
|
242
|
+
- **what**: Action hash or description being certified
|
|
243
|
+
- **when**: ISO-8601 timestamp of the action
|
|
244
|
+
- **why**: Instruction hash, goal, or reason for the action
|
|
245
|
+
|
|
246
|
+
Any additional key-value pairs can be passed via *metadata*.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
file_hash: 64-character lowercase hex SHA-256 digest.
|
|
250
|
+
file_name: Original file name.
|
|
251
|
+
author: Author / owner name.
|
|
252
|
+
who: 4W — agent identity.
|
|
253
|
+
what: 4W — action hash or description.
|
|
254
|
+
when: 4W — ISO-8601 timestamp.
|
|
255
|
+
why: 4W — instruction or reason.
|
|
256
|
+
metadata: Arbitrary key-value metadata stored alongside the proof.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
A :class:`Certification` with the on-chain proof details.
|
|
260
|
+
"""
|
|
261
|
+
if not self.api_key:
|
|
262
|
+
raise ValueError("api_key is required — call register() or pass an api_key")
|
|
263
|
+
|
|
264
|
+
proof_metadata: Dict[str, Any] = dict(metadata) if metadata else {}
|
|
265
|
+
if who is not None:
|
|
266
|
+
proof_metadata["who"] = who
|
|
267
|
+
if what is not None:
|
|
268
|
+
proof_metadata["what"] = what
|
|
269
|
+
if when is not None:
|
|
270
|
+
proof_metadata["when"] = when
|
|
271
|
+
if why is not None:
|
|
272
|
+
proof_metadata["why"] = why
|
|
273
|
+
|
|
274
|
+
payload: Dict[str, Any] = {
|
|
275
|
+
"filename": file_name,
|
|
276
|
+
"file_hash": file_hash,
|
|
277
|
+
"author_name": author,
|
|
278
|
+
}
|
|
279
|
+
if proof_metadata:
|
|
280
|
+
payload["metadata"] = proof_metadata
|
|
281
|
+
data = self._request("POST", "/api/proof", json=payload)
|
|
282
|
+
return Certification.from_dict(data)
|
|
283
|
+
|
|
284
|
+
def batch_certify(
|
|
285
|
+
self,
|
|
286
|
+
files: List[Dict[str, Any]],
|
|
287
|
+
) -> BatchResult:
|
|
288
|
+
"""Certify multiple files in a single API call (up to 50).
|
|
289
|
+
|
|
290
|
+
Each entry in *files* must contain:
|
|
291
|
+
|
|
292
|
+
- ``path`` (str): Path to the file **or**
|
|
293
|
+
- ``file_hash`` (str): Pre-computed SHA-256 hex hash
|
|
294
|
+
- ``file_name`` (str): File name (required when using ``file_hash``)
|
|
295
|
+
- ``author`` (str): Author name (applied to the entire batch)
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
files: List of file descriptors.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A :class:`BatchResult` with individual results and a summary.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
ValueError: If more than 50 files are provided.
|
|
305
|
+
"""
|
|
306
|
+
if not self.api_key:
|
|
307
|
+
raise ValueError("api_key is required — call register() or pass an api_key")
|
|
308
|
+
|
|
309
|
+
if len(files) > 50:
|
|
310
|
+
raise ValueError("Batch certification supports a maximum of 50 files")
|
|
311
|
+
|
|
312
|
+
entries: List[Dict[str, Any]] = []
|
|
313
|
+
for f in files:
|
|
314
|
+
if "path" in f:
|
|
315
|
+
p = Path(f["path"])
|
|
316
|
+
fhash = hash_file(p)
|
|
317
|
+
fname = f.get("file_name", p.name)
|
|
318
|
+
elif "file_hash" in f:
|
|
319
|
+
fhash = f["file_hash"]
|
|
320
|
+
fname = f.get("file_name", "unknown")
|
|
321
|
+
else:
|
|
322
|
+
raise ValueError("Each file entry must contain either 'path' or 'file_hash'")
|
|
323
|
+
|
|
324
|
+
entry: Dict[str, Any] = {
|
|
325
|
+
"filename": fname,
|
|
326
|
+
"file_hash": fhash,
|
|
327
|
+
}
|
|
328
|
+
if f.get("metadata"):
|
|
329
|
+
entry["metadata"] = f["metadata"]
|
|
330
|
+
entries.append(entry)
|
|
331
|
+
|
|
332
|
+
payload: Dict[str, Any] = {"files": entries}
|
|
333
|
+
author = None
|
|
334
|
+
for f in files:
|
|
335
|
+
if f.get("author"):
|
|
336
|
+
author = f["author"]
|
|
337
|
+
break
|
|
338
|
+
if author:
|
|
339
|
+
payload["author_name"] = author
|
|
340
|
+
|
|
341
|
+
data = self._request("POST", "/api/batch", json=payload)
|
|
342
|
+
return BatchResult.from_dict(data)
|
|
343
|
+
|
|
344
|
+
def verify(self, proof_id: str) -> Certification:
|
|
345
|
+
"""Retrieve a certification by its proof ID.
|
|
346
|
+
|
|
347
|
+
This is a public endpoint and does not require authentication.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
proof_id: The certification / proof UUID.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
A :class:`Certification` with the proof details.
|
|
354
|
+
"""
|
|
355
|
+
data = self._request("GET", f"/api/proof/{proof_id}", auth_required=False)
|
|
356
|
+
return Certification.from_dict(data)
|
|
357
|
+
|
|
358
|
+
def verify_hash(self, file_hash: str) -> Certification:
|
|
359
|
+
"""Look up a certification by its SHA-256 file hash.
|
|
360
|
+
|
|
361
|
+
This is a public endpoint and does not require authentication.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
file_hash: The 64-character hex SHA-256 digest.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
A :class:`Certification` with the proof details.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
NotFoundError: If no certification exists for this hash.
|
|
371
|
+
"""
|
|
372
|
+
data = self._request("GET", f"/api/proof/hash/{file_hash}", auth_required=False)
|
|
373
|
+
return Certification.from_dict(data)
|
|
374
|
+
|
|
375
|
+
def get_pricing(self) -> PricingInfo:
|
|
376
|
+
"""Retrieve current pricing information.
|
|
377
|
+
|
|
378
|
+
This is a public endpoint and does not require authentication.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
A :class:`PricingInfo` with tier details and payment methods.
|
|
382
|
+
"""
|
|
383
|
+
data = self._request("GET", "/api/pricing", auth_required=False)
|
|
384
|
+
return PricingInfo.from_dict(data)
|
xproof/exceptions.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Exception classes for the xProof SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class XProofError(Exception):
|
|
5
|
+
"""Base exception for all xProof SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int = 0, response: object = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.response = response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(XProofError):
|
|
15
|
+
"""Raised when API key authentication fails (401/403)."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str = "Invalid or missing API key", response: object = None) -> None:
|
|
18
|
+
super().__init__(message, status_code=401, response=response)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotFoundError(XProofError):
|
|
22
|
+
"""Raised when a requested resource is not found (404)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str = "Resource not found", response: object = None) -> None:
|
|
25
|
+
super().__init__(message, status_code=404, response=response)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationError(XProofError):
|
|
29
|
+
"""Raised when the request body fails server-side validation (400)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str = "Invalid request data", response: object = None) -> None:
|
|
32
|
+
super().__init__(message, status_code=400, response=response)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConflictError(XProofError):
|
|
36
|
+
"""Raised when the file hash has already been certified (409)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str = "File already certified", certification_id: str = "", response: object = None) -> None:
|
|
39
|
+
super().__init__(message, status_code=409, response=response)
|
|
40
|
+
self.certification_id = certification_id
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RateLimitError(XProofError):
|
|
44
|
+
"""Raised when rate limits are exceeded (429)."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str = "Rate limit exceeded", response: object = None) -> None:
|
|
47
|
+
super().__init__(message, status_code=429, response=response)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ServerError(XProofError):
|
|
51
|
+
"""Raised when the server returns a 5xx error."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str = "Internal server error", status_code: int = 500, response: object = None) -> None:
|
|
54
|
+
super().__init__(message, status_code=status_code, response=response)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from .langchain import XProofCallbackHandler
|
|
3
|
+
except ImportError:
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from .crewai import XProofCertifyTool, XProofTool, XProofCrewCallback
|
|
8
|
+
try:
|
|
9
|
+
from .crewai import XProofCrewTool
|
|
10
|
+
except ImportError:
|
|
11
|
+
pass
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|