crow-central-agency 0.26.7 → 0.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/_shared/schemas/agent.schema.js +2 -1
- package/dist/config/env.js +6 -0
- package/dist/mcp/agents/search-workspace.js +3 -3
- package/dist/mcp/artifacts/list-artifacts.js +3 -3
- package/dist/mcp/artifacts/list-circle-artifacts.js +3 -3
- package/dist/mcp/claude-mcp-adapter.js +30 -2
- package/dist/mcp/copilot-mcp-adapter.js +10 -2
- package/dist/mcp/feed/get-feed-items.js +3 -3
- package/dist/mcp/feed/get-recent-feed-items.js +3 -3
- package/dist/mcp/feed/list-feeds.js +3 -3
- package/dist/mcp/feed/search-feed-items.js +3 -3
- package/dist/mcp/places/geocode-place.js +42 -18
- package/dist/mcp/places/places-format-utils.js +47 -1
- package/dist/mcp/places/places-routing-input.js +49 -0
- package/dist/mcp/places/search-nearby-places.js +12 -1
- package/dist/mcp/reminders/list-reminders.js +3 -3
- package/dist/mcp/tool-utils.js +27 -10
- package/dist/public/assets/{architectureDiagram-3BPJPVTR-BACYknch.js → architectureDiagram-3BPJPVTR-DeXG4IF1.js} +1 -1
- package/dist/public/assets/{chunk-727SXJPM-BCyrHTp7.js → chunk-727SXJPM-ClhCvSxl.js} +1 -1
- package/dist/public/assets/{chunk-AQP2D5EJ-DQW6WpUt.js → chunk-AQP2D5EJ-S71PlnyF.js} +1 -1
- package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-vP76aZOW.js → classDiagram-4FO5ZUOK-t2EdfGSJ.js} +1 -1
- package/dist/public/assets/{classDiagram-4FO5ZUOK-vP76aZOW.js → classDiagram-v2-Q7XG4LA2-t2EdfGSJ.js} +1 -1
- package/dist/public/assets/{dagre-BM42HDAG-cX3IYed4.js → dagre-BM42HDAG-BUN2v9KO.js} +1 -1
- package/dist/public/assets/{diagram-2AECGRRQ-BdnHDBWj.js → diagram-2AECGRRQ-BsEdgIXV.js} +1 -1
- package/dist/public/assets/{diagram-5GNKFQAL-BEF52y8F.js → diagram-5GNKFQAL-DkDNM-w8.js} +1 -1
- package/dist/public/assets/{diagram-LMA3HP47-CkJFMgD9.js → diagram-LMA3HP47-bemQvtc4.js} +1 -1
- package/dist/public/assets/{diagram-OG6HWLK6-CzOfEubM.js → diagram-OG6HWLK6-DTt1CSKj.js} +1 -1
- package/dist/public/assets/{erDiagram-TEJ5UH35-B6X8wID6.js → erDiagram-TEJ5UH35-Ctez1ZUQ.js} +1 -1
- package/dist/public/assets/{flowDiagram-I6XJVG4X-Bx048skw.js → flowDiagram-I6XJVG4X-6cl55wrS.js} +1 -1
- package/dist/public/assets/{index-BuB5ROMJ.js → index-ISMoAJEC.js} +6 -6
- package/dist/public/assets/{infoDiagram-5YYISTIA-BfOovwvr.js → infoDiagram-5YYISTIA-DJG5GveT.js} +1 -1
- package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-Bq2pdr7u.js → ishikawaDiagram-YF4QCWOH-BiRWZQoC.js} +1 -1
- package/dist/public/assets/{kanban-definition-UN3LZRKU-BYNOT0q9.js → kanban-definition-UN3LZRKU--LGs8j1A.js} +1 -1
- package/dist/public/assets/{mindmap-definition-RKZ34NQL-ZqyFQHUp.js → mindmap-definition-RKZ34NQL-CBTeUNeJ.js} +1 -1
- package/dist/public/assets/{pieDiagram-4H26LBE5-adI7z4xv.js → pieDiagram-4H26LBE5-Bj8lCWKM.js} +1 -1
- package/dist/public/assets/{requirementDiagram-4Y6WPE33-DSQpGrFx.js → requirementDiagram-4Y6WPE33-DNbmiPTP.js} +1 -1
- package/dist/public/assets/{sequenceDiagram-3UESZ5HK-CedIlDfK.js → sequenceDiagram-3UESZ5HK-DOZcdL-V.js} +1 -1
- package/dist/public/assets/{stateDiagram-AJRCARHV-Dyff9k9-.js → stateDiagram-AJRCARHV-BCpPGAsz.js} +1 -1
- package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-BYjnQQxJ.js → stateDiagram-v2-BHNVJYJU-hUzXDK6m.js} +1 -1
- package/dist/public/assets/{timeline-definition-PNZ67QCA-BXAcpPyH.js → timeline-definition-PNZ67QCA-BsNHIWNc.js} +1 -1
- package/dist/public/assets/{vennDiagram-CIIHVFJN-CmL9UiW5.js → vennDiagram-CIIHVFJN-DCZMeVJ8.js} +1 -1
- package/dist/public/assets/{wardleyDiagram-YWT4CUSO-rPbjnaXo.js → wardleyDiagram-YWT4CUSO-B15lhDxS.js} +1 -1
- package/dist/public/assets/{xychartDiagram-2RQKCTM6-BTJUCOrb.js → xychartDiagram-2RQKCTM6-u23J3wWJ.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/routes/agent.routes.js +3 -0
- package/dist/routines/gmail-notification-routine.js +6 -1
- package/dist/sensors/geolocation-sensor.js +2 -2
- package/dist/services/agent-registry.js +4 -1
- package/dist/services/google/google-request.js +5 -1
- package/dist/services/google/google-request.types.js +11 -1
- package/dist/services/places/google/google-address-utils.js +51 -0
- package/dist/services/places/google/google-opening-hours-parser.js +105 -0
- package/dist/services/places/google/google-place-mapper.js +180 -0
- package/dist/services/places/google/google-place-type-mapping.js +70 -0
- package/dist/services/places/google/google-places-adapter.js +129 -0
- package/dist/services/places/google/google-places-adapter.types.js +105 -0
- package/dist/services/places/google/google-places-client.js +166 -0
- package/dist/services/places/google/google-request-utils.js +22 -0
- package/dist/services/places/google/google-search-area.js +31 -0
- package/dist/services/places/google/google-transit-mapping.js +43 -0
- package/dist/services/places/places-manager.js +19 -1
- package/dist/services/places/places-manager.types.js +37 -0
- package/dist/utils/number-utils.js +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -107,6 +107,9 @@ See `.env.example` for the full list, including:
|
|
|
107
107
|
- `FEED_TEXT_GENERATION_*` — optional OpenAI-compatible endpoint used by the feed manager to summarize feed items into a consistent length for better agent consumption.
|
|
108
108
|
- `FEED_MAX_SUMMARIZATION_ITEMS` — cap on items summarized per refresh (default: `50`). Items left unsummarized become retry candidates on the next refresh; when the retry backlog exceeds the cap, retries are skipped so a feed isn't permanently blocked by hundreds or thousands of stuck unsummarized items.
|
|
109
109
|
- `AUDIO_GENERATION_*` — optional Gemini TTS configuration that powers the play-message button on the agent console. See [Audio generation](#audio-generation) below.
|
|
110
|
+
- `PLACES_DEFAULT_SOURCE` — default provider for the Places tools when a lookup doesn't request one explicitly: `OSM` (default, no key needed) or `GOOGLE`. `GOOGLE` requires `GOOGLE_PLACES_API_KEY`; without it, lookups fall back to OSM. See [Places](#places) below.
|
|
111
|
+
- `GOOGLE_PLACES_API_KEY` — Google API key that registers the Google Places adapter alongside the default OSM source. See [Places](#places) below.
|
|
112
|
+
- `PHOTON_API_URL` / `OVERPASS_INTERPRETER_URL` — override the geocoding / nearby-search endpoints the OSM places source calls (default to the public Photon and Overpass instances).
|
|
110
113
|
- `GOOGLE_CONNECTOR_CLIENT_ID` / `GOOGLE_CONNECTOR_CLIENT_SECRET` / `CONNECTOR_CALLBACK_URL` — OAuth credentials for the Google connector. Required if you want agents to access Gmail / Calendar / Contacts. See [Connectors](#connectors) below.
|
|
111
114
|
- `OAUTH_PENDING_STATE_TTL_MS` — how long an unfinished OAuth flow stays valid before being swept (default: `600_000`, i.e. 10 minutes).
|
|
112
115
|
- `OTEL_*` — optional OpenTelemetry export.
|
|
@@ -139,6 +142,73 @@ The `AUDIO_GENERATION_API_KEY` must be a Google API key with access to the
|
|
|
139
142
|
You can use the same key as `TEXT_GENERATION_API_KEY` if that is also a
|
|
140
143
|
Gemini-backed configuration, but the two are read independently.
|
|
141
144
|
|
|
145
|
+
## Places
|
|
146
|
+
|
|
147
|
+
The **Places** MCP server gives agents location tools — `geocode_place`,
|
|
148
|
+
`search_nearby_places`, and `get_place_details`. It is enabled per-agent from
|
|
149
|
+
the **MCP Servers** section of the agent editor.
|
|
150
|
+
|
|
151
|
+
Two providers back these tools:
|
|
152
|
+
|
|
153
|
+
- **OSM** (default) — uses the public OpenStreetMap-based services (Photon for
|
|
154
|
+
geocoding, Overpass for nearby search). No API key required, so the Places
|
|
155
|
+
tools work out of the box.
|
|
156
|
+
- **GOOGLE** — uses the Google Places API (New) and Geocoding API for richer
|
|
157
|
+
results. Opt in by setting `GOOGLE_PLACES_API_KEY`.
|
|
158
|
+
|
|
159
|
+
`PLACES_DEFAULT_SOURCE` selects which provider serves lookups that don't name a
|
|
160
|
+
source explicitly (`OSM` or `GOOGLE`, defaulting to `OSM`). When
|
|
161
|
+
`GOOGLE_PLACES_API_KEY` is set, the Google adapter is registered alongside OSM;
|
|
162
|
+
if `PLACES_DEFAULT_SOURCE=GOOGLE` is set without a key, lookups fall back to OSM.
|
|
163
|
+
|
|
164
|
+
### Configuration
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
PLACES_DEFAULT_SOURCE=GOOGLE
|
|
168
|
+
GOOGLE_PLACES_API_KEY=<your Google API key>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Getting a Google Places API key
|
|
172
|
+
|
|
173
|
+
The key is a plain Google API key (not an OAuth client — unrelated to the
|
|
174
|
+
Google connector setup below).
|
|
175
|
+
|
|
176
|
+
**1. Sign in to Google Cloud Console.**
|
|
177
|
+
|
|
178
|
+
Go to [console.cloud.google.com](https://console.cloud.google.com/) and select
|
|
179
|
+
(or create) a project. Unlike the OAuth connector, the Places and Geocoding
|
|
180
|
+
APIs are **paid** and require a billing account on the project. Google provides
|
|
181
|
+
a recurring monthly free allotment, but you must still enable billing to use
|
|
182
|
+
the key.
|
|
183
|
+
|
|
184
|
+
**2. Enable the required APIs.**
|
|
185
|
+
|
|
186
|
+
Open **APIs & Services → Library** and enable both:
|
|
187
|
+
|
|
188
|
+
- **Places API (New)** — powers `search_nearby_places` and `get_place_details`.
|
|
189
|
+
- **Geocoding API** — powers reverse geocoding for `geocode_place`.
|
|
190
|
+
|
|
191
|
+
Click each result, then click **Enable**.
|
|
192
|
+
|
|
193
|
+
**3. Create the API key.**
|
|
194
|
+
|
|
195
|
+
Open **APIs & Services → Credentials → Create Credentials → API key**. Copy the
|
|
196
|
+
generated key.
|
|
197
|
+
|
|
198
|
+
> [!NOTE]
|
|
199
|
+
> Restrict the key to just the **Places API (New)** and **Geocoding API** under
|
|
200
|
+
> the key's **API restrictions** so a leaked key can't be used against other
|
|
201
|
+
> Google services.
|
|
202
|
+
|
|
203
|
+
**4. Add the key to your `.env` and restart Crow.**
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
GOOGLE_PLACES_API_KEY=<paste the API key>
|
|
207
|
+
PLACES_DEFAULT_SOURCE=GOOGLE
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Restart the server so it picks up the new env vars.
|
|
211
|
+
|
|
142
212
|
## Connectors
|
|
143
213
|
|
|
144
214
|
Connectors are a framework-level capability - they are not tools agents call
|
|
@@ -317,7 +317,8 @@ export const UpdateAgentInputSchema = z.object({
|
|
|
317
317
|
workspace: z.string().optional(),
|
|
318
318
|
persona: z.string().optional(),
|
|
319
319
|
model: z.string().optional(),
|
|
320
|
-
effort
|
|
320
|
+
// null explicitly clears the effort (back to provider default); undefined leaves it unchanged.
|
|
321
|
+
effort: ReasoningEffortSchema.nullish(),
|
|
321
322
|
permissionMode: PermissionModeSchema.optional(),
|
|
322
323
|
settingSources: z.array(SettingSourceSchema).optional(),
|
|
323
324
|
settingSourceConfig: SettingSourceConfigInputSchema.optional(),
|
package/dist/config/env.js
CHANGED
|
@@ -2,6 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { expandPath } from "../utils/fs-utils.js";
|
|
5
|
+
import { PLACES_SOURCE } from "../services/places/places-manager.types.js";
|
|
5
6
|
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const DIST_DIR = path.resolve(CONFIG_DIR, "..");
|
|
7
8
|
const DEFAULT_STATIC_DIR = path.join(DIST_DIR, "public");
|
|
@@ -15,6 +16,9 @@ function getBoolean(key) {
|
|
|
15
16
|
const value = process.env[key]?.trim().toLowerCase();
|
|
16
17
|
return value === "true" || value === "1";
|
|
17
18
|
}
|
|
19
|
+
function resolvePlacesSource(key) {
|
|
20
|
+
return process.env[key]?.trim().toUpperCase() === PLACES_SOURCE.GOOGLE ? PLACES_SOURCE.GOOGLE : PLACES_SOURCE.OSM;
|
|
21
|
+
}
|
|
18
22
|
function readRequired(key) {
|
|
19
23
|
const value = process.env[key]?.trim();
|
|
20
24
|
if (!value) {
|
|
@@ -74,6 +78,8 @@ export const env = {
|
|
|
74
78
|
GMAIL_CHECK_INTERVAL_IN_MINUTES: getOptionalNumber("GMAIL_CHECK_INTERVAL_IN_MINUTES"),
|
|
75
79
|
PHOTON_API_URL: getOptional("PHOTON_API_URL") ?? "https://photon.komoot.io",
|
|
76
80
|
OVERPASS_INTERPRETER_URL: getOptional("OVERPASS_INTERPRETER_URL") ?? "https://overpass-api.de/api/interpreter",
|
|
81
|
+
PLACES_DEFAULT_SOURCE: resolvePlacesSource("PLACES_DEFAULT_SOURCE"),
|
|
82
|
+
GOOGLE_PLACES_API_KEY: getOptional("GOOGLE_PLACES_API_KEY"),
|
|
77
83
|
};
|
|
78
84
|
/**
|
|
79
85
|
* Eagerly validate all required environment variables. Call once during boot
|
|
@@ -21,16 +21,16 @@ export function getSearchWorkspaceToolConfig(agentId, documentSearchService, tas
|
|
|
21
21
|
.number()
|
|
22
22
|
.optional()
|
|
23
23
|
.describe(`Maximum number of hits to return per page. Default ${DEFAULT_SEARCH_LIMIT}.`),
|
|
24
|
-
|
|
24
|
+
offset: z.number().optional().describe("Number of hits to skip before the page starts (0-based offset)."),
|
|
25
25
|
};
|
|
26
|
-
const handler = async ({ query, sources, limit,
|
|
26
|
+
const handler = async ({ query, sources, limit, offset }) => {
|
|
27
27
|
try {
|
|
28
28
|
const filter = buildAccessFilter(agentId, taskManager, circleManager, sources);
|
|
29
29
|
const hits = documentSearchService.search(query, { filter, limit: MAX_SEARCH_LIMIT });
|
|
30
30
|
if (hits.length === 0) {
|
|
31
31
|
return textToolResult([`No matches found for query "${query}".`]);
|
|
32
32
|
}
|
|
33
|
-
const pagination = applyPagination(hits, limit || DEFAULT_SEARCH_LIMIT,
|
|
33
|
+
const pagination = applyPagination(hits, limit || DEFAULT_SEARCH_LIMIT, offset);
|
|
34
34
|
const header = formatPaginationHeader(`Workspace search for query "${query}"`, pagination);
|
|
35
35
|
return textToolResult(header.concat("", renderHits(pagination.items)));
|
|
36
36
|
}
|
|
@@ -16,9 +16,9 @@ export function getListArtifactsToolConfig(agentId, artifactManager, registry, c
|
|
|
16
16
|
.optional()
|
|
17
17
|
.describe("Filter by tags. Only artifacts that have every specified tag are returned."),
|
|
18
18
|
limit: z.number().optional().describe("Number of artifacts to return per page."),
|
|
19
|
-
|
|
19
|
+
offset: z.number().optional().describe("Number of artifacts to skip before the page starts (0-based offset)."),
|
|
20
20
|
};
|
|
21
|
-
const handler = async ({ agent_id, type, tags, limit,
|
|
21
|
+
const handler = async ({ agent_id, type, tags, limit, offset }) => {
|
|
22
22
|
const targetId = agent_id ?? agentId;
|
|
23
23
|
if (agent_id) {
|
|
24
24
|
try {
|
|
@@ -47,7 +47,7 @@ export function getListArtifactsToolConfig(agentId, artifactManager, registry, c
|
|
|
47
47
|
const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
|
|
48
48
|
return textToolResult([`No artifacts found for agent ${targetId}${suffix}.`]);
|
|
49
49
|
}
|
|
50
|
-
const pagination = applyPagination(artifacts, limit || DEFAULT_ARTIFACTS_LIMIT,
|
|
50
|
+
const pagination = applyPagination(artifacts, limit || DEFAULT_ARTIFACTS_LIMIT, offset);
|
|
51
51
|
const lines = pagination.items.map((artifact) => `- ${artifact.filename} (owner: ${artifact.entityId}, type: ${artifact.type}, modified: ${formatLocalDateTime(new Date(artifact.updatedTimestamp), userTimezone)})`);
|
|
52
52
|
const header = formatPaginationHeader(`Artifacts for agent ${targetId}`, pagination);
|
|
53
53
|
return textToolResult(header.concat("", lines));
|
|
@@ -16,9 +16,9 @@ export function getListCircleArtifactsToolConfig(agentId, artifactManager, senso
|
|
|
16
16
|
.optional()
|
|
17
17
|
.describe("Filter by tags. Only artifacts that have every specified tag are returned."),
|
|
18
18
|
limit: z.number().optional().describe("Number of artifacts to return per page."),
|
|
19
|
-
|
|
19
|
+
offset: z.number().optional().describe("Number of artifacts to skip before the page starts (0-based offset)."),
|
|
20
20
|
};
|
|
21
|
-
const handler = async ({ circle_id, type, tags, limit,
|
|
21
|
+
const handler = async ({ circle_id, type, tags, limit, offset }) => {
|
|
22
22
|
if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
|
|
23
23
|
return textToolResult(["You are not a direct member of this circle."], true);
|
|
24
24
|
}
|
|
@@ -38,7 +38,7 @@ export function getListCircleArtifactsToolConfig(agentId, artifactManager, senso
|
|
|
38
38
|
const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
|
|
39
39
|
return textToolResult([`No artifacts found for circle ${circle_id}${suffix}.`]);
|
|
40
40
|
}
|
|
41
|
-
const pagination = applyPagination(artifacts, limit || DEFAULT_CIRCLE_ARTIFACTS_LIMIT,
|
|
41
|
+
const pagination = applyPagination(artifacts, limit || DEFAULT_CIRCLE_ARTIFACTS_LIMIT, offset);
|
|
42
42
|
const lines = pagination.items.map((artifact) => `- ${artifact.filename} (type: ${artifact.type}, modified: ${formatLocalDateTime(new Date(artifact.updatedTimestamp), userTimezone)})`);
|
|
43
43
|
const header = formatPaginationHeader(`Artifacts for circle ${circle_id}`, pagination);
|
|
44
44
|
return textToolResult(header.concat("", lines));
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { MCP_CONFIG_TYPE } from "../_shared/index.js";
|
|
3
|
-
|
|
3
|
+
import { buildStrictToolSchema } from "./tool-utils.js";
|
|
4
|
+
const hasRegisteredTools = (instance) => {
|
|
5
|
+
if (!("_registeredTools" in instance)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const registeredTools = instance._registeredTools;
|
|
9
|
+
return typeof registeredTools === "object" && registeredTools !== null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Swap each registered tool's input schema for a strict variant so unknown parameters are rejected instead of
|
|
13
|
+
* silently dropped. The raw shape is passed to tool() first so the SDK caches per-field descriptions; the strict
|
|
14
|
+
* object reuses the same field instances, so those descriptions survive the swap (a strict object passed directly
|
|
15
|
+
* to tool() would lose them). Guarded so an SDK internals change degrades to the default lenient
|
|
16
|
+
* behavior rather than throwing.
|
|
17
|
+
*/
|
|
18
|
+
function applyStrictToolSchemas(instance, tools) {
|
|
19
|
+
if (!hasRegisteredTools(instance)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const registeredTool of tools) {
|
|
23
|
+
const entry = instance._registeredTools[registeredTool.name];
|
|
24
|
+
if (entry) {
|
|
25
|
+
entry.inputSchema = buildStrictToolSchema(registeredTool.inputSchema);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Wrap a server's registered tools into an in-process Claude SDK MCP server with strict parameter validation. */
|
|
4
30
|
export function toClaudeServer(name, tools) {
|
|
5
|
-
|
|
31
|
+
const server = createSdkMcpServer({
|
|
6
32
|
name,
|
|
7
33
|
tools: tools.map((registeredTool) => tool(registeredTool.name, registeredTool.description, registeredTool.inputSchema, registeredTool.handler, {
|
|
8
34
|
annotations: registeredTool.annotations,
|
|
9
35
|
})),
|
|
10
36
|
});
|
|
37
|
+
applyStrictToolSchemas(server.instance, tools);
|
|
38
|
+
return server;
|
|
11
39
|
}
|
|
12
40
|
/** Map a Crow MCP transport to the Claude SDK's stdio/http/sse server config. */
|
|
13
41
|
export function toClaudeTransport(transport) {
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { convertMcpCallToolResult } from "@github/copilot-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { MCP_CONFIG_TYPE } from "../_shared/index.js";
|
|
4
|
+
import { buildStrictToolSchema, getValidationErrorToolResult } from "./tool-utils.js";
|
|
4
5
|
/** Flatten an internal server's tools into Copilot in-process tools, prefixed to keep `mcp__server__tool` names. */
|
|
5
6
|
export function toCopilotTools(server) {
|
|
6
7
|
return server.tools.map((registeredTool) => toCopilotTool(registeredTool, server));
|
|
7
8
|
}
|
|
8
9
|
function toCopilotTool(registeredTool, server) {
|
|
10
|
+
const inputSchema = buildStrictToolSchema(registeredTool.inputSchema);
|
|
9
11
|
return {
|
|
10
12
|
name: `${server.mcpToolPrefix}${registeredTool.name}`,
|
|
11
13
|
description: registeredTool.description,
|
|
12
|
-
parameters: z.toJSONSchema(
|
|
13
|
-
handler: async (args, invocation) =>
|
|
14
|
+
parameters: z.toJSONSchema(inputSchema),
|
|
15
|
+
handler: async (args, invocation) => {
|
|
16
|
+
const parsed = inputSchema.safeParse(args);
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
return toCopilotToolResult(getValidationErrorToolResult(registeredTool.name, parsed.error));
|
|
19
|
+
}
|
|
20
|
+
return toCopilotToolResult(await registeredTool.handler(parsed.data, invocation));
|
|
21
|
+
},
|
|
14
22
|
skipPermission: server.isAutoApproved,
|
|
15
23
|
};
|
|
16
24
|
}
|
|
@@ -8,9 +8,9 @@ export function getFeedItemsToolConfig(agentId, registry, feedManager, sensorMan
|
|
|
8
8
|
const inputSchema = {
|
|
9
9
|
feedId: z.string().describe("The news/RSS feed ID from which to get items."),
|
|
10
10
|
limit: z.number().optional().describe(`Number of items to return per page.`),
|
|
11
|
-
|
|
11
|
+
offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
|
|
12
12
|
};
|
|
13
|
-
const handler = async ({ feedId, limit,
|
|
13
|
+
const handler = async ({ feedId, limit, offset }) => {
|
|
14
14
|
try {
|
|
15
15
|
const visibleFeedIds = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
|
|
16
16
|
if (!visibleFeedIds.has(feedId)) {
|
|
@@ -22,7 +22,7 @@ export function getFeedItemsToolConfig(agentId, registry, feedManager, sensorMan
|
|
|
22
22
|
}
|
|
23
23
|
const timezone = await sensorManager.getUserTimezone();
|
|
24
24
|
const allItems = await feedManager.getItemsFromFeed(feedId);
|
|
25
|
-
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT,
|
|
25
|
+
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
|
|
26
26
|
const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feed.title, timezone));
|
|
27
27
|
const header = formatPaginationHeader(`Items from feed [${feed.title}]`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
|
|
28
28
|
return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
|
|
@@ -13,9 +13,9 @@ export function getRecentFeedItemsToolConfig(agentId, registry, feedManager, sen
|
|
|
13
13
|
.optional()
|
|
14
14
|
.describe(`Number of minutes to look back from now for recent items (default: ${DEFAULT_RECENCY_IN_MINUTES}).`),
|
|
15
15
|
limit: z.number().optional().describe(`Number of items to return per page.`),
|
|
16
|
-
|
|
16
|
+
offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
|
|
17
17
|
};
|
|
18
|
-
const handler = async ({ recencyInMinutes, limit,
|
|
18
|
+
const handler = async ({ recencyInMinutes, limit, offset }) => {
|
|
19
19
|
try {
|
|
20
20
|
const timezone = await sensorManager.getUserTimezone();
|
|
21
21
|
const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
|
|
@@ -27,7 +27,7 @@ export function getRecentFeedItemsToolConfig(agentId, registry, feedManager, sen
|
|
|
27
27
|
if (!allItems.length) {
|
|
28
28
|
return textToolResult([`No items found in the last ${effectiveRecency} minutes. Try increase the minutes.`]);
|
|
29
29
|
}
|
|
30
|
-
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT,
|
|
30
|
+
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
|
|
31
31
|
const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feedsMap.get(item.feedId), timezone));
|
|
32
32
|
const header = formatPaginationHeader(`Recent items from all feeds (last ${effectiveRecency} minutes)`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
|
|
33
33
|
return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
|
|
@@ -7,14 +7,14 @@ export const LIST_FEEDS_TOOL_NAME = "list_feeds";
|
|
|
7
7
|
export function getListFeedsToolConfig(agentId, registry, feedManager, sensorManager, systemSettingsManager) {
|
|
8
8
|
const inputSchema = {
|
|
9
9
|
limit: z.number().optional().describe(`Number of feeds to return per page.`),
|
|
10
|
-
|
|
10
|
+
offset: z.number().optional().describe("Number of feeds to skip before the page starts (0-based offset)."),
|
|
11
11
|
};
|
|
12
|
-
const handler = async ({ limit,
|
|
12
|
+
const handler = async ({ limit, offset }) => {
|
|
13
13
|
try {
|
|
14
14
|
const timezone = await sensorManager.getUserTimezone();
|
|
15
15
|
const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
|
|
16
16
|
const feeds = await feedManager.getFeeds(Array.from(visibleFeedIdSet));
|
|
17
|
-
const pagination = applyPagination(feeds, limit || DEFAULT_FEEDS_LIMIT,
|
|
17
|
+
const pagination = applyPagination(feeds, limit || DEFAULT_FEEDS_LIMIT, offset);
|
|
18
18
|
const formattedFeeds = pagination.items.map((feed) => formatFeed(feed, timezone));
|
|
19
19
|
const header = formatPaginationHeader("Available feeds", pagination);
|
|
20
20
|
return textToolResult(header.concat("", formattedFeeds));
|
|
@@ -9,9 +9,9 @@ export function getSearchFeedItemsToolConfig(agentId, registry, feedManager, sen
|
|
|
9
9
|
query: z.string().describe("Natural-language phrase or topic to match against feed items."),
|
|
10
10
|
feedId: z.string().optional().describe("Optional feed ID to filter search results to a specific feed."),
|
|
11
11
|
limit: z.number().optional().describe(`Number of items to return per page.`),
|
|
12
|
-
|
|
12
|
+
offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
|
|
13
13
|
};
|
|
14
|
-
const handler = async ({ query, feedId, limit,
|
|
14
|
+
const handler = async ({ query, feedId, limit, offset }) => {
|
|
15
15
|
try {
|
|
16
16
|
const timezone = await sensorManager.getUserTimezone();
|
|
17
17
|
const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
|
|
@@ -25,7 +25,7 @@ export function getSearchFeedItemsToolConfig(agentId, registry, feedManager, sen
|
|
|
25
25
|
if (!allItems.length) {
|
|
26
26
|
return textToolResult([`No items found matching "${query}".`]);
|
|
27
27
|
}
|
|
28
|
-
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT,
|
|
28
|
+
const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
|
|
29
29
|
const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feedsMap.get(item.feedId), timezone));
|
|
30
30
|
const header = formatPaginationHeader(`Items matching "${query}"`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
|
|
31
31
|
return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PLACES_SOURCE } from "../../services/places/places-manager.types.js";
|
|
2
3
|
import { getErrorToolResult, textToolResult } from "../tool-utils.js";
|
|
3
4
|
import { formatPlaceSummary } from "./places-format-utils.js";
|
|
5
|
+
import { resolveRoutingOrigin, routingDescriptionLine, routingOriginInputSchema } from "./places-routing-input.js";
|
|
4
6
|
const DEFAULT_GEOCODE_LIMIT = 10;
|
|
5
7
|
const MAX_GEOCODE_LIMIT = 25;
|
|
6
8
|
export const GEOCODE_PLACE_TOOL_NAME = "geocode_place";
|
|
9
|
+
const NEUTRAL_DESCRIPTION = "Resolve a free-text query - a place name, full address, or landmark - to matching places. " +
|
|
10
|
+
"Use get_place_details on a returned id for opening hours, contact info, and other attributes.";
|
|
11
|
+
const NEUTRAL_RESULT_GUIDANCE = "Refine with a more specific name, address, or the city/country disambiguators if needed.";
|
|
12
|
+
const DESCRIPTION_BY_SOURCE = {
|
|
13
|
+
[PLACES_SOURCE.GOOGLE]: "Resolve a free-text query to matching places. Accepts a place name, a full address, a landmark, or a " +
|
|
14
|
+
"natural-language / descriptive POI search (e.g. 'rooftop bars in Shibuya'). " +
|
|
15
|
+
"Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
|
|
16
|
+
[PLACES_SOURCE.OSM]: "Resolve a free-text query to matching places. Best results come from a precise name or full address; " +
|
|
17
|
+
"descriptive or category-style queries degrade - for category discovery use search_nearby_places. " +
|
|
18
|
+
"Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
|
|
19
|
+
};
|
|
20
|
+
const RESULT_GUIDANCE_BY_SOURCE = {
|
|
21
|
+
[PLACES_SOURCE.GOOGLE]: "Phrase queries in natural language (names, full addresses, or descriptive POI searches) for stronger matches.",
|
|
22
|
+
[PLACES_SOURCE.OSM]: "Use a precise name or full address; descriptive or category queries return weak matches.",
|
|
23
|
+
};
|
|
24
|
+
function selectForSource(map, source, fallback) {
|
|
25
|
+
return map[source] ?? fallback;
|
|
26
|
+
}
|
|
7
27
|
export function getGeocodePlaceToolConfig(placesManager) {
|
|
28
|
+
const defaultSource = placesManager.getDefaultSource();
|
|
29
|
+
const baseDescription = selectForSource(DESCRIPTION_BY_SOURCE, defaultSource, NEUTRAL_DESCRIPTION);
|
|
30
|
+
const routingLine = routingDescriptionLine(defaultSource);
|
|
31
|
+
const description = routingLine ? `${baseDescription} ${routingLine}` : baseDescription;
|
|
32
|
+
const resultGuidance = selectForSource(RESULT_GUIDANCE_BY_SOURCE, defaultSource, NEUTRAL_RESULT_GUIDANCE);
|
|
8
33
|
const inputSchema = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
city: z.string().min(1).optional().describe("City, town, or locality. Example: 'London', 'Paris'."),
|
|
12
|
-
postcode: z.string().min(1).optional().describe("Postal / ZIP code."),
|
|
34
|
+
query: z.string().min(1).describe("A place name, a full address, or a landmark (e.g. 'Eiffel Tower')."),
|
|
35
|
+
city: z.string().min(1).optional().describe("City, town, or locality to disambiguate the query."),
|
|
13
36
|
country: z
|
|
14
37
|
.string()
|
|
15
38
|
.min(1)
|
|
16
39
|
.optional()
|
|
17
|
-
.describe("Country name or ISO 3166-1 alpha-2 code (e.g. 'France' or 'FR')
|
|
40
|
+
.describe("Country name or ISO 3166-1 alpha-2 code (e.g. 'France' or 'FR') to disambiguate same-named places."),
|
|
18
41
|
nearLatitude: z
|
|
19
42
|
.number()
|
|
20
43
|
.min(-90)
|
|
@@ -22,6 +45,7 @@ export function getGeocodePlaceToolConfig(placesManager) {
|
|
|
22
45
|
.optional()
|
|
23
46
|
.describe("Optional latitude for ranking bias - prefers candidates near this point."),
|
|
24
47
|
nearLongitude: z.number().min(-180).max(180).optional().describe("Optional longitude paired with nearLatitude."),
|
|
48
|
+
...routingOriginInputSchema,
|
|
25
49
|
limit: z
|
|
26
50
|
.number()
|
|
27
51
|
.int()
|
|
@@ -31,14 +55,18 @@ export function getGeocodePlaceToolConfig(placesManager) {
|
|
|
31
55
|
.describe(`Max results to return (default ${DEFAULT_GEOCODE_LIMIT}, max ${MAX_GEOCODE_LIMIT}).`),
|
|
32
56
|
};
|
|
33
57
|
const handler = async (args) => {
|
|
34
|
-
|
|
58
|
+
if ((args.nearLatitude === undefined) !== (args.nearLongitude === undefined)) {
|
|
59
|
+
return textToolResult(["nearLatitude and nearLongitude must both be provided or both omitted."], true);
|
|
60
|
+
}
|
|
61
|
+
const routing = resolveRoutingOrigin(args);
|
|
62
|
+
if ("error" in routing) {
|
|
63
|
+
return textToolResult([routing.error], true);
|
|
64
|
+
}
|
|
65
|
+
const queryParts = [args.query, args.city, args.country]
|
|
35
66
|
.map((part) => part?.trim())
|
|
36
67
|
.filter((part) => part !== undefined && part.length > 0);
|
|
37
68
|
if (queryParts.length === 0) {
|
|
38
|
-
return textToolResult(["
|
|
39
|
-
}
|
|
40
|
-
if ((args.nearLatitude === undefined) !== (args.nearLongitude === undefined)) {
|
|
41
|
-
return textToolResult(["nearLatitude and nearLongitude must both be provided or both omitted."], true);
|
|
69
|
+
return textToolResult(["query must contain non-whitespace text."], true);
|
|
42
70
|
}
|
|
43
71
|
const queryText = queryParts.join(", ");
|
|
44
72
|
try {
|
|
@@ -48,14 +76,15 @@ export function getGeocodePlaceToolConfig(placesManager) {
|
|
|
48
76
|
const places = await placesManager.geocode({
|
|
49
77
|
text: queryText,
|
|
50
78
|
near,
|
|
79
|
+
routingOrigin: routing.routingOrigin,
|
|
51
80
|
limit: args.limit ?? DEFAULT_GEOCODE_LIMIT,
|
|
52
81
|
});
|
|
53
82
|
if (places.length === 0) {
|
|
54
|
-
return textToolResult([`No places found matching "${queryText}"
|
|
83
|
+
return textToolResult([`No places found matching "${queryText}".`, resultGuidance]);
|
|
55
84
|
}
|
|
56
85
|
const header = `[Geocode results: ${places.length} for "${queryText}"]`;
|
|
57
86
|
const body = places.map(formatPlaceSummary).join("\n");
|
|
58
|
-
return textToolResult([header, body]);
|
|
87
|
+
return textToolResult([header, body, resultGuidance]);
|
|
59
88
|
}
|
|
60
89
|
catch (error) {
|
|
61
90
|
return getErrorToolResult(error, "Failed to geocode place.");
|
|
@@ -63,12 +92,7 @@ export function getGeocodePlaceToolConfig(placesManager) {
|
|
|
63
92
|
};
|
|
64
93
|
const config = {
|
|
65
94
|
name: GEOCODE_PLACE_TOOL_NAME,
|
|
66
|
-
description
|
|
67
|
-
"Resolve a known place to coordinates: a named landmark, business, or specific street address. ",
|
|
68
|
-
"Provide whichever structured fields you have - the more you fill, the more precise the match. ",
|
|
69
|
-
"Not a discovery tool: for 'cafes near me', 'good restaurants', or any category / subjective query, use search_nearby_places instead. ",
|
|
70
|
-
"Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
|
|
71
|
-
].join(""),
|
|
95
|
+
description,
|
|
72
96
|
inputSchema,
|
|
73
97
|
handler,
|
|
74
98
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WEEKDAY, } from "../../services/places/places-manager.types.js";
|
|
1
|
+
import { BUSINESS_STATUS, WEEKDAY, } from "../../services/places/places-manager.types.js";
|
|
2
2
|
const WEEKDAY_LABEL = {
|
|
3
3
|
[WEEKDAY.MONDAY]: "Monday",
|
|
4
4
|
[WEEKDAY.TUESDAY]: "Tuesday",
|
|
@@ -19,8 +19,38 @@ export function formatPlaceSummary(place) {
|
|
|
19
19
|
if (place.address !== undefined && place.address.length > 0) {
|
|
20
20
|
lines.push(` - Address: ${place.address}`);
|
|
21
21
|
}
|
|
22
|
+
if (place.businessStatus !== undefined && place.businessStatus !== BUSINESS_STATUS.OPERATIONAL) {
|
|
23
|
+
lines.push(` - Business status: ${place.businessStatus}`);
|
|
24
|
+
}
|
|
25
|
+
if (place.mapsUrl !== undefined && place.mapsUrl.length > 0) {
|
|
26
|
+
lines.push(` - Map: ${place.mapsUrl}`);
|
|
27
|
+
}
|
|
28
|
+
if (place.routing !== undefined) {
|
|
29
|
+
const { durationSeconds, distanceMeters, travelMode, directionsUrl } = place.routing;
|
|
30
|
+
lines.push(` - Travel: ${humanizeDuration(durationSeconds)} · ${humanizeDistance(distanceMeters)} (${travelMode})`);
|
|
31
|
+
if (directionsUrl !== undefined && directionsUrl.length > 0) {
|
|
32
|
+
lines.push(` - Directions: ${directionsUrl}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
return lines.join("\n");
|
|
23
36
|
}
|
|
37
|
+
/** "8 min" under an hour, "1 h 12 min" (or "2 h" on the hour) above. */
|
|
38
|
+
function humanizeDuration(totalSeconds) {
|
|
39
|
+
const totalMinutes = Math.round(totalSeconds / 60);
|
|
40
|
+
if (totalMinutes < 60) {
|
|
41
|
+
return `${totalMinutes} min`;
|
|
42
|
+
}
|
|
43
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
44
|
+
const minutes = totalMinutes % 60;
|
|
45
|
+
return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`;
|
|
46
|
+
}
|
|
47
|
+
/** "850 m" under a kilometre, "3.2 km" (one decimal) above. */
|
|
48
|
+
function humanizeDistance(meters) {
|
|
49
|
+
if (meters < 1000) {
|
|
50
|
+
return `${Math.round(meters)} m`;
|
|
51
|
+
}
|
|
52
|
+
return `${(meters / 1000).toFixed(1)} km`;
|
|
53
|
+
}
|
|
24
54
|
/** Full-detail block returned by `places_get_details`. Layers attributes on top of the summary. */
|
|
25
55
|
export function formatPlaceDetails(details) {
|
|
26
56
|
const lines = [formatPlaceSummary(details)];
|
|
@@ -48,6 +78,12 @@ export function formatPlaceDetails(details) {
|
|
|
48
78
|
if (details.wheelchairAccess !== undefined) {
|
|
49
79
|
lines.push(` - Wheelchair access: ${details.wheelchairAccess}`);
|
|
50
80
|
}
|
|
81
|
+
if (details.transit !== undefined && details.transit.lines.length > 0) {
|
|
82
|
+
lines.push(" - Transit lines:");
|
|
83
|
+
for (const line of details.transit.lines) {
|
|
84
|
+
lines.push(` ${formatTransitLine(line)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
51
87
|
if (details.description !== undefined && details.description.length > 0) {
|
|
52
88
|
lines.push(" - Description:");
|
|
53
89
|
for (const descriptionLine of details.description.split(/\r?\n/)) {
|
|
@@ -56,6 +92,16 @@ export function formatPlaceDetails(details) {
|
|
|
56
92
|
}
|
|
57
93
|
return lines.join("\n");
|
|
58
94
|
}
|
|
95
|
+
function formatTransitLine(line) {
|
|
96
|
+
let formatted = `${line.vehicleType} ${line.shortName ?? line.name}`;
|
|
97
|
+
if (line.shortName !== undefined && line.shortName !== line.name) {
|
|
98
|
+
formatted += ` (${line.name})`;
|
|
99
|
+
}
|
|
100
|
+
if (line.operator !== undefined) {
|
|
101
|
+
formatted += ` — ${line.operator}`;
|
|
102
|
+
}
|
|
103
|
+
return formatted;
|
|
104
|
+
}
|
|
59
105
|
function formatOpeningHoursLines(hours) {
|
|
60
106
|
if (hours.alwaysOpen) {
|
|
61
107
|
return ["Open 24/7"];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PLACES_SOURCE, TRAVEL_MODE, } from "../../services/places/places-manager.types.js";
|
|
3
|
+
const TRAVEL_MODE_VALUES = Object.values(TRAVEL_MODE);
|
|
4
|
+
/** Optional routing-origin inputs shared by the place search and geocode tools. */
|
|
5
|
+
export const routingOriginInputSchema = {
|
|
6
|
+
originLatitude: z
|
|
7
|
+
.number()
|
|
8
|
+
.min(-90)
|
|
9
|
+
.max(90)
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Latitude of the travel origin; pair with originLongitude to attach travel time and distance."),
|
|
12
|
+
originLongitude: z
|
|
13
|
+
.number()
|
|
14
|
+
.min(-180)
|
|
15
|
+
.max(180)
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Longitude of the travel origin; must be paired with originLatitude."),
|
|
18
|
+
travelMode: z
|
|
19
|
+
.enum(TRAVEL_MODE_VALUES)
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Travel mode for the origin (default DRIVE). Ignored unless an origin is provided."),
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Validate the both-or-neither origin pair and build a `RoutingOrigin`. A
|
|
25
|
+
* `travelMode` without an origin is a no-op rather than an error.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveRoutingOrigin(args) {
|
|
28
|
+
const { originLatitude, originLongitude, travelMode } = args;
|
|
29
|
+
if ((originLatitude === undefined) !== (originLongitude === undefined)) {
|
|
30
|
+
return { error: "originLatitude and originLongitude must both be provided or both omitted." };
|
|
31
|
+
}
|
|
32
|
+
if (originLatitude === undefined || originLongitude === undefined) {
|
|
33
|
+
return { routingOrigin: undefined };
|
|
34
|
+
}
|
|
35
|
+
const routingOrigin = { point: { latitude: originLatitude, longitude: originLongitude } };
|
|
36
|
+
if (travelMode) {
|
|
37
|
+
routingOrigin.travelMode = travelMode;
|
|
38
|
+
}
|
|
39
|
+
return { routingOrigin };
|
|
40
|
+
}
|
|
41
|
+
const ROUTING_DESCRIPTION_BY_SOURCE = {
|
|
42
|
+
[PLACES_SOURCE.GOOGLE]: "Provide originLatitude/originLongitude to attach travel time and distance to each result.",
|
|
43
|
+
[PLACES_SOURCE.OSM]: "Travel time and distance to results are not available on this source.",
|
|
44
|
+
};
|
|
45
|
+
/** One-line, source-aware note about routing support for a tool description. */
|
|
46
|
+
export function routingDescriptionLine(source) {
|
|
47
|
+
return ROUTING_DESCRIPTION_BY_SOURCE[source];
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=places-routing-input.js.map
|
|
@@ -2,12 +2,17 @@ import { z } from "zod";
|
|
|
2
2
|
import { PLACE_CATEGORY } from "../../services/places/places-manager.types.js";
|
|
3
3
|
import { getErrorToolResult, textToolResult } from "../tool-utils.js";
|
|
4
4
|
import { formatPlaceSummary } from "./places-format-utils.js";
|
|
5
|
+
import { resolveRoutingOrigin, routingDescriptionLine, routingOriginInputSchema } from "./places-routing-input.js";
|
|
5
6
|
const DEFAULT_SEARCH_NEARBY_LIMIT = 20;
|
|
6
7
|
const MAX_SEARCH_NEARBY_LIMIT = 50;
|
|
7
8
|
const MAX_SEARCH_NEARBY_RADIUS_METERS = 50_000;
|
|
8
9
|
const CATEGORY_VALUES = Object.values(PLACE_CATEGORY);
|
|
9
10
|
export const SEARCH_NEARBY_PLACES_TOOL_NAME = "search_nearby_places";
|
|
11
|
+
const BASE_DESCRIPTION = "Find places of a given category within a radius around a point. Use get_place_details with a returned id " +
|
|
12
|
+
"for opening hours, contact info, and other attributes.";
|
|
10
13
|
export function getSearchNearbyPlacesToolConfig(placesManager) {
|
|
14
|
+
const routingLine = routingDescriptionLine(placesManager.getDefaultSource());
|
|
15
|
+
const description = routingLine ? `${BASE_DESCRIPTION} ${routingLine}` : BASE_DESCRIPTION;
|
|
11
16
|
const inputSchema = {
|
|
12
17
|
latitude: z.number().min(-90).max(90).describe("Latitude of the search center."),
|
|
13
18
|
longitude: z.number().min(-180).max(180).describe("Longitude of the search center."),
|
|
@@ -20,6 +25,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
|
|
|
20
25
|
category: z
|
|
21
26
|
.enum(CATEGORY_VALUES)
|
|
22
27
|
.describe("Curated place category to filter by. Use OTHER only as a last resort - prefer a more specific value."),
|
|
28
|
+
...routingOriginInputSchema,
|
|
23
29
|
limit: z
|
|
24
30
|
.number()
|
|
25
31
|
.int()
|
|
@@ -29,6 +35,10 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
|
|
|
29
35
|
.describe(`Max results to return (default ${DEFAULT_SEARCH_NEARBY_LIMIT}, max ${MAX_SEARCH_NEARBY_LIMIT}).`),
|
|
30
36
|
};
|
|
31
37
|
const handler = async (args) => {
|
|
38
|
+
const routing = resolveRoutingOrigin(args);
|
|
39
|
+
if ("error" in routing) {
|
|
40
|
+
return textToolResult([routing.error], true);
|
|
41
|
+
}
|
|
32
42
|
try {
|
|
33
43
|
const places = await placesManager.searchPlaces({
|
|
34
44
|
area: {
|
|
@@ -37,6 +47,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
|
|
|
37
47
|
radiusMeters: args.radiusMeters,
|
|
38
48
|
},
|
|
39
49
|
category: args.category,
|
|
50
|
+
routingOrigin: routing.routingOrigin,
|
|
40
51
|
limit: args.limit ?? DEFAULT_SEARCH_NEARBY_LIMIT,
|
|
41
52
|
});
|
|
42
53
|
const areaLabel = `within ${args.radiusMeters}m of ${args.latitude.toFixed(6)}, ${args.longitude.toFixed(6)}`;
|
|
@@ -53,7 +64,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
|
|
|
53
64
|
};
|
|
54
65
|
const config = {
|
|
55
66
|
name: SEARCH_NEARBY_PLACES_TOOL_NAME,
|
|
56
|
-
description
|
|
67
|
+
description,
|
|
57
68
|
inputSchema,
|
|
58
69
|
handler,
|
|
59
70
|
};
|