feat: snip a region into chat, and make links clickable (#14)
This commit was merged in pull request #14.
This commit is contained in:
+76
-10
@@ -1,5 +1,6 @@
|
||||
import { formatNow, getLocationContext, weekdayName } from "./location";
|
||||
import { webFetch, webSearch } from "./ollama";
|
||||
import type { GeoLookup } from "./geo";
|
||||
import type { ToolDefinition } from "./types";
|
||||
import { errorMessage } from "./utils";
|
||||
|
||||
@@ -19,6 +20,21 @@ export interface ToolContext {
|
||||
signal?: AbortSignal;
|
||||
/** Injectable clock so tests are deterministic. */
|
||||
now?: Date;
|
||||
/**
|
||||
* The user's Location preference, if they set one.
|
||||
*
|
||||
* Injected rather than read here, because this module is deliberately free of
|
||||
* Vicinae imports and preferences arrive through that API.
|
||||
*/
|
||||
place?: string;
|
||||
/**
|
||||
* Resolves a rough place from the user's IP address, when that is switched on.
|
||||
*
|
||||
* Injected for the same reason as `place`, and absent when IP geolocation is
|
||||
* off, which is what keeps this module from being able to reach the network
|
||||
* for a location on its own.
|
||||
*/
|
||||
ipPlace?: (options: { signal?: AbortSignal }) => Promise<GeoLookup>;
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
@@ -45,7 +61,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
function: {
|
||||
name: "get_location",
|
||||
description:
|
||||
"Get the user's approximate location context: timezone, UTC offset, locale, region, and city. Use it for local questions such as weather, business hours, or travel timing. This is derived from the device timezone, not GPS, so treat it as a region rather than an address.",
|
||||
"Get where the user is, for local questions such as weather, business hours, or travel timing. Returns the place they configured in preferences, plus the timezone, UTC offset, locale and region. Without a configured place this is region-level, not an address, and the timezone area it reports is the timezone's own name rather than the user's city, so never treat it as one.",
|
||||
parameters: { type: "object", properties: {}, required: [] },
|
||||
},
|
||||
},
|
||||
@@ -140,7 +156,7 @@ export async function executeTool(
|
||||
return { content: describeNow(context.now), isError: false };
|
||||
|
||||
case "get_location":
|
||||
return { content: describeLocation(context.now), isError: false };
|
||||
return { content: await describeLocation(context), isError: false };
|
||||
|
||||
case "web_search":
|
||||
return await runWebSearch(args, context);
|
||||
@@ -172,18 +188,68 @@ function describeNow(now?: Date): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function describeLocation(now?: Date): string {
|
||||
const date = now ?? new Date();
|
||||
const location = getLocationContext(date);
|
||||
async function describeLocation(context: ToolContext): Promise<string> {
|
||||
const date = context.now ?? new Date();
|
||||
const configured = context.place?.trim() || undefined;
|
||||
|
||||
const lines = [
|
||||
// A place the user typed wins outright, and while it is set there is no reason
|
||||
// to hand their IP address to anyone.
|
||||
const lookup =
|
||||
configured || !context.ipPlace ? undefined : await context.ipPlace({ signal: context.signal });
|
||||
const ip = lookup?.status === "ok" ? lookup.place : undefined;
|
||||
|
||||
const location = getLocationContext(date, undefined, configured ?? ip?.place);
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
// The place comes first because it is the only answer that is about where the
|
||||
// user actually is, rather than about which zone the clock is set to.
|
||||
if (location.place) lines.push(`Place: ${location.place}`);
|
||||
|
||||
lines.push(
|
||||
`Timezone: ${location.timezone}`,
|
||||
`UTC offset: ${location.utcOffset}`,
|
||||
`Locale: ${location.locale}`,
|
||||
];
|
||||
if (location.city) lines.push(`City: ${location.city}`);
|
||||
if (location.region) lines.push(`Region: ${location.region}`);
|
||||
lines.push("Note: derived from the device timezone. It indicates region, not a precise address.");
|
||||
);
|
||||
|
||||
if (location.region) {
|
||||
lines.push(`Region: ${location.region}${location.regionCode ? ` (${location.regionCode})` : ""}`);
|
||||
}
|
||||
if (location.timezoneArea) {
|
||||
lines.push(`Timezone area: ${location.timezoneArea}`);
|
||||
}
|
||||
|
||||
// A mismatch between the clock and the IP is worth surfacing: it usually means
|
||||
// a VPN or a device still set to somewhere the user has left.
|
||||
if (ip?.timezone && ip.timezone !== location.timezone) {
|
||||
lines.push(`IP-reported timezone: ${ip.timezone}, which differs from the device timezone`);
|
||||
}
|
||||
|
||||
if (configured) {
|
||||
lines.push(
|
||||
"Note: Place is the user's own setting, so prefer it for anything that depends on exactly where they are.",
|
||||
);
|
||||
} else if (ip) {
|
||||
lines.push(
|
||||
`Note: Place came from an IP address lookup via ${ip.source}, so treat it as a rough guess rather than the user's city. An IP address locates where the request left the ISP's network, which can be hundreds of kilometres away, and it can be out of date if they have moved since.`,
|
||||
);
|
||||
} else if (lookup?.status === "failed") {
|
||||
// Deliberately not the "no Location is set" wording below. A failed lookup
|
||||
// and an unset preference are different problems, and reporting the wrong
|
||||
// one sends the user looking in the wrong place. This is also the only
|
||||
// channel that reaches them: the model paraphrases, so it is told to say so.
|
||||
lines.push(
|
||||
`Note: IP geolocation is switched on but the lookup failed (${lookup.message}), so no place could be resolved. Tell the user this plainly. Setting Location in preferences avoids the lookup and the network entirely. Everything below is derived from the device timezone and locale, so it is region-level.`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"Note: no Location is set in preferences. Everything above is derived from the device timezone and locale, so it is accurate to a region and not to a city. Ask which city they are in when an exact place matters.",
|
||||
);
|
||||
}
|
||||
|
||||
if (location.timezoneArea) {
|
||||
lines.push("The timezone area is the timezone's own name, not the user's city.");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user