diff --git a/src/gateway/chat/build-tools.ts b/src/gateway/chat/build-tools.ts index e69b0ad..e74163a 100644 --- a/src/gateway/chat/build-tools.ts +++ b/src/gateway/chat/build-tools.ts @@ -844,7 +844,12 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) { type: 'function', function: { name: 'weather_search', - description: 'Get current weather or 5-day forecast for any city or coordinates using OpenWeather API. ALWAYS use this for weather/forecast/temperature queries — do NOT use web_search for weather.', + // 2026-07-29: this used to say "ALWAYS use this for weather/forecast/temperature + // queries", which crowded out the richer sibling tools — the model reached for it every + // time and could only ever report a temperature range and a sky word, so answers came + // out as one bare sentence with no precipitation chance at all. The "not web_search" + // part is still worth keeping; the "always" part was steering to the weakest option. + description: 'Current weather or 5-day min/max forecast for any city/coordinates (OpenWeather). Returns temperature range and sky condition only — NO precipitation chance, humidity or wind. For a forecast the user will act on ("비 와?", "우산 챙겨야 해?", tomorrow/this week), prefer weather_openmeteo, which adds precipitation probability and hourly detail. Never use web_search for weather.', parameters: { type: 'object', required: ['location'], properties: { @@ -873,7 +878,7 @@ export function createBuildTools(isOrchestrationSkillEnabled: () => boolean) { type: 'function', function: { name: 'weather_openmeteo', - description: 'Detailed hourly weather forecast via Open-Meteo (free, no key). Supports ECMWF/GFS models, UV index, soil temp, precipitation probability, and more for up to 16 days.', + description: 'PREFERRED forecast tool. Detailed weather via Open-Meteo (free, no key) — includes precipitation probability, hourly detail, UV index, ECMWF/GFS models, up to 16 days. Use this for any "will it rain / what is tomorrow like / this week" question; weather_search only carries temperature and a sky word, which is not enough to answer those.', parameters: { type: 'object', required: ['location'], properties: { diff --git a/src/gateway/chat/system-prompt.ts b/src/gateway/chat/system-prompt.ts index a8b1f24..e656d0c 100644 --- a/src/gateway/chat/system-prompt.ts +++ b/src/gateway/chat/system-prompt.ts @@ -124,5 +124,5 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string { ANTI-HALLUCINATION: When a tool returns a result, report EXACTLY what the tool returned — never contradict or ignore tool output. If a tool says "(no rows)", say so. Never invent data, file contents, table names, or command output. If you don't know something, call a tool to find out or say you don't know. VERIFY CONCRETE FACTS: for questions with a checkable real-world answer — specs, prices, versions, release dates, comparisons, "what models/options exist" — call web_search first and base the answer on what it returns, even if you're confident you already know it. Specific-sounding numbers you produce from memory (TFLOPS, VRAM, prices, dates) are exactly the kind of detail that goes stale or was never right — don't present them as fact unless a tool actually returned them. This does NOT apply to coding help, creative writing, opinions, or general reasoning — only to claims a search could confirm or refute. This also does NOT apply to the user's own private infra nicknames (지서버, 클로서버, and similar) — those are personal hardware, not public products, so web_search cannot verify them; answer those from USER.md/SOUL.md/[RECALLED_FACTS] context instead. TEMPORAL CONSISTENCY: The current date and day-of-week is given above ("Current date: ${dateStr}") — this is ground truth, more reliable than any search snippet's phrasing. Before answering whether something is open/trading/in-session RIGHT NOW (stock markets, exchanges, offices, stores), first check today's day-of-week against real-world facts (e.g. NYSE and KOSPI do not trade on Saturdays, Sundays, or market holidays, regardless of what time it is) — a market-hours formula alone is not enough if today isn't even a trading day. A search result reporting a "closing price" or "today's headline" is not proof today is a trading/business day — cross-check it against the actual current date above, and if they conflict (e.g. a stale cached result, or a result that doesn't state its own date), trust the current date and say so explicitly rather than presenting the search result as if it were live.${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock} CHEMISTRY NOTATION: NEVER draw molecular structures as ASCII art (H/C/#/=/\\/| characters arranged to look like a diagram) — it always renders as garbled, misaligned text. Instead: for a formula or reaction, use LaTeX inside $...$ (e.g. $\\ce{C4H10}$, $\\ce{2H2 + O2 -> 2H2O}$ — mhchem extension is loaded). For an actual 2D structure with real bond lines (rings, branches), output a \`\`\`smiles\`\`\` code block containing the SMILES string (e.g. \`\`\`smiles\\nc1ccccc1\\n\`\`\` for benzene, \`\`\`smiles\\nCC(=O)Oc1ccccc1C(=O)O\\n\`\`\` for aspirin) — the client automatically renders it as a proper 2D diagram with bond lines.${codeOutputRuleBlock}${packageInstallRuleBlock} -OUTPUT FORMAT: When presenting 3+ items (emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph. For news specifically: use a table with columns 제목|요약|출처, but write each 요약 as 2-3 full sentences covering the article's actual content — not a headline fragment restated. Include EVERY distinct article the news tool returned (it returns up to 10 per call and they are already filtered to the last 48 hours) — do not cherry-pick 3 of them into a "highlights" table. If several calls returned overlapping stories, merge duplicates but keep the union, aiming for a 8-10 row digest whenever that many distinct articles came back. For weather: do NOT force a table — explain current conditions and forecast in natural, fuller sentences (trend, precipitation chance, notable changes vs yesterday/normal), not just bare numbers.${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`; +OUTPUT FORMAT: When presenting 3+ items (emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph. For news specifically: use a table with columns 제목|요약|출처, but write each 요약 as 2-3 full sentences covering the article's actual content — not a headline fragment restated. Include EVERY distinct article the news tool returned (it returns up to 10 per call and they are already filtered to the last 48 hours) — do not cherry-pick 3 of them into a "highlights" table. If several calls returned overlapping stories, merge duplicates but keep the union, aiming for a 8-10 row digest whenever that many distinct articles came back. For weather: do NOT force a table — explain conditions in natural, fuller sentences rather than bare numbers, and use everything the tool actually returned: the multi-day trend it gives you (is it warming, cooling, steady?), precipitation chance, and anything notable. If the tool you called did not return precipitation chance, do NOT invent one and do NOT quietly skip it — call weather_openmeteo, which provides it. Never pad a weather answer with figures no tool gave you.${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`; } diff --git a/src/tools/weather.ts b/src/tools/weather.ts index a7a819a..b49f777 100644 --- a/src/tools/weather.ts +++ b/src/tools/weather.ts @@ -111,6 +111,39 @@ const KR_CITY_COORDS: Record = { '평택': [36.9921, 127.1128], 'pyeongtaek': [36.9921, 127.1128], '안산': [37.3219, 126.8310], 'ansan': [37.3219, 126.8310], '용인': [37.2411, 127.1776], 'yongin': [37.2411, 127.1776], + // 2026-07-29: added after "내일 구미 날씨" silently resolved to the WRONG place. Open-Meteo's + // geocoder returns ten 구미 hits for Korea and the real 구미시 (36.12, 128.34) is not among + // them — the list is dominated by tiny hamlets, and because every entry comes back with an + // empty population field the "pick the most populous" tiebreak degrades to results[0], a + // village in 강원도 at 38.16 — two degrees of latitude away, i.e. a different climate. + // No error was ever raised; the user just got someone else's weather. + // Any mid-size Korean city the geocoder cannot pin down belongs in this table. + '구미': [36.1195, 128.3446], 'gumi': [36.1195, 128.3446], + '김해': [35.2285, 128.8894], 'gimhae': [35.2285, 128.8894], + '안양': [37.3943, 126.9568], 'anyang': [37.3943, 126.9568], + '부천': [37.5034, 126.7660], 'bucheon': [37.5034, 126.7660], + '남양주': [37.6360, 127.2165], 'namyangju': [37.6360, 127.2165], + '화성': [37.1996, 126.8310], 'hwaseong': [37.1996, 126.8310], + '시흥': [37.3800, 126.8029], 'siheung': [37.3800, 126.8029], + '파주': [37.7599, 126.7800], 'paju': [37.7599, 126.7800], + '의정부': [37.7381, 127.0337], 'uijeongbu': [37.7381, 127.0337], + '김포': [37.6152, 126.7156], 'gimpo': [37.6152, 126.7156], + '광명': [37.4786, 126.8646], 'gwangmyeong': [37.4786, 126.8646], + '군포': [37.3617, 126.9352], 'gunpo': [37.3617, 126.9352], + '이천': [37.2723, 127.4350], 'icheon': [37.2723, 127.4350], + '양산': [35.3350, 129.0372], 'yangsan': [35.3350, 129.0372], + '거제': [34.8806, 128.6212], 'geoje': [34.8806, 128.6212], + '통영': [34.8544, 128.4331], 'tongyeong': [34.8544, 128.4331], + '경주': [35.8562, 129.2247], 'gyeongju': [35.8562, 129.2247], + '안동': [36.5684, 128.7294], 'andong': [36.5684, 128.7294], + '아산': [36.7898, 127.0018], 'asan': [36.7898, 127.0018], + '충주': [36.9910, 127.9259], 'chungju': [36.9910, 127.9259], + '원주': [37.3422, 127.9202], 'wonju': [37.3422, 127.9202], + '군산': [35.9676, 126.7369], 'gunsan': [35.9676, 126.7369], + '익산': [35.9483, 126.9576], 'iksan': [35.9483, 126.9576], + '순천': [34.9506, 127.4872], 'suncheon': [34.9506, 127.4872], + '광양': [34.9407, 127.6959], 'gwangyang': [34.9407, 127.6959], + '서귀포': [33.2541, 126.5601], 'seogwipo': [33.2541, 126.5601], }; // Same-named cities across countries (e.g. "Rome" → Rome, Italy vs Rome, Georgia US)