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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.0.0
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.0.0"
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"
@@ -21,4 +21,4 @@ __all__ = [
21
21
  "PaymentRequired",
22
22
  "RateLimited",
23
23
  ]
24
- __version__ = "1.29.0"
24
+ __version__ = "2.1.0"
@@ -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(token: str | None = None) -> list[Any]:
167
- """Convenience: returns the full TensorFeed CrewAI tool list."""
168
- return [
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: returns the full tool list
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(token: str | None = None) -> list[Any]:
198
- """Convenience: returns all TensorFeed LangChain tools."""
199
- return [
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.0.0
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