Add a new tool
A new capability in FRIDAY is two things, always together: the capability itself, and the intent pattern that routes to it. Skip the pattern and the small chat model will happily fabricate success for a tool it never called.
app.register_capability(...) — new or fixed — must also have an intent pattern in core/intent_recognizer.py, unless it is intentionally LLM-routed only (rare). context_terms, aliases, and description feed the LLM RouteScorer, not the deterministic recogniser.1. Register the capability
Inside your module's setup(app), register a tool spec, a handler, and capability metadata. The handler receives the raw text and a parsed args dict.
def setup(app):
app.register_capability(
spec={
"name": "set_brightness",
"description": "Set the display brightness to a percentage.",
"parameters": {"level": {"type": "integer"}},
"aliases": ["brightness", "screen brightness"],
"context_terms": ["dim", "bright", "display"],
},
handler=handle_set_brightness,
connectivity="local", # local | online
side_effect_level="write", # read | write | critical
)
def handle_set_brightness(text, args):
level = int(args.get("level", 50))
# ...do the work...
return f"Brightness set to {level}."2. Add a deterministic intent pattern
Pick (or add) a _parse_<domain> method in core/intent_recognizer.py, add your regex(es), and return the canonical action dict. Always gate on tool presence so the parser is harmless when the capability isn't loaded.
def _parse_brightness(self, clause: str):
# Gate: do nothing if the capability isn't registered
if "set_brightness" not in getattr(self.router, "_tools_by_name", {}):
return None
m = re.search(
r"\b(?:set|change|make|put|turn)\b.*\bbrightness\b.*?(\d{1,3}|max|min|fifty)",
clause, re.I,
)
if not m:
return None
raw = m.group(1).lower()
level = {"max": 100, "min": 0, "fifty": 50}.get(raw, None)
level = level if level is not None else max(0, min(100, int(raw)))
return {
"tool": "set_brightness",
"args": {"level": level},
"text": clause,
"domain": "system",
}Then register the parser in the _parse_clause chain, minding order:
# Narrow / explicit parsers go BEFORE broad catch-alls.
# e.g. _parse_screen_lock before _parse_help so "lock screen"
# never matches a help query.
for parser in (
self._parse_pending_selection, # guards always first
...
self._parse_brightness, # your new parser
...
self._parse_greeting, # lowest priority
):3. Make the pattern robust
Cover at least these axes so real speech actually matches:
- Verb variants — set / change / make / put / turn.
- Object variants — “lock screen” / “lock the screen” / “lock yourself”.
- Word order — “brightness 80” and “set 80 brightness”.
- Spoken cardinals — “fifty” → 50, “max” → 100, “minimum” → 0.
- Optional argument shapes — “unlock screen” and “unlock with pin 1234” route to the same tool.
- Filler tolerance — “Friday rescan my apps” and “rescan apps please”.
battery, volume, screenshot) without a verb anchor — those words appear in unrelated sentences (“the battery in my car died”) and cause false-positive routing. Don't poach phrasings that belong to another parser.4. Tests are mandatory
Add tests/test_<domain>_intent.py following the _make_recognizer(tools=[...]) pattern. Parametrize the positive phrasings and include at least one negative phrasing that must not match.
import pytest
from tests.helpers import _make_recognizer
@pytest.mark.parametrize("phrase,level", [
("set brightness to 60", 60),
("make the screen brightness fifty", 50),
("turn brightness to max", 100),
("brightness 80", 80),
])
def test_brightness_matches(phrase, level):
rec = _make_recognizer(tools=["set_brightness"])
action = rec.plan(phrase).steps[0]
assert action.tool == "set_brightness"
assert action.args["level"] == level
def test_brightness_negative():
rec = _make_recognizer(tools=["set_brightness"])
assert rec.plan("the future is bright for us").steps == []5. Update the testing guide
Add or update the matching T-N.M entry in docs/testing_guide.md. Its You say field lists the natural phrasings a user can speak — which doubles as the live spec of what your parser must accept.