tensorfeed 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/PKG-INFO +30 -2
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/README.md +28 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/pyproject.toml +2 -2
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/__init__.py +1 -1
- tensorfeed-2.1.0/tensorfeed/_premium_tools.py +341 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/crewai.py +137 -3
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/langchain.py +179 -4
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed.egg-info/PKG-INFO +30 -2
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed.egg-info/SOURCES.txt +3 -1
- tensorfeed-2.1.0/tests/test_premium_tools.py +213 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/setup.cfg +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/client.py +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/llamaindex.py +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed/web3_signer.py +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed.egg-info/dependency_links.txt +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed.egg-info/requires.txt +0 -0
- {tensorfeed-2.0.0 → tensorfeed-2.1.0}/tensorfeed.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tensorfeed
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
|
|
5
5
|
Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://tensorfeed.ai
|
|
@@ -197,6 +197,34 @@ print(tf.balance())
|
|
|
197
197
|
rec = tf.routing(task="code")
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
## Framework Tools (LangChain, CrewAI)
|
|
201
|
+
|
|
202
|
+
Drop TensorFeed into an agent framework as ready-made tools.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install 'tensorfeed[langchain]' # or 'tensorfeed[crewai]'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from tensorfeed.langchain import tensorfeed_tools, tensorfeed_premium_tools
|
|
210
|
+
|
|
211
|
+
# Five free tools (news, status, attention, harnesses, routing preview)
|
|
212
|
+
free = tensorfeed_tools()
|
|
213
|
+
|
|
214
|
+
# Free plus the paid catalog (whats_new, full routing, compare models,
|
|
215
|
+
# cost projection, news search, provider deep dive, status leaderboard)
|
|
216
|
+
all_tools = tensorfeed_tools(token="YOUR_TOKEN", include_premium=True)
|
|
217
|
+
|
|
218
|
+
# Just the paid catalog
|
|
219
|
+
paid = tensorfeed_premium_tools(token="YOUR_TOKEN")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
CrewAI is identical: `from tensorfeed.crewai import tensorfeed_tools, tensorfeed_premium_tools`.
|
|
223
|
+
|
|
224
|
+
The default `tensorfeed_tools()` stays free-only and unchanged, so existing code keeps working. Premium is opt-in via `include_premium=True`.
|
|
225
|
+
|
|
226
|
+
**Payment posture.** The premium tools never move funds. Attach them with or without a token: called without spendable credits they return a short, actionable guidance string (explaining that an operator provisions credits out of band) instead of paying or raising. There is no code path from a tool call to the wallet or the web3 signer. Credit purchases stay an explicit, per-action operator decision, by design. This is enforced in one shared module and covered by tests, including a static guard that fails if a future change introduces an autonomous-payment path.
|
|
227
|
+
|
|
200
228
|
## Error Handling
|
|
201
229
|
|
|
202
230
|
```python
|
|
@@ -164,6 +164,34 @@ print(tf.balance())
|
|
|
164
164
|
rec = tf.routing(task="code")
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
## Framework Tools (LangChain, CrewAI)
|
|
168
|
+
|
|
169
|
+
Drop TensorFeed into an agent framework as ready-made tools.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pip install 'tensorfeed[langchain]' # or 'tensorfeed[crewai]'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from tensorfeed.langchain import tensorfeed_tools, tensorfeed_premium_tools
|
|
177
|
+
|
|
178
|
+
# Five free tools (news, status, attention, harnesses, routing preview)
|
|
179
|
+
free = tensorfeed_tools()
|
|
180
|
+
|
|
181
|
+
# Free plus the paid catalog (whats_new, full routing, compare models,
|
|
182
|
+
# cost projection, news search, provider deep dive, status leaderboard)
|
|
183
|
+
all_tools = tensorfeed_tools(token="YOUR_TOKEN", include_premium=True)
|
|
184
|
+
|
|
185
|
+
# Just the paid catalog
|
|
186
|
+
paid = tensorfeed_premium_tools(token="YOUR_TOKEN")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
CrewAI is identical: `from tensorfeed.crewai import tensorfeed_tools, tensorfeed_premium_tools`.
|
|
190
|
+
|
|
191
|
+
The default `tensorfeed_tools()` stays free-only and unchanged, so existing code keeps working. Premium is opt-in via `include_premium=True`.
|
|
192
|
+
|
|
193
|
+
**Payment posture.** The premium tools never move funds. Attach them with or without a token: called without spendable credits they return a short, actionable guidance string (explaining that an operator provisions credits out of band) instead of paying or raising. There is no code path from a tool call to the wallet or the web3 signer. Credit purchases stay an explicit, per-action operator decision, by design. This is enforced in one shared module and covered by tests, including a static guard that fails if a future change introduces an autonomous-payment path.
|
|
194
|
+
|
|
167
195
|
## Error Handling
|
|
168
196
|
|
|
169
197
|
```python
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tensorfeed"
|
|
7
|
-
version = "2.
|
|
8
|
-
description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)"
|
|
7
|
+
version = "2.1.0"
|
|
8
|
+
description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Shared premium-tool plumbing for the framework adapters.
|
|
2
|
+
|
|
3
|
+
Internal module. Powers ``tensorfeed.langchain.tensorfeed_premium_tools()``
|
|
4
|
+
and ``tensorfeed.crewai.tensorfeed_premium_tools()``. It is framework
|
|
5
|
+
agnostic: it owns the canonical tool descriptions, the result
|
|
6
|
+
formatters, and the one piece of behavior that must never drift between
|
|
7
|
+
frameworks, the payment posture.
|
|
8
|
+
|
|
9
|
+
Payment posture (load-bearing, do not weaken):
|
|
10
|
+
|
|
11
|
+
These tools never move funds. A premium call made without a valid
|
|
12
|
+
token returns a clear, actionable guidance string explaining how an
|
|
13
|
+
operator provisions credits out of band. The tool does not, and will
|
|
14
|
+
not, initiate a USDC transfer on its own. Autonomous payment from
|
|
15
|
+
inside a tool call is a deliberate non-feature: credit purchases stay
|
|
16
|
+
an explicit, per-action operator decision. Centralizing this here (and
|
|
17
|
+
testing it once) keeps every framework binding honest.
|
|
18
|
+
|
|
19
|
+
Each adapter writes its own thin, explicitly typed function or tool
|
|
20
|
+
class so the framework gets a good argument schema for the model, then
|
|
21
|
+
routes the call through ``safe_call`` with the shared formatter. The
|
|
22
|
+
descriptions are shared verbatim so LangChain and CrewAI surface
|
|
23
|
+
identical copy to the agent.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Callable
|
|
29
|
+
|
|
30
|
+
from .client import PaymentRequired, RateLimited, TensorFeedError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# The exact, actionable message an agent sees when a premium tool is
|
|
34
|
+
# called without spendable credits. It tells the model what is true
|
|
35
|
+
# (this needs credits), what will not happen (no automatic payment),
|
|
36
|
+
# and what the path is (an operator provisions a token out of band).
|
|
37
|
+
PREMIUM_PAYMENT_GUIDANCE = (
|
|
38
|
+
"This is a paid TensorFeed endpoint (1 credit) and no spendable "
|
|
39
|
+
"credit token is available. This tool will not buy credits or move "
|
|
40
|
+
"any funds on its own, by design. To enable it, an operator must "
|
|
41
|
+
"provision credits out of band (buy_credits() then confirm() with a "
|
|
42
|
+
"USDC payment on Base, or the documented x402 flow) and pass the "
|
|
43
|
+
"resulting token when constructing the tools, for example "
|
|
44
|
+
"tensorfeed_premium_tools(token='...'). Until then, prefer the free "
|
|
45
|
+
"TensorFeed tools (news, status, attention, harnesses, routing "
|
|
46
|
+
"preview) for this question."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _billing_line(result: dict[str, Any]) -> str:
|
|
51
|
+
"""One-line cost footer if the response carries billing metadata."""
|
|
52
|
+
billing = result.get("billing")
|
|
53
|
+
if not isinstance(billing, dict):
|
|
54
|
+
return ""
|
|
55
|
+
charged = billing.get("credits_charged")
|
|
56
|
+
remaining = billing.get("balance")
|
|
57
|
+
bits = []
|
|
58
|
+
if charged is not None:
|
|
59
|
+
bits.append(f"{charged} credit(s) charged")
|
|
60
|
+
if remaining is not None:
|
|
61
|
+
bits.append(f"{remaining} remaining")
|
|
62
|
+
return f"\n[{', '.join(bits)}]" if bits else ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def safe_call(
|
|
66
|
+
invoke: Callable[[], dict[str, Any]],
|
|
67
|
+
formatter: Callable[[dict[str, Any]], str],
|
|
68
|
+
) -> str:
|
|
69
|
+
"""Run a premium client call and return agent-readable text.
|
|
70
|
+
|
|
71
|
+
Never raises for the expected money and quota cases. A raised
|
|
72
|
+
exception inside an agent tool call usually aborts the agent or
|
|
73
|
+
produces an opaque trace; a structured guidance string lets the
|
|
74
|
+
model reason about what to do next (typically: tell the user it
|
|
75
|
+
needs credits, or fall back to a free tool).
|
|
76
|
+
|
|
77
|
+
* no token / token rejected (402): the payment guidance string
|
|
78
|
+
* rate limited (429): a short back-off message
|
|
79
|
+
* other API error: a concise one-line error
|
|
80
|
+
* success: the tool's own formatter output
|
|
81
|
+
|
|
82
|
+
The PaymentRequired and missing-token paths are exactly where an
|
|
83
|
+
autonomous-payment bug would hide. There is intentionally no code
|
|
84
|
+
path here that touches the wallet or the web3 signer.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
result = invoke()
|
|
88
|
+
except PaymentRequired as e:
|
|
89
|
+
hint = ""
|
|
90
|
+
payload = getattr(e, "payload", None)
|
|
91
|
+
if isinstance(payload, dict):
|
|
92
|
+
need = payload.get("credits_required")
|
|
93
|
+
if need is not None:
|
|
94
|
+
hint = f" (endpoint requires {need} credit(s))"
|
|
95
|
+
return PREMIUM_PAYMENT_GUIDANCE + hint
|
|
96
|
+
except ValueError:
|
|
97
|
+
# client raises ValueError when no token is set on the client
|
|
98
|
+
return PREMIUM_PAYMENT_GUIDANCE
|
|
99
|
+
except RateLimited:
|
|
100
|
+
return (
|
|
101
|
+
"TensorFeed rate limit hit. Wait and retry, or reduce call "
|
|
102
|
+
"frequency. Free public endpoints allow 120 requests/minute "
|
|
103
|
+
"per IP; bearer tokens are exempt."
|
|
104
|
+
)
|
|
105
|
+
except TensorFeedError as e:
|
|
106
|
+
return f"TensorFeed API error {e.status_code}: {e}"
|
|
107
|
+
try:
|
|
108
|
+
return formatter(result) + _billing_line(result)
|
|
109
|
+
except Exception: # defensive: a shape change must not break the agent
|
|
110
|
+
return (
|
|
111
|
+
"TensorFeed returned a response in an unexpected shape. Raw "
|
|
112
|
+
f"top-level keys: {sorted(result) if isinstance(result, dict) else type(result).__name__}."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── Shared descriptions (identical copy across frameworks) ─────────────
|
|
117
|
+
#
|
|
118
|
+
# Every description states the credit cost up front so the model can
|
|
119
|
+
# weigh it before calling, and what the tool is uniquely good for so it
|
|
120
|
+
# is picked for the right question.
|
|
121
|
+
|
|
122
|
+
DESCRIPTIONS: dict[str, str] = {
|
|
123
|
+
"tensorfeed_whats_new": (
|
|
124
|
+
"PAID, 1 credit. The agent boot brief: a single curated summary "
|
|
125
|
+
"of AI model pricing changes, new and removed models, service "
|
|
126
|
+
"incidents that started or resolved, current operational counts, "
|
|
127
|
+
"and the top news headlines, over the last 1 to 7 days. Call "
|
|
128
|
+
"this once at startup instead of polling many free endpoints. "
|
|
129
|
+
"Inputs: days (1-7, default 1), news_limit (1-25, default 10)."
|
|
130
|
+
),
|
|
131
|
+
"tensorfeed_routing": (
|
|
132
|
+
"PAID, 1 credit. Full ranked model routing: the top-N AI models "
|
|
133
|
+
"for a task with a complete score breakdown (quality, "
|
|
134
|
+
"availability, cost, latency). Use this when the free top-1 "
|
|
135
|
+
"preview is not enough. Inputs: task (code|reasoning|creative|"
|
|
136
|
+
"general), budget (optional max blended USD per 1M tokens), "
|
|
137
|
+
"top_n (1-10, default 5)."
|
|
138
|
+
),
|
|
139
|
+
"tensorfeed_compare_models": (
|
|
140
|
+
"PAID, 1 credit. Side-by-side comparison of 2 to 5 AI models: "
|
|
141
|
+
"pricing, normalized benchmarks, live provider status, context "
|
|
142
|
+
"window, capabilities, recent news, plus rankings for cheapest "
|
|
143
|
+
"blended and most context. Input: ids (comma-separated model "
|
|
144
|
+
"ids or display names, 2 to 5)."
|
|
145
|
+
),
|
|
146
|
+
"tensorfeed_cost_projection": (
|
|
147
|
+
"PAID, 1 credit. Project the cost of a token-usage workload "
|
|
148
|
+
"across 1 to 10 models over day, week, month, and year, with a "
|
|
149
|
+
"cheapest-monthly ranking. Inputs: models (comma-separated), "
|
|
150
|
+
"input_tokens_per_day, output_tokens_per_day, horizon (optional)."
|
|
151
|
+
),
|
|
152
|
+
"tensorfeed_news_search": (
|
|
153
|
+
"PAID, 1 credit. Full-text search over the TensorFeed AI news "
|
|
154
|
+
"corpus with relevance scoring and a recency boost. Filter by "
|
|
155
|
+
"query, date range, provider, and category. Use this instead of "
|
|
156
|
+
"the free latest-news feed when you need to find specific "
|
|
157
|
+
"coverage. Inputs: q, from_date, to_date, provider, category, "
|
|
158
|
+
"limit (1-100, default 25)."
|
|
159
|
+
),
|
|
160
|
+
"tensorfeed_provider_deepdive": (
|
|
161
|
+
"PAID, 1 credit. One AI provider's full profile in a single "
|
|
162
|
+
"call: live status, every model with pricing and benchmarks "
|
|
163
|
+
"joined in, recent news, and agent-traffic attribution. Doing "
|
|
164
|
+
"this from free endpoints takes several round-trips and a "
|
|
165
|
+
"non-trivial join. Input: provider (id or display name)."
|
|
166
|
+
),
|
|
167
|
+
"tensorfeed_status_leaderboard": (
|
|
168
|
+
"PAID, 1 credit. Cross-provider AI uptime leaderboard over the "
|
|
169
|
+
"full 90-day retention window, with incident_count and "
|
|
170
|
+
"mttr_minutes per provider. Built for vendor selection and "
|
|
171
|
+
"post-incident review. Inputs: from_date, to_date (optional, "
|
|
172
|
+
"default last 30 days)."
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
PREMIUM_TOOL_NAMES: list[str] = list(DESCRIPTIONS)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── Shared result formatters ───────────────────────────────────────────
|
|
180
|
+
#
|
|
181
|
+
# Compact, deterministic, defensive. They turn a response dict into a
|
|
182
|
+
# short text block an LLM can read. Lists are truncated. Missing keys
|
|
183
|
+
# degrade to a sane default rather than raising (safe_call also guards
|
|
184
|
+
# the formatter, this is belt and suspenders).
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def format_whats_new(r: dict[str, Any]) -> str:
|
|
188
|
+
w = r.get("window", {})
|
|
189
|
+
s = r.get("summary", {})
|
|
190
|
+
lines = [f"What's new ({w.get('days', '?')}d window):"]
|
|
191
|
+
pricing = r.get("pricing", {}) or {}
|
|
192
|
+
changed = pricing.get("changes", []) or []
|
|
193
|
+
added = pricing.get("new_models", []) or []
|
|
194
|
+
removed = pricing.get("removed_models", []) or []
|
|
195
|
+
lines.append(
|
|
196
|
+
f" pricing: {len(changed)} changes, {len(added)} new, "
|
|
197
|
+
f"{len(removed)} removed models"
|
|
198
|
+
)
|
|
199
|
+
for c in changed[:5]:
|
|
200
|
+
lines.append(
|
|
201
|
+
f" {c.get('model', '?')} {c.get('field', '')}: "
|
|
202
|
+
f"{c.get('from')} -> {c.get('to')}"
|
|
203
|
+
)
|
|
204
|
+
status = r.get("status", {}) or {}
|
|
205
|
+
incidents = status.get("incidents", []) or []
|
|
206
|
+
lines.append(f" status: {len(incidents)} incident(s) in window")
|
|
207
|
+
for i in incidents[:3]:
|
|
208
|
+
lines.append(
|
|
209
|
+
f" {i.get('provider', '?')}: {i.get('state', i.get('status', ''))}"
|
|
210
|
+
)
|
|
211
|
+
news = r.get("news", []) or []
|
|
212
|
+
lines.append(f" news: {len(news)} headlines")
|
|
213
|
+
for n in news[:5]:
|
|
214
|
+
lines.append(f" - {n.get('title', '')} ({n.get('source', '')})")
|
|
215
|
+
if s:
|
|
216
|
+
lines.append(f" counts: {s}")
|
|
217
|
+
return "\n".join(lines)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def format_routing(r: dict[str, Any]) -> str:
|
|
221
|
+
recs = r.get("recommendations") or r.get("recommendation") or []
|
|
222
|
+
if isinstance(recs, dict):
|
|
223
|
+
recs = [recs]
|
|
224
|
+
if not recs:
|
|
225
|
+
return "No routing recommendation returned."
|
|
226
|
+
lines = ["Model routing (ranked):"]
|
|
227
|
+
for x in recs[:10]:
|
|
228
|
+
model = x.get("model", {})
|
|
229
|
+
name = model.get("name") if isinstance(model, dict) else model
|
|
230
|
+
score = x.get("composite_score", x.get("score"))
|
|
231
|
+
try:
|
|
232
|
+
score_s = f"{float(score):.3f}"
|
|
233
|
+
except (TypeError, ValueError):
|
|
234
|
+
score_s = str(score)
|
|
235
|
+
lines.append(
|
|
236
|
+
f" #{x.get('rank', '?')} {name} score={score_s}"
|
|
237
|
+
f" provider={x.get('provider', model.get('provider') if isinstance(model, dict) else '')}"
|
|
238
|
+
)
|
|
239
|
+
return "\n".join(lines)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def format_compare_models(r: dict[str, Any]) -> str:
|
|
243
|
+
models = r.get("models", []) or []
|
|
244
|
+
lines = [f"Compared {len(models)} model(s):"]
|
|
245
|
+
for m in models[:5]:
|
|
246
|
+
name = m.get("name") or m.get("id") or "?"
|
|
247
|
+
if m.get("unmatched"):
|
|
248
|
+
lines.append(f" {name}: (no match)")
|
|
249
|
+
continue
|
|
250
|
+
pricing = m.get("pricing", {}) or {}
|
|
251
|
+
lines.append(
|
|
252
|
+
f" {name}: in {pricing.get('inputPrice', '?')} / "
|
|
253
|
+
f"out {pricing.get('outputPrice', '?')} per 1M, "
|
|
254
|
+
f"ctx {m.get('context_window', '?')}"
|
|
255
|
+
)
|
|
256
|
+
rankings = r.get("rankings", {}) or {}
|
|
257
|
+
if rankings.get("cheapest_blended"):
|
|
258
|
+
lines.append(f" cheapest blended: {rankings['cheapest_blended']}")
|
|
259
|
+
if rankings.get("most_context"):
|
|
260
|
+
lines.append(f" most context: {rankings['most_context']}")
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def format_cost_projection(r: dict[str, Any]) -> str:
|
|
265
|
+
proj = r.get("projections", []) or []
|
|
266
|
+
lines = ["Cost projection (per model):"]
|
|
267
|
+
for p in proj[:10]:
|
|
268
|
+
name = p.get("model", "?")
|
|
269
|
+
monthly = p.get("monthly", p.get("monthly_cost"))
|
|
270
|
+
lines.append(f" {name}: ~{monthly}/month")
|
|
271
|
+
ranked = r.get("ranked_cheapest_monthly", []) or []
|
|
272
|
+
if ranked:
|
|
273
|
+
head = ranked[0]
|
|
274
|
+
name = head.get("model") if isinstance(head, dict) else head
|
|
275
|
+
lines.append(f" cheapest monthly: {name}")
|
|
276
|
+
return "\n".join(lines)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def format_news_search(r: dict[str, Any]) -> str:
|
|
280
|
+
results = r.get("results", []) or []
|
|
281
|
+
lines = [
|
|
282
|
+
f"News search: {r.get('matched', len(results))} matched, "
|
|
283
|
+
f"showing {len(results)}"
|
|
284
|
+
]
|
|
285
|
+
for a in results[:10]:
|
|
286
|
+
lines.append(
|
|
287
|
+
f" - {a.get('title', '')} ({a.get('source', '')}, "
|
|
288
|
+
f"{a.get('published_at', '')}) {a.get('url', '')}"
|
|
289
|
+
)
|
|
290
|
+
return "\n".join(lines)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def format_provider_deepdive(r: dict[str, Any]) -> str:
|
|
294
|
+
prov = r.get("provider", "?")
|
|
295
|
+
status = r.get("status", {})
|
|
296
|
+
st = status.get("status") if isinstance(status, dict) else status
|
|
297
|
+
models = r.get("models", []) or []
|
|
298
|
+
lines = [
|
|
299
|
+
f"{prov}: status={st}, {len(models)} model(s), "
|
|
300
|
+
f"{r.get('recent_news_count', 0)} recent news, "
|
|
301
|
+
f"{r.get('agent_traffic_24h', 0)} agent hits/24h"
|
|
302
|
+
]
|
|
303
|
+
for m in models[:6]:
|
|
304
|
+
lines.append(
|
|
305
|
+
f" {m.get('name', m.get('id', '?'))}: "
|
|
306
|
+
f"tier {m.get('tier', '?')}, ctx {m.get('context_window', '?')}"
|
|
307
|
+
)
|
|
308
|
+
for n in (r.get("recent_news", []) or [])[:5]:
|
|
309
|
+
lines.append(f" news: {n.get('title', '')} ({n.get('source', '')})")
|
|
310
|
+
return "\n".join(lines)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def format_status_leaderboard(r: dict[str, Any]) -> str:
|
|
314
|
+
entries = r.get("entries", []) or []
|
|
315
|
+
if not entries:
|
|
316
|
+
return (
|
|
317
|
+
"No leaderboard data for the requested window "
|
|
318
|
+
f"({r.get('error', 'no_data')})."
|
|
319
|
+
)
|
|
320
|
+
lines = ["AI provider uptime leaderboard:"]
|
|
321
|
+
for e in entries[:15]:
|
|
322
|
+
lines.append(
|
|
323
|
+
f" #{e.get('rank', '?')} {e.get('provider', '?')}: "
|
|
324
|
+
f"{e.get('uptime_pct', '?')}% uptime, "
|
|
325
|
+
f"{e.get('incident_count', 0)} incidents, "
|
|
326
|
+
f"MTTR {e.get('mttr_minutes', '?')}m"
|
|
327
|
+
)
|
|
328
|
+
return "\n".join(lines)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Maps tool name -> (client method name, formatter). The adapters use
|
|
332
|
+
# this so the call target and the formatter never drift apart.
|
|
333
|
+
FORMATTERS: dict[str, Callable[[dict[str, Any]], str]] = {
|
|
334
|
+
"tensorfeed_whats_new": format_whats_new,
|
|
335
|
+
"tensorfeed_routing": format_routing,
|
|
336
|
+
"tensorfeed_compare_models": format_compare_models,
|
|
337
|
+
"tensorfeed_cost_projection": format_cost_projection,
|
|
338
|
+
"tensorfeed_news_search": format_news_search,
|
|
339
|
+
"tensorfeed_provider_deepdive": format_provider_deepdive,
|
|
340
|
+
"tensorfeed_status_leaderboard": format_status_leaderboard,
|
|
341
|
+
}
|
|
@@ -11,6 +11,11 @@ Provides a set of CrewAI-compatible tools:
|
|
|
11
11
|
* ``tensorfeed_harnesses_tool()`` - coding harness leaderboard
|
|
12
12
|
* ``tensorfeed_routing_tool()`` - top model recommendation
|
|
13
13
|
|
|
14
|
+
Plus the paid catalog via ``tensorfeed_premium_tools()`` (1 credit per
|
|
15
|
+
call). Premium tools never move funds: called without a credit token
|
|
16
|
+
they return a clear guidance string instead of paying or raising.
|
|
17
|
+
Provisioning credits stays an explicit operator action.
|
|
18
|
+
|
|
14
19
|
Usage::
|
|
15
20
|
|
|
16
21
|
from crewai import Agent
|
|
@@ -33,6 +38,7 @@ import json
|
|
|
33
38
|
from typing import Any
|
|
34
39
|
|
|
35
40
|
from .client import TensorFeed
|
|
41
|
+
from ._premium_tools import DESCRIPTIONS, FORMATTERS, safe_call
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
def _require_crewai() -> Any:
|
|
@@ -163,12 +169,140 @@ def tensorfeed_routing_tool(token: str | None = None) -> Any:
|
|
|
163
169
|
return _Tool()
|
|
164
170
|
|
|
165
171
|
|
|
166
|
-
def tensorfeed_tools(
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
def tensorfeed_tools(
|
|
173
|
+
token: str | None = None,
|
|
174
|
+
*,
|
|
175
|
+
include_premium: bool = False,
|
|
176
|
+
) -> list[Any]:
|
|
177
|
+
"""Convenience: the TensorFeed CrewAI tool list.
|
|
178
|
+
|
|
179
|
+
Returns the five free tools by default. Pass
|
|
180
|
+
``include_premium=True`` to also attach the paid catalog (1 credit
|
|
181
|
+
per call). Premium tools are safe to attach without a token: called
|
|
182
|
+
with no credits they return guidance instead of paying or raising.
|
|
183
|
+
"""
|
|
184
|
+
tools = [
|
|
169
185
|
tensorfeed_news_tool(token),
|
|
170
186
|
tensorfeed_status_tool(token),
|
|
171
187
|
tensorfeed_attention_tool(token),
|
|
172
188
|
tensorfeed_harnesses_tool(token),
|
|
173
189
|
tensorfeed_routing_tool(token),
|
|
174
190
|
]
|
|
191
|
+
if include_premium:
|
|
192
|
+
tools.extend(tensorfeed_premium_tools(token))
|
|
193
|
+
return tools
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── Premium tools (paid, 1 credit each, never auto-pay) ───────────────
|
|
197
|
+
#
|
|
198
|
+
# Each is a thin CrewAI BaseTool whose _run routes through the shared
|
|
199
|
+
# safe_call, so the payment posture and formatting match the LangChain
|
|
200
|
+
# binding exactly and are tested once in test_premium_tools.py.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _premium_tool(token: str | None, tool_name: str, runner) -> Any:
|
|
204
|
+
BaseTool = _require_crewai()
|
|
205
|
+
fmt = FORMATTERS[tool_name]
|
|
206
|
+
|
|
207
|
+
class _Tool(BaseTool): # type: ignore[misc, valid-type]
|
|
208
|
+
name: str = tool_name
|
|
209
|
+
description: str = DESCRIPTIONS[tool_name]
|
|
210
|
+
|
|
211
|
+
def _run(self, **kwargs: Any) -> str:
|
|
212
|
+
return safe_call(lambda: runner(_client(token), kwargs), fmt)
|
|
213
|
+
|
|
214
|
+
return _Tool()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def tensorfeed_whats_new_tool(token: str | None = None) -> Any:
|
|
218
|
+
return _premium_tool(
|
|
219
|
+
token,
|
|
220
|
+
"tensorfeed_whats_new",
|
|
221
|
+
lambda c, kw: c.whats_new(
|
|
222
|
+
days=kw.get("days", 1), news_limit=kw.get("news_limit", 10)
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def tensorfeed_routing_full_tool(token: str | None = None) -> Any:
|
|
228
|
+
return _premium_tool(
|
|
229
|
+
token,
|
|
230
|
+
"tensorfeed_routing",
|
|
231
|
+
lambda c, kw: c.routing(
|
|
232
|
+
task=kw.get("task", "general"),
|
|
233
|
+
budget=kw.get("budget"),
|
|
234
|
+
top_n=kw.get("top_n", 5),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def tensorfeed_compare_models_tool(token: str | None = None) -> Any:
|
|
240
|
+
return _premium_tool(
|
|
241
|
+
token,
|
|
242
|
+
"tensorfeed_compare_models",
|
|
243
|
+
lambda c, kw: c.compare_models(ids=kw.get("ids", "")),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def tensorfeed_cost_projection_tool(token: str | None = None) -> Any:
|
|
248
|
+
return _premium_tool(
|
|
249
|
+
token,
|
|
250
|
+
"tensorfeed_cost_projection",
|
|
251
|
+
lambda c, kw: c.cost_projection(
|
|
252
|
+
models=kw.get("models", ""),
|
|
253
|
+
input_tokens_per_day=kw.get("input_tokens_per_day", 0),
|
|
254
|
+
output_tokens_per_day=kw.get("output_tokens_per_day", 0),
|
|
255
|
+
horizon=kw.get("horizon"),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def tensorfeed_news_search_tool(token: str | None = None) -> Any:
|
|
261
|
+
return _premium_tool(
|
|
262
|
+
token,
|
|
263
|
+
"tensorfeed_news_search",
|
|
264
|
+
lambda c, kw: c.news_search(
|
|
265
|
+
q=kw.get("q"),
|
|
266
|
+
from_date=kw.get("from_date"),
|
|
267
|
+
to_date=kw.get("to_date"),
|
|
268
|
+
provider=kw.get("provider"),
|
|
269
|
+
category=kw.get("category"),
|
|
270
|
+
limit=kw.get("limit", 25),
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def tensorfeed_provider_deepdive_tool(token: str | None = None) -> Any:
|
|
276
|
+
return _premium_tool(
|
|
277
|
+
token,
|
|
278
|
+
"tensorfeed_provider_deepdive",
|
|
279
|
+
lambda c, kw: c.provider_deepdive(kw.get("provider", "")),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def tensorfeed_status_leaderboard_tool(token: str | None = None) -> Any:
|
|
284
|
+
return _premium_tool(
|
|
285
|
+
token,
|
|
286
|
+
"tensorfeed_status_leaderboard",
|
|
287
|
+
lambda c, kw: c.status_leaderboard(
|
|
288
|
+
from_date=kw.get("from_date"), to_date=kw.get("to_date")
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def tensorfeed_premium_tools(token: str | None = None) -> list[Any]:
|
|
294
|
+
"""The paid TensorFeed CrewAI tool catalog (1 credit per call).
|
|
295
|
+
|
|
296
|
+
Safe to attach without a token: each tool returns actionable
|
|
297
|
+
guidance instead of paying or raising when no credits are
|
|
298
|
+
available. None of these tools move funds.
|
|
299
|
+
"""
|
|
300
|
+
return [
|
|
301
|
+
tensorfeed_whats_new_tool(token),
|
|
302
|
+
tensorfeed_routing_full_tool(token),
|
|
303
|
+
tensorfeed_compare_models_tool(token),
|
|
304
|
+
tensorfeed_cost_projection_tool(token),
|
|
305
|
+
tensorfeed_news_search_tool(token),
|
|
306
|
+
tensorfeed_provider_deepdive_tool(token),
|
|
307
|
+
tensorfeed_status_leaderboard_tool(token),
|
|
308
|
+
]
|
|
@@ -11,7 +11,12 @@ Provides:
|
|
|
11
11
|
* ``TensorFeedAttentionTool`` - tool wrapping ``/api/attention``
|
|
12
12
|
* ``TensorFeedHarnessesTool`` - tool wrapping ``/api/harnesses``
|
|
13
13
|
* ``TensorFeedNewsLoader`` - LangChain document loader for news articles
|
|
14
|
-
* ``tensorfeed_tools()`` - convenience:
|
|
14
|
+
* ``tensorfeed_tools()`` - convenience: free tools, optionally premium
|
|
15
|
+
* ``tensorfeed_premium_tools()`` - the paid catalog (1 credit each)
|
|
16
|
+
|
|
17
|
+
The premium tools never move funds. Called without a credit token they
|
|
18
|
+
return a clear guidance string instead of paying or raising. Provisioning
|
|
19
|
+
credits stays an explicit operator action; see ``_premium_tools``.
|
|
15
20
|
|
|
16
21
|
Usage::
|
|
17
22
|
|
|
@@ -32,6 +37,7 @@ import json
|
|
|
32
37
|
from typing import Any
|
|
33
38
|
|
|
34
39
|
from .client import TensorFeed
|
|
40
|
+
from ._premium_tools import DESCRIPTIONS, FORMATTERS, safe_call
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
def _require_langchain() -> tuple[Any, Any]:
|
|
@@ -194,15 +200,184 @@ def TensorFeedHarnessesTool(token: str | None = None):
|
|
|
194
200
|
)
|
|
195
201
|
|
|
196
202
|
|
|
197
|
-
def tensorfeed_tools(
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
def tensorfeed_tools(
|
|
204
|
+
token: str | None = None,
|
|
205
|
+
*,
|
|
206
|
+
include_premium: bool = False,
|
|
207
|
+
) -> list[Any]:
|
|
208
|
+
"""Convenience: the TensorFeed LangChain tools.
|
|
209
|
+
|
|
210
|
+
Returns the five free tools by default. Pass
|
|
211
|
+
``include_premium=True`` to also attach the paid catalog (1 credit
|
|
212
|
+
per call). The premium tools are safe to attach even without a
|
|
213
|
+
token: called with no credits they return guidance instead of
|
|
214
|
+
paying or raising, so an agent can still reason about them.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
token: Optional bearer token for premium calls. Free tools
|
|
218
|
+
ignore it.
|
|
219
|
+
include_premium: Append the premium tools as well.
|
|
220
|
+
"""
|
|
221
|
+
tools = [
|
|
200
222
|
TensorFeedNewsTool(token),
|
|
201
223
|
TensorFeedStatusTool(token),
|
|
202
224
|
TensorFeedRoutingTool(token),
|
|
203
225
|
TensorFeedAttentionTool(token),
|
|
204
226
|
TensorFeedHarnessesTool(token),
|
|
205
227
|
]
|
|
228
|
+
if include_premium:
|
|
229
|
+
tools.extend(tensorfeed_premium_tools(token))
|
|
230
|
+
return tools
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Premium tools (paid, 1 credit each, never auto-pay) ───────────────
|
|
234
|
+
#
|
|
235
|
+
# Each tool has an explicit typed signature so LangChain builds a clean
|
|
236
|
+
# argument schema for the model. The call is routed through the shared
|
|
237
|
+
# ``safe_call`` so the payment posture and result formatting are
|
|
238
|
+
# identical to the CrewAI binding and tested once.
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _premium_structured_tool(name: str, fn):
|
|
242
|
+
StructuredTool, _ = _require_langchain()
|
|
243
|
+
return StructuredTool.from_function(
|
|
244
|
+
func=fn, name=name, description=DESCRIPTIONS[name]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def TensorFeedWhatsNewTool(token: str | None = None):
|
|
249
|
+
"""Paid agent boot brief. 1 credit. Will not auto-pay."""
|
|
250
|
+
|
|
251
|
+
def _whats_new(days: int = 1, news_limit: int = 10) -> str:
|
|
252
|
+
return safe_call(
|
|
253
|
+
lambda: _client(token).whats_new(days=days, news_limit=news_limit),
|
|
254
|
+
FORMATTERS["tensorfeed_whats_new"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return _premium_structured_tool("tensorfeed_whats_new", _whats_new)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def TensorFeedRoutingFullTool(token: str | None = None):
|
|
261
|
+
"""Paid full ranked routing. 1 credit. Will not auto-pay."""
|
|
262
|
+
|
|
263
|
+
def _routing(
|
|
264
|
+
task: str = "general",
|
|
265
|
+
budget: float | None = None,
|
|
266
|
+
top_n: int = 5,
|
|
267
|
+
) -> str:
|
|
268
|
+
return safe_call(
|
|
269
|
+
lambda: _client(token).routing(task=task, budget=budget, top_n=top_n),
|
|
270
|
+
FORMATTERS["tensorfeed_routing"],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return _premium_structured_tool("tensorfeed_routing", _routing)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def TensorFeedCompareModelsTool(token: str | None = None):
|
|
277
|
+
"""Paid 2-5 model comparison. 1 credit. Will not auto-pay."""
|
|
278
|
+
|
|
279
|
+
def _compare(ids: str) -> str:
|
|
280
|
+
return safe_call(
|
|
281
|
+
lambda: _client(token).compare_models(ids=ids),
|
|
282
|
+
FORMATTERS["tensorfeed_compare_models"],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return _premium_structured_tool("tensorfeed_compare_models", _compare)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def TensorFeedCostProjectionTool(token: str | None = None):
|
|
289
|
+
"""Paid workload cost projection. 1 credit. Will not auto-pay."""
|
|
290
|
+
|
|
291
|
+
def _cost(
|
|
292
|
+
models: str,
|
|
293
|
+
input_tokens_per_day: float,
|
|
294
|
+
output_tokens_per_day: float,
|
|
295
|
+
horizon: str | None = None,
|
|
296
|
+
) -> str:
|
|
297
|
+
return safe_call(
|
|
298
|
+
lambda: _client(token).cost_projection(
|
|
299
|
+
models=models,
|
|
300
|
+
input_tokens_per_day=input_tokens_per_day,
|
|
301
|
+
output_tokens_per_day=output_tokens_per_day,
|
|
302
|
+
horizon=horizon,
|
|
303
|
+
),
|
|
304
|
+
FORMATTERS["tensorfeed_cost_projection"],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return _premium_structured_tool("tensorfeed_cost_projection", _cost)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def TensorFeedNewsSearchTool(token: str | None = None):
|
|
311
|
+
"""Paid full-text news search. 1 credit. Will not auto-pay."""
|
|
312
|
+
|
|
313
|
+
def _search(
|
|
314
|
+
q: str | None = None,
|
|
315
|
+
from_date: str | None = None,
|
|
316
|
+
to_date: str | None = None,
|
|
317
|
+
provider: str | None = None,
|
|
318
|
+
category: str | None = None,
|
|
319
|
+
limit: int = 25,
|
|
320
|
+
) -> str:
|
|
321
|
+
return safe_call(
|
|
322
|
+
lambda: _client(token).news_search(
|
|
323
|
+
q=q,
|
|
324
|
+
from_date=from_date,
|
|
325
|
+
to_date=to_date,
|
|
326
|
+
provider=provider,
|
|
327
|
+
category=category,
|
|
328
|
+
limit=limit,
|
|
329
|
+
),
|
|
330
|
+
FORMATTERS["tensorfeed_news_search"],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return _premium_structured_tool("tensorfeed_news_search", _search)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def TensorFeedProviderDeepDiveTool(token: str | None = None):
|
|
337
|
+
"""Paid one-call provider profile. 1 credit. Will not auto-pay."""
|
|
338
|
+
|
|
339
|
+
def _deepdive(provider: str) -> str:
|
|
340
|
+
return safe_call(
|
|
341
|
+
lambda: _client(token).provider_deepdive(provider),
|
|
342
|
+
FORMATTERS["tensorfeed_provider_deepdive"],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return _premium_structured_tool("tensorfeed_provider_deepdive", _deepdive)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def TensorFeedStatusLeaderboardTool(token: str | None = None):
|
|
349
|
+
"""Paid 90-day uptime leaderboard. 1 credit. Will not auto-pay."""
|
|
350
|
+
|
|
351
|
+
def _leaderboard(
|
|
352
|
+
from_date: str | None = None,
|
|
353
|
+
to_date: str | None = None,
|
|
354
|
+
) -> str:
|
|
355
|
+
return safe_call(
|
|
356
|
+
lambda: _client(token).status_leaderboard(
|
|
357
|
+
from_date=from_date, to_date=to_date
|
|
358
|
+
),
|
|
359
|
+
FORMATTERS["tensorfeed_status_leaderboard"],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return _premium_structured_tool("tensorfeed_status_leaderboard", _leaderboard)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def tensorfeed_premium_tools(token: str | None = None) -> list[Any]:
|
|
366
|
+
"""The paid TensorFeed tool catalog (1 credit per call).
|
|
367
|
+
|
|
368
|
+
Safe to attach without a token: each tool returns actionable
|
|
369
|
+
guidance instead of paying or raising when no credits are
|
|
370
|
+
available. None of these tools move funds.
|
|
371
|
+
"""
|
|
372
|
+
return [
|
|
373
|
+
TensorFeedWhatsNewTool(token),
|
|
374
|
+
TensorFeedRoutingFullTool(token),
|
|
375
|
+
TensorFeedCompareModelsTool(token),
|
|
376
|
+
TensorFeedCostProjectionTool(token),
|
|
377
|
+
TensorFeedNewsSearchTool(token),
|
|
378
|
+
TensorFeedProviderDeepDiveTool(token),
|
|
379
|
+
TensorFeedStatusLeaderboardTool(token),
|
|
380
|
+
]
|
|
206
381
|
|
|
207
382
|
|
|
208
383
|
# ── Document loader ───────────────────────────────────────────────
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tensorfeed
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, agent payments via USDC on Base (optional web3 auto-send)
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
|
|
5
5
|
Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://tensorfeed.ai
|
|
@@ -197,6 +197,34 @@ print(tf.balance())
|
|
|
197
197
|
rec = tf.routing(task="code")
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
## Framework Tools (LangChain, CrewAI)
|
|
201
|
+
|
|
202
|
+
Drop TensorFeed into an agent framework as ready-made tools.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install 'tensorfeed[langchain]' # or 'tensorfeed[crewai]'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from tensorfeed.langchain import tensorfeed_tools, tensorfeed_premium_tools
|
|
210
|
+
|
|
211
|
+
# Five free tools (news, status, attention, harnesses, routing preview)
|
|
212
|
+
free = tensorfeed_tools()
|
|
213
|
+
|
|
214
|
+
# Free plus the paid catalog (whats_new, full routing, compare models,
|
|
215
|
+
# cost projection, news search, provider deep dive, status leaderboard)
|
|
216
|
+
all_tools = tensorfeed_tools(token="YOUR_TOKEN", include_premium=True)
|
|
217
|
+
|
|
218
|
+
# Just the paid catalog
|
|
219
|
+
paid = tensorfeed_premium_tools(token="YOUR_TOKEN")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
CrewAI is identical: `from tensorfeed.crewai import tensorfeed_tools, tensorfeed_premium_tools`.
|
|
223
|
+
|
|
224
|
+
The default `tensorfeed_tools()` stays free-only and unchanged, so existing code keeps working. Premium is opt-in via `include_premium=True`.
|
|
225
|
+
|
|
226
|
+
**Payment posture.** The premium tools never move funds. Attach them with or without a token: called without spendable credits they return a short, actionable guidance string (explaining that an operator provisions credits out of band) instead of paying or raising. There is no code path from a tool call to the wallet or the web3 signer. Credit purchases stay an explicit, per-action operator decision, by design. This is enforced in one shared module and covered by tests, including a static guard that fails if a future change introduces an autonomous-payment path.
|
|
227
|
+
|
|
200
228
|
## Error Handling
|
|
201
229
|
|
|
202
230
|
```python
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
tensorfeed/__init__.py
|
|
4
|
+
tensorfeed/_premium_tools.py
|
|
4
5
|
tensorfeed/client.py
|
|
5
6
|
tensorfeed/crewai.py
|
|
6
7
|
tensorfeed/langchain.py
|
|
@@ -10,4 +11,5 @@ tensorfeed.egg-info/PKG-INFO
|
|
|
10
11
|
tensorfeed.egg-info/SOURCES.txt
|
|
11
12
|
tensorfeed.egg-info/dependency_links.txt
|
|
12
13
|
tensorfeed.egg-info/requires.txt
|
|
13
|
-
tensorfeed.egg-info/top_level.txt
|
|
14
|
+
tensorfeed.egg-info/top_level.txt
|
|
15
|
+
tests/test_premium_tools.py
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tests for the shared premium-tool plumbing.
|
|
2
|
+
|
|
3
|
+
Network-free. Covers the safe_call behavior matrix, the formatters,
|
|
4
|
+
the framework bindings (skipped if the framework is not installed),
|
|
5
|
+
and a static guard that the premium-tool path can never reach the
|
|
6
|
+
wallet or the web3 signer.
|
|
7
|
+
|
|
8
|
+
Run from sdk/python::
|
|
9
|
+
|
|
10
|
+
python -m unittest discover -s tests -t .
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
from tensorfeed import PaymentRequired, RateLimited, TensorFeed, TensorFeedError
|
|
19
|
+
from tensorfeed import _premium_tools as pt
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SafeCallBehavior(unittest.TestCase):
|
|
23
|
+
def test_payment_required_returns_guidance_not_raise(self):
|
|
24
|
+
def boom():
|
|
25
|
+
raise PaymentRequired(402, {})
|
|
26
|
+
|
|
27
|
+
out = pt.safe_call(boom, lambda r: "unreachable")
|
|
28
|
+
self.assertIn("will not", out.lower())
|
|
29
|
+
self.assertIn("credit", out.lower())
|
|
30
|
+
# The whole point: no exception escapes a tool call.
|
|
31
|
+
self.assertIsInstance(out, str)
|
|
32
|
+
|
|
33
|
+
def test_payment_required_appends_credits_hint(self):
|
|
34
|
+
def boom():
|
|
35
|
+
raise PaymentRequired(402, {"credits_required": 2})
|
|
36
|
+
|
|
37
|
+
out = pt.safe_call(boom, lambda r: "unreachable")
|
|
38
|
+
self.assertIn("2 credit", out)
|
|
39
|
+
|
|
40
|
+
def test_missing_token_value_error_returns_guidance(self):
|
|
41
|
+
def boom():
|
|
42
|
+
raise ValueError("whats_new() requires a token.")
|
|
43
|
+
|
|
44
|
+
out = pt.safe_call(boom, lambda r: "unreachable")
|
|
45
|
+
self.assertEqual(out, pt.PREMIUM_PAYMENT_GUIDANCE)
|
|
46
|
+
|
|
47
|
+
def test_rate_limited_returns_backoff_message(self):
|
|
48
|
+
def boom():
|
|
49
|
+
raise RateLimited(429, {})
|
|
50
|
+
|
|
51
|
+
out = pt.safe_call(boom, lambda r: "unreachable")
|
|
52
|
+
self.assertIn("rate limit", out.lower())
|
|
53
|
+
|
|
54
|
+
def test_other_api_error_is_concise(self):
|
|
55
|
+
def boom():
|
|
56
|
+
raise TensorFeedError(500, {"error": "kaboom"})
|
|
57
|
+
|
|
58
|
+
out = pt.safe_call(boom, lambda r: "unreachable")
|
|
59
|
+
self.assertIn("500", out)
|
|
60
|
+
self.assertNotIn("Traceback", out)
|
|
61
|
+
|
|
62
|
+
def test_success_runs_formatter_and_billing_footer(self):
|
|
63
|
+
result = {"value": 1, "billing": {"credits_charged": 1, "balance": 49}}
|
|
64
|
+
out = pt.safe_call(lambda: result, lambda r: f"value={r['value']}")
|
|
65
|
+
self.assertIn("value=1", out)
|
|
66
|
+
self.assertIn("1 credit(s) charged", out)
|
|
67
|
+
self.assertIn("49 remaining", out)
|
|
68
|
+
|
|
69
|
+
def test_formatter_exception_is_contained(self):
|
|
70
|
+
def bad_formatter(r):
|
|
71
|
+
raise KeyError("missing")
|
|
72
|
+
|
|
73
|
+
out = pt.safe_call(lambda: {"a": 1}, bad_formatter)
|
|
74
|
+
self.assertIn("unexpected shape", out)
|
|
75
|
+
self.assertIn("'a'", out) # surfaced the real keys
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GuidanceContract(unittest.TestCase):
|
|
79
|
+
def test_guidance_states_no_autonomous_payment(self):
|
|
80
|
+
g = pt.PREMIUM_PAYMENT_GUIDANCE.lower()
|
|
81
|
+
self.assertIn("will not", g)
|
|
82
|
+
self.assertTrue("move any funds" in g or "move funds" in g)
|
|
83
|
+
self.assertIn("operator", g)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NoAutonomousPaymentStaticGuard(unittest.TestCase):
|
|
87
|
+
"""A future edit must not be able to wire payment into a tool call.
|
|
88
|
+
|
|
89
|
+
The premium-tool module is the single chokepoint for every paid
|
|
90
|
+
tool. If it never references the web3 signer or the purchase path,
|
|
91
|
+
no tool can autonomously spend. This asserts that invariant at the
|
|
92
|
+
source level so it fails loudly if someone changes it.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def test_premium_tools_source_has_no_payment_path(self):
|
|
96
|
+
src = inspect.getsource(pt)
|
|
97
|
+
for forbidden in ("web3_signer", "purchase_credits", "private_key"):
|
|
98
|
+
self.assertNotIn(forbidden, src)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Formatters(unittest.TestCase):
|
|
102
|
+
def test_each_formatter_smoke(self):
|
|
103
|
+
samples = {
|
|
104
|
+
"tensorfeed_whats_new": {
|
|
105
|
+
"window": {"days": 1},
|
|
106
|
+
"pricing": {
|
|
107
|
+
"changes": [{"model": "X", "field": "blended", "from": 1, "to": 2}],
|
|
108
|
+
"new_models": [],
|
|
109
|
+
"removed_models": [],
|
|
110
|
+
},
|
|
111
|
+
"status": {"incidents": [{"provider": "openai", "state": "down"}]},
|
|
112
|
+
"news": [{"title": "T", "source": "S"}],
|
|
113
|
+
"summary": {"total": 1},
|
|
114
|
+
},
|
|
115
|
+
"tensorfeed_routing": {
|
|
116
|
+
"recommendations": [
|
|
117
|
+
{"rank": 1, "model": {"name": "Opus", "provider": "anthropic"},
|
|
118
|
+
"composite_score": 0.91}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"tensorfeed_compare_models": {
|
|
122
|
+
"models": [{"name": "A", "pricing": {"inputPrice": 1, "outputPrice": 2},
|
|
123
|
+
"context_window": 200000}],
|
|
124
|
+
"rankings": {"cheapest_blended": "A"},
|
|
125
|
+
},
|
|
126
|
+
"tensorfeed_cost_projection": {
|
|
127
|
+
"projections": [{"model": "A", "monthly": 12.5}],
|
|
128
|
+
"ranked_cheapest_monthly": [{"model": "A"}],
|
|
129
|
+
},
|
|
130
|
+
"tensorfeed_news_search": {
|
|
131
|
+
"matched": 3,
|
|
132
|
+
"results": [{"title": "T", "source": "S", "published_at": "2026-05-17",
|
|
133
|
+
"url": "u"}],
|
|
134
|
+
},
|
|
135
|
+
"tensorfeed_provider_deepdive": {
|
|
136
|
+
"provider": "anthropic",
|
|
137
|
+
"status": {"status": "operational"},
|
|
138
|
+
"models": [{"name": "Opus", "tier": "flagship", "context_window": 200000}],
|
|
139
|
+
"recent_news_count": 2,
|
|
140
|
+
"recent_news": [{"title": "T", "source": "S"}],
|
|
141
|
+
"agent_traffic_24h": 100,
|
|
142
|
+
},
|
|
143
|
+
"tensorfeed_status_leaderboard": {
|
|
144
|
+
"entries": [{"rank": 1, "provider": "Anthropic", "uptime_pct": 99.9,
|
|
145
|
+
"incident_count": 0, "mttr_minutes": 0}]
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
for name, fmt in pt.FORMATTERS.items():
|
|
149
|
+
with self.subTest(tool=name):
|
|
150
|
+
text = fmt(samples[name])
|
|
151
|
+
self.assertIsInstance(text, str)
|
|
152
|
+
self.assertTrue(text.strip())
|
|
153
|
+
|
|
154
|
+
def test_status_leaderboard_handles_no_data(self):
|
|
155
|
+
out = pt.format_status_leaderboard({"entries": [], "error": "no_data"})
|
|
156
|
+
self.assertIn("no_data", out)
|
|
157
|
+
|
|
158
|
+
def test_names_and_descriptions_and_formatters_align(self):
|
|
159
|
+
self.assertEqual(set(pt.DESCRIPTIONS), set(pt.FORMATTERS))
|
|
160
|
+
self.assertEqual(set(pt.PREMIUM_TOOL_NAMES), set(pt.DESCRIPTIONS))
|
|
161
|
+
for name, desc in pt.DESCRIPTIONS.items():
|
|
162
|
+
self.assertIn("1 credit", desc, msg=name)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ClientIntegrationOffline(unittest.TestCase):
|
|
166
|
+
"""End-to-end through the real client, no network.
|
|
167
|
+
|
|
168
|
+
A tokenless client raises ValueError from _require_token BEFORE any
|
|
169
|
+
HTTP call, so routing it through safe_call exercises the real
|
|
170
|
+
no-credit path offline and proves no network and no payment occur.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def test_tokenless_premium_call_yields_guidance_no_network(self):
|
|
174
|
+
tf = TensorFeed() # no token
|
|
175
|
+
out = pt.safe_call(
|
|
176
|
+
lambda: tf.whats_new(), pt.FORMATTERS["tensorfeed_whats_new"]
|
|
177
|
+
)
|
|
178
|
+
self.assertEqual(out, pt.PREMIUM_PAYMENT_GUIDANCE)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class FrameworkBindings(unittest.TestCase):
|
|
182
|
+
def test_langchain_premium_tools_build(self):
|
|
183
|
+
try:
|
|
184
|
+
from tensorfeed.langchain import tensorfeed_premium_tools, tensorfeed_tools
|
|
185
|
+
except Exception as e: # pragma: no cover
|
|
186
|
+
self.skipTest(f"langchain import failed: {e}")
|
|
187
|
+
try:
|
|
188
|
+
tools = tensorfeed_premium_tools(token=None)
|
|
189
|
+
except ImportError:
|
|
190
|
+
self.skipTest("langchain_core not installed")
|
|
191
|
+
names = {t.name for t in tools}
|
|
192
|
+
self.assertEqual(names, set(pt.DESCRIPTIONS))
|
|
193
|
+
# Default tool list stays free-only (backward compatible).
|
|
194
|
+
free = tensorfeed_tools()
|
|
195
|
+
self.assertTrue(all(t.name not in pt.DESCRIPTIONS for t in free))
|
|
196
|
+
combined = {t.name for t in tensorfeed_tools(include_premium=True)}
|
|
197
|
+
self.assertTrue(set(pt.DESCRIPTIONS).issubset(combined))
|
|
198
|
+
|
|
199
|
+
def test_crewai_premium_tools_build(self):
|
|
200
|
+
try:
|
|
201
|
+
from tensorfeed.crewai import tensorfeed_premium_tools
|
|
202
|
+
except Exception as e: # pragma: no cover
|
|
203
|
+
self.skipTest(f"crewai import failed: {e}")
|
|
204
|
+
try:
|
|
205
|
+
tools = tensorfeed_premium_tools(token=None)
|
|
206
|
+
except ImportError:
|
|
207
|
+
self.skipTest("crewai not installed")
|
|
208
|
+
names = {t.name for t in tools}
|
|
209
|
+
self.assertEqual(names, set(pt.DESCRIPTIONS))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|