@blockrun/franklin 3.8.28 → 3.8.30
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.
- package/dist/agent/intent-prefetch.d.ts +0 -3
- package/dist/agent/intent-prefetch.js +10 -105
- package/dist/agent/loop.js +1 -1
- package/dist/panel/html.js +81 -2
- package/dist/panel/server.js +48 -1
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.js +5 -1
- package/package.json +1 -1
|
@@ -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
|
-
// ───
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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))
|
package/dist/agent/loop.js
CHANGED
|
@@ -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.
|
|
710
|
+
lastRoutedCategory = routing.category || '';
|
|
711
711
|
if (loopCount === 1) {
|
|
712
712
|
onEvent({
|
|
713
713
|
kind: 'text_delta',
|
package/dist/panel/html.js
CHANGED
|
@@ -274,6 +274,29 @@ a:hover { text-decoration:underline; }
|
|
|
274
274
|
.empty { color:var(--text-dim); text-align:center; padding:56px 24px; font-size:13px; }
|
|
275
275
|
|
|
276
276
|
/* ── Wallet page ── */
|
|
277
|
+
.chain-switcher {
|
|
278
|
+
display:inline-flex; padding:3px; gap:2px;
|
|
279
|
+
background:oklch(0 0 0 / 35%); border:1px solid var(--border);
|
|
280
|
+
border-radius:10px; margin-bottom:14px;
|
|
281
|
+
}
|
|
282
|
+
.chain-switcher button {
|
|
283
|
+
font-family:var(--mono); font-size:12px; font-weight:600;
|
|
284
|
+
letter-spacing:0.6px; text-transform:uppercase;
|
|
285
|
+
padding:7px 18px; border-radius:7px;
|
|
286
|
+
background:transparent; border:none; color:var(--text-muted);
|
|
287
|
+
cursor:pointer; transition:all .15s ease;
|
|
288
|
+
}
|
|
289
|
+
.chain-switcher button:hover:not(.active):not(:disabled) {
|
|
290
|
+
color:var(--text); background:oklch(1 0 0 / 5%);
|
|
291
|
+
}
|
|
292
|
+
.chain-switcher button.active {
|
|
293
|
+
background:var(--brand); color:#fff;
|
|
294
|
+
}
|
|
295
|
+
.chain-switcher button:disabled { opacity:0.5; cursor:wait; }
|
|
296
|
+
.chain-switcher-note {
|
|
297
|
+
margin-left:10px; font-size:12px; color:var(--text-dim);
|
|
298
|
+
font-style:italic;
|
|
299
|
+
}
|
|
277
300
|
.wallet-grid { display:grid; grid-template-columns:1.1fr 1fr; gap:14px; }
|
|
278
301
|
.wallet-grid .card { display:flex; flex-direction:column; gap:10px; }
|
|
279
302
|
.wallet-receive { grid-row:span 2; align-items:flex-start; }
|
|
@@ -472,8 +495,14 @@ a:hover { text-decoration:underline; }
|
|
|
472
495
|
<div class="tab" id="tab-wallet">
|
|
473
496
|
<div class="content-header">
|
|
474
497
|
<h2>Wallet</h2>
|
|
475
|
-
<p>Receive USDC, back up your key, or
|
|
498
|
+
<p>Receive USDC, back up your key, or switch chains</p>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<div class="chain-switcher" role="tablist" aria-label="Payment chain">
|
|
502
|
+
<button type="button" data-chain="base" id="chain-btn-base" role="tab">Base</button>
|
|
503
|
+
<button type="button" data-chain="solana" id="chain-btn-solana" role="tab">Solana</button>
|
|
476
504
|
</div>
|
|
505
|
+
<span class="chain-switcher-note" id="chain-switcher-note"></span>
|
|
477
506
|
|
|
478
507
|
<div class="wallet-grid">
|
|
479
508
|
<div class="card wallet-receive">
|
|
@@ -841,6 +870,14 @@ async function loadWallet() {
|
|
|
841
870
|
document.getElementById('wallet-balance-big').textContent = usdBig(w.balance) + ' USDC';
|
|
842
871
|
document.getElementById('wallet-chain-pill').textContent = w.chain || '—';
|
|
843
872
|
|
|
873
|
+
// Chain switcher — highlight active button
|
|
874
|
+
const baseBtn = document.getElementById('chain-btn-base');
|
|
875
|
+
const solanaBtn = document.getElementById('chain-btn-solana');
|
|
876
|
+
if (baseBtn && solanaBtn) {
|
|
877
|
+
baseBtn.classList.toggle('active', w.chain === 'base');
|
|
878
|
+
solanaBtn.classList.toggle('active', w.chain === 'solana');
|
|
879
|
+
}
|
|
880
|
+
|
|
844
881
|
// QR via server — never leak address to third parties
|
|
845
882
|
const qrBox = document.getElementById('wallet-qr');
|
|
846
883
|
const hint = document.getElementById('wallet-qr-hint');
|
|
@@ -848,7 +885,7 @@ async function loadWallet() {
|
|
|
848
885
|
const svg = await fetch('/api/wallet/qr?data=' + encodeURIComponent(addr)).then(r => r.ok ? r.text() : null);
|
|
849
886
|
qrBox.innerHTML = svg || '';
|
|
850
887
|
hint.textContent = w.chain === 'solana'
|
|
851
|
-
? 'Scan to send USDC (Solana) to this address.'
|
|
888
|
+
? 'Scan to send USDC (Solana SPL) to this address.'
|
|
852
889
|
: 'Scan to send USDC on Base to this address.';
|
|
853
890
|
} else {
|
|
854
891
|
qrBox.innerHTML = '';
|
|
@@ -856,6 +893,48 @@ async function loadWallet() {
|
|
|
856
893
|
}
|
|
857
894
|
}
|
|
858
895
|
|
|
896
|
+
// Chain switcher — click "Base" or "Solana" to flip payment chain.
|
|
897
|
+
// Creates a wallet on the target chain if one does not exist yet.
|
|
898
|
+
// Note: a currently-running franklin agent reads its chain at startup,
|
|
899
|
+
// so a mid-session switch only affects the next agent invocation.
|
|
900
|
+
['chain-btn-base', 'chain-btn-solana'].forEach((id) => {
|
|
901
|
+
const btn = document.getElementById(id);
|
|
902
|
+
if (!btn) return;
|
|
903
|
+
btn.addEventListener('click', async () => {
|
|
904
|
+
const target = btn.getAttribute('data-chain');
|
|
905
|
+
const note = document.getElementById('chain-switcher-note');
|
|
906
|
+
const baseBtn = document.getElementById('chain-btn-base');
|
|
907
|
+
const solanaBtn = document.getElementById('chain-btn-solana');
|
|
908
|
+
// Skip if already active
|
|
909
|
+
if (btn.classList.contains('active')) return;
|
|
910
|
+
baseBtn.disabled = true;
|
|
911
|
+
solanaBtn.disabled = true;
|
|
912
|
+
note.textContent = 'Switching to ' + target + '…';
|
|
913
|
+
try {
|
|
914
|
+
const r = await fetch('/api/chain', {
|
|
915
|
+
method: 'POST',
|
|
916
|
+
headers: { 'Content-Type': 'application/json' },
|
|
917
|
+
body: JSON.stringify({ chain: target }),
|
|
918
|
+
});
|
|
919
|
+
const data = await r.json().catch(() => ({}));
|
|
920
|
+
if (!r.ok || !data.ok) {
|
|
921
|
+
note.textContent = 'Error: ' + (data.error || r.statusText);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
note.textContent = 'Switched to ' + target + ' · restart Franklin to use this chain';
|
|
925
|
+
await loadWallet();
|
|
926
|
+
// Sidebar balance + address also refresh
|
|
927
|
+
document.getElementById('sidebar-balance').textContent = usdBig(data.balance) + ' USDC';
|
|
928
|
+
document.getElementById('sidebar-addr').textContent = (data.address || '').slice(0, 6) + '…' + (data.address || '').slice(-4);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
note.textContent = 'Error: ' + (err && err.message ? err.message : 'network error');
|
|
931
|
+
} finally {
|
|
932
|
+
baseBtn.disabled = false;
|
|
933
|
+
solanaBtn.disabled = false;
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
859
938
|
// Copy button
|
|
860
939
|
document.getElementById('wallet-copy-btn').addEventListener('click', async () => {
|
|
861
940
|
const addr = document.getElementById('wallet-address-full').textContent;
|
package/dist/panel/server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import http from 'node:http';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { BLOCKRUN_DIR, loadChain } from '../config.js';
|
|
9
|
+
import { BLOCKRUN_DIR, loadChain, saveChain } from '../config.js';
|
|
10
10
|
import { getStatsSummary } from '../stats/tracker.js';
|
|
11
11
|
import { generateInsights } from '../stats/insights.js';
|
|
12
12
|
import { listSessions, loadSessionHistory } from '../session/storage.js';
|
|
@@ -313,6 +313,53 @@ export function createPanelServer(port) {
|
|
|
313
313
|
}
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
316
|
+
// ─── Chain switch (loopback only) ───────────────────────────────
|
|
317
|
+
// Switches the active payment chain (base ↔ solana) for subsequent
|
|
318
|
+
// Franklin runs. Writes ~/.blockrun/payment-chain, then ensures a
|
|
319
|
+
// wallet exists on the target chain (creates if missing). Returns
|
|
320
|
+
// the new wallet address + balance so the UI can re-render without
|
|
321
|
+
// a follow-up round trip.
|
|
322
|
+
//
|
|
323
|
+
// NOTE: a currently-running `franklin` agent reads the chain once
|
|
324
|
+
// at startup. The Panel switch takes effect immediately for Panel
|
|
325
|
+
// reads and for the *next* agent invocation, but won't flip chain
|
|
326
|
+
// mid-session for an already-running agent. UI copy makes this clear.
|
|
327
|
+
if (p === '/api/chain' && req.method === 'POST') {
|
|
328
|
+
if (!isLoopback(req)) {
|
|
329
|
+
json(res, { error: 'forbidden' }, 403);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const raw = await readBody(req);
|
|
334
|
+
const body = JSON.parse(raw);
|
|
335
|
+
const target = body.chain;
|
|
336
|
+
if (target !== 'base' && target !== 'solana') {
|
|
337
|
+
json(res, { error: 'chain must be "base" or "solana"' }, 400);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
saveChain(target);
|
|
341
|
+
// Creates-or-loads the wallet on the target chain.
|
|
342
|
+
let address = '';
|
|
343
|
+
let balance = 0;
|
|
344
|
+
if (target === 'solana') {
|
|
345
|
+
const { setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
346
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
347
|
+
address = await client.getWalletAddress();
|
|
348
|
+
balance = await client.getBalance();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const { setupAgentWallet } = await import('@blockrun/llm');
|
|
352
|
+
const client = setupAgentWallet({ silent: true });
|
|
353
|
+
address = client.getWalletAddress();
|
|
354
|
+
balance = await client.getBalance();
|
|
355
|
+
}
|
|
356
|
+
json(res, { ok: true, chain: target, address, balance });
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
json(res, { error: err.message }, 500);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
316
363
|
if (p === '/api/markets') {
|
|
317
364
|
// Snapshot of every active data provider for the Markets panel:
|
|
318
365
|
// pipeline wiring (which endpoint serves which asset class), live
|
package/dist/router/index.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/router/index.js
CHANGED
|
@@ -265,7 +265,8 @@ function classicRouteRequest(prompt, profile) {
|
|
|
265
265
|
}
|
|
266
266
|
const model = tierConfigs[tier].primary;
|
|
267
267
|
const savings = computeSavings(model);
|
|
268
|
-
|
|
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