Spaces:
Running
Running
File size: 6,896 Bytes
210ef30 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """Tools that run in the brain: search, weather, reminders, memory. Blueprint 8.1 and 8.2."""
from datetime import datetime, timedelta, tzinfo
from typing import Any
import httpx
from ..memory import service as memory
from ..memory.scope import forced_scope
from ..persona.types import Mode
from . import weather
from .registry import Tool, ToolContext, ToolOutcome, failed, ok, schema, string
DEFAULT_CITY = "Kolkata"
#: A reminder "in the past" by less than this is treated as "now" — the model computed the time
#: a few seconds before the call landed.
PAST_TOLERANCE = timedelta(minutes=2)
async def _web_search(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
"""Blueprint 8.2: a separate grounded call; the sources become chips under the reply."""
answer, sources = await ctx.llm.grounded_search(str(args["query"]))
if not answer:
return ToolOutcome(failed("the search found nothing useful"))
return ToolOutcome(ok("searched", answer=answer), sources=sources)
async def _get_weather(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
place = (
str(args.get("location") or "").strip()
or str(ctx.hud.get("city") or "").strip()
or str((ctx.settings.get("hud") or {}).get("city") or "").strip()
or DEFAULT_CITY
)
client = ctx.http or httpx.AsyncClient(timeout=10.0)
try:
report = await weather.current(client, place)
except weather.WeatherError as exc:
return ToolOutcome(failed(str(exc)))
finally:
if ctx.http is None:
await client.aclose()
return ToolOutcome(ok("weather", **report))
def parse_when(raw: str, ctx: ToolContext) -> datetime | None:
"""An ISO time from the model. A naive one is read in HIS timezone, never UTC (CLAUDE.md)."""
try:
when = datetime.fromisoformat(raw.strip().replace("Z", "+00:00"))
except ValueError:
return None
if when.tzinfo is None:
when = when.replace(tzinfo=ctx.tz)
return when
def local_label(when: datetime, tz: tzinfo) -> str:
local = when.astimezone(tz)
hour = local.hour % 12 or 12
suffix = "AM" if local.hour < 12 else "PM"
return f"{local.strftime('%a %d %b')}, {hour}:{local.minute:02d} {suffix}"
async def _set_reminder(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
when = parse_when(str(args["when_iso"]), ctx)
if when is None:
return ToolOutcome(failed("when_iso must be an ISO 8601 date and time"))
if when < ctx.now - PAST_TOLERANCE:
return ToolOutcome(failed("that time has already passed"))
when = max(when, ctx.now)
text = str(args["text"]).strip()
row = await ctx.store.add_reminder(ctx.user_id, ctx.mode, text, when)
return ToolOutcome(ok("reminder set", id=str(row["id"]), due=local_label(when, ctx.tz)))
async def _list_reminders(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
rows = await ctx.store.list_reminders(ctx.user_id)
# Normal mode never lists a reminder that was created in GF mode — its text may be private.
visible = [row for row in rows if ctx.mode is Mode.GF or row["mode"] == Mode.NORMAL.value]
return ToolOutcome(
ok(
f"{len(visible)} upcoming",
reminders=[
{
"id": str(row["id"]),
"text": row["text"],
"due": local_label(row["due_at"], ctx.tz),
}
for row in visible
],
)
)
async def _cancel_reminder(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
cancelled = await ctx.store.cancel_reminder(ctx.user_id, str(args["id"]))
return ToolOutcome(ok("cancelled") if cancelled else failed("no reminder with that id"))
async def _remember(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
scope = forced_scope(args.get("scope"), ctx.mode)
row, duplicate = await memory.remember(
ctx.store, ctx.llm, ctx.user_id, str(args["fact"]), scope, is_manual=True
)
if duplicate is not None:
return ToolOutcome(ok("already remembered", existing=duplicate.content))
return ToolOutcome(ok("remembered", scope=scope.value))
async def _forget(args: dict[str, Any], ctx: ToolContext) -> ToolOutcome:
"""Finds the memory but never deletes it: he confirms on the device first (blueprint 8.1)."""
hit = await memory.find_to_forget(ctx.store, ctx.llm, ctx.user_id, str(args["query"]), ctx.mode)
if hit is None:
return ToolOutcome(failed("nothing like that is in memory"))
action = {"type": "confirm_forget", "memoryId": hit.id, "content": hit.content}
return ToolOutcome(ok("asked him to confirm", found=hit.content), client_actions=[action])
SERVER_TOOLS: list[Tool] = [
Tool(
"web_search",
"Search the web and get a current answer with sources — news, scores, prices, anything "
"that changes. Use it instead of guessing about recent events.",
schema({"query": string("What to find out")}, ["query"]),
_web_search,
),
Tool(
"get_weather",
"Current weather and today's range. Omit location to use where he is.",
schema({"location": string("A city or place name")}),
_get_weather,
),
Tool(
"set_reminder",
"Set a reminder that arrives as a notification at a specific time. Work out the exact "
"time from CURRENT TIME in your instructions and his timezone.",
schema(
{
"text": string("What to remind him about, as he should read it"),
"when_iso": string("ISO 8601 local date and time, e.g. 2026-09-19T14:45:00"),
},
["text", "when_iso"],
),
_set_reminder,
),
Tool(
"list_reminders",
"List his upcoming reminders.",
schema({}),
_list_reminders,
),
Tool(
"cancel_reminder",
"Cancel a reminder by its id (get ids from list_reminders).",
schema({"id": string("The reminder id")}, ["id"]),
_cancel_reminder,
),
Tool(
"remember",
"Save a fact about him for the long term, when he explicitly asks you to remember "
"something. One short third-person sentence in English.",
schema(
{
"fact": string("The fact, e.g. 'His dentist appointment is on Friday.'"),
"scope": string(
"private for anything romantic or intimate; shared for everything else",
["shared", "private"],
),
},
["fact"],
),
_remember,
),
Tool(
"forget",
"Find a memory he wants deleted. He confirms on his device before anything is deleted.",
schema({"query": string("What he wants forgotten")}, ["query"]),
_forget,
),
]
|