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.
@@ -0,0 +1,11 @@
1
+ # Node
2
+ node_modules/
3
+ dist/
4
+ *.tsbuildinfo
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.pyc
9
+ build/
10
+ *.egg-info/
11
+ .eggs/
@@ -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,6 @@
1
+ """LangChain tool for ChronoVerify image capture-time and provenance verification."""
2
+
3
+ from langchain_chronoverify.tool import ChronoVerifyTool
4
+
5
+ __all__ = ["ChronoVerifyTool"]
6
+ __version__ = "0.1.0"
@@ -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()