119 lines
5.0 KiB
Python
119 lines
5.0 KiB
Python
"""
|
|
title: Quantitative Question Nudge
|
|
author: owuitools
|
|
version: 0.2.0
|
|
description: Raises tool-use salience on measurement-bearing questions that are phrased casually.
|
|
"""
|
|
|
|
# WHY THIS EXISTS
|
|
#
|
|
# The system prompt already says that reasoning tasks need tools and that
|
|
# ruling a cause out is itself a claim. It does not fire on questions that
|
|
# are phrased conversationally. Measured case:
|
|
#
|
|
# "im logging vibration off a motor at 500Hz, motor runs 3600 rpm but
|
|
# the biggest fft peak is at 40Hz. is my sensor broken?"
|
|
#
|
|
# Zero tool calls, and the answer stated "aliasing cannot move a 60 Hz peak
|
|
# to 40 Hz - so 40 Hz is a REAL vibration". True of the fundamental and
|
|
# wrong as an elimination: the 9th harmonic at 540 Hz folds to exactly
|
|
# 40.0 Hz at that sample rate. That sends someone hunting a phantom
|
|
# resonance instead of fitting an anti-alias filter. Adding a paragraph to
|
|
# the system prompt did not change it - which is this suite's most repeated
|
|
# finding: static prompt text loses to the shape of the conversation.
|
|
#
|
|
# An inlet note is a different lever. It arrives as the most recent thing
|
|
# in the conversation rather than as line 40 of a long preamble, so it is
|
|
# read as part of the current task. It is invisible to the user: inlet
|
|
# rewrites what goes to the model, not what is displayed.
|
|
#
|
|
# Deliberately narrow. It fires only when a message carries an actual
|
|
# measurement - a number with a unit - because that is what distinguishes
|
|
# "is my sensor broken" from "is Postgres a good choice". A nudge on every
|
|
# message would be one more paragraph the model learns to skim.
|
|
#
|
|
# WHY IT NAMES THE TOOLS
|
|
#
|
|
# The first version said "check it with a tool" and left the choice open.
|
|
# On a compound-interest question the model duly reached for a tool - and
|
|
# reached for web_search, to look up a formula it already knew, then did
|
|
# the actual arithmetic in its head and got it wrong. Tool-seeking without
|
|
# tool-selection. The note now says which tool does what, and says out loud
|
|
# that searching for a formula you know is not the point.
|
|
|
|
import re
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# A number bound to a unit. Bare numbers are excluded on purpose: "give me
|
|
# 3 options" is not a measurement. Currency and durations are in because
|
|
# "$4,000 at 5% for 10 years" is as quantitative as anything in volts, and
|
|
# the arithmetic is exactly where in-head answers go wrong.
|
|
_MEASUREMENT = re.compile(
|
|
r"[$£€]\s?\d"
|
|
r"|\b\d+(?:\.\d+)?\s?"
|
|
r"(?:hz|khz|mhz|ghz|rpm|v|kv|mv|a|ma|w|kw|mw|va|kva|ohm|ohms|"
|
|
r"db|dbm|s|ms|us|ns|m|mm|cm|km|kg|g|n|nm|pa|kpa|bar|psi|"
|
|
r"c|f|k|%|gb|mb|kb|tb|bps|kbps|mbps|gbps|awg|bit|bits|"
|
|
r"years?|yrs?|months?|weeks?|days?)\b",
|
|
re.I,
|
|
)
|
|
|
|
_NOTE = (
|
|
"Reminder for this question: it contains measurements, so it is a technical question "
|
|
"however casually it is worded. Before asserting any number, mechanism or standard, check "
|
|
"it with a tool: the calculators (ohms_law_power, component_calc, wiring_calc, pcb_calc, "
|
|
"subnet_calc) for what they cover, matlab_run for any other arithmetic - compounding, unit "
|
|
"conversions, statistics, counting - and web_search then fetch_page for facts and "
|
|
"standards, not for formulas you already know. In particular, if you are about to rule a "
|
|
"cause OUT, enumerate the variants of that cause and test the ones you have not considered "
|
|
"- a correct elimination of the obvious version and a missed elimination of a subtler "
|
|
"version look identical in the answer, and send the user to fix the wrong thing."
|
|
)
|
|
|
|
|
|
class Filter:
|
|
class Valves(BaseModel):
|
|
enabled: bool = Field(
|
|
default=True,
|
|
description="Turn the nudge off without deleting the filter.",
|
|
)
|
|
min_chars: int = Field(
|
|
default=40,
|
|
description="Skip very short messages. 'what is 3 dB' needs no reminder.",
|
|
)
|
|
|
|
def __init__(self):
|
|
self.valves = self.Valves()
|
|
|
|
async def inlet(self, body: dict, __user__=None) -> dict:
|
|
if not self.valves.enabled:
|
|
return body
|
|
try:
|
|
messages = body.get("messages") or []
|
|
if not messages:
|
|
return body
|
|
|
|
last_user = None
|
|
for message in reversed(messages):
|
|
if message.get("role") == "user":
|
|
last_user = message
|
|
break
|
|
if last_user is None:
|
|
return body
|
|
|
|
text = last_user.get("content")
|
|
if not isinstance(text, str) or len(text) < self.valves.min_chars:
|
|
return body
|
|
if not _MEASUREMENT.search(text):
|
|
return body
|
|
|
|
# Appended AFTER the user's turn, not before it. Inserted
|
|
# before, this was read as more preamble and changed nothing;
|
|
# the last message in the list is the one that gets weight.
|
|
messages.append({"role": "system", "content": _NOTE})
|
|
body["messages"] = messages
|
|
except Exception:
|
|
return body
|
|
return body
|