langchain-tensorust 0.1.0__py3-none-any.whl
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_tensorust/__init__.py +33 -0
- langchain_tensorust/_client.py +189 -0
- langchain_tensorust/py.typed +0 -0
- langchain_tensorust/toolkits.py +114 -0
- langchain_tensorust/tools.py +332 -0
- langchain_tensorust-0.1.0.dist-info/METADATA +63 -0
- langchain_tensorust-0.1.0.dist-info/RECORD +9 -0
- langchain_tensorust-0.1.0.dist-info/WHEEL +4 -0
- langchain_tensorust-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LangChain integration for the Tensorust Ontology.
|
|
2
|
+
|
|
3
|
+
Ownership, private credit and compliance lookups from US public records (SEC, state business
|
|
4
|
+
registries, county property rolls, FCC, federal awards, OFAC, CMS) as LangChain tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from langchain_tensorust._client import DEFAULT_BASE_URL, TensorustAPIWrapper
|
|
8
|
+
from langchain_tensorust.toolkits import TensorustToolkit
|
|
9
|
+
from langchain_tensorust.tools import (
|
|
10
|
+
TensorustContractorCredit,
|
|
11
|
+
TensorustDatasetSearch,
|
|
12
|
+
TensorustIDROffer,
|
|
13
|
+
TensorustLenderMarks,
|
|
14
|
+
TensorustLookupOrganization,
|
|
15
|
+
TensorustSanctionsScreen,
|
|
16
|
+
TensorustWhoOwnsHome,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DEFAULT_BASE_URL",
|
|
23
|
+
"TensorustAPIWrapper",
|
|
24
|
+
"TensorustContractorCredit",
|
|
25
|
+
"TensorustDatasetSearch",
|
|
26
|
+
"TensorustIDROffer",
|
|
27
|
+
"TensorustLenderMarks",
|
|
28
|
+
"TensorustLookupOrganization",
|
|
29
|
+
"TensorustSanctionsScreen",
|
|
30
|
+
"TensorustToolkit",
|
|
31
|
+
"TensorustWhoOwnsHome",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""HTTP client for the Tensorust Ontology endpoint (JSON-RPC ``tools/call``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from langchain_core.tools import ToolException
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://shop.tensorust.com/api/mcp"
|
|
15
|
+
_LOOPBACK = {"localhost", "127.0.0.1", "::1"}
|
|
16
|
+
_USER_AGENT = "langchain-tensorust"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TensorustAPIWrapper(BaseModel):
|
|
20
|
+
"""Calls the Tensorust Ontology over HTTPS.
|
|
21
|
+
|
|
22
|
+
The endpoint speaks MCP's JSON-RPC, so a plain ``tools/call`` POST works without an MCP
|
|
23
|
+
session or SDK. With no API key every tool answers with its free preview; with a
|
|
24
|
+
``tdk_...`` key (argument ``api_key`` or env ``TENSORUST_API_KEY``) the key is sent as a
|
|
25
|
+
bearer token and tools that support it return the full record.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from langchain_tensorust import TensorustAPIWrapper
|
|
30
|
+
|
|
31
|
+
api = TensorustAPIWrapper() # keyless: free previews
|
|
32
|
+
print(api.call("lookup_organization", {"name": "Invitation Homes"}))
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
api_key: SecretStr | None = Field(default=None)
|
|
39
|
+
"""Tensorust API key (``tdk_...``). Falls back to ``TENSORUST_API_KEY``. Optional."""
|
|
40
|
+
|
|
41
|
+
base_url: str = Field(default="")
|
|
42
|
+
"""Endpoint URL. Falls back to ``TENSORUST_API_URL``, then the public endpoint."""
|
|
43
|
+
|
|
44
|
+
timeout: float = 30.0
|
|
45
|
+
"""Request timeout in seconds."""
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="before")
|
|
48
|
+
@classmethod
|
|
49
|
+
def _from_env(cls, values: Any) -> Any:
|
|
50
|
+
if not isinstance(values, dict):
|
|
51
|
+
return values
|
|
52
|
+
if not values.get("api_key"):
|
|
53
|
+
key = os.environ.get("TENSORUST_API_KEY", "").strip()
|
|
54
|
+
if key:
|
|
55
|
+
values["api_key"] = key
|
|
56
|
+
if not values.get("base_url"):
|
|
57
|
+
values["base_url"] = os.environ.get("TENSORUST_API_URL", "").strip() or DEFAULT_BASE_URL
|
|
58
|
+
return values
|
|
59
|
+
|
|
60
|
+
@model_validator(mode="after")
|
|
61
|
+
def _check_url(self) -> TensorustAPIWrapper:
|
|
62
|
+
self.base_url = self.base_url.rstrip("/")
|
|
63
|
+
parsed = urlsplit(self.base_url)
|
|
64
|
+
ok = bool(parsed.hostname) and (
|
|
65
|
+
parsed.scheme == "https" or (parsed.scheme == "http" and parsed.hostname in _LOOPBACK)
|
|
66
|
+
)
|
|
67
|
+
if not ok:
|
|
68
|
+
# The key travels as a bearer token, so never send it over plain http to a remote host.
|
|
69
|
+
msg = f"Tensorust base_url must be an https:// URL with a host, got {self.base_url!r}"
|
|
70
|
+
raise ValueError(msg)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def has_key(self) -> bool:
|
|
75
|
+
"""True when an API key is configured (tools then ask for the full record)."""
|
|
76
|
+
return bool(self.api_key and self.api_key.get_secret_value().strip())
|
|
77
|
+
|
|
78
|
+
def headers(self) -> dict[str, str]:
|
|
79
|
+
"""Request headers, with the bearer token when a key is configured."""
|
|
80
|
+
headers = {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
"Accept": "application/json",
|
|
83
|
+
"User-Agent": _USER_AGENT,
|
|
84
|
+
}
|
|
85
|
+
if self.has_key and self.api_key is not None:
|
|
86
|
+
headers["Authorization"] = f"Bearer {self.api_key.get_secret_value().strip()}"
|
|
87
|
+
return headers
|
|
88
|
+
|
|
89
|
+
def with_full(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
"""Add ``full: true`` when a key is configured; keyless calls get the preview."""
|
|
91
|
+
return {**arguments, "full": True} if self.has_key else arguments
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def payload(tool: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
"""The JSON-RPC body for one ``tools/call``."""
|
|
96
|
+
return {
|
|
97
|
+
"jsonrpc": "2.0",
|
|
98
|
+
"id": 1,
|
|
99
|
+
"method": "tools/call",
|
|
100
|
+
"params": {"name": tool, "arguments": arguments},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def call(self, tool: str, arguments: dict[str, Any]) -> str:
|
|
104
|
+
"""Call one Tensorust tool and return its text answer.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ToolException: on a transport error, an HTTP error or a tool error, with a
|
|
108
|
+
message the agent can act on.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
response = httpx.post(
|
|
112
|
+
self.base_url,
|
|
113
|
+
json=self.payload(tool, arguments),
|
|
114
|
+
headers=self.headers(),
|
|
115
|
+
timeout=self.timeout,
|
|
116
|
+
)
|
|
117
|
+
except httpx.HTTPError as e:
|
|
118
|
+
msg = f"Tensorust request failed: {e}"
|
|
119
|
+
raise ToolException(msg) from e
|
|
120
|
+
return parse_response(response)
|
|
121
|
+
|
|
122
|
+
async def acall(self, tool: str, arguments: dict[str, Any]) -> str:
|
|
123
|
+
"""Async :meth:`call`."""
|
|
124
|
+
try:
|
|
125
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
126
|
+
response = await client.post(
|
|
127
|
+
self.base_url, json=self.payload(tool, arguments), headers=self.headers()
|
|
128
|
+
)
|
|
129
|
+
except httpx.HTTPError as e:
|
|
130
|
+
msg = f"Tensorust request failed: {e}"
|
|
131
|
+
raise ToolException(msg) from e
|
|
132
|
+
return parse_response(response)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def parse_response(response: httpx.Response) -> str:
|
|
136
|
+
"""Turn a JSON-RPC response into the tool's text, or raise ToolException."""
|
|
137
|
+
if not response.is_success:
|
|
138
|
+
label = {
|
|
139
|
+
401: "Tensorust authentication failed (401): check TENSORUST_API_KEY",
|
|
140
|
+
402: "This Tensorust call needs a paid key or payment (402)",
|
|
141
|
+
429: "Tensorust rate limit exceeded (429)",
|
|
142
|
+
}.get(response.status_code, f"Tensorust API error ({response.status_code})")
|
|
143
|
+
raise ToolException(label)
|
|
144
|
+
try:
|
|
145
|
+
body = response.json()
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
msg = "The Tensorust API returned a non-JSON response."
|
|
148
|
+
raise ToolException(msg) from e
|
|
149
|
+
if not isinstance(body, dict):
|
|
150
|
+
msg = "Unexpected response from the Tensorust API."
|
|
151
|
+
raise ToolException(msg)
|
|
152
|
+
if body.get("error"):
|
|
153
|
+
error = body["error"]
|
|
154
|
+
message = error.get("message") if isinstance(error, dict) else str(error)
|
|
155
|
+
raise ToolException(f"Tensorust API error: {message}")
|
|
156
|
+
result = body.get("result")
|
|
157
|
+
content = result.get("content") if isinstance(result, dict) else None
|
|
158
|
+
if not isinstance(content, list):
|
|
159
|
+
msg = "Unexpected response from the Tensorust API."
|
|
160
|
+
raise ToolException(msg)
|
|
161
|
+
text = "\n".join(str(part.get("text", "")) for part in content if isinstance(part, dict))
|
|
162
|
+
if isinstance(result, dict) and result.get("isError"):
|
|
163
|
+
raise ToolException(text or "The Tensorust tool returned an error.")
|
|
164
|
+
return text
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def merge_screen(name: str, sanctions: str, vendors: str) -> str:
|
|
168
|
+
"""Combine the OFAC search and the restricted-vendor screen into one JSON answer."""
|
|
169
|
+
result: dict[str, Any] = {"name": name}
|
|
170
|
+
try:
|
|
171
|
+
found = json.loads(sanctions)
|
|
172
|
+
result["sanctions"] = {
|
|
173
|
+
"source": "OFAC sanctions lists",
|
|
174
|
+
"total": found.get("total", 0),
|
|
175
|
+
"matches": found.get("items", []),
|
|
176
|
+
}
|
|
177
|
+
except (ValueError, AttributeError):
|
|
178
|
+
result["sanctions"] = {"error": sanctions}
|
|
179
|
+
try:
|
|
180
|
+
screen = json.loads(vendors)
|
|
181
|
+
keep = ("as_of", "lists_pulled", "matched_lines", "by_vendor", "by_rule")
|
|
182
|
+
section = {key: screen[key] for key in keep if key in screen}
|
|
183
|
+
section["flags"] = [
|
|
184
|
+
flag for item in screen.get("items", []) for flag in item.get("flags", [])
|
|
185
|
+
]
|
|
186
|
+
result["restricted_vendor_lists"] = section
|
|
187
|
+
except (ValueError, AttributeError, TypeError):
|
|
188
|
+
result["restricted_vendor_lists"] = {"error": vendors}
|
|
189
|
+
return json.dumps(result, indent=2)
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""TensorustToolkit: the curated tools, or every tool on the Tensorust MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from langchain_core.tools import BaseTool, BaseToolkit
|
|
9
|
+
from pydantic import ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from langchain_tensorust._client import TensorustAPIWrapper
|
|
12
|
+
from langchain_tensorust.tools import (
|
|
13
|
+
TensorustContractorCredit,
|
|
14
|
+
TensorustDatasetSearch,
|
|
15
|
+
TensorustIDROffer,
|
|
16
|
+
TensorustLenderMarks,
|
|
17
|
+
TensorustLookupOrganization,
|
|
18
|
+
TensorustSanctionsScreen,
|
|
19
|
+
TensorustWhoOwnsHome,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_CURATED = (
|
|
23
|
+
TensorustLookupOrganization,
|
|
24
|
+
TensorustWhoOwnsHome,
|
|
25
|
+
TensorustLenderMarks,
|
|
26
|
+
TensorustContractorCredit,
|
|
27
|
+
TensorustSanctionsScreen,
|
|
28
|
+
TensorustIDROffer,
|
|
29
|
+
TensorustDatasetSearch,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TensorustToolkit(BaseToolkit):
|
|
34
|
+
"""Ownership, private credit and compliance tools backed by the Tensorust Ontology.
|
|
35
|
+
|
|
36
|
+
`get_tools()` returns seven curated tools with typed inputs. `aget_mcp_tools()` loads
|
|
37
|
+
every tool the Tensorust MCP server exposes (about 40, including the FCC clearinghouse,
|
|
38
|
+
E-Rate, ROAD Act and research-security exposure tools) through `langchain-mcp-adapters`;
|
|
39
|
+
install it with `pip install "langchain-tensorust[mcp]"`.
|
|
40
|
+
|
|
41
|
+
Both work without a key (free previews). Set `TENSORUST_API_KEY` to a `tdk_...` key for
|
|
42
|
+
full records.
|
|
43
|
+
|
|
44
|
+
Setup:
|
|
45
|
+
```bash
|
|
46
|
+
pip install -U langchain-tensorust
|
|
47
|
+
export TENSORUST_API_KEY="tdk_..." # optional
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Instantiate:
|
|
51
|
+
```python
|
|
52
|
+
from langchain_tensorust import TensorustToolkit
|
|
53
|
+
|
|
54
|
+
toolkit = TensorustToolkit()
|
|
55
|
+
tools = toolkit.get_tools()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use within an agent:
|
|
59
|
+
```python
|
|
60
|
+
from langchain.agents import create_agent
|
|
61
|
+
|
|
62
|
+
agent = create_agent("anthropic:claude-sonnet-4-6", tools=toolkit.get_tools())
|
|
63
|
+
agent.invoke({"messages": [("user", "Who controls Invitation Homes?")]})
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
68
|
+
|
|
69
|
+
api_wrapper: TensorustAPIWrapper = Field(default_factory=TensorustAPIWrapper)
|
|
70
|
+
"""Client shared by every tool. Reads TENSORUST_API_KEY / TENSORUST_API_URL."""
|
|
71
|
+
|
|
72
|
+
include: Sequence[str] | None = None
|
|
73
|
+
"""Optional subset of tool names to return from `get_tools()`."""
|
|
74
|
+
|
|
75
|
+
def get_tools(self) -> list[BaseTool]:
|
|
76
|
+
"""The curated Tensorust tools, sharing this toolkit's client."""
|
|
77
|
+
tools: list[BaseTool] = [cls(api_wrapper=self.api_wrapper) for cls in _CURATED]
|
|
78
|
+
if self.include is not None:
|
|
79
|
+
wanted = set(self.include)
|
|
80
|
+
unknown = wanted - {t.name for t in tools}
|
|
81
|
+
if unknown:
|
|
82
|
+
msg = f"Unknown Tensorust tool names: {sorted(unknown)}"
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
tools = [t for t in tools if t.name in wanted]
|
|
85
|
+
return tools
|
|
86
|
+
|
|
87
|
+
def mcp_connection(self) -> dict[str, Any]:
|
|
88
|
+
"""The `langchain-mcp-adapters` connection for the Tensorust MCP server."""
|
|
89
|
+
headers = {
|
|
90
|
+
k: v
|
|
91
|
+
for k, v in self.api_wrapper.headers().items()
|
|
92
|
+
if k in {"Authorization", "User-Agent"}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
"transport": "streamable_http",
|
|
96
|
+
"url": self.api_wrapper.base_url,
|
|
97
|
+
"headers": headers,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async def aget_mcp_tools(self) -> list[BaseTool]:
|
|
101
|
+
"""Every tool on the Tensorust MCP server, as LangChain tools.
|
|
102
|
+
|
|
103
|
+
Requires `pip install "langchain-tensorust[mcp]"`.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
107
|
+
except ImportError as e: # pragma: no cover - exercised only without the extra
|
|
108
|
+
msg = (
|
|
109
|
+
"aget_mcp_tools needs langchain-mcp-adapters: "
|
|
110
|
+
'pip install "langchain-tensorust[mcp]"'
|
|
111
|
+
)
|
|
112
|
+
raise ImportError(msg) from e
|
|
113
|
+
client = MultiServerMCPClient({"tensorust": self.mcp_connection()}) # type: ignore[dict-item]
|
|
114
|
+
return list(await client.get_tools())
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""LangChain tools for the Tensorust Ontology: ownership, private credit and compliance lookups."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from langchain_core.callbacks import AsyncCallbackManagerForToolRun, CallbackManagerForToolRun
|
|
8
|
+
from langchain_core.tools import BaseTool, ToolException
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from langchain_tensorust._client import TensorustAPIWrapper, merge_screen
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _TensorustTool(BaseTool):
|
|
15
|
+
"""Shared plumbing: one Tensorust tool call per invocation, sync and async."""
|
|
16
|
+
|
|
17
|
+
api_wrapper: TensorustAPIWrapper = Field(default_factory=TensorustAPIWrapper)
|
|
18
|
+
"""Client for the Tensorust endpoint. Reads TENSORUST_API_KEY / TENSORUST_API_URL."""
|
|
19
|
+
|
|
20
|
+
handle_tool_error: bool = True
|
|
21
|
+
"""Return Tensorust errors (bad input, 401, 429) to the agent as text instead of raising."""
|
|
22
|
+
|
|
23
|
+
#: Name of the remote tool this class calls.
|
|
24
|
+
remote_tool: str = ""
|
|
25
|
+
|
|
26
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def _run(self, run_manager: CallbackManagerForToolRun | None = None, **kwargs: Any) -> str:
|
|
30
|
+
return self.api_wrapper.call(self.remote_tool, self._arguments(**kwargs))
|
|
31
|
+
|
|
32
|
+
async def _arun(
|
|
33
|
+
self, run_manager: AsyncCallbackManagerForToolRun | None = None, **kwargs: Any
|
|
34
|
+
) -> str:
|
|
35
|
+
return await self.api_wrapper.acall(self.remote_tool, self._arguments(**kwargs))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _required(value: str | None, what: str) -> str:
|
|
39
|
+
value = (value or "").strip()
|
|
40
|
+
if not value:
|
|
41
|
+
raise ToolException(f"{what} is required.")
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OrganizationInput(BaseModel):
|
|
46
|
+
"""Input for TensorustLookupOrganization."""
|
|
47
|
+
|
|
48
|
+
name: str = Field(
|
|
49
|
+
description="Organization name, e.g. 'Invitation Homes' or 'Lockheed Martin'."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TensorustLookupOrganization(_TensorustTool):
|
|
54
|
+
"""Find out who owns or controls a US company or organization.
|
|
55
|
+
|
|
56
|
+
Resolves a name to its legal entity or control group and returns its data sources, linked
|
|
57
|
+
objects by type (subsidiaries, parent, homes owned, FCC licenses, federal awards, lobbying)
|
|
58
|
+
and the federal rules that apply to it, with other close name matches.
|
|
59
|
+
|
|
60
|
+
Setup:
|
|
61
|
+
```bash
|
|
62
|
+
pip install -U langchain-tensorust
|
|
63
|
+
export TENSORUST_API_KEY="tdk_..." # optional: without it you get the free preview
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Invocation:
|
|
67
|
+
```python
|
|
68
|
+
from langchain_tensorust import TensorustLookupOrganization
|
|
69
|
+
|
|
70
|
+
tool = TensorustLookupOrganization()
|
|
71
|
+
tool.invoke({"name": "Invitation Homes"})
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: str = "tensorust_lookup_organization"
|
|
76
|
+
description: str = (
|
|
77
|
+
"Look up who owns or controls a US company or organization from public records "
|
|
78
|
+
"(SEC subsidiary lists, state business registries, county property rolls, FCC, "
|
|
79
|
+
"federal awards). Input: the organization name. Returns JSON with the parent or "
|
|
80
|
+
"control group, linked subsidiaries, homes, licenses and awards, and the federal "
|
|
81
|
+
"rules that apply to it."
|
|
82
|
+
)
|
|
83
|
+
args_schema: type[BaseModel] = OrganizationInput
|
|
84
|
+
remote_tool: str = "lookup_organization"
|
|
85
|
+
|
|
86
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
87
|
+
return self.api_wrapper.with_full(
|
|
88
|
+
{"name": _required(kwargs.get("name"), "An organization name")}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class HomeInput(BaseModel):
|
|
93
|
+
"""Input for TensorustWhoOwnsHome."""
|
|
94
|
+
|
|
95
|
+
address: str = Field(description="Street address, e.g. '2746 Lofty Elm St'.")
|
|
96
|
+
zip_code: str = Field(description="5-digit ZIP code, e.g. '77494'.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TensorustWhoOwnsHome(_TensorustTool):
|
|
100
|
+
"""Find the owner of record of a US single-family home and its parent control group.
|
|
101
|
+
|
|
102
|
+
Coverage is entity-owned homes in 17 large counties (Houston, Dallas, Fort Worth,
|
|
103
|
+
San Antonio, Austin, Phoenix, Las Vegas, Atlanta, Tampa, Orlando, Jacksonville,
|
|
104
|
+
Charlotte, Memphis and others).
|
|
105
|
+
|
|
106
|
+
Invocation:
|
|
107
|
+
```python
|
|
108
|
+
from langchain_tensorust import TensorustWhoOwnsHome
|
|
109
|
+
|
|
110
|
+
TensorustWhoOwnsHome().invoke({"address": "2746 Lofty Elm St", "zip_code": "77494"})
|
|
111
|
+
```
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
name: str = "tensorust_who_owns_home"
|
|
115
|
+
description: str = (
|
|
116
|
+
"Find the owner of record of a US single-family home by street address and ZIP, and, "
|
|
117
|
+
"when the owner is a company, its parent control group, how many homes the group holds "
|
|
118
|
+
"and whether it counts as a large institutional investor (350+ homes)."
|
|
119
|
+
)
|
|
120
|
+
args_schema: type[BaseModel] = HomeInput
|
|
121
|
+
remote_tool: str = "who_owns_home"
|
|
122
|
+
|
|
123
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"address": _required(kwargs.get("address"), "A street address"),
|
|
126
|
+
"zip": _required(kwargs.get("zip_code"), "A ZIP code"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CompanyInput(BaseModel):
|
|
131
|
+
"""Input for the private credit tools."""
|
|
132
|
+
|
|
133
|
+
company: str = Field(description="Company name, e.g. 'Pluralsight'.")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TensorustLenderMarks(_TensorustTool):
|
|
137
|
+
"""See what private credit lenders (BDCs) say a company's first-lien loan is worth.
|
|
138
|
+
|
|
139
|
+
Returns the fair value each BDC fund holding the loan reported in its latest 10-Q, as a
|
|
140
|
+
share of par, with the manager and the gap between the lowest and highest mark.
|
|
141
|
+
|
|
142
|
+
Invocation:
|
|
143
|
+
```python
|
|
144
|
+
from langchain_tensorust import TensorustLenderMarks
|
|
145
|
+
|
|
146
|
+
TensorustLenderMarks().invoke({"company": "Pluralsight"})
|
|
147
|
+
```
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
name: str = "tensorust_lender_marks"
|
|
151
|
+
description: str = (
|
|
152
|
+
"Get what private credit lenders (BDC funds such as Ares, Blue Owl, Golub) marked a "
|
|
153
|
+
"private company's first-lien loan at in their latest 10-Q filings, as a share of par, "
|
|
154
|
+
"and how far the lenders disagree. A low or widely split mark is an early sign of "
|
|
155
|
+
"credit stress. Input: the borrower's name."
|
|
156
|
+
)
|
|
157
|
+
args_schema: type[BaseModel] = CompanyInput
|
|
158
|
+
remote_tool: str = "lender_marks"
|
|
159
|
+
|
|
160
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
161
|
+
return self.api_wrapper.with_full(
|
|
162
|
+
{"name": _required(kwargs.get("company"), "A company name")}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ContractorInput(BaseModel):
|
|
167
|
+
"""Input for TensorustContractorCredit."""
|
|
168
|
+
|
|
169
|
+
company: str = Field(
|
|
170
|
+
description="Contractor name, 12-character UEI or 9-digit EIN, e.g. 'Peraton'."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TensorustContractorCredit(_TensorustTool):
|
|
175
|
+
"""Check the credit health of a US federal contractor financed by private credit.
|
|
176
|
+
|
|
177
|
+
Invocation:
|
|
178
|
+
```python
|
|
179
|
+
from langchain_tensorust import TensorustContractorCredit
|
|
180
|
+
|
|
181
|
+
TensorustContractorCredit().invoke({"company": "Peraton"})
|
|
182
|
+
```
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
name: str = "tensorust_contractor_credit"
|
|
186
|
+
description: str = (
|
|
187
|
+
"Check the credit health of a US federal contractor: its federal obligations by agency, "
|
|
188
|
+
"what each private credit lender marks its loan at and how the mark moved, its "
|
|
189
|
+
"headcount trend and stress signals. Input: contractor name, UEI or EIN."
|
|
190
|
+
)
|
|
191
|
+
args_schema: type[BaseModel] = ContractorInput
|
|
192
|
+
remote_tool: str = "contractor_credit"
|
|
193
|
+
|
|
194
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
195
|
+
return self.api_wrapper.with_full(
|
|
196
|
+
{"name": _required(kwargs.get("company"), "A contractor name")}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ScreenInput(BaseModel):
|
|
201
|
+
"""Input for TensorustSanctionsScreen."""
|
|
202
|
+
|
|
203
|
+
name: str = Field(description="Company or vendor name, e.g. 'Hikvision'.")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TensorustSanctionsScreen(_TensorustTool):
|
|
207
|
+
"""Screen a company or vendor name against OFAC and the US restricted-vendor lists.
|
|
208
|
+
|
|
209
|
+
Checks OFAC sanctions and the DoD 1260H, Section 889, FCC Covered List, UFLPA and
|
|
210
|
+
BIOSECURE lists, and returns each hit with the rule and citation. A screen is a starting
|
|
211
|
+
point for review, not a legal determination.
|
|
212
|
+
|
|
213
|
+
Invocation:
|
|
214
|
+
```python
|
|
215
|
+
from langchain_tensorust import TensorustSanctionsScreen
|
|
216
|
+
|
|
217
|
+
TensorustSanctionsScreen().invoke({"name": "Hikvision"})
|
|
218
|
+
```
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
name: str = "tensorust_sanctions_screen"
|
|
222
|
+
description: str = (
|
|
223
|
+
"Screen a company or vendor name against the OFAC sanctions lists and the US "
|
|
224
|
+
"restricted-vendor lists (DoD 1260H, Section 889, FCC Covered List, UFLPA, BIOSECURE). "
|
|
225
|
+
"Returns JSON with each match, the rule it triggers and the citation."
|
|
226
|
+
)
|
|
227
|
+
args_schema: type[BaseModel] = ScreenInput
|
|
228
|
+
remote_tool: str = "query_ontology"
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _screen_args(name: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
232
|
+
return {"name": name, "type": "Sanction", "limit": 10}, {"lines": [{"vendor": name}]}
|
|
233
|
+
|
|
234
|
+
def _run(self, run_manager: CallbackManagerForToolRun | None = None, **kwargs: Any) -> str:
|
|
235
|
+
name = _required(kwargs.get("name"), "A company or vendor name")
|
|
236
|
+
sanctions_args, vendor_args = self._screen_args(name)
|
|
237
|
+
sanctions = self.api_wrapper.call("query_ontology", sanctions_args)
|
|
238
|
+
vendors = self.api_wrapper.call("exposure_screen_purchases", vendor_args)
|
|
239
|
+
return merge_screen(name, sanctions, vendors)
|
|
240
|
+
|
|
241
|
+
async def _arun(
|
|
242
|
+
self, run_manager: AsyncCallbackManagerForToolRun | None = None, **kwargs: Any
|
|
243
|
+
) -> str:
|
|
244
|
+
name = _required(kwargs.get("name"), "A company or vendor name")
|
|
245
|
+
sanctions_args, vendor_args = self._screen_args(name)
|
|
246
|
+
sanctions = await self.api_wrapper.acall("query_ontology", sanctions_args)
|
|
247
|
+
vendors = await self.api_wrapper.acall("exposure_screen_purchases", vendor_args)
|
|
248
|
+
return merge_screen(name, sanctions, vendors)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class IDROfferInput(BaseModel):
|
|
252
|
+
"""Input for TensorustIDROffer."""
|
|
253
|
+
|
|
254
|
+
code: str = Field(description="CPT or HCPCS code, e.g. '99284'.")
|
|
255
|
+
payer: str | None = Field(
|
|
256
|
+
default=None, description="Payer email domain or common name, e.g. 'uhc.com' or 'Aetna'."
|
|
257
|
+
)
|
|
258
|
+
state: str | None = Field(default=None, description="Two-letter state of service, e.g. 'TX'.")
|
|
259
|
+
qpa: float | None = Field(
|
|
260
|
+
default=None,
|
|
261
|
+
description="Qualifying payment amount in dollars, to get the band in dollars.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TensorustIDROffer(_TensorustTool):
|
|
266
|
+
"""No Surprises Act arbitration (federal IDR) outcomes for a billing code.
|
|
267
|
+
|
|
268
|
+
Invocation:
|
|
269
|
+
```python
|
|
270
|
+
from langchain_tensorust import TensorustIDROffer
|
|
271
|
+
|
|
272
|
+
TensorustIDROffer().invoke({"code": "99284", "payer": "uhc.com", "state": "TX"})
|
|
273
|
+
```
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
name: str = "tensorust_idr_offer"
|
|
277
|
+
description: str = (
|
|
278
|
+
"Look up federal No Surprises Act arbitration (IDR) outcomes from the CMS public use "
|
|
279
|
+
"file: for a CPT/HCPCS code, payer and state, how many disputes were decided, how often "
|
|
280
|
+
"the provider won and which offer band (as multiples of the QPA) won."
|
|
281
|
+
)
|
|
282
|
+
args_schema: type[BaseModel] = IDROfferInput
|
|
283
|
+
remote_tool: str = "idr_offer"
|
|
284
|
+
|
|
285
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
286
|
+
arguments: dict[str, Any] = {"code": _required(kwargs.get("code"), "A CPT or HCPCS code")}
|
|
287
|
+
payer = (kwargs.get("payer") or "").strip()
|
|
288
|
+
state = (kwargs.get("state") or "").strip()
|
|
289
|
+
if payer:
|
|
290
|
+
arguments["payer"] = payer
|
|
291
|
+
if state:
|
|
292
|
+
arguments["state"] = state.upper()
|
|
293
|
+
if kwargs.get("qpa") is not None:
|
|
294
|
+
arguments["qpa"] = kwargs["qpa"]
|
|
295
|
+
return arguments
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class DatasetSearchInput(BaseModel):
|
|
299
|
+
"""Input for TensorustDatasetSearch."""
|
|
300
|
+
|
|
301
|
+
query: str = Field(
|
|
302
|
+
description="Words to match, e.g. 'going concern', 'E-Rate', 'BDC non-accrual'."
|
|
303
|
+
)
|
|
304
|
+
limit: int = Field(default=10, ge=1, le=50, description="Maximum datasets to return.")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TensorustDatasetSearch(_TensorustTool):
|
|
308
|
+
"""Search the Tensorust catalog of entity-level datasets built from public records.
|
|
309
|
+
|
|
310
|
+
Invocation:
|
|
311
|
+
```python
|
|
312
|
+
from langchain_tensorust import TensorustDatasetSearch
|
|
313
|
+
|
|
314
|
+
TensorustDatasetSearch().invoke({"query": "BDC non-accrual"})
|
|
315
|
+
```
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
name: str = "tensorust_dataset_search"
|
|
319
|
+
description: str = (
|
|
320
|
+
"Search a catalog of entity-level datasets built from US public records (SEC, "
|
|
321
|
+
"USAspending, FCC, CMS, EPA, DOL, county rolls): money owed, at risk, unclaimed or "
|
|
322
|
+
"about to expire, one row per organization. Returns each dataset's name, row count, "
|
|
323
|
+
"page and free sample CSV link."
|
|
324
|
+
)
|
|
325
|
+
args_schema: type[BaseModel] = DatasetSearchInput
|
|
326
|
+
remote_tool: str = "list_datasets"
|
|
327
|
+
|
|
328
|
+
def _arguments(self, **kwargs: Any) -> dict[str, Any]:
|
|
329
|
+
return {
|
|
330
|
+
"query": _required(kwargs.get("query"), "A search query"),
|
|
331
|
+
"limit": int(kwargs.get("limit") or 10),
|
|
332
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-tensorust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for the Tensorust Ontology: who owns or controls US companies and homes, private credit loan marks, contractor credit, OFAC and banned-vendor screens, from public records.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mattbusel/langchain-tensorust
|
|
6
|
+
Project-URL: Documentation, https://github.com/Mattbusel/langchain-tensorust/blob/main/docs/index.md
|
|
7
|
+
Project-URL: Changelog, https://github.com/Mattbusel/langchain-tensorust/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/Mattbusel/langchain-tensorust/issues
|
|
9
|
+
Author: Matthew Busel
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,kyb,langchain,mcp,ofac,ownership,private-credit,sanctions,tools
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx<1,>=0.27
|
|
24
|
+
Requires-Dist: langchain-core<2,>=1.0
|
|
25
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: langchain-mcp-adapters>=0.1; extra == 'mcp'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# langchain-tensorust
|
|
31
|
+
|
|
32
|
+
LangChain tools for the [Tensorust Ontology](https://tensorust.com): ownership, private
|
|
33
|
+
credit and compliance lookups built from US public records (SEC subsidiary lists and BDC
|
|
34
|
+
schedules of investments, state business registries, county property rolls, FCC, federal
|
|
35
|
+
awards, OFAC, the CMS Federal IDR file).
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -U langchain-tensorust
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from langchain.agents import create_agent
|
|
43
|
+
from langchain_tensorust import TensorustToolkit
|
|
44
|
+
|
|
45
|
+
agent = create_agent("anthropic:claude-sonnet-4-6", tools=TensorustToolkit().get_tools())
|
|
46
|
+
agent.invoke({"messages": [("user", "Who controls Invitation Homes, and what do lenders mark Pluralsight's loan at?")]})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Tool | Answers |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `TensorustLookupOrganization` | Who owns or controls a company: parent, control group, subsidiaries, homes, FCC licenses, awards, rules that apply |
|
|
52
|
+
| `TensorustWhoOwnsHome` | Owner of record of a US single-family home and its parent control group |
|
|
53
|
+
| `TensorustLenderMarks` | What each BDC lender marks a private company's first-lien loan at, and the spread |
|
|
54
|
+
| `TensorustContractorCredit` | Credit health of a federal contractor: obligations, lender marks, headcount trend |
|
|
55
|
+
| `TensorustSanctionsScreen` | OFAC plus DoD 1260H, Section 889, FCC Covered List, UFLPA and BIOSECURE screen |
|
|
56
|
+
| `TensorustIDROffer` | No Surprises Act arbitration outcomes for a CPT code, payer and state |
|
|
57
|
+
| `TensorustDatasetSearch` | Search 145 entity-level datasets built from public records |
|
|
58
|
+
|
|
59
|
+
No key is needed: every tool answers with a free preview. Set `TENSORUST_API_KEY` to a
|
|
60
|
+
`tdk_...` key for full records. `await TensorustToolkit().aget_mcp_tools()` (with the `mcp`
|
|
61
|
+
extra) loads every tool on the Tensorust MCP server, about 40.
|
|
62
|
+
|
|
63
|
+
Docs: https://github.com/Mattbusel/langchain-tensorust/blob/main/docs/index.md
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
langchain_tensorust/__init__.py,sha256=UnA-ANAfNBeXMz2OA8pB7pW6xkRWQE9Ru7XVouDt__8,957
|
|
2
|
+
langchain_tensorust/_client.py,sha256=wcB1npaWia-OBkC5mLjZ3cfaNUYpG7YQoSCLa-200QM,7604
|
|
3
|
+
langchain_tensorust/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
langchain_tensorust/toolkits.py,sha256=54KKF9ohyBXvclAi-pdvZ1XmRKFpE82Ztted9HhUoTk,4019
|
|
5
|
+
langchain_tensorust/tools.py,sha256=Cb9nhQotWADopohNKANFVX1XqYRNXG9hFjTEd7D4Oa0,12601
|
|
6
|
+
langchain_tensorust-0.1.0.dist-info/METADATA,sha256=Jk1I9fZme3EbrRieu0xI6-wb1lHUqe-AtqoV2MRuRaE,3283
|
|
7
|
+
langchain_tensorust-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
langchain_tensorust-0.1.0.dist-info/licenses/LICENSE,sha256=TpCqwl9Iq4EFUnYGbR2dGUSmF5tDy-hCISSPJasaP10,1070
|
|
9
|
+
langchain_tensorust-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Busel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|