From da3d5e558a329f6ab729a547476c8795dec44559 Mon Sep 17 00:00:00 2001
From: Fsocietyhhh <1211904451@qq.com>
Date: Thu, 16 Jul 2026 11:03:59 +0800
Subject: [PATCH 1/2] fix: route web prediction prompts to market data
---
src/hooks/use-franklin-chat.ts | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/hooks/use-franklin-chat.ts b/src/hooks/use-franklin-chat.ts
index 63c15be..980e289 100644
--- a/src/hooks/use-franklin-chat.ts
+++ b/src/hooks/use-franklin-chat.ts
@@ -372,6 +372,24 @@ const TOOL_ACTIVATION_NOTE =
"\n\nIMPORTANT: Your tools are hidden by default. To use any capability (web search, prediction markets, prices, music, phone), " +
"first call activate_tool with the names you need, then call the tool on the following turn.";
+// The web client intentionally starts with a tiny tool inventory, but an
+// explicit prediction-market request is not ambiguous: routing it through a
+// general web search loses the live, structured Predexon result and encourages
+// the model to answer from search snippets. The CLI exposes this capability
+// directly, so mirror that behaviour here for clear Polymarket/odds requests.
+// The focused-tool buttons still take precedence over this automatic routing.
+function impliedToolForPrompt(prompt: string): string | undefined {
+ const text = prompt.toLowerCase();
+ if (
+ /\b(polymarket|kalshi|limitless|predict\.fun)\b/.test(text) ||
+ /\b(prediction market|betting odds|implied probability|odds)\b/.test(text) ||
+ /预测市场|赔率|盘口|胜率/.test(prompt)
+ ) {
+ return "search_prediction_markets";
+ }
+ return undefined;
+}
+
// Split inline … chain-of-thought out of a model's text.
function splitThinking(raw: string): { answer: string; thinking: string } {
let thinking = "";
@@ -991,7 +1009,7 @@ export function useFranklinChat(
let effectiveModel = base === "blockrun/auto" ? resolveAuto(lastPrompt) : base;
if (hasImage && !isVisionModel(effectiveModel)) effectiveModel = pickVisionSibling(effectiveModel);
modelRef.current = effectiveModel;
- await runChatWithTools(history, effectiveModel, forceTool);
+ await runChatWithTools(history, effectiveModel, forceTool ?? impliedToolForPrompt(lastPrompt));
}
// Detached, per-conversation media job (image/video). Adds the user message,
From 1fb96cdded77068384e3ea203b3c2feca5a5a6a9 Mon Sep 17 00:00:00 2001
From: 1bcMax
Date: Thu, 16 Jul 2026 00:19:47 -0500
Subject: [PATCH 2/2] fix: tighten implied prediction-market routing to kill
forced-paid-call false positives
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A regex match here forces a paid tool_choice call the model cannot
decline, so the matcher must be near-zero false positive. Review of
PR #21 found the opposite:
- bare "odds" fired on everyday English ("odds and ends", "odds
ratio", "odds of rolling two sixes") and made "betting odds" dead
- bare "limitless" fired on the common adjective; now requires market
context (limitless.exchange / limitless exchange|market(s))
- bare CJK 胜率/盘口/赔率 fired on game win rates, order-book slang and
lottery questions; now require betting context (预测/押注/下注/博彩)
- the canonical plural "prediction markets" never matched because the
trailing \b rejected the "s"; now prediction markets? and
implied probabilit(y|ies) both match
Also from review:
- implied routing now only runs with a connected wallet, so wallet-less
visitors asking a matching question get an answer instead of a
wallet-connect wall
- the focus nudge no longer claims "the user selected" a capability the
regex chose; on a residual false positive the model is told to ignore
an irrelevant result and answer the actual question
---
src/hooks/use-franklin-chat.ts | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/src/hooks/use-franklin-chat.ts b/src/hooks/use-franklin-chat.ts
index 980e289..7125aa1 100644
--- a/src/hooks/use-franklin-chat.ts
+++ b/src/hooks/use-franklin-chat.ts
@@ -378,12 +378,20 @@ const TOOL_ACTIVATION_NOTE =
// the model to answer from search snippets. The CLI exposes this capability
// directly, so mirror that behaviour here for clear Polymarket/odds requests.
// The focused-tool buttons still take precedence over this automatic routing.
+//
+// A match here forces a PAID tool call the model cannot decline, so the
+// patterns must be near-zero false positive: venue names (except "limitless",
+// a common adjective, which needs market context) and unambiguous
+// prediction-market phrases only. Bare "odds"/胜率/盘口 fire on everyday
+// language ("odds and ends", game win rates, order-book slang) and are
+// deliberately excluded; ambiguous CJK betting terms require betting context.
function impliedToolForPrompt(prompt: string): string | undefined {
const text = prompt.toLowerCase();
if (
- /\b(polymarket|kalshi|limitless|predict\.fun)\b/.test(text) ||
- /\b(prediction market|betting odds|implied probability|odds)\b/.test(text) ||
- /预测市场|赔率|盘口|胜率/.test(prompt)
+ /\b(polymarket|kalshi|predict\.fun|limitless\.exchange|limitless (?:exchange|markets?))\b/.test(text) ||
+ /\b(prediction markets?|betting odds|market odds|implied probabilit(?:y|ies))\b/.test(text) ||
+ /预测市场/.test(prompt) ||
+ (/赔率|盘口|胜率/.test(prompt) && /预测|押注|下注|博彩/.test(prompt))
) {
return "search_prediction_markets";
}
@@ -824,13 +832,18 @@ export function useFranklinChat(
// (mirrors Franklin CLI's src/agent/llm.ts). Tool-use / tool-result blocks
// live only in this API-local array; the user only sees their message and
// the final answer text.
- async function runChatWithTools(history: ChatMessage[], model: string, forceTool?: string) {
+ async function runChatWithTools(history: ChatMessage[], model: string, forceTool?: string, impliedTool?: boolean) {
setStatus("thinking");
type ApiMsg = Record;
// Focus mode: nudge the model to use the chosen tool (tool_choice forces it,
- // this just improves the phrasing of the answer afterwards).
+ // this just improves the phrasing of the answer afterwards). Implied routing
+ // (regex-detected, not user-selected) must NOT claim the user chose the tool:
+ // on a false-positive match the honest framing lets the model discard an
+ // irrelevant result and answer the actual question.
const focusNudge = forceTool
- ? `\n\nThe user selected the ${toolLabel(forceTool)} capability — use the ${forceTool} tool to answer this turn, then summarize the result clearly.`
+ ? impliedTool
+ ? `\n\nThe request looks like a prediction-market question, so the ${forceTool} tool has been pre-selected for this turn. If its result is irrelevant to what the user actually asked, ignore it and answer the question directly.`
+ : `\n\nThe user selected the ${toolLabel(forceTool)} capability — use the ${forceTool} tool to answer this turn, then summarize the result clearly.`
: "";
const systemPrompt = `${FRANKLIN_SYSTEM_PROMPT}\n\n${systemPromptDateLine(userTz())}\n\n${FRANKLIN_TOOLS_PROMPT}${TOOL_ACTIVATION_NOTE}${focusNudge}`;
// Convert a user-attached image to an Anthropic image content block.
@@ -1009,7 +1022,12 @@ export function useFranklinChat(
let effectiveModel = base === "blockrun/auto" ? resolveAuto(lastPrompt) : base;
if (hasImage && !isVisionModel(effectiveModel)) effectiveModel = pickVisionSibling(effectiveModel);
modelRef.current = effectiveModel;
- await runChatWithTools(history, effectiveModel, forceTool ?? impliedToolForPrompt(lastPrompt));
+ // Implied routing only runs with a connected wallet: forcing a paid tool on
+ // a wallet-less visitor would replace their answer with a connect-wallet
+ // prompt. Without a wallet the model routes normally (and can still ask).
+ const implied =
+ !forceTool && isConnectedRef.current ? impliedToolForPrompt(lastPrompt) : undefined;
+ await runChatWithTools(history, effectiveModel, forceTool ?? implied, Boolean(implied));
}
// Detached, per-conversation media job (image/video). Adds the user message,