Skip to content

Commit 59a6e69

Browse files
authored
feat: surface why_recommended and declare capability/cost/provider fields (#102)
* feat: surface why_recommended and declare capability/cost/provider fields The live Discover/Inspect APIs now return why_recommended, capabilities (standardized descriptors with coverage tags), expected_cost, provider_id and provider_logo_url, but the toolkit neither displayed nor declared them. - CLI: discover output renders a 'why:' line with the recommendation reason (truncated at 160 chars), so agents see ranking rationale. - Python SDK: adds ToolCapability/ToolCapabilityTag models; ToolInfo gains capabilities, expected_cost, why_recommended, provider_id and provider_logo_url. - MCP: same additions to the hand-written ToolInfo declarations. - OpenAPI spec: documents why_recommended, capabilities (new PublicToolCapability/PublicCapabilityTag schemas) and provider_logo_url on PublicCapabilityResult; artifacts regenerated. Advances #65 (why_recommended display sub-task). * feat(cli): show capabilities and expected_cost in inspect output inspect responses include standardized capability descriptors and a pre-call cost estimate; render them in the metadata table: Capability: MKT.BARS.ADJUSTED (US, CN, HK, JP, UK, DE, KR, IN, +2 more) Est. cost: 24.2 credits Coverage tag lists are capped at 8 entries. Malformed or absent fields render nothing.
1 parent 2be27b8 commit 59a6e69

9 files changed

Lines changed: 329 additions & 0 deletions

File tree

docs/openapi/qveris-public-api.openapi.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5519,6 +5519,43 @@
55195519
}
55205520
}
55215521
},
5522+
"PublicCapabilityTag": {
5523+
"type": "object",
5524+
"title": "PublicCapabilityTag",
5525+
"additionalProperties": true,
5526+
"description": "Coverage tag attached to a capability descriptor (e.g. market coverage).",
5527+
"properties": {
5528+
"id": {
5529+
"type": "string"
5530+
},
5531+
"name": {
5532+
"type": "string"
5533+
},
5534+
"type": {
5535+
"type": "string"
5536+
},
5537+
"description": {
5538+
"type": "string"
5539+
}
5540+
}
5541+
},
5542+
"PublicToolCapability": {
5543+
"type": "object",
5544+
"title": "PublicToolCapability",
5545+
"additionalProperties": true,
5546+
"description": "Standardized capability descriptor attached to a capability result (e.g. MKT.BARS.ADJUSTED with market coverage tags).",
5547+
"properties": {
5548+
"id": {
5549+
"type": "string"
5550+
},
5551+
"tag": {
5552+
"type": "array",
5553+
"items": {
5554+
"$ref": "#/components/schemas/PublicCapabilityTag"
5555+
}
5556+
}
5557+
}
5558+
},
55225559
"PublicCapabilityResult": {
55235560
"type": "object",
55245561
"title": "PublicCapabilityResult",
@@ -5548,6 +5585,9 @@
55485585
"provider_description": {
55495586
"type": "string"
55505587
},
5588+
"provider_logo_url": {
5589+
"type": "string"
5590+
},
55515591
"category": {
55525592
"type": "string"
55535593
},
@@ -5565,12 +5605,23 @@
55655605
},
55665606
"description": "Categories/tags attached to the capability. Current responses return category objects; legacy responses returned plain strings."
55675607
},
5608+
"capabilities": {
5609+
"type": "array",
5610+
"items": {
5611+
"$ref": "#/components/schemas/PublicToolCapability"
5612+
},
5613+
"description": "Standardized capability descriptors with coverage tags."
5614+
},
55685615
"region": {
55695616
"type": "string"
55705617
},
55715618
"score": {
55725619
"type": "number"
55735620
},
5621+
"why_recommended": {
5622+
"type": "string",
5623+
"description": "Human-readable explanation of why this capability was recommended for the query. Returned by Discover."
5624+
},
55745625
"params": {
55755626
"type": "array",
55765627
"items": {

packages/cli/src/output/formatter.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export function formatDiscoverResult(result) {
6767
lines.push(` ${dim("tags:")} ${dim(categories)}`);
6868
}
6969

70+
// Line 6: recommendation reason (if present)
71+
const why = stringifyDesc(t.why_recommended);
72+
if (why) {
73+
lines.push(` ${dim("why:")} ${dim(why.length > 160 ? why.slice(0, 160) + "..." : why)}`);
74+
}
75+
7076
lines.push("");
7177
}
7278

@@ -115,10 +121,18 @@ export function formatInspectResult(tools) {
115121
lines.push(` Provider: ${pLine}`);
116122
}
117123
if (categories) lines.push(` Categories: ${categories}`);
124+
for (const cap of formatCapabilities(t.capabilities)) {
125+
lines.push(` Capability: ${cap}`);
126+
}
118127
lines.push(` Region: ${region}`);
119128
lines.push(` Latency: ${bold(avgTime)}`);
120129
lines.push(` Success: ${green(successRate)}`);
121130
lines.push(` Billing: ${yellow(billingText || "N/A")}`);
131+
const expectedCost =
132+
typeof t.expected_cost === "string" || typeof t.expected_cost === "number"
133+
? String(t.expected_cost).trim()
134+
: "";
135+
if (expectedCost) lines.push(` Est. cost: ${yellow(`${expectedCost} credits`)}`);
122136
if (t.has_last_execution) lines.push(` Verified: ${green("\u2713 has execution history")}`);
123137
if (docsUrl) lines.push(` Docs: ${cyan(docsUrl)}`);
124138

@@ -311,6 +325,31 @@ function formatCategories(categories) {
311325
return names.join(", ");
312326
}
313327

328+
/**
329+
* Capabilities are objects like {id, tag: [{id, name, type, description}]}.
330+
* Returns one display line per capability: "MKT.BARS.ADJUSTED (US, CN, HK)".
331+
*/
332+
function formatCapabilities(capabilities) {
333+
if (!Array.isArray(capabilities)) return [];
334+
const rendered = [];
335+
for (const cap of capabilities) {
336+
if (!cap || typeof cap !== "object" || typeof cap.id !== "string" || !cap.id.trim()) continue;
337+
const tags = Array.isArray(cap.tag)
338+
? cap.tag
339+
.map((t) => {
340+
if (!t || typeof t !== "object") return "";
341+
if (typeof t.id === "string" && t.id.trim()) return t.id.trim();
342+
return stringifyDesc(t.name);
343+
})
344+
.filter(Boolean)
345+
: [];
346+
const shown = tags.slice(0, 8);
347+
const suffix = tags.length > 8 ? `, +${tags.length - 8} more` : "";
348+
rendered.push(shown.length > 0 ? `${cap.id.trim()} (${shown.join(", ")}${suffix})` : cap.id.trim());
349+
}
350+
return rendered;
351+
}
352+
314353
/** Handle description that may be a string, i18n object {en: "...", zh: "..."}, or other. */
315354
function stringifyDesc(desc) {
316355
if (!desc) return "";

packages/cli/test/formatter.test.mjs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,45 @@ test("formatDiscoverResult omits tags line when categories are missing or empty"
6161
}
6262
});
6363

64+
function discoverResultWithTool(tool) {
65+
return {
66+
search_id: "s-1",
67+
total: 1,
68+
results: [
69+
{
70+
tool_id: "provider.tool.retrieve.v1.abc123",
71+
name: "Sample Tool",
72+
description: "A sample tool.",
73+
...tool,
74+
},
75+
],
76+
};
77+
}
78+
79+
test("formatDiscoverResult renders why_recommended as a why line", () => {
80+
const output = formatDiscoverResult(
81+
discoverResultWithTool({
82+
why_recommended: "Recommended because it matched both semantic and keyword relevance signals.",
83+
}),
84+
);
85+
assert.ok(output.includes("why: Recommended because it matched both semantic and keyword relevance signals."));
86+
});
87+
88+
test("formatDiscoverResult truncates long why_recommended text", () => {
89+
const output = formatDiscoverResult(
90+
discoverResultWithTool({ why_recommended: "x".repeat(200) }),
91+
);
92+
assert.ok(output.includes(`why: ${"x".repeat(160)}...`));
93+
assert.ok(!output.includes("x".repeat(161)));
94+
});
95+
96+
test("formatDiscoverResult omits why line when why_recommended is missing or empty", () => {
97+
for (const why_recommended of [undefined, "", null]) {
98+
const output = formatDiscoverResult(discoverResultWithTool({ why_recommended }));
99+
assert.ok(!output.includes("why:"));
100+
}
101+
});
102+
64103
test("formatInspectResult renders object categories as names", () => {
65104
const output = formatInspectResult([
66105
{
@@ -75,3 +114,57 @@ test("formatInspectResult renders object categories as names", () => {
75114
assert.ok(!output.includes("[object Object]"));
76115
assert.ok(output.includes("Categories: Market Data, forex"));
77116
});
117+
118+
test("formatInspectResult renders capability lines with coverage tags", () => {
119+
const output = formatInspectResult([
120+
{
121+
tool_id: "provider.tool.retrieve.v1.abc123",
122+
name: "Sample Tool",
123+
capabilities: [
124+
{
125+
id: "MKT.BARS.ADJUSTED",
126+
tag: [
127+
{ id: "US", name: "United States", type: "market" },
128+
{ id: "CN", name: "Mainland China", type: "market" },
129+
],
130+
},
131+
{ id: "MKT.BARS.RAW" },
132+
],
133+
},
134+
]);
135+
assert.ok(output.includes("Capability: MKT.BARS.ADJUSTED (US, CN)"));
136+
assert.ok(output.includes("Capability: MKT.BARS.RAW"));
137+
});
138+
139+
test("formatInspectResult caps capability tag list at 8 entries", () => {
140+
const tag = Array.from({ length: 11 }, (_, i) => ({ id: `M${i}` }));
141+
const output = formatInspectResult([
142+
{ tool_id: "t.v1", name: "Sample Tool", capabilities: [{ id: "CAP.X", tag }] },
143+
]);
144+
assert.ok(output.includes("Capability: CAP.X (M0, M1, M2, M3, M4, M5, M6, M7, +3 more)"));
145+
});
146+
147+
test("formatInspectResult omits capability lines when missing or malformed", () => {
148+
for (const capabilities of [undefined, [], "nope", [null, {}, { id: " " }]]) {
149+
const output = formatInspectResult([
150+
{ tool_id: "t.v1", name: "Sample Tool", capabilities },
151+
]);
152+
assert.ok(!output.includes("Capability:"));
153+
}
154+
});
155+
156+
test("formatInspectResult renders expected_cost as Est. cost row", () => {
157+
const output = formatInspectResult([
158+
{ tool_id: "t.v1", name: "Sample Tool", expected_cost: "24.2" },
159+
]);
160+
assert.ok(output.includes("Est. cost: 24.2 credits"));
161+
});
162+
163+
test("formatInspectResult omits Est. cost row when expected_cost is absent or empty", () => {
164+
for (const expected_cost of [undefined, null, "", " ", {}]) {
165+
const output = formatInspectResult([
166+
{ tool_id: "t.v1", name: "Sample Tool", expected_cost },
167+
]);
168+
assert.ok(!output.includes("Est. cost:"));
169+
}
170+
});

packages/mcp/src/generated/openapi.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,28 @@ export interface components {
11731173
} & {
11741174
[key: string]: unknown;
11751175
};
1176+
/**
1177+
* PublicCapabilityTag
1178+
* @description Coverage tag attached to a capability descriptor (e.g. market coverage).
1179+
*/
1180+
PublicCapabilityTag: {
1181+
id?: string;
1182+
name?: string;
1183+
type?: string;
1184+
description?: string;
1185+
} & {
1186+
[key: string]: unknown;
1187+
};
1188+
/**
1189+
* PublicToolCapability
1190+
* @description Standardized capability descriptor attached to a capability result (e.g. MKT.BARS.ADJUSTED with market coverage tags).
1191+
*/
1192+
PublicToolCapability: {
1193+
id?: string;
1194+
tag?: components["schemas"]["PublicCapabilityTag"][];
1195+
} & {
1196+
[key: string]: unknown;
1197+
};
11761198
/** PublicCapabilityResult */
11771199
PublicCapabilityResult: {
11781200
tool_id: string;
@@ -1182,11 +1204,16 @@ export interface components {
11821204
provider_id?: string;
11831205
provider_name?: string;
11841206
provider_description?: string;
1207+
provider_logo_url?: string;
11851208
category?: string;
11861209
/** @description Categories/tags attached to the capability. Current responses return category objects; legacy responses returned plain strings. */
11871210
categories?: (components["schemas"]["PublicToolCategory"] | string)[];
1211+
/** @description Standardized capability descriptors with coverage tags. */
1212+
capabilities?: components["schemas"]["PublicToolCapability"][];
11881213
region?: string;
11891214
score?: number;
1215+
/** @description Human-readable explanation of why this capability was recommended for the query. Returned by Discover. */
1216+
why_recommended?: string;
11901217
params?: components["schemas"]["PublicToolParameter"][];
11911218
examples?: {
11921219
[key: string]: unknown;

packages/mcp/src/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,25 @@ export interface ToolCategory {
128128
description?: string;
129129
}
130130

131+
/**
132+
* Coverage tag attached to a capability (e.g. market coverage).
133+
*/
134+
export interface ToolCapabilityTag {
135+
id?: string;
136+
name?: string;
137+
type?: string;
138+
description?: string;
139+
}
140+
141+
/**
142+
* Standardized capability descriptor attached to a tool
143+
* (e.g. "MKT.BARS.ADJUSTED" with market coverage tags).
144+
*/
145+
export interface ToolCapability {
146+
id?: string;
147+
tag?: ToolCapabilityTag[];
148+
}
149+
131150
/**
132151
* Information about a tool returned from search results.
133152
* Contains everything needed to understand and execute the tool.
@@ -145,6 +164,12 @@ export interface ToolInfo {
145164
/** Tool categories/tags: category objects, or plain strings in legacy responses */
146165
categories?: Array<string | ToolCategory>;
147166

167+
/** Standardized capability descriptors with coverage tags */
168+
capabilities?: ToolCapability[];
169+
170+
/** Provider identifier */
171+
provider_id?: string;
172+
148173
/** Name of the organization/service providing this tool */
149174
provider_name?: string;
150175

@@ -154,6 +179,9 @@ export interface ToolInfo {
154179
/** Provider website URL */
155180
provider_website_url?: string;
156181

182+
/** Provider logo URL */
183+
provider_logo_url?: string;
184+
157185
/**
158186
* Geographic availability of the tool.
159187
* - "global" - Available worldwide
@@ -174,9 +202,15 @@ export interface ToolInfo {
174202
/** Structured rule-level billing metadata when available */
175203
billing_rule?: BillingRule;
176204

205+
/** Pre-call cost estimate in credits, when available */
206+
expected_cost?: string | number;
207+
177208
/** Relevance score for the search query (0.0 - 1.0, higher = better match) */
178209
final_score?: number;
179210

211+
/** Human-readable explanation of why this tool was recommended (Discover results only) */
212+
why_recommended?: string;
213+
180214
/** Whether this tool has been executed before (verified in production) */
181215
has_last_execution?: boolean;
182216

packages/python-sdk/qveris/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Message,
99
SearchResponse,
1010
StreamEvent,
11+
ToolCapability,
12+
ToolCapabilityTag,
1113
ToolCategory,
1214
ToolExecutionResponse,
1315
ToolInfo,
@@ -23,6 +25,8 @@
2325
"AgentConfig",
2426
"Message",
2527
"StreamEvent",
28+
"ToolCapability",
29+
"ToolCapabilityTag",
2630
"ToolCategory",
2731
"ToolInfo",
2832
"ToolParameter",

0 commit comments

Comments
 (0)