Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/crewai-tools/src/crewai_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool
from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool
from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool
from crewai_tools.tools.markovian_stamp_tool.markovian_stamp_tool import (
MarkovianStampTool,
)
from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool
from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import (
MergeAgentHandlerTool,
Expand Down Expand Up @@ -275,6 +278,7 @@
"LlamaIndexTool",
"MCPServerAdapter",
"MDXSearchTool",
"MarkovianStampTool",
"MergeAgentHandlerTool",
"MongoDBVectorSearchConfig",
"MongoDBVectorSearchTool",
Expand Down
4 changes: 4 additions & 0 deletions lib/crewai-tools/src/crewai_tools/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@
from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool
from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool
from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool
from crewai_tools.tools.markovian_stamp_tool.markovian_stamp_tool import (
MarkovianStampTool,
)
from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool
from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import (
MergeAgentHandlerTool,
Expand Down Expand Up @@ -258,6 +261,7 @@
"LinkupSearchTool",
"LlamaIndexTool",
"MDXSearchTool",
"MarkovianStampTool",
"MergeAgentHandlerTool",
"MongoDBToolSchema",
"MongoDBVectorSearchConfig",
Expand Down
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.
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)}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)
92 changes: 92 additions & 0 deletions lib/crewai-tools/tests/tools/markovian_stamp_tool_test.py
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
Comment thread
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