langchain-chronoverify 0.1.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.
- langchain_chronoverify-0.1.0/.gitignore +11 -0
- langchain_chronoverify-0.1.0/PKG-INFO +64 -0
- langchain_chronoverify-0.1.0/README.md +48 -0
- langchain_chronoverify-0.1.0/langchain_chronoverify/__init__.py +6 -0
- langchain_chronoverify-0.1.0/langchain_chronoverify/tool.py +97 -0
- langchain_chronoverify-0.1.0/pyproject.toml +34 -0
- langchain_chronoverify-0.1.0/tests/test_tool.py +58 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-chronoverify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tool for ChronoVerify: verify when a photo was taken and its provenance (C2PA Content Credentials, EXIF, pixel forensics).
|
|
5
|
+
Project-URL: Homepage, https://chronoverify.com
|
|
6
|
+
Project-URL: Documentation, https://chronoverify.com/integrations/langchain
|
|
7
|
+
Project-URL: API reference, https://chronoverify.com/method
|
|
8
|
+
Project-URL: Source, https://github.com/beeswaxpat/chronoverify-agent-recipes
|
|
9
|
+
Author-email: ChronoVerify <support@chronoverify.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: c2pa,capture-time,content-credentials,exif,image-verification,langchain,provenance
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# langchain-chronoverify
|
|
18
|
+
|
|
19
|
+
LangChain tool for [ChronoVerify](https://chronoverify.com): verify when a photo was taken and its provenance. It reads EXIF and XMP, cryptographically validates C2PA Content Credentials against the official trust lists, and runs classical pixel forensics, returning one plain-language verdict with a confidence score.
|
|
20
|
+
|
|
21
|
+
Provenance validation, not a deepfake or AI-generation detector. Verdicts are investigative triage to support human review, not proof.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install langchain-chronoverify
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from langchain_chronoverify import ChronoVerifyTool
|
|
33
|
+
|
|
34
|
+
tool = ChronoVerifyTool() # keyless: free, rate-limited public path
|
|
35
|
+
|
|
36
|
+
result = tool.invoke({"url": "https://example.com/photo.jpg"})
|
|
37
|
+
print(result["verdict"], result["confidence"])
|
|
38
|
+
# verdict is one of: provenance_confirmed, consistent, inconclusive,
|
|
39
|
+
# metadata_anomaly, manipulation_indicated
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
With an agent:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from langchain.agents import create_agent # or your agent constructor
|
|
46
|
+
|
|
47
|
+
agent = create_agent(model, tools=[ChronoVerifyTool()])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API key (optional)
|
|
51
|
+
|
|
52
|
+
The keyless path is free and rate limited per IP. For your own quota, pass `api_key="cv_live_..."` or set `CHRONOVERIFY_API_KEY`. A free key (no card, 100 verifications per month):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -X POST https://chronoverify.com/v1/keys/free \
|
|
56
|
+
-H "Content-Type: application/json" -d '{"email": "you@example.com"}'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Links
|
|
60
|
+
|
|
61
|
+
- Machine-readable onboarding: https://chronoverify.com/v1/onboarding
|
|
62
|
+
- API docs: https://chronoverify.com/method
|
|
63
|
+
- Response JSON Schema: https://chronoverify.com/v1/verify.schema.json
|
|
64
|
+
- Pricing: https://chronoverify.com/pricing
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# langchain-chronoverify
|
|
2
|
+
|
|
3
|
+
LangChain tool for [ChronoVerify](https://chronoverify.com): verify when a photo was taken and its provenance. It reads EXIF and XMP, cryptographically validates C2PA Content Credentials against the official trust lists, and runs classical pixel forensics, returning one plain-language verdict with a confidence score.
|
|
4
|
+
|
|
5
|
+
Provenance validation, not a deepfake or AI-generation detector. Verdicts are investigative triage to support human review, not proof.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install langchain-chronoverify
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from langchain_chronoverify import ChronoVerifyTool
|
|
17
|
+
|
|
18
|
+
tool = ChronoVerifyTool() # keyless: free, rate-limited public path
|
|
19
|
+
|
|
20
|
+
result = tool.invoke({"url": "https://example.com/photo.jpg"})
|
|
21
|
+
print(result["verdict"], result["confidence"])
|
|
22
|
+
# verdict is one of: provenance_confirmed, consistent, inconclusive,
|
|
23
|
+
# metadata_anomaly, manipulation_indicated
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
With an agent:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from langchain.agents import create_agent # or your agent constructor
|
|
30
|
+
|
|
31
|
+
agent = create_agent(model, tools=[ChronoVerifyTool()])
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API key (optional)
|
|
35
|
+
|
|
36
|
+
The keyless path is free and rate limited per IP. For your own quota, pass `api_key="cv_live_..."` or set `CHRONOVERIFY_API_KEY`. A free key (no card, 100 verifications per month):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -X POST https://chronoverify.com/v1/keys/free \
|
|
40
|
+
-H "Content-Type: application/json" -d '{"email": "you@example.com"}'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Links
|
|
44
|
+
|
|
45
|
+
- Machine-readable onboarding: https://chronoverify.com/v1/onboarding
|
|
46
|
+
- API docs: https://chronoverify.com/method
|
|
47
|
+
- Response JSON Schema: https://chronoverify.com/v1/verify.schema.json
|
|
48
|
+
- Pricing: https://chronoverify.com/pricing
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""ChronoVerify tool for LangChain agents.
|
|
2
|
+
|
|
3
|
+
Calls POST https://chronoverify.com/v1/verify and returns the typed verdict
|
|
4
|
+
object. Works keyless on the free, rate-limited public path; pass an API key
|
|
5
|
+
(or set CHRONOVERIFY_API_KEY) to meter against your own quota. Machine
|
|
6
|
+
contract: https://chronoverify.com/v1/onboarding
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Optional, Type
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from langchain_core.tools import BaseTool
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
_ENDPOINT = "https://chronoverify.com/v1/verify"
|
|
19
|
+
|
|
20
|
+
_DESCRIPTION = (
|
|
21
|
+
"Verify when a photo was taken and its provenance. Reads EXIF and XMP, "
|
|
22
|
+
"cryptographically validates C2PA Content Credentials against the official "
|
|
23
|
+
"trust lists, and runs classical pixel forensics, returning ONE verdict "
|
|
24
|
+
"(provenance_confirmed, consistent, inconclusive, metadata_anomaly, or "
|
|
25
|
+
"manipulation_indicated) with a 0 to 100 confidence, capture time, device, "
|
|
26
|
+
"location, and SHA-256/512 fingerprints. Use it before trusting a "
|
|
27
|
+
"user-submitted or sourced image: insurance claims, KYC, marketplace "
|
|
28
|
+
"listings, journalism and OSINT, or EU AI Act Article 50 transparency "
|
|
29
|
+
"checks. Works on any image, signed or not. It validates provenance and is "
|
|
30
|
+
"NOT a deepfake or AI-generation detector; results are investigative triage "
|
|
31
|
+
"to support human review, not proof. Provide exactly one of url or "
|
|
32
|
+
"file_path."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ChronoVerifyInput(BaseModel):
|
|
37
|
+
"""Input for the ChronoVerify verification tool."""
|
|
38
|
+
|
|
39
|
+
url: Optional[str] = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="Publicly reachable direct image URL; the server fetches it.",
|
|
42
|
+
)
|
|
43
|
+
file_path: Optional[str] = Field(
|
|
44
|
+
default=None,
|
|
45
|
+
description="Local path of an image file to upload.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ChronoVerifyTool(BaseTool):
|
|
50
|
+
"""LangChain tool that verifies image capture time and provenance."""
|
|
51
|
+
|
|
52
|
+
name: str = "verify_image_provenance"
|
|
53
|
+
description: str = _DESCRIPTION
|
|
54
|
+
args_schema: Type[BaseModel] = ChronoVerifyInput
|
|
55
|
+
|
|
56
|
+
api_key: Optional[str] = None
|
|
57
|
+
"""ChronoVerify API key (cv_live_...). Falls back to the CHRONOVERIFY_API_KEY
|
|
58
|
+
environment variable, then to the free, rate-limited keyless path. A free
|
|
59
|
+
key: POST https://chronoverify.com/v1/keys/free with an email field."""
|
|
60
|
+
|
|
61
|
+
timeout: float = 30.0
|
|
62
|
+
|
|
63
|
+
def _headers(self) -> dict:
|
|
64
|
+
key = self.api_key or os.environ.get("CHRONOVERIFY_API_KEY")
|
|
65
|
+
return {"Authorization": f"Bearer {key}"} if key else {}
|
|
66
|
+
|
|
67
|
+
def _run(
|
|
68
|
+
self,
|
|
69
|
+
url: Optional[str] = None,
|
|
70
|
+
file_path: Optional[str] = None,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> dict:
|
|
73
|
+
provided = [v for v in (url, file_path) if v]
|
|
74
|
+
if len(provided) != 1:
|
|
75
|
+
raise ValueError("Provide exactly one of url or file_path.")
|
|
76
|
+
if url:
|
|
77
|
+
resp = requests.post(
|
|
78
|
+
_ENDPOINT,
|
|
79
|
+
data={"url": url},
|
|
80
|
+
headers=self._headers(),
|
|
81
|
+
timeout=self.timeout,
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
with open(file_path, "rb") as fh: # type: ignore[arg-type]
|
|
85
|
+
resp = requests.post(
|
|
86
|
+
_ENDPOINT,
|
|
87
|
+
files={"file": (os.path.basename(file_path), fh)}, # type: ignore[arg-type]
|
|
88
|
+
headers=self._headers(),
|
|
89
|
+
timeout=self.timeout,
|
|
90
|
+
)
|
|
91
|
+
if resp.status_code != 200:
|
|
92
|
+
try:
|
|
93
|
+
detail = resp.json().get("detail", resp.text)
|
|
94
|
+
except Exception:
|
|
95
|
+
detail = resp.text
|
|
96
|
+
raise RuntimeError(f"ChronoVerify HTTP {resp.status_code}: {detail}")
|
|
97
|
+
return resp.json()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-chronoverify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain tool for ChronoVerify: verify when a photo was taken and its provenance (C2PA Content Credentials, EXIF, pixel forensics)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "ChronoVerify", email = "support@chronoverify.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"langchain",
|
|
15
|
+
"image-verification",
|
|
16
|
+
"provenance",
|
|
17
|
+
"c2pa",
|
|
18
|
+
"content-credentials",
|
|
19
|
+
"exif",
|
|
20
|
+
"capture-time",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"langchain-core>=0.3,<2",
|
|
24
|
+
"requests>=2.28",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://chronoverify.com"
|
|
29
|
+
Documentation = "https://chronoverify.com/integrations/langchain"
|
|
30
|
+
"API reference" = "https://chronoverify.com/method"
|
|
31
|
+
Source = "https://github.com/beeswaxpat/chronoverify-agent-recipes"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["langchain_chronoverify"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Mocked tests for ChronoVerifyTool (no live network)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from langchain_chronoverify import ChronoVerifyTool
|
|
12
|
+
|
|
13
|
+
VERDICT = {"verdict": "consistent", "confidence": 61, "integrity": {"sha256": "ab" * 32}}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resp(status=200, body=VERDICT):
|
|
17
|
+
r = MagicMock()
|
|
18
|
+
r.status_code = status
|
|
19
|
+
r.json.return_value = body
|
|
20
|
+
return r
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_url_call_returns_verdict():
|
|
24
|
+
tool = ChronoVerifyTool()
|
|
25
|
+
with patch("langchain_chronoverify.tool.requests.post", return_value=_resp()) as post:
|
|
26
|
+
out = tool.invoke({"url": "https://example.com/a.jpg"})
|
|
27
|
+
assert out["verdict"] == "consistent"
|
|
28
|
+
assert post.call_args.kwargs["data"] == {"url": "https://example.com/a.jpg"}
|
|
29
|
+
assert "Authorization" not in post.call_args.kwargs["headers"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_api_key_header_sent():
|
|
33
|
+
tool = ChronoVerifyTool(api_key="cv_live_test")
|
|
34
|
+
with patch("langchain_chronoverify.tool.requests.post", return_value=_resp()) as post:
|
|
35
|
+
tool.invoke({"url": "https://example.com/a.jpg"})
|
|
36
|
+
assert post.call_args.kwargs["headers"]["Authorization"] == "Bearer cv_live_test"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_requires_exactly_one_input():
|
|
40
|
+
tool = ChronoVerifyTool()
|
|
41
|
+
with pytest.raises(Exception):
|
|
42
|
+
tool.invoke({})
|
|
43
|
+
with pytest.raises(Exception):
|
|
44
|
+
tool.invoke({"url": "https://x.example/a.jpg", "file_path": "a.jpg"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_http_error_surfaces_detail():
|
|
48
|
+
tool = ChronoVerifyTool()
|
|
49
|
+
err = _resp(status=429, body={"detail": "Rate limit exceeded"})
|
|
50
|
+
with patch("langchain_chronoverify.tool.requests.post", return_value=err):
|
|
51
|
+
with pytest.raises(Exception) as exc:
|
|
52
|
+
tool.invoke({"url": "https://example.com/a.jpg"})
|
|
53
|
+
assert "429" in str(exc.value) and "Rate limit" in str(exc.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_honest_description():
|
|
57
|
+
d = ChronoVerifyTool().description
|
|
58
|
+
assert "NOT a deepfake" in d and "provenance" in d.lower()
|