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,