-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Add MarkovianStampTool for verifiable provenance receipts #6388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MarkovianProtocol
wants to merge
2
commits into
crewAIInc:main
Choose a base branch
from
MarkovianProtocol:add-markovian-stamp-tool
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # MarkovianStampTool | ||
|
|
||
| The **MarkovianStampTool** creates a verifiable provenance receipt for any text | ||
| using the [Markovian Protocol](https://markovianprotocol.com). It is useful when | ||
| an agent needs to prove that an output, decision, or document existed at a point | ||
| in time. | ||
|
|
||
| Stamping commits a hash of the data to the chain, anchored to Bitcoin, and | ||
| returns a Merkle root plus a public verify URL. Anyone can confirm the record at | ||
| `https://api.quantsynth.net/verify/<merkle_root>` with no account. | ||
|
|
||
| Markovian proves that data existed, not that it is correct. Provenance, not | ||
| truth. | ||
|
|
||
| --- | ||
|
|
||
| ## Description | ||
|
|
||
| This tool: | ||
|
|
||
| * Accepts any **text** and stamps it on the Markovian Protocol. | ||
| * Returns a **Merkle root**, **block height**, and a **public verify URL**. | ||
| * Requires **no account, wallet, or API key** for the free tier. | ||
| * Accepts an optional **API key** for attributed or pro usage. | ||
|
|
||
| It prefers the [`markovian`](https://pypi.org/project/markovian/) SDK and falls | ||
| back to a plain HTTP POST when the SDK is not installed. | ||
|
|
||
| --- | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install markovian | ||
| ``` | ||
|
|
||
| `requests` is already a dependency of `crewai-tools`, so the tool also works | ||
| without the SDK installed. | ||
|
|
||
| --- | ||
|
|
||
| ## Arguments | ||
|
|
||
| | Argument | Type | Required | Description | | ||
| | -------- | ----- | -------- | ---------------------------------------------------------------- | | ||
| | `data` | `str` | Yes | The content to stamp (an agent output, decision, or document). | | ||
| | `label` | `str` | No | Optional human-readable label attached to the stamp. | | ||
|
|
||
| Constructor options: `api_key` (optional), `wallet` (optional), `base_url`, | ||
| `timeout`. `MARKOVIAN_API_KEY` is read from the environment when set. | ||
|
|
||
| --- | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| from crewai_tools import MarkovianStampTool | ||
|
|
||
| tool = MarkovianStampTool() | ||
| receipt = tool.run(data="The market thesis approved by the agent at 15:00 UTC.") | ||
| print(receipt) | ||
| ``` | ||
|
|
||
| Example output: | ||
|
|
||
| ```text | ||
| Markovian provenance receipt (markovian-provenance/v1): | ||
| merkle_root: 65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466 | ||
| block_height: 135213 | ||
| verify_url: https://api.quantsynth.net/verify/65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466 | ||
| ``` | ||
|
|
||
| Give an agent a one-line way to stamp its final answer: | ||
|
|
||
| ```python | ||
| from crewai import Agent | ||
| from crewai_tools import MarkovianStampTool | ||
|
|
||
| agent = Agent( | ||
| role="Analyst", | ||
| goal="Produce analysis and stamp it for provenance.", | ||
| backstory="Stamps every deliverable so it can be independently verified.", | ||
| tools=[MarkovianStampTool()], | ||
| ) | ||
| ``` |
Empty file.
137 changes: 137 additions & 0 deletions
137
lib/crewai-tools/src/crewai_tools/tools/markovian_stamp_tool/markovian_stamp_tool.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import json | ||
| import os | ||
| from typing import Any | ||
|
|
||
| from crewai.tools import BaseTool, EnvVar | ||
| from pydantic import BaseModel, Field | ||
| import requests | ||
|
|
||
|
|
||
| MARKOVIAN_BASE_URL = "https://api.quantsynth.net" | ||
|
|
||
|
|
||
| class MarkovianStampToolInput(BaseModel): | ||
| """Input schema for MarkovianStampTool.""" | ||
|
|
||
| data: str = Field( | ||
| ..., | ||
| description=( | ||
| "The content to stamp, for example an agent's final answer, a " | ||
| "decision, or any text whose existence you want to prove." | ||
| ), | ||
| ) | ||
| label: str | None = Field( | ||
| default=None, | ||
| description="Optional human-readable label to attach to the stamp.", | ||
| ) | ||
|
|
||
|
|
||
| class MarkovianStampTool(BaseTool): | ||
| """Stamp any data on the Markovian Protocol and return a verifiable receipt. | ||
|
|
||
| Markovian is a content-agnostic provenance primitive. Stamping commits a | ||
| hash of the data to the chain, anchored to Bitcoin, and returns a Merkle | ||
| root plus a public verify URL. It proves the data existed at a point in | ||
| time, not that the data is correct: provenance, not truth. | ||
|
|
||
| No account, wallet, or API key is required. Provide an api_key only for | ||
| attributed or pro usage. | ||
| """ | ||
|
|
||
| name: str = "Markovian Stamp" | ||
| description: str = ( | ||
| "Create a verifiable provenance receipt for any text on the Markovian " | ||
| "Protocol. Returns a Merkle root and a public verify URL, anchored to " | ||
| "Bitcoin. Use it to prove an agent output existed at a point in time. " | ||
| "No account or API key required." | ||
| ) | ||
| args_schema: type[BaseModel] = MarkovianStampToolInput | ||
|
|
||
| package_dependencies: list[str] = Field(default_factory=lambda: ["markovian"]) | ||
| env_vars: list[EnvVar] = Field( | ||
| default_factory=lambda: [ | ||
| EnvVar( | ||
| name="MARKOVIAN_API_KEY", | ||
| description="Optional API key for attributed or pro usage.", | ||
| required=False, | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
| api_key: str | None = None | ||
| wallet: str | None = None | ||
| base_url: str = MARKOVIAN_BASE_URL | ||
| timeout: int = 30 | ||
|
|
||
| def __init__( | ||
| self, | ||
| api_key: str | None = None, | ||
| wallet: str | None = None, | ||
| base_url: str = MARKOVIAN_BASE_URL, | ||
| timeout: int = 30, | ||
| **kwargs: Any, | ||
| ) -> None: | ||
| super().__init__(**kwargs) | ||
| self.api_key = api_key or os.environ.get("MARKOVIAN_API_KEY") | ||
| self.wallet = wallet | ||
| self.base_url = base_url | ||
| self.timeout = timeout | ||
|
|
||
| def _stamp(self, data: str, label: str | None) -> dict: | ||
| """Stamp via the markovian SDK when available, else a plain POST.""" | ||
| try: | ||
| from markovian import MarkovianClient | ||
|
|
||
| client = MarkovianClient(api_key=self.api_key, timeout=self.timeout) | ||
| return client.stamp(data, wallet=self.wallet, label=label) | ||
| except ImportError: | ||
| payload: dict = {"data": data, "label": label} | ||
| if self.wallet: | ||
| payload["wallet"] = self.wallet | ||
| headers = { | ||
| "User-Agent": "crewai-tools-markovian", | ||
| "Accept": "application/json", | ||
| "Content-Type": "application/json", | ||
| } | ||
| if self.api_key: | ||
| headers["X-API-Key"] = self.api_key | ||
| resp = requests.post( | ||
| f"{self.base_url}/stamp", | ||
| json=payload, | ||
| headers=headers, | ||
| timeout=self.timeout, | ||
| ) | ||
| resp.raise_for_status() | ||
| return resp.json() | ||
|
|
||
| def _run(self, data: str, label: str | None = None, **_: Any) -> str: | ||
| try: | ||
| receipt = self._stamp(data, label) | ||
| except requests.Timeout: | ||
| return "Markovian stamp timed out. Please try again later." | ||
| except requests.HTTPError as exc: | ||
| status = exc.response.status_code if exc.response is not None else "?" | ||
| body = exc.response.text[:160] if exc.response is not None else "" | ||
| return f"Markovian stamp failed: HTTP {status} {body}" | ||
| except Exception as exc: | ||
| return f"Markovian stamp failed: {exc}" | ||
|
|
||
| if not isinstance(receipt, dict): | ||
| return f"Markovian stamp returned an unexpected response: {receipt}" | ||
|
|
||
| merkle_root = receipt.get("merkle_root") | ||
| if not merkle_root: | ||
| return f"Markovian stamp returned no merkle_root: {json.dumps(receipt)}" | ||
|
|
||
| verify_url = receipt.get("verify_url", f"{self.base_url}/verify/{merkle_root}") | ||
| block_height = receipt.get("block_height") | ||
| lines = [ | ||
| "Markovian provenance receipt (markovian-provenance/v1):", | ||
| f" merkle_root: {merkle_root}", | ||
| f" block_height: {block_height}", | ||
| f" verify_url: {verify_url}", | ||
| ] | ||
| return "\n".join(lines) | ||
|
|
||
| async def _arun(self, data: str, label: str | None = None, **kwargs: Any) -> str: | ||
| return self._run(data, label=label, **kwargs) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import sys | ||
| from types import ModuleType | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import requests | ||
|
|
||
| from crewai_tools import MarkovianStampTool | ||
|
|
||
|
|
||
| SAMPLE_RECEIPT = { | ||
| "ok": True, | ||
| "stamp_id": 1, | ||
| "merkle_root": "65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466", | ||
| "data_hash": "b1000ed4a5bbf3bc3280c706e85374bec6ef5524ee877425b8454a8f5b73e1da", | ||
| "block_height": 135213, | ||
| "verify_url": "https://api.quantsynth.net/verify/65ddefa2b8d3fb994f2a4037f9dd8278688138bf1c5eaa9cdb64c73c02663466", | ||
| } | ||
|
|
||
|
|
||
| def test_run_uses_sdk_when_available(monkeypatch): | ||
| tool = MarkovianStampTool() | ||
|
|
||
| fake_client = MagicMock() | ||
| fake_client.stamp.return_value = SAMPLE_RECEIPT | ||
| fake_client_cls = MagicMock(return_value=fake_client) | ||
|
|
||
| fake_module = ModuleType("markovian") | ||
| fake_module.MarkovianClient = fake_client_cls | ||
| monkeypatch.setitem(sys.modules, "markovian", fake_module) | ||
|
|
||
| result = tool.run(data="hello world") | ||
|
|
||
| fake_client_cls.assert_called_once() | ||
| fake_client.stamp.assert_called_once() | ||
| args, kwargs = fake_client.stamp.call_args | ||
| assert args[0] == "hello world" | ||
| assert SAMPLE_RECEIPT["merkle_root"] in result | ||
| assert SAMPLE_RECEIPT["verify_url"] in result | ||
| assert "135213" in result | ||
|
|
||
|
|
||
| def test_run_falls_back_to_post(monkeypatch): | ||
| monkeypatch.delenv("MARKOVIAN_API_KEY", raising=False) | ||
| tool = MarkovianStampTool() | ||
|
|
||
| mock_resp = MagicMock() | ||
| mock_resp.json.return_value = SAMPLE_RECEIPT | ||
| mock_resp.raise_for_status.return_value = None | ||
|
|
||
| def fake_import(name, *args, **kwargs): | ||
| if name == "markovian": | ||
| raise ImportError("no markovian") | ||
| return original_import(name, *args, **kwargs) | ||
|
|
||
| import builtins | ||
|
|
||
| original_import = builtins.__import__ | ||
| with patch("builtins.__import__", side_effect=fake_import): | ||
| with patch("requests.post", return_value=mock_resp) as mock_post: | ||
| result = tool.run(data="hello world") | ||
|
|
||
| mock_post.assert_called_once() | ||
| assert SAMPLE_RECEIPT["merkle_root"] in result | ||
|
|
||
|
|
||
| def test_run_handles_http_error(): | ||
| tool = MarkovianStampTool() | ||
| err = requests.HTTPError() | ||
| err.response = MagicMock(status_code=500, text="boom") | ||
|
|
||
| with patch.object(tool, "_stamp", side_effect=err): | ||
| result = tool.run(data="hello world") | ||
|
|
||
| assert "Markovian stamp failed" in result | ||
| assert "500" in result | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| assert "boom" in result | ||
|
|
||
|
|
||
| def test_run_handles_non_dict_response(): | ||
| tool = MarkovianStampTool() | ||
| with patch.object(tool, "_stamp", return_value=["not", "a", "dict"]): | ||
| result = tool.run(data="hello world") | ||
|
|
||
| assert "unexpected response" in result | ||
|
|
||
|
|
||
| def test_run_handles_missing_merkle_root(): | ||
| tool = MarkovianStampTool() | ||
| with patch.object(tool, "_stamp", return_value={"ok": True}): | ||
| result = tool.run(data="hello world") | ||
|
|
||
| assert "no merkle_root" in result | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.