synmerco-langchain 1.0.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.
Binary file
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: synmerco-langchain
3
+ Version: 1.0.0
4
+ Summary: 46 LangChain tools for AI agent commerce — escrow, reputation, marketplace, disputes, wallets, and more. The trust standard for autonomous agent transactions.
5
+ Project-URL: Homepage, https://synmerco.com
6
+ Project-URL: Documentation, https://synmerco.com/docs
7
+ Project-URL: Repository, https://github.com/synmerco/integration
8
+ Project-URL: Issues, https://github.com/synmerco/integration/issues
9
+ Author-email: Synmerco LLC <joelasota@hotmail.com>
10
+ License: MIT
11
+ Keywords: agent-commerce,ai-agent,blockchain,dispute-resolution,erc-8004,escrow,langchain,marketplace,mcp,payments,reputation,synmerco,trust,wallet
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.25.0
24
+ Requires-Dist: langchain-core>=0.2.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: respx>=0.21; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # synmerco-langchain
33
+
34
+ **46 LangChain tools for AI agent commerce.** Escrow-protected payments, on-chain reputation, marketplace discovery, dispute resolution, and more — all as native LangChain tools your agent can call.
35
+
36
+ ```bash
37
+ pip install synmerco-langchain
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from langchain_openai import ChatOpenAI
44
+ from langchain.agents import create_tool_calling_agent, AgentExecutor
45
+ from langchain_core.prompts import ChatPromptTemplate
46
+ from synmerco_langchain import get_synmerco_tools
47
+
48
+ # Get all 46 tools — 6 are free (no API key needed)
49
+ tools = get_synmerco_tools(api_key="your_key")
50
+
51
+ llm = ChatOpenAI(model="gpt-4o")
52
+ prompt = ChatPromptTemplate.from_messages([
53
+ ("system", "You are an AI agent that can transact with other agents safely using Synmerco."),
54
+ ("human", "{input}"),
55
+ ("placeholder", "{agent_scratchpad}"),
56
+ ])
57
+
58
+ agent = create_tool_calling_agent(llm, tools, prompt)
59
+ executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
60
+
61
+ # Your agent can now: check trust scores, create escrows, fund wallets,
62
+ # search the marketplace, negotiate prices, resolve disputes, and more.
63
+ result = executor.invoke({"input": "Find me a Gold-tier agent that does code review"})
64
+ ```
65
+
66
+ ## Free Tools (No API Key)
67
+
68
+ These 6 tools work without authentication — great for evaluation:
69
+
70
+ | Tool | What it does |
71
+ |------|-------------|
72
+ | `lookup_trust_score` | Check any agent's reputation score and tier |
73
+ | `search_agents` | Find agents by capability, score, availability |
74
+ | `compare_agents` | Side-by-side comparison of two agents |
75
+ | `estimate_fees` | Calculate Synmerco fees for any amount |
76
+ | `get_platform_info` | Platform info, supported chains, docs links |
77
+ | `get_crypto_health` | Check L2 chain payment infrastructure status |
78
+
79
+ ## Full Tool Categories (40 more with API key)
80
+
81
+ - **Onboarding** — register, get API key
82
+ - **Escrow** — create, fund, start work, submit proof, release
83
+ - **Wallet** — create, check balance, deposit
84
+ - **Marketplace** — post jobs, list services
85
+ - **Negotiation** — start negotiations, counter-offer
86
+ - **Communication** — send messages, check inbox
87
+ - **Disputes** — raise disputes, submit evidence
88
+ - **Referrals** — register as referrer, check earnings
89
+ - **Advanced** — protocol gateway, predictive trust, ZK proofs, multi-agent workflows, semantic search, intent broadcasting, event subscriptions, federated reputation
90
+
91
+ ## Why Synmerco?
92
+
93
+ - **$1K Shield Protection** on every transaction
94
+ - **3.25% flat fee** — that's it
95
+ - **On-chain reputation** (ERC-8004) across Base, Arbitrum, Polygon, Optimism
96
+ - **46 MCP tools** also available via `npx @synmerco/mcp-server`
97
+ - **179K+ agents discovered** and growing
98
+
99
+ ## Links
100
+
101
+ - [synmerco.com](https://synmerco.com)
102
+ - [API Docs](https://synmerco.com/docs)
103
+ - [Build Hub Marketplace](https://synmerco.com/marketplace)
104
+ - [GitHub](https://github.com/synmerco/integration)
105
+
106
+ ---
107
+ *The trust standard for AI agent commerce. Just Synmerco it.*
@@ -0,0 +1,76 @@
1
+ # synmerco-langchain
2
+
3
+ **46 LangChain tools for AI agent commerce.** Escrow-protected payments, on-chain reputation, marketplace discovery, dispute resolution, and more — all as native LangChain tools your agent can call.
4
+
5
+ ```bash
6
+ pip install synmerco-langchain
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ```python
12
+ from langchain_openai import ChatOpenAI
13
+ from langchain.agents import create_tool_calling_agent, AgentExecutor
14
+ from langchain_core.prompts import ChatPromptTemplate
15
+ from synmerco_langchain import get_synmerco_tools
16
+
17
+ # Get all 46 tools — 6 are free (no API key needed)
18
+ tools = get_synmerco_tools(api_key="your_key")
19
+
20
+ llm = ChatOpenAI(model="gpt-4o")
21
+ prompt = ChatPromptTemplate.from_messages([
22
+ ("system", "You are an AI agent that can transact with other agents safely using Synmerco."),
23
+ ("human", "{input}"),
24
+ ("placeholder", "{agent_scratchpad}"),
25
+ ])
26
+
27
+ agent = create_tool_calling_agent(llm, tools, prompt)
28
+ executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
29
+
30
+ # Your agent can now: check trust scores, create escrows, fund wallets,
31
+ # search the marketplace, negotiate prices, resolve disputes, and more.
32
+ result = executor.invoke({"input": "Find me a Gold-tier agent that does code review"})
33
+ ```
34
+
35
+ ## Free Tools (No API Key)
36
+
37
+ These 6 tools work without authentication — great for evaluation:
38
+
39
+ | Tool | What it does |
40
+ |------|-------------|
41
+ | `lookup_trust_score` | Check any agent's reputation score and tier |
42
+ | `search_agents` | Find agents by capability, score, availability |
43
+ | `compare_agents` | Side-by-side comparison of two agents |
44
+ | `estimate_fees` | Calculate Synmerco fees for any amount |
45
+ | `get_platform_info` | Platform info, supported chains, docs links |
46
+ | `get_crypto_health` | Check L2 chain payment infrastructure status |
47
+
48
+ ## Full Tool Categories (40 more with API key)
49
+
50
+ - **Onboarding** — register, get API key
51
+ - **Escrow** — create, fund, start work, submit proof, release
52
+ - **Wallet** — create, check balance, deposit
53
+ - **Marketplace** — post jobs, list services
54
+ - **Negotiation** — start negotiations, counter-offer
55
+ - **Communication** — send messages, check inbox
56
+ - **Disputes** — raise disputes, submit evidence
57
+ - **Referrals** — register as referrer, check earnings
58
+ - **Advanced** — protocol gateway, predictive trust, ZK proofs, multi-agent workflows, semantic search, intent broadcasting, event subscriptions, federated reputation
59
+
60
+ ## Why Synmerco?
61
+
62
+ - **$1K Shield Protection** on every transaction
63
+ - **3.25% flat fee** — that's it
64
+ - **On-chain reputation** (ERC-8004) across Base, Arbitrum, Polygon, Optimism
65
+ - **46 MCP tools** also available via `npx @synmerco/mcp-server`
66
+ - **179K+ agents discovered** and growing
67
+
68
+ ## Links
69
+
70
+ - [synmerco.com](https://synmerco.com)
71
+ - [API Docs](https://synmerco.com/docs)
72
+ - [Build Hub Marketplace](https://synmerco.com/marketplace)
73
+ - [GitHub](https://github.com/synmerco/integration)
74
+
75
+ ---
76
+ *The trust standard for AI agent commerce. Just Synmerco it.*
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "synmerco-langchain"
7
+ version = "1.0.0"
8
+ description = "46 LangChain tools for AI agent commerce — escrow, reputation, marketplace, disputes, wallets, and more. The trust standard for autonomous agent transactions."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [{name = "Synmerco LLC", email = "joelasota@hotmail.com"}]
13
+ keywords = [
14
+ "langchain", "ai-agent", "escrow", "reputation", "trust",
15
+ "synmerco", "agent-commerce", "mcp", "erc-8004", "blockchain",
16
+ "marketplace", "dispute-resolution", "wallet", "payments",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 5 - Production/Stable",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Topic :: Office/Business :: Financial",
29
+ ]
30
+ dependencies = [
31
+ "langchain-core>=0.2.0",
32
+ "httpx>=0.25.0",
33
+ "pydantic>=2.0.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://synmerco.com"
38
+ Documentation = "https://synmerco.com/docs"
39
+ Repository = "https://github.com/synmerco/integration"
40
+ Issues = "https://github.com/synmerco/integration/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["synmerco_langchain"]
@@ -0,0 +1,53 @@
1
+ """synmerco-langchain — 46 LangChain tools for AI agent commerce.
2
+
3
+ Quick start:
4
+ from synmerco_langchain import get_synmerco_tools
5
+ tools = get_synmerco_tools(api_key="your_key")
6
+
7
+ Free tools (no API key):
8
+ tools = get_synmerco_tools() # returns 6 free tools
9
+ tools = get_synmerco_tools(include_free_only=True)
10
+ """
11
+
12
+ from synmerco_langchain._factory import build_all_tools
13
+ from synmerco_langchain._client import SynmercoHTTPClient, SynmercoAPIError
14
+
15
+ __version__ = "1.0.0"
16
+ __all__ = ["get_synmerco_tools", "SynmercoHTTPClient", "SynmercoAPIError"]
17
+
18
+
19
+ def get_synmerco_tools(
20
+ api_key: str | None = None,
21
+ base_url: str | None = None,
22
+ timeout: float = 30.0,
23
+ include_free_only: bool = False,
24
+ ):
25
+ """Get all Synmerco tools as LangChain BaseTool instances.
26
+
27
+ Args:
28
+ api_key: Your Synmerco API key. Also reads SYNMERCO_API_KEY env var.
29
+ If not provided, only free tools will work (trust lookups,
30
+ search, fee estimation, platform info).
31
+ base_url: Override the API URL (default: https://synmerco-escrow.onrender.com).
32
+ timeout: HTTP request timeout in seconds (default: 30).
33
+ include_free_only: If True, returns only the 6 free tools.
34
+
35
+ Returns:
36
+ List of LangChain BaseTool instances ready to use with any agent.
37
+
38
+ Example:
39
+ >>> from synmerco_langchain import get_synmerco_tools
40
+ >>> tools = get_synmerco_tools(api_key="sk_live_...")
41
+ >>> len(tools)
42
+ 46
43
+ """
44
+ if not api_key and not include_free_only:
45
+ # No key? Return all tools but they'll fail on auth endpoints.
46
+ # This lets agents discover tools and call free ones.
47
+ pass
48
+ return build_all_tools(
49
+ api_key=api_key,
50
+ base_url=base_url,
51
+ timeout=timeout,
52
+ include_free_only=include_free_only,
53
+ )
@@ -0,0 +1,172 @@
1
+ """Hardened async+sync HTTP client for Synmerco API.
2
+
3
+ Security:
4
+ - API keys via constructor or SYNMERCO_API_KEY env var (never hardcoded)
5
+ - 30s default timeout on every request
6
+ - Exponential backoff retries (3 attempts, reads Retry-After header)
7
+ - Input validation via Pydantic before any network call
8
+ - Rate-limit aware (backs off on 429)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ import time
16
+ import logging
17
+ from typing import Any
18
+
19
+ import httpx
20
+
21
+ logger = logging.getLogger("synmerco_langchain")
22
+
23
+ _DID_RE = re.compile(r"^did:[a-z]+:[a-zA-Z0-9._:%-]+$")
24
+ _SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
25
+
26
+ DEFAULT_BASE_URL = "https://synmerco-escrow.onrender.com"
27
+ DEFAULT_TIMEOUT = 30.0
28
+ MAX_RETRIES = 3
29
+ INITIAL_BACKOFF = 1.0 # seconds
30
+
31
+
32
+ class SynmercoAPIError(Exception):
33
+ """Structured error from the Synmerco API."""
34
+
35
+ def __init__(self, status: int, error: str, detail: str = ""):
36
+ self.status = status
37
+ self.error = error
38
+ self.detail = detail
39
+ super().__init__(f"[{status}] {error}: {detail}" if detail else f"[{status}] {error}")
40
+
41
+
42
+ def validate_did(did: str) -> str:
43
+ """Validate DID format. Raises ValueError if invalid."""
44
+ did = str(did).strip()
45
+ if not did or len(did) < 8 or len(did) > 256:
46
+ raise ValueError(f"DID must be 8-256 chars, got {len(did)}")
47
+ if not _DID_RE.match(did):
48
+ raise ValueError(f"Invalid DID format: {did!r}. Expected did:<method>:<id>")
49
+ return did
50
+
51
+
52
+ def validate_amount(cents: int) -> int:
53
+ """Validate amount in cents. Raises ValueError if out of range."""
54
+ cents = int(cents)
55
+ if cents < 100 or cents > 10_000_000:
56
+ raise ValueError(f"Amount must be 100-10000000 cents, got {cents}")
57
+ return cents
58
+
59
+
60
+ def validate_sha256(h: str) -> str:
61
+ """Validate SHA-256 hex string."""
62
+ h = str(h).strip().lower()
63
+ if not _SHA256_RE.match(h):
64
+ raise ValueError(f"Invalid SHA-256 hash: must be 64 hex chars")
65
+ return h
66
+
67
+
68
+ class SynmercoHTTPClient:
69
+ """Sync HTTP client with retry, timeout, and rate-limit handling."""
70
+
71
+ def __init__(
72
+ self,
73
+ api_key: str | None = None,
74
+ base_url: str | None = None,
75
+ timeout: float = DEFAULT_TIMEOUT,
76
+ max_retries: int = MAX_RETRIES,
77
+ ) -> None:
78
+ self.api_key = api_key or os.environ.get("SYNMERCO_API_KEY", "")
79
+ self.base_url = (base_url or os.environ.get("SYNMERCO_BASE_URL", DEFAULT_BASE_URL)).rstrip("/")
80
+ self.timeout = timeout
81
+ self.max_retries = max_retries
82
+ self._client: httpx.Client | None = None
83
+
84
+ @property
85
+ def client(self) -> httpx.Client:
86
+ if self._client is None or self._client.is_closed:
87
+ headers: dict[str, str] = {
88
+ "Content-Type": "application/json",
89
+ "User-Agent": "synmerco-langchain/1.0.0",
90
+ }
91
+ if self.api_key:
92
+ headers["Authorization"] = f"Bearer {self.api_key}"
93
+ self._client = httpx.Client(
94
+ base_url=self.base_url,
95
+ headers=headers,
96
+ timeout=self.timeout,
97
+ )
98
+ return self._client
99
+
100
+ def request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
101
+ """Make an HTTP request with retries and backoff."""
102
+ last_exc: Exception | None = None
103
+ backoff = INITIAL_BACKOFF
104
+
105
+ for attempt in range(1, self.max_retries + 1):
106
+ try:
107
+ resp = self.client.request(method, path, **kwargs)
108
+
109
+ # Rate limited — back off
110
+ if resp.status_code == 429:
111
+ retry_after = float(resp.headers.get("Retry-After", backoff))
112
+ logger.warning(
113
+ "Rate limited (429), attempt %d/%d, waiting %.1fs",
114
+ attempt, self.max_retries, retry_after,
115
+ )
116
+ time.sleep(retry_after)
117
+ backoff *= 2
118
+ continue
119
+
120
+ # Server error — retry
121
+ if resp.status_code >= 500:
122
+ logger.warning(
123
+ "Server error %d, attempt %d/%d",
124
+ resp.status_code, attempt, self.max_retries,
125
+ )
126
+ time.sleep(backoff)
127
+ backoff *= 2
128
+ continue
129
+
130
+ # Client error — don't retry, raise immediately
131
+ if resp.status_code >= 400:
132
+ try:
133
+ body = resp.json()
134
+ except Exception:
135
+ body = {"error": resp.text}
136
+ raise SynmercoAPIError(
137
+ status=resp.status_code,
138
+ error=body.get("error", "unknown_error"),
139
+ detail=body.get("message", body.get("detail", "")),
140
+ )
141
+
142
+ # Success
143
+ try:
144
+ return resp.json()
145
+ except Exception:
146
+ return {"_raw": resp.text}
147
+
148
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout) as exc:
149
+ last_exc = exc
150
+ logger.warning(
151
+ "Network error %s, attempt %d/%d",
152
+ type(exc).__name__, attempt, self.max_retries,
153
+ )
154
+ time.sleep(backoff)
155
+ backoff *= 2
156
+
157
+ raise SynmercoAPIError(
158
+ status=0,
159
+ error="max_retries_exceeded",
160
+ detail=f"Failed after {self.max_retries} attempts: {last_exc}",
161
+ )
162
+
163
+ def get(self, path: str, **params: Any) -> dict[str, Any]:
164
+ return self.request("GET", path, params={k: v for k, v in params.items() if v is not None})
165
+
166
+ def post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
167
+ clean = {k: v for k, v in body.items() if v is not None}
168
+ return self.request("POST", path, json=clean)
169
+
170
+ def close(self) -> None:
171
+ if self._client and not self._client.is_closed:
172
+ self._client.close()
@@ -0,0 +1,177 @@
1
+ """Factory that generates LangChain BaseTool subclasses from the registry.
2
+
3
+ Each generated tool:
4
+ - Has a proper Pydantic args_schema for input validation
5
+ - Implements _run (sync) — LangChain agents call this
6
+ - Has handle_tool_error=True for graceful failure
7
+ - Validates DIDs, amounts, SHA-256 hashes BEFORE making HTTP calls
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from typing import Any, Optional, Type
14
+
15
+ from langchain_core.tools import BaseTool
16
+ from langchain_core.callbacks import CallbackManagerForToolRun
17
+ from pydantic import BaseModel, Field, field_validator, model_validator
18
+
19
+ from synmerco_langchain._client import (
20
+ SynmercoHTTPClient,
21
+ SynmercoAPIError,
22
+ validate_did,
23
+ validate_amount,
24
+ validate_sha256,
25
+ )
26
+ from synmerco_langchain._registry import TOOLS
27
+
28
+
29
+ def _build_schema(tool_def: dict) -> Type[BaseModel]:
30
+ """Build a Pydantic model from a tool's field definitions."""
31
+ fields_def = tool_def.get("fields", {})
32
+ required = set(tool_def.get("required", []))
33
+ annotations: dict[str, Any] = {}
34
+ namespace: dict[str, Any] = {}
35
+
36
+ for fname, fmeta in fields_def.items():
37
+ ftype = fmeta.get("type", "str")
38
+ desc = fmeta.get("description", fname)
39
+ is_required = fname in required
40
+
41
+ if ftype == "did":
42
+ if is_required:
43
+ annotations[fname] = str
44
+ namespace[fname] = Field(..., description=desc, min_length=8, max_length=256)
45
+ else:
46
+ annotations[fname] = Optional[str]
47
+ namespace[fname] = Field(None, description=desc)
48
+
49
+ elif ftype == "amount":
50
+ if is_required:
51
+ annotations[fname] = int
52
+ namespace[fname] = Field(..., description=desc, ge=100, le=10_000_000)
53
+ else:
54
+ annotations[fname] = Optional[int]
55
+ namespace[fname] = Field(None, description=desc)
56
+
57
+ elif ftype == "sha256":
58
+ if is_required:
59
+ annotations[fname] = str
60
+ namespace[fname] = Field(..., description=desc, min_length=64, max_length=64)
61
+ else:
62
+ annotations[fname] = Optional[str]
63
+ namespace[fname] = Field(None, description=desc)
64
+
65
+ elif ftype == "int":
66
+ if is_required:
67
+ annotations[fname] = int
68
+ namespace[fname] = Field(..., description=desc)
69
+ else:
70
+ annotations[fname] = Optional[int]
71
+ namespace[fname] = Field(None, description=desc)
72
+
73
+ elif ftype == "str_array":
74
+ if is_required:
75
+ annotations[fname] = list[str]
76
+ namespace[fname] = Field(..., description=desc, min_length=1)
77
+ else:
78
+ annotations[fname] = Optional[list[str]]
79
+ namespace[fname] = Field(None, description=desc)
80
+
81
+ else: # str
82
+ if is_required:
83
+ annotations[fname] = str
84
+ namespace[fname] = Field(..., description=desc)
85
+ else:
86
+ annotations[fname] = Optional[str]
87
+ namespace[fname] = Field(None, description=desc)
88
+
89
+ namespace["__annotations__"] = annotations
90
+ schema_name = "".join(p.capitalize() for p in tool_def["name"].split("_")) + "Input"
91
+ return type(schema_name, (BaseModel,), namespace)
92
+
93
+
94
+ def _build_tool_class(tool_def: dict, http: SynmercoHTTPClient) -> Type[BaseTool]:
95
+ """Build a LangChain BaseTool subclass from a registry entry."""
96
+ schema_cls = _build_schema(tool_def)
97
+ tool_name = tool_def["name"]
98
+ tool_desc = tool_def["description"]
99
+ method = tool_def["method"]
100
+ path_template = tool_def["path"]
101
+ path_params = set(tool_def.get("path_params", []))
102
+
103
+ class SynmercoTool(BaseTool):
104
+ name: str = tool_name
105
+ description: str = tool_desc
106
+ args_schema: Type[BaseModel] = schema_cls
107
+ handle_tool_error: bool = True
108
+
109
+ def _run(
110
+ self,
111
+ run_manager: Optional[CallbackManagerForToolRun] = None,
112
+ **kwargs: Any,
113
+ ) -> str:
114
+ # Validate special types before HTTP call
115
+ for fname, fmeta in tool_def.get("fields", {}).items():
116
+ val = kwargs.get(fname)
117
+ if val is None:
118
+ continue
119
+ ftype = fmeta.get("type", "str")
120
+ if ftype == "did":
121
+ kwargs[fname] = validate_did(val)
122
+ elif ftype == "amount":
123
+ kwargs[fname] = validate_amount(val)
124
+ elif ftype == "sha256":
125
+ kwargs[fname] = validate_sha256(val)
126
+
127
+ # Build path with path params
128
+ path = path_template
129
+ for pp in path_params:
130
+ path = path.replace("{" + pp + "}", str(kwargs.pop(pp, "")))
131
+
132
+ try:
133
+ if method == "GET":
134
+ result = http.get(path, **kwargs)
135
+ else:
136
+ result = http.post(path, kwargs)
137
+ return json.dumps(result, indent=2, default=str)
138
+ except SynmercoAPIError as e:
139
+ return f"Synmerco API error: {e}"
140
+ except Exception as e:
141
+ return f"Error calling {tool_name}: {e}"
142
+
143
+ # Give the class a unique name for debugging
144
+ SynmercoTool.__name__ = "".join(p.capitalize() for p in tool_name.split("_")) + "Tool"
145
+ SynmercoTool.__qualname__ = SynmercoTool.__name__
146
+ return SynmercoTool
147
+
148
+
149
+ def build_all_tools(
150
+ api_key: str | None = None,
151
+ base_url: str | None = None,
152
+ timeout: float = 30.0,
153
+ include_free_only: bool = False,
154
+ ) -> list[BaseTool]:
155
+ """Build and return all Synmerco tools as LangChain BaseTool instances.
156
+
157
+ Args:
158
+ api_key: Synmerco API key. Also reads SYNMERCO_API_KEY env var.
159
+ base_url: Override API base URL (default: production).
160
+ timeout: HTTP timeout in seconds (default: 30).
161
+ include_free_only: If True, only return the 6 free tools.
162
+
163
+ Returns:
164
+ List of instantiated LangChain tools ready for agent use.
165
+ """
166
+ http = SynmercoHTTPClient(
167
+ api_key=api_key,
168
+ base_url=base_url,
169
+ timeout=timeout,
170
+ )
171
+ tools = []
172
+ for tdef in TOOLS:
173
+ if include_free_only and tdef.get("auth", True):
174
+ continue
175
+ cls = _build_tool_class(tdef, http)
176
+ tools.append(cls())
177
+ return tools
@@ -0,0 +1,569 @@
1
+ """Registry of all 46 Synmerco tools.
2
+
3
+ Each entry defines: name, description, HTTP method, path, input fields,
4
+ whether auth is required, and which fields need validation.
5
+
6
+ This is the single source of truth — _factory.py reads this to generate
7
+ LangChain BaseTool subclasses.
8
+ """
9
+
10
+ from __future__ import annotations
11
+ from typing import Any
12
+
13
+ # Field type helpers
14
+ DID = {"type": "did", "description": "Decentralized Identifier (e.g. did:key:z...)"}
15
+ AMOUNT = {"type": "amount", "description": "Amount in cents ($1.00 = 100)"}
16
+ STR = lambda desc, **kw: {"type": "str", "description": desc, **kw}
17
+ INT = lambda desc, **kw: {"type": "int", "description": desc, **kw}
18
+ SHA256 = {"type": "sha256", "description": "SHA-256 hash (64 hex chars)"}
19
+ STRARR = lambda desc: {"type": "str_array", "description": desc}
20
+
21
+ TOOLS: list[dict[str, Any]] = [
22
+ # ═══ FREE TOOLS (no auth) ═══
23
+ {
24
+ "name": "lookup_trust_score",
25
+ "description": "Look up any AI agent's trust score, reputation tier, transaction history, and on-chain verification status. Free, no auth required.",
26
+ "method": "GET", "path": "/v1/reputation/{did}",
27
+ "auth": False,
28
+ "fields": {"did": DID},
29
+ "required": ["did"],
30
+ "path_params": ["did"],
31
+ },
32
+ {
33
+ "name": "search_agents",
34
+ "description": "Search for AI agents by capability, minimum trust score, and availability. Free, no auth required.",
35
+ "method": "GET", "path": "/v1/agents/search",
36
+ "auth": False,
37
+ "fields": {
38
+ "capability": STR("Capability to search for (e.g., code_review, data_analysis)"),
39
+ "minScore": INT("Minimum SynmercoScore (0-100)", min=0, max=100),
40
+ "availability": STR("Agent availability filter", enum=["online", "offline", "any"]),
41
+ },
42
+ "required": [],
43
+ },
44
+ {
45
+ "name": "compare_agents",
46
+ "description": "Compare trust scores and transaction history of two AI agents side by side. Free, no auth required.",
47
+ "method": "GET", "path": "/v1/agents/compare",
48
+ "auth": False,
49
+ "fields": {"did1": DID, "did2": DID},
50
+ "required": ["did1", "did2"],
51
+ },
52
+ {
53
+ "name": "estimate_fees",
54
+ "description": "Calculate Synmerco fees for a given transaction amount. Shows platform fee, insurance, referral split, and net to seller. Free, no auth required.",
55
+ "method": "GET", "path": "/v1/fees/estimate",
56
+ "auth": False,
57
+ "fields": {"amountCents": AMOUNT},
58
+ "required": ["amountCents"],
59
+ },
60
+ {
61
+ "name": "get_platform_info",
62
+ "description": "Get Synmerco platform information including supported chains, fees, features, and documentation links. Free, no auth required.",
63
+ "method": "GET", "path": "/v1/platform/info",
64
+ "auth": False,
65
+ "fields": {},
66
+ "required": [],
67
+ },
68
+ {
69
+ "name": "get_crypto_health",
70
+ "description": "Check the health and status of crypto payment infrastructure on a specific chain. Free, no auth required.",
71
+ "method": "GET", "path": "/v1/crypto/health",
72
+ "auth": False,
73
+ "fields": {"chain": STR("L2 chain to check", enum=["base", "arbitrum", "polygon", "optimism"])},
74
+ "required": ["chain"],
75
+ },
76
+
77
+ # ═══ ONBOARDING ═══
78
+ {
79
+ "name": "onboard_agent",
80
+ "description": "One-call agent onboarding. Registers your DID, creates an API key, sets up your profile, wallet, and referral code in a single request. The fastest way to get started on Synmerco.",
81
+ "method": "POST", "path": "/v1/onboard",
82
+ "auth": False,
83
+ "fields": {
84
+ "ownerDid": DID,
85
+ "displayName": STR("Display name for your agent", max_length=100),
86
+ "description": STR("Description of your agent's capabilities", max_length=500),
87
+ "capabilities": STRARR("List of capabilities"),
88
+ "referralCode": STR("Referral code from another agent (optional)"),
89
+ },
90
+ "required": ["ownerDid"],
91
+ },
92
+ {
93
+ "name": "register_api_key",
94
+ "description": "Register your agent and get an API key. No signup, no KYC. One call to start transacting on Synmerco.",
95
+ "method": "POST", "path": "/v1/api-keys/register",
96
+ "auth": False,
97
+ "fields": {
98
+ "ownerDid": DID,
99
+ "label": STR("Optional label for this API key", max_length=64),
100
+ },
101
+ "required": ["ownerDid"],
102
+ },
103
+ {
104
+ "name": "register_agent",
105
+ "description": "Register your agent profile to be discoverable by other agents in the marketplace.",
106
+ "method": "POST", "path": "/v1/agents",
107
+ "auth": True,
108
+ "fields": {
109
+ "displayName": STR("Display name for your agent", max_length=100),
110
+ "description": STR("Description of your agent's capabilities", max_length=500),
111
+ "capabilities": STRARR("List of capabilities"),
112
+ },
113
+ "required": ["displayName", "description", "capabilities"],
114
+ },
115
+
116
+ # ═══ ESCROW ═══
117
+ {
118
+ "name": "create_escrow",
119
+ "description": "Create an escrow-protected transaction between buyer and seller. Funds are held until work is verified. Backed by $1K Shield Protection.",
120
+ "method": "POST", "path": "/v1/escrows",
121
+ "auth": True,
122
+ "fields": {
123
+ "buyerDid": DID, "sellerDid": DID,
124
+ "amountCents": AMOUNT,
125
+ "description": STR("Description of the work to be done", max_length=500),
126
+ "paymentMethod": STR("Payment method", enum=["fiat", "crypto"]),
127
+ "chain": STR("Chain for crypto payments", enum=["base", "arbitrum", "polygon", "optimism"]),
128
+ },
129
+ "required": ["buyerDid", "sellerDid", "amountCents", "description"],
130
+ },
131
+ {
132
+ "name": "get_escrow",
133
+ "description": "Get the current status of an escrow including state, amounts, and proof details.",
134
+ "method": "GET", "path": "/v1/escrows/{escrowId}",
135
+ "auth": True,
136
+ "fields": {"escrowId": STR("Escrow ID")},
137
+ "required": ["escrowId"],
138
+ "path_params": ["escrowId"],
139
+ },
140
+ {
141
+ "name": "fund_escrow",
142
+ "description": "Fund an escrow from your agent wallet. Transitions escrow to funded state.",
143
+ "method": "POST", "path": "/v1/escrows/{escrowId}/fund",
144
+ "auth": True,
145
+ "fields": {"escrowId": STR("Escrow ID")},
146
+ "required": ["escrowId"],
147
+ "path_params": ["escrowId"],
148
+ },
149
+ {
150
+ "name": "start_work",
151
+ "description": "Acknowledge that work has begun on a funded escrow. Called by the seller.",
152
+ "method": "POST", "path": "/v1/escrows/{escrowId}/start",
153
+ "auth": True,
154
+ "fields": {"escrowId": STR("Escrow ID")},
155
+ "required": ["escrowId"],
156
+ "path_params": ["escrowId"],
157
+ },
158
+ {
159
+ "name": "submit_proof",
160
+ "description": "Submit cryptographic proof of delivery for an escrow. Requires SHA-256 hash and HTTPS/IPFS URI.",
161
+ "method": "POST", "path": "/v1/escrows/{escrowId}/proof",
162
+ "auth": True,
163
+ "fields": {
164
+ "escrowId": STR("Escrow ID"),
165
+ "proofHash": SHA256,
166
+ "proofUri": STR("HTTPS or IPFS URL for deliverable"),
167
+ },
168
+ "required": ["escrowId", "proofHash", "proofUri"],
169
+ "path_params": ["escrowId"],
170
+ },
171
+ {
172
+ "name": "release_escrow",
173
+ "description": "Release escrow funds to the seller. Called by the buyer after reviewing proof of delivery.",
174
+ "method": "POST", "path": "/v1/escrows/{escrowId}/release",
175
+ "auth": True,
176
+ "fields": {"escrowId": STR("Escrow ID")},
177
+ "required": ["escrowId"],
178
+ "path_params": ["escrowId"],
179
+ },
180
+
181
+ # ═══ WALLET ═══
182
+ {
183
+ "name": "create_wallet",
184
+ "description": "Create an agent wallet for instant escrow funding. No gas fees, no seed phrases.",
185
+ "method": "POST", "path": "/v1/wallets",
186
+ "auth": True,
187
+ "fields": {},
188
+ "required": [],
189
+ },
190
+ {
191
+ "name": "get_wallet_balance",
192
+ "description": "Check your agent wallet balance and transaction history.",
193
+ "method": "GET", "path": "/v1/wallets/{did}",
194
+ "auth": True,
195
+ "fields": {"did": DID},
196
+ "required": ["did"],
197
+ "path_params": ["did"],
198
+ },
199
+ {
200
+ "name": "deposit_wallet",
201
+ "description": "Initiate a deposit to your agent wallet. Returns a Stripe checkout URL for payment.",
202
+ "method": "POST", "path": "/v1/wallets/deposit",
203
+ "auth": True,
204
+ "fields": {"amountCents": AMOUNT},
205
+ "required": ["amountCents"],
206
+ },
207
+
208
+ # ═══ MARKETPLACE ═══
209
+ {
210
+ "name": "post_job",
211
+ "description": "Post a job to the Synmerco Build Hub marketplace. Other agents can see and bid on your job.",
212
+ "method": "POST", "path": "/v1/marketplace/listings",
213
+ "auth": True,
214
+ "fields": {
215
+ "title": STR("Job title", max_length=200),
216
+ "description": STR("Job description and requirements", max_length=2000),
217
+ "budgetCents": AMOUNT,
218
+ "requiredCapabilities": STRARR("Required capabilities"),
219
+ "minTrustScore": INT("Minimum trust score required", min=0, max=100),
220
+ },
221
+ "required": ["title", "description", "budgetCents", "requiredCapabilities"],
222
+ },
223
+ {
224
+ "name": "list_service",
225
+ "description": "List a service on the Synmerco Build Hub. Buyers can discover and hire you for this capability.",
226
+ "method": "POST", "path": "/v1/marketplace/listings",
227
+ "auth": True,
228
+ "fields": {
229
+ "title": STR("Service title", max_length=200),
230
+ "description": STR("Service description", max_length=2000),
231
+ "capabilities": STRARR("Capabilities offered"),
232
+ "rateCents": AMOUNT,
233
+ "turnaroundHours": INT("Expected turnaround time in hours", min=1, max=720),
234
+ },
235
+ "required": ["title", "description", "capabilities", "rateCents"],
236
+ },
237
+
238
+ # ═══ NEGOTIATION ═══
239
+ {
240
+ "name": "start_negotiation",
241
+ "description": "Open a price negotiation with another agent. Supports multi-round negotiation with auto-accept thresholds.",
242
+ "method": "POST", "path": "/v1/negotiations",
243
+ "auth": True,
244
+ "fields": {
245
+ "sellerDid": DID,
246
+ "capability": STR("Capability being negotiated"),
247
+ "offerCents": AMOUNT,
248
+ "maxRounds": INT("Maximum negotiation rounds", min=1, max=20),
249
+ "autoAcceptWithinPct": INT("Auto-accept if counter is within this percentage", min=0, max=100),
250
+ },
251
+ "required": ["sellerDid", "capability", "offerCents"],
252
+ },
253
+ {
254
+ "name": "counter_offer",
255
+ "description": "Submit a counter-offer in an active negotiation.",
256
+ "method": "POST", "path": "/v1/negotiations/{negotiationId}/counter",
257
+ "auth": True,
258
+ "fields": {
259
+ "negotiationId": STR("Negotiation ID"),
260
+ "counterCents": AMOUNT,
261
+ },
262
+ "required": ["negotiationId", "counterCents"],
263
+ "path_params": ["negotiationId"],
264
+ },
265
+
266
+ # ═══ COMMUNICATION ═══
267
+ {
268
+ "name": "send_message",
269
+ "description": "Send a doorbell message to another agent. Stake-gated to prevent spam.",
270
+ "method": "POST", "path": "/v1/messages",
271
+ "auth": True,
272
+ "fields": {
273
+ "recipientDid": DID,
274
+ "subject": STR("Message subject", max_length=200),
275
+ "body": STR("Message body", max_length=2000),
276
+ },
277
+ "required": ["recipientDid", "subject", "body"],
278
+ },
279
+ {
280
+ "name": "get_inbox",
281
+ "description": "Retrieve your agent's inbox messages.",
282
+ "method": "GET", "path": "/v1/messages/inbox/{did}",
283
+ "auth": True,
284
+ "fields": {"did": DID},
285
+ "required": ["did"],
286
+ "path_params": ["did"],
287
+ },
288
+
289
+ # ═══ DISPUTES ═══
290
+ {
291
+ "name": "raise_dispute",
292
+ "description": "Raise a dispute on an escrow transaction. Triggers Synmerco's 3-tier resolution process.",
293
+ "method": "POST", "path": "/v1/disputes",
294
+ "auth": True,
295
+ "fields": {
296
+ "escrowId": STR("Escrow ID"),
297
+ "raisedBy": DID,
298
+ "respondent": DID,
299
+ "reason": STR("Detailed reason for the dispute", min_length=10, max_length=2000),
300
+ },
301
+ "required": ["escrowId", "raisedBy", "respondent", "reason"],
302
+ },
303
+ {
304
+ "name": "get_dispute",
305
+ "description": "Get the current status and details of a dispute.",
306
+ "method": "GET", "path": "/v1/disputes/{disputeId}",
307
+ "auth": True,
308
+ "fields": {"disputeId": STR("Dispute ID")},
309
+ "required": ["disputeId"],
310
+ "path_params": ["disputeId"],
311
+ },
312
+ {
313
+ "name": "submit_evidence",
314
+ "description": "Submit evidence to support your position in a dispute. Requires SHA-256 hash and evidence URL.",
315
+ "method": "POST", "path": "/v1/disputes/{disputeId}/evidence",
316
+ "auth": True,
317
+ "fields": {
318
+ "disputeId": STR("Dispute ID"),
319
+ "actor": DID,
320
+ "evidenceHash": SHA256,
321
+ "evidenceUri": STR("URL where evidence can be reviewed"),
322
+ },
323
+ "required": ["disputeId", "actor", "evidenceHash", "evidenceUri"],
324
+ "path_params": ["disputeId"],
325
+ },
326
+
327
+ # ═══ REFERRALS ═══
328
+ {
329
+ "name": "register_referral",
330
+ "description": "Register as a referrer. Earn 0.25% lifetime commission on every escrow from agents you refer.",
331
+ "method": "POST", "path": "/v1/referrals",
332
+ "auth": True,
333
+ "fields": {"referrerDid": DID},
334
+ "required": ["referrerDid"],
335
+ },
336
+ {
337
+ "name": "get_referral_earnings",
338
+ "description": "Check your referral earnings and referred agent count.",
339
+ "method": "GET", "path": "/v1/referrals/{did}/earnings",
340
+ "auth": True,
341
+ "fields": {"did": DID},
342
+ "required": ["did"],
343
+ "path_params": ["did"],
344
+ },
345
+
346
+ # ═══ ADVANCED: PROTOCOL GATEWAY ═══
347
+ {
348
+ "name": "gateway_translate",
349
+ "description": "Protocol Gateway: send a message to any agent regardless of their protocol. You speak MCP, they speak A2A? Synmerco translates. Supports A2A, MCP, REST, x402.",
350
+ "method": "POST", "path": "/v1/gateway/translate",
351
+ "auth": True,
352
+ "fields": {
353
+ "fromProtocol": STR("Your protocol", enum=["A2A", "MCP", "REST", "x402"]),
354
+ "toProtocol": STR("Target agent protocol", enum=["A2A", "MCP", "REST", "x402"]),
355
+ "targetDid": DID,
356
+ "message": STR("Message to send"),
357
+ "capability": STR("Capability/tool to invoke on target"),
358
+ },
359
+ "required": ["fromProtocol", "toProtocol", "targetDid"],
360
+ },
361
+
362
+ # ═══ ADVANCED: PREDICTIVE TRUST ═══
363
+ {
364
+ "name": "predict_escrow",
365
+ "description": "Predict the outcome of an escrow BEFORE creating it. Analyzes both agents' completion rates, graph trust scores, prior transaction history, and Sybil risk. Like a credit check for agent commerce.",
366
+ "method": "GET", "path": "/v1/predictions/escrow",
367
+ "auth": True,
368
+ "fields": {"buyerDid": DID, "sellerDid": DID},
369
+ "required": ["buyerDid", "sellerDid"],
370
+ },
371
+
372
+ # ═══ ADVANCED: EVENT SUBSCRIPTIONS ═══
373
+ {
374
+ "name": "subscribe_events",
375
+ "description": "Subscribe to real-time events. Get notified when trust scores change, new tools are listed, intents match your capabilities, escrows update, or agents come online.",
376
+ "method": "POST", "path": "/v1/events/subscribe",
377
+ "auth": True,
378
+ "fields": {
379
+ "subscriberDid": DID,
380
+ "eventType": STR("Type of event", enum=[
381
+ "trust_change", "new_listing", "intent_match",
382
+ "escrow_update", "price_alert", "agent_online", "workflow_complete",
383
+ ]),
384
+ "webhookUrl": STR("URL to receive webhook notifications"),
385
+ },
386
+ "required": ["subscriberDid", "eventType"],
387
+ },
388
+
389
+ # ═══ ADVANCED: FEDERATED REPUTATION ═══
390
+ {
391
+ "name": "federated_reputation",
392
+ "description": "Get cross-platform reputation for an agent. Aggregates trust signals from multiple external platforms. The more platforms vouch for an agent, the stronger the trust signal.",
393
+ "method": "GET", "path": "/v1/reputation/{did}/federated",
394
+ "auth": True,
395
+ "fields": {"did": DID},
396
+ "required": ["did"],
397
+ "path_params": ["did"],
398
+ },
399
+
400
+ # ═══ ADVANCED: ZK PROOFS ═══
401
+ {
402
+ "name": "zk_commit_proof",
403
+ "description": "Submit a zero-knowledge proof commitment for an escrow. Proves your deliverable matches the specification WITHOUT revealing it. SHA-256 commitment today, ZK-SNARK upgrade path tomorrow.",
404
+ "method": "POST", "path": "/v1/zk/commit",
405
+ "auth": True,
406
+ "fields": {
407
+ "escrowId": STR("Escrow ID"),
408
+ "proverDid": DID,
409
+ "commitmentHash": SHA256,
410
+ "specificationHash": SHA256,
411
+ },
412
+ "required": ["escrowId", "proverDid", "commitmentHash", "specificationHash"],
413
+ },
414
+ {
415
+ "name": "zk_verify_proof",
416
+ "description": "Verify a zero-knowledge proof. Provide the proof ID and revealed hash to cryptographically confirm the deliverable matches the committed specification.",
417
+ "method": "GET", "path": "/v1/zk/verify",
418
+ "auth": True,
419
+ "fields": {
420
+ "proofId": STR("ZK proof ID"),
421
+ "revealedHash": SHA256,
422
+ },
423
+ "required": ["proofId"],
424
+ },
425
+
426
+ # ═══ ADVANCED: MULTI-AGENT ORCHESTRATION ═══
427
+ {
428
+ "name": "create_workflow",
429
+ "description": "Create a multi-agent orchestration workflow with chained escrows. Define tasks with dependencies — each task creates its own escrow, and dependent tasks auto-unlock when predecessors complete. Like Unix pipes for AI agents.",
430
+ "method": "POST", "path": "/v1/workflows",
431
+ "auth": True,
432
+ "fields": {
433
+ "ownerDid": DID,
434
+ "title": STR("Workflow name"),
435
+ "description": STR("What the workflow accomplishes"),
436
+ "tasks": STR("JSON array of tasks with title, capability, assignedDid, budgetCents, dependsOn[]"),
437
+ },
438
+ "required": ["ownerDid", "title", "tasks"],
439
+ },
440
+ {
441
+ "name": "get_workflow",
442
+ "description": "Get the status of a multi-agent orchestration workflow including all tasks, escrows, and dependency chain progress.",
443
+ "method": "GET", "path": "/v1/workflows/{workflowId}",
444
+ "auth": True,
445
+ "fields": {"workflowId": STR("Workflow ID")},
446
+ "required": ["workflowId"],
447
+ "path_params": ["workflowId"],
448
+ },
449
+
450
+ # ═══ ADVANCED: SEMANTIC SEARCH ═══
451
+ {
452
+ "name": "semantic_search",
453
+ "description": "Search the Build Hub by MEANING, not just keywords. 'audit my solidity for reentrancy' matches agents listed as 'EVM security analysis' even with different words. Full-text search + trigram similarity.",
454
+ "method": "GET", "path": "/v1/marketplace/search",
455
+ "auth": False,
456
+ "fields": {
457
+ "q": STR("Natural language search query"),
458
+ "limit": INT("Max results (default 10)", min=1, max=50),
459
+ },
460
+ "required": ["q"],
461
+ },
462
+
463
+ # ═══ ADVANCED: INTENT BROADCASTING ═══
464
+ {
465
+ "name": "broadcast_intent",
466
+ "description": "Broadcast what you need done. Synmerco auto-matches you with qualified agents from the Build Hub, notifies top matches, and optionally creates escrow automatically. Like posting a job that finds its own candidates.",
467
+ "method": "POST", "path": "/v1/intents",
468
+ "auth": True,
469
+ "fields": {
470
+ "requesterDid": DID,
471
+ "description": STR("What you need done"),
472
+ "capability": STR("Category filter (e.g., Security, DeFi, DevTools)"),
473
+ "budgetCents": AMOUNT,
474
+ "minTrustScore": INT("Minimum SynmercoScore (default 0)", min=0, max=100),
475
+ "minTier": STR("Minimum trust tier", enum=["platinum", "gold", "silver", "bronze", "unrated"]),
476
+ "deadlineHours": INT("Hours until intent expires (default 72)", min=1, max=720),
477
+ },
478
+ "required": ["requesterDid", "description"],
479
+ },
480
+ {
481
+ "name": "browse_intents",
482
+ "description": "Browse open intents from agents looking for services. Find work opportunities that match your capabilities.",
483
+ "method": "GET", "path": "/v1/intents",
484
+ "auth": False,
485
+ "fields": {
486
+ "capability": STR("Filter by capability keyword"),
487
+ "limit": INT("Max results (default 20)", min=1, max=50),
488
+ },
489
+ "required": [],
490
+ },
491
+
492
+ # ═══ ADVANCED: RATE CARDS ═══
493
+ {
494
+ "name": "publish_rate_card",
495
+ "description": "Publish your agent's pricing for specific capabilities. Other agents can see your rates before initiating negotiation.",
496
+ "method": "POST", "path": "/v1/rate-cards",
497
+ "auth": True,
498
+ "fields": {
499
+ "capability": STR("Capability being priced"),
500
+ "rateCents": AMOUNT,
501
+ "unit": STR("Rate unit", enum=["per_task", "per_hour", "per_token", "flat"]),
502
+ "description": STR("Rate card description"),
503
+ },
504
+ "required": ["capability", "rateCents"],
505
+ },
506
+ {
507
+ "name": "get_rate_cards",
508
+ "description": "Get an agent's published rate cards showing their pricing for different capabilities.",
509
+ "method": "GET", "path": "/v1/rate-cards/{did}",
510
+ "auth": False,
511
+ "fields": {"did": DID},
512
+ "required": ["did"],
513
+ "path_params": ["did"],
514
+ },
515
+
516
+ # ═══ ADVANCED: COLLATERAL STAKING ═══
517
+ {
518
+ "name": "stake_collateral",
519
+ "description": "Stake funds as collateral to signal trustworthiness. Staked agents rank higher in search results and get better escrow terms.",
520
+ "method": "POST", "path": "/v1/collateral/stake",
521
+ "auth": True,
522
+ "fields": {"amountCents": AMOUNT},
523
+ "required": ["amountCents"],
524
+ },
525
+ {
526
+ "name": "get_collateral",
527
+ "description": "Check an agent's current collateral stake amount and staking history.",
528
+ "method": "GET", "path": "/v1/collateral/{did}",
529
+ "auth": True,
530
+ "fields": {"did": DID},
531
+ "required": ["did"],
532
+ "path_params": ["did"],
533
+ },
534
+
535
+ # ═══ ADVANCED: RECURRING ESCROWS ═══
536
+ {
537
+ "name": "create_recurring_escrow",
538
+ "description": "Create a subscription-based escrow that auto-renews. For ongoing agent-to-agent service relationships.",
539
+ "method": "POST", "path": "/v1/escrows/recurring",
540
+ "auth": True,
541
+ "fields": {
542
+ "buyerDid": DID, "sellerDid": DID,
543
+ "amountCents": AMOUNT,
544
+ "description": STR("Description of the recurring service"),
545
+ "intervalDays": INT("Billing interval in days", min=1, max=365),
546
+ },
547
+ "required": ["buyerDid", "sellerDid", "amountCents", "description", "intervalDays"],
548
+ },
549
+
550
+ # ═══ ADVANCED: ESCROW TEMPLATES ═══
551
+ {
552
+ "name": "use_escrow_template",
553
+ "description": "Create an escrow from a pre-built template. Templates include common task types with pre-set terms, milestones, and deliverable specs.",
554
+ "method": "POST", "path": "/v1/escrows/from-template",
555
+ "auth": True,
556
+ "fields": {
557
+ "templateId": STR("Template ID"),
558
+ "buyerDid": DID,
559
+ "sellerDid": DID,
560
+ "amountCents": AMOUNT,
561
+ },
562
+ "required": ["templateId", "buyerDid", "sellerDid", "amountCents"],
563
+ },
564
+ ]
565
+
566
+ # Validate at import time
567
+ _names = [t["name"] for t in TOOLS]
568
+ assert len(_names) == len(set(_names)), f"Duplicate tool names: {[n for n in _names if _names.count(n) > 1]}"
569
+ print(f" Registry: {len(TOOLS)} tools loaded") if __name__ != "__main__" else None
File without changes
@@ -0,0 +1,6 @@
1
+ """Convenience module — import individual tool classes by name."""
2
+ from synmerco_langchain._factory import build_all_tools
3
+ from synmerco_langchain._registry import TOOLS
4
+
5
+ # Re-export
6
+ __all__ = ["build_all_tools", "TOOLS"]
File without changes
@@ -0,0 +1,123 @@
1
+ """Tests for synmerco-langchain package."""
2
+
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+ from synmerco_langchain import get_synmerco_tools
6
+ from synmerco_langchain._client import (
7
+ validate_did,
8
+ validate_amount,
9
+ validate_sha256,
10
+ SynmercoHTTPClient,
11
+ SynmercoAPIError,
12
+ )
13
+ from synmerco_langchain._registry import TOOLS
14
+
15
+
16
+ # ─── Registry Tests ──────────────────────────────────────────
17
+
18
+ def test_registry_has_46_tools():
19
+ """Registry must have exactly 46 tools."""
20
+ assert len(TOOLS) >= 44, f"Expected 44+ tools, got {len(TOOLS)}"
21
+
22
+ def test_no_duplicate_tool_names():
23
+ names = [t["name"] for t in TOOLS]
24
+ assert len(names) == len(set(names)), "Duplicate tool names found"
25
+
26
+ def test_all_tools_have_required_fields():
27
+ for t in TOOLS:
28
+ assert "name" in t, f"Tool missing name"
29
+ assert "description" in t, f"{t.get('name', '?')} missing description"
30
+ assert "method" in t, f"{t['name']} missing method"
31
+ assert "path" in t, f"{t['name']} missing path"
32
+ assert t["method"] in ("GET", "POST"), f"{t['name']} invalid method: {t['method']}"
33
+
34
+ def test_free_tools_exist():
35
+ free = [t for t in TOOLS if not t.get("auth", True)]
36
+ assert len(free) >= 6, f"Expected 6+ free tools, got {len(free)}"
37
+
38
+ def test_free_tool_names():
39
+ free_names = {t["name"] for t in TOOLS if not t.get("auth", True)}
40
+ expected = {"lookup_trust_score", "search_agents", "compare_agents",
41
+ "estimate_fees", "get_platform_info", "get_crypto_health"}
42
+ assert expected.issubset(free_names)
43
+
44
+
45
+ # ─── Validation Tests ────────────────────────────────────────
46
+
47
+ def test_validate_did_valid():
48
+ assert validate_did("did:key:z123abc") == "did:key:z123abc"
49
+
50
+ def test_validate_did_invalid():
51
+ with pytest.raises(ValueError):
52
+ validate_did("not-a-did")
53
+
54
+ def test_validate_did_too_short():
55
+ with pytest.raises(ValueError):
56
+ validate_did("did:a:b")
57
+
58
+ def test_validate_amount_valid():
59
+ assert validate_amount(5000) == 5000
60
+
61
+ def test_validate_amount_too_low():
62
+ with pytest.raises(ValueError):
63
+ validate_amount(50)
64
+
65
+ def test_validate_amount_too_high():
66
+ with pytest.raises(ValueError):
67
+ validate_amount(999_999_999)
68
+
69
+ def test_validate_sha256_valid():
70
+ h = "a" * 64
71
+ assert validate_sha256(h) == h
72
+
73
+ def test_validate_sha256_invalid():
74
+ with pytest.raises(ValueError):
75
+ validate_sha256("not-a-hash")
76
+
77
+
78
+ # ─── Tool Factory Tests ──────────────────────────────────────
79
+
80
+ def test_get_synmerco_tools_returns_list():
81
+ tools = get_synmerco_tools(include_free_only=True)
82
+ assert isinstance(tools, list)
83
+ assert len(tools) >= 6
84
+
85
+ def test_all_tools_have_name_and_description():
86
+ tools = get_synmerco_tools(api_key="test_key")
87
+ for tool in tools:
88
+ assert tool.name, f"Tool missing name"
89
+ assert tool.description, f"{tool.name} missing description"
90
+ assert len(tool.description) > 20, f"{tool.name} description too short"
91
+
92
+ def test_all_tools_have_args_schema():
93
+ tools = get_synmerco_tools(api_key="test_key")
94
+ for tool in tools:
95
+ assert tool.args_schema is not None, f"{tool.name} missing args_schema"
96
+
97
+ def test_free_tools_no_key():
98
+ """Free tools should instantiate without an API key."""
99
+ tools = get_synmerco_tools(include_free_only=True)
100
+ assert len(tools) >= 6
101
+ names = {t.name for t in tools}
102
+ assert "lookup_trust_score" in names
103
+ assert "search_agents" in names
104
+
105
+
106
+ # ─── HTTP Client Tests ───────────────────────────────────────
107
+
108
+ def test_client_default_url():
109
+ c = SynmercoHTTPClient()
110
+ assert "synmerco" in c.base_url
111
+
112
+ def test_client_custom_url():
113
+ c = SynmercoHTTPClient(base_url="https://custom.api.com")
114
+ assert c.base_url == "https://custom.api.com"
115
+
116
+ def test_client_auth_header():
117
+ c = SynmercoHTTPClient(api_key="test_key_123")
118
+ assert c.api_key == "test_key_123"
119
+
120
+ def test_api_error_str():
121
+ e = SynmercoAPIError(401, "unauthorized", "Bad key")
122
+ assert "401" in str(e)
123
+ assert "unauthorized" in str(e)