@blockrun/franklin 3.8.28 → 3.8.29

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.
@@ -51,9 +51,6 @@ export interface PrefetchResult {
51
51
  * decide to skip injection entirely and let the model try its own way. */
52
52
  anyOk: boolean;
53
53
  }
54
- /** Parse the classifier's one-line reply. Very strict — any junk → null. */
55
- export declare function parseIntentReply(reply: string): Intent;
56
- export declare function classifyIntent(userInput: string, client: ModelClient): Promise<Intent>;
57
54
  /** Run the prefetch for an intent. Concurrent fan-out for price + news. */
58
55
  export declare function prefetchForIntent(intent: Intent, client: ModelClient): Promise<PrefetchResult | null>;
59
56
  /**
@@ -26,111 +26,16 @@
26
26
  * coordination gap (harness fetches, model synthesizes)."
27
27
  */
28
28
  import { getStockPrice, getPrice } from '../trading/data.js';
29
- // ─── Classifier ──────────────────────────────────────────────────────────
30
- // llama-4-maverick: same rationale as the router classifier — emits plain
31
- // text under tight max_tokens rather than routing through thinking blocks.
32
- const CLASSIFIER_MODEL = process.env.FRANKLIN_PREFETCH_MODEL || 'nvidia/llama-4-maverick';
33
- const CLASSIFIER_TIMEOUT_MS = 2_500;
34
- const CLASSIFIER_PROMPT = `You extract PREFETCH INTENT from a user message for a CLI agent that has live market-data tools.
35
-
36
- Your job: decide whether Franklin should fetch live data BEFORE the main model answers, so the answer is grounded in real data instead of model memory.
37
-
38
- Output one of:
39
-
40
- 1. STOCK <TICKER> <MARKET> <NEWS>
41
- When the user asks about a specific publicly-traded equity — by ticker (CRCL, AAPL, NVDA, 7203, 0005) or by company name that maps to one (Circle → CRCL, Apple → AAPL, Toyota → 7203, HSBC → 0005).
42
- MARKET: us | hk | jp | kr | gb | de | fr | nl | ie | lu | cn | ca
43
- NEWS: yes if the user also asks "why / what happened / analysis"; no otherwise.
44
- Default market: us.
45
-
46
- 2. CRYPTO <SYMBOL> <NEWS>
47
- When the user asks about a cryptocurrency by symbol or name (BTC, ETH, Bitcoin, Ethereum, SOL, Solana).
48
- NEWS: yes if asks why / recent news.
49
-
50
- 3. NONE
51
- Any other message: greetings, coding questions, general chat, questions about non-traded entities.
52
-
53
- Rules:
54
- - If the company could be either public or private and you're unsure, assume PUBLIC and emit STOCK with your best ticker guess. The tool will 404 gracefully if wrong.
55
- - One output line only. No explanation. No punctuation beyond what's shown.
56
- - Ticker in UPPERCASE.
57
-
58
- Examples:
59
- User: 帮我看看 CRCL 股票 → STOCK CRCL us no
60
- User: should I sell Circle stock? → STOCK CRCL us no
61
- User: why did CRCL drop this week → STOCK CRCL us yes
62
- User: BTC 现在价格 → CRYPTO BTC no
63
- User: 为什么以太坊跌了 → CRYPTO ETH yes
64
- User: Toyota 股价 → STOCK 7203 jp no
65
- User: hi how are you → NONE
66
- User: fix the bug in foo.ts → NONE
67
-
68
- Answer with just the one-line directive.`;
69
- /** Parse the classifier's one-line reply. Very strict — any junk → null. */
70
- export function parseIntentReply(reply) {
71
- const line = reply.trim().split('\n')[0].trim().toUpperCase();
72
- if (!line || line.startsWith('NONE'))
73
- return null;
74
- const stockMatch = line.match(/^STOCK\s+([A-Z0-9.\-]+)\s+([A-Z]{2})\s+(YES|NO)\b/);
75
- if (stockMatch) {
76
- const market = stockMatch[2].toLowerCase();
77
- const validMarkets = ['us', 'hk', 'jp', 'kr', 'gb', 'de', 'fr', 'nl', 'ie', 'lu', 'cn', 'ca'];
78
- if (!validMarkets.includes(market))
79
- return null;
80
- return {
81
- kind: 'ticker',
82
- symbol: stockMatch[1],
83
- market: market,
84
- assetClass: 'stock',
85
- wantNews: stockMatch[3] === 'YES',
86
- };
87
- }
88
- const cryptoMatch = line.match(/^CRYPTO\s+([A-Z0-9.\-]+)\s+(YES|NO)\b/);
89
- if (cryptoMatch) {
90
- return {
91
- kind: 'ticker',
92
- symbol: cryptoMatch[1],
93
- assetClass: 'crypto',
94
- wantNews: cryptoMatch[2] === 'YES',
95
- };
96
- }
97
- return null;
98
- }
99
- export async function classifyIntent(userInput, client) {
100
- if (process.env.FRANKLIN_NO_PREFETCH === '1')
101
- return null;
102
- const trimmed = userInput.trim();
103
- // Only the cheapest gate — skip very short inputs that can't be a real
104
- // market question ("hi", "ok", "thanks"). 6 chars covers those while
105
- // still letting short-form Chinese / ticker prompts through, e.g.
106
- // "BTC 价格" (6), "CRCL 多少" (7). Longer prompts all route to the LLM
107
- // classifier, which decides NONE cheaply when not market-related.
108
- if (trimmed.length < 6)
109
- return null;
110
- const ctrl = new AbortController();
111
- const timer = setTimeout(() => ctrl.abort(), CLASSIFIER_TIMEOUT_MS);
112
- try {
113
- const result = await client.complete({
114
- model: CLASSIFIER_MODEL,
115
- system: CLASSIFIER_PROMPT,
116
- messages: [{ role: 'user', content: trimmed.slice(0, 800) }],
117
- tools: [],
118
- max_tokens: 24,
119
- }, ctrl.signal);
120
- let raw = '';
121
- for (const part of result.content) {
122
- if (typeof part === 'object' && part.type === 'text' && part.text)
123
- raw += part.text;
124
- }
125
- return parseIntentReply(raw);
126
- }
127
- catch {
128
- return null;
129
- }
130
- finally {
131
- clearTimeout(timer);
132
- }
133
- }
29
+ // ─── Intent source ──────────────────────────────────────────────────────
30
+ //
31
+ // Historical note: this file used to host its own LLM classifier
32
+ // (`classifyIntent` + `parseIntentReply` + a ~40-line STOCK/CRYPTO/NONE
33
+ // prompt). Since v3.8.27 the unified `turn-analyzer.ts` produces intent
34
+ // as part of a single pre-turn call, and `loop.ts` reads
35
+ // `turnAnalysis.intent` directly — the standalone classifier was dead
36
+ // code with no remaining callers. Removed in v3.8.29. The TurnIntent
37
+ // shape lives in turn-analyzer and is consumed by `prefetchForIntent`
38
+ // below.
134
39
  // ─── Prefetch dispatcher ─────────────────────────────────────────────────
135
40
  function formatUsd(n) {
136
41
  if (!Number.isFinite(n))
@@ -707,7 +707,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
707
707
  routingConfidence = routing.confidence;
708
708
  routingSavings = routing.savings;
709
709
  lastRoutedModel = routing.model;
710
- lastRoutedCategory = routing.signals[0] || '';
710
+ lastRoutedCategory = routing.category || '';
711
711
  if (loopCount === 1) {
712
712
  onEvent({
713
713
  kind: 'text_delta',
@@ -9,6 +9,7 @@
9
9
  * and picks the model with the best quality-to-cost ratio for that category.
10
10
  * Local Elo adjustments personalize routing per user over time.
11
11
  */
12
+ import { type Category } from './categories.js';
12
13
  export type Tier = 'SIMPLE' | 'MEDIUM' | 'COMPLEX' | 'REASONING';
13
14
  export type RoutingProfile = 'auto' | 'eco' | 'premium' | 'free';
14
15
  export interface RoutingResult {
@@ -17,6 +18,7 @@ export interface RoutingResult {
17
18
  confidence: number;
18
19
  signals: string[];
19
20
  savings: number;
21
+ category?: Category;
20
22
  }
21
23
  export type TierClassifier = (prompt: string) => Promise<Tier | null>;
22
24
  /**
@@ -265,7 +265,8 @@ function classicRouteRequest(prompt, profile) {
265
265
  }
266
266
  const model = tierConfigs[tier].primary;
267
267
  const savings = computeSavings(model);
268
- return { model, tier, confidence, signals, savings };
268
+ const category = detectCategory(prompt, loadLearnedWeights()?.category_keywords).category;
269
+ return { model, tier, confidence, signals, savings, category };
269
270
  }
270
271
  // ─── LLM-based classifier ───
271
272
  //
@@ -385,12 +386,14 @@ export async function routeRequestAsync(prompt, profile = 'auto', classify = llm
385
386
  default: tierConfigs = AUTO_TIERS;
386
387
  }
387
388
  const model = tierConfigs[tier].primary;
389
+ const category = detectCategory(prompt, loadLearnedWeights()?.category_keywords).category;
388
390
  return {
389
391
  model,
390
392
  tier,
391
393
  confidence: 0.85, // LLM classification — medium-high confidence
392
394
  signals: ['llm-classified'],
393
395
  savings: computeSavings(model),
396
+ category,
394
397
  };
395
398
  }
396
399
  /**
@@ -481,6 +484,7 @@ export function routeRequest(prompt, profile = 'auto') {
481
484
  confidence,
482
485
  signals: [category],
483
486
  savings,
487
+ category,
484
488
  };
485
489
  }
486
490
  // Fall through to classic if selectModel returns null (no candidates for category)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.28",
3
+ "version": "3.8.29",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {