Skip to content

Commit ddf216f

Browse files
authored
Assistant: Add Inspect Variables tool (#7662)
Today, Assistant is provided with a summary of the currently active variables, but it must use ad-hoc R or Python code to get more information on each variable. This change adds a variable inspection tool to Assistant, which it can use to look up variable details. It effectively gives the Assistant access to the Variables pane, so it can expand any value in the tree and see what's inside it, and uses the variables comm just like the UI does. <img width="406" alt="image" src="https://github.com/user-attachments/assets/9e1a3ec8-f760-4179-9d71-7d3c5fac60ca" /> Addresses #7081 ### QA Notes - Assistant will still run R or Python code to get information on variables in some cases. This tool lets Assistant look inside variables, but it can't answer questions like "what is the largest value in the first column". - Additional work is needed to let Assistant see into data objects without running R/Python code and scraping the output. That work is tracked in #7114. Tests: `@:variables` `@:assistant`
1 parent a0a3cb4 commit ddf216f

File tree

29 files changed

+614
-99
lines changed

29 files changed

+614
-99
lines changed

extensions/positron-assistant/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,36 @@
189189
"positron-assistant"
190190
]
191191
},
192+
{
193+
"name": "inspectVariables",
194+
"displayName": "Inspect Variables",
195+
"modelDescription": "Inspect one or more variables in a console session, listing their child values, if any. Can be used to list the columns in a dataframe, the items in a column or array, or the elements of a list. Returns an array containing an array of children for each variable in the request. IMPORTANT: always use the access_key (even if it is a JSON string), NOT the display_name to refer to a variable. Example: for a session with identifier s-123 and variables [ { display_name: 'df', access_key: 'df-key' }, { 'display_name: 'foo', 'access_key': 'foo-key' } ], a request could be look like { 'sessionIdentifier': 's-123', 'accessKeys': [ ['df-key'], ['foo-key', 'bar-key'] ] }; the response would be an array of arrays, where each inner array contains the child variables of the corresponding variable in the request. The first inner array would contain the columns of the dataframe, and the second inner array would contain the elements of the list 'bar' inside the list 'foo'. Cannot be called when there is no session (the session identifier is empty or not defined). Can be called without any access keys to list all root-level variables in the session.",
196+
"canBeReferencedInPrompt": false,
197+
"inputSchema": {
198+
"type": "object",
199+
"properties": {
200+
"sessionIdentifier": {
201+
"type": "string",
202+
"description": "The identifier of the console session to inspect."
203+
},
204+
"accessKeys": {
205+
"type": "array",
206+
"description": "An array of the variables to inspect. Each entry is an array of access keys that represent the path to the variable to inspect.",
207+
"items": {
208+
"type": "array",
209+
"description": "A list of access keys that identify a variable by specifying its path.",
210+
"items": {
211+
"type": "string",
212+
"description": "An access key that uniquely identifies a variable among its siblings."
213+
}
214+
}
215+
}
216+
}
217+
},
218+
"tags": [
219+
"positron-assistant"
220+
]
221+
},
192222
{
193223
"name": "getPlot",
194224
"displayName": "View active plot",

extensions/positron-assistant/src/tools.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export enum PositronAssistantToolName {
1313
EditFile = 'vscode_editFile_internal',
1414
ExecuteCode = 'executeCode',
1515
GetPlot = 'getPlot',
16+
InspectVariables = 'inspectVariables',
1617
SelectionEdit = 'selectionEdit',
1718
}
1819

@@ -250,6 +251,38 @@ export function registerAssistantTools(
250251
});
251252

252253
context.subscriptions.push(getPlotTool);
254+
255+
const inspectVariablesTool = vscode.lm.registerTool<{ sessionIdentifier: string; accessKeys: Array<Array<string>> }>(PositronAssistantToolName.InspectVariables, {
256+
/**
257+
* Called to inspect one or more variables in the current session.
258+
*
259+
* @param options The options for the tool invocation.
260+
* @param token The cancellation token.
261+
*
262+
* @returns A vscode.LanguageModelToolResult.
263+
*/
264+
invoke: async (options, token) => {
265+
266+
// If no session identifier is provided, return an empty array.
267+
if (!options.input.sessionIdentifier || options.input.sessionIdentifier === 'undefined') {
268+
return new vscode.LanguageModelToolResult([
269+
new vscode.LanguageModelTextPart('[[]]')
270+
]);
271+
}
272+
273+
// Call the Positron API to get the session variables
274+
const result = await positron.runtime.getSessionVariables(
275+
options.input.sessionIdentifier,
276+
options.input.accessKeys);
277+
278+
// Return the result as a JSON string to the model
279+
return new vscode.LanguageModelToolResult([
280+
new vscode.LanguageModelTextPart(JSON.stringify(result))
281+
]);
282+
}
283+
});
284+
285+
context.subscriptions.push(inspectVariablesTool);
253286
}
254287

255288
/**
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import 'mocha';
7+
import * as positron from 'positron';
8+
import * as vscode from 'vscode';
9+
import { assertNoRpcFromEntry } from '../../utils.js';
10+
import assert from 'assert';
11+
12+
suite('positron API - ai', () => {
13+
14+
suiteSetup(async () => {
15+
await vscode.extensions.getExtension('vscode.vscode-api-tests')?.activate();
16+
});
17+
18+
teardown(async function () {
19+
assertNoRpcFromEntry([positron, 'positron']);
20+
});
21+
22+
test('getSupportedProviders returns providers', async () => {
23+
const providers = await positron.ai.getSupportedProviders();
24+
assert.ok(Array.isArray(providers), 'Providers should be an array');
25+
});
26+
27+
test('getCurrentPlotUri returns expected type', async () => {
28+
const plotUri = await positron.ai.getCurrentPlotUri();
29+
assert.ok(plotUri === undefined || typeof plotUri === 'string',
30+
'Plot URI should be either undefined or a string');
31+
});
32+
33+
test('registerChatAgent handles valid agent data', async () => {
34+
const agentData: positron.ai.ChatAgentData = {
35+
id: 'test.agent',
36+
name: 'Test Agent',
37+
fullName: 'Test Chat Agent',
38+
description: 'A test chat agent',
39+
isDefault: false,
40+
metadata: {
41+
isSticky: true
42+
},
43+
slashCommands: [
44+
{
45+
name: 'test',
46+
description: 'Run a test command',
47+
isSticky: true
48+
}
49+
],
50+
locations: [positron.PositronChatAgentLocation.Panel],
51+
disambiguation: [
52+
{
53+
category: 'test',
54+
description: 'Test category',
55+
examples: ['example 1', 'example 2']
56+
}
57+
]
58+
};
59+
60+
const disposable = await positron.ai.registerChatAgent(agentData);
61+
assert.ok(disposable, 'Should return a valid disposable');
62+
disposable.dispose();
63+
});
64+
65+
test('getPositronChatContext returns valid context for request', async () => {
66+
// Create a proper ChatRequest with required properties
67+
// Using 'any' to bypass type checking as we're only testing getPositronChatContext
68+
// which only uses the location property from the request
69+
const request: Partial<vscode.ChatRequest> = {
70+
prompt: 'test request',
71+
command: 'ask',
72+
references: [],
73+
location: vscode.ChatLocation.Panel,
74+
model: {
75+
id: 'test-model',
76+
name: 'Test Model',
77+
vendor: 'test',
78+
family: 'test-family',
79+
version: '1.0.0',
80+
capabilities: {
81+
supportsImageToText: false,
82+
supportsToolCalling: false
83+
},
84+
maxInputTokens: 2000,
85+
sendRequest: async () => ({
86+
stream: (async function* () { yield ''; })(),
87+
text: (async function* () { yield ''; })()
88+
}),
89+
countTokens: async () => 0
90+
}
91+
};
92+
93+
const context = await positron.ai.getPositronChatContext(request as vscode.ChatRequest);
94+
95+
assert.ok(context, 'Context should be returned');
96+
// Context may contain console, variables, shell, but these may be undefined
97+
// depending on the current state, so we just check the type
98+
assert.ok('console' in context || 'variables' in context || 'shell' in context,
99+
'Context should have expected structure');
100+
});
101+
});

0 commit comments

Comments
 (0)