@theia/ai-core 1.59.0 → 1.60.0-next.43
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/lib/browser/agent-preferences.d.ts +4 -0
- package/lib/browser/agent-preferences.d.ts.map +1 -0
- package/lib/browser/agent-preferences.js +74 -0
- package/lib/browser/agent-preferences.js.map +1 -0
- package/lib/browser/ai-activation-service.d.ts +1 -0
- package/lib/browser/ai-activation-service.d.ts.map +1 -1
- package/lib/browser/ai-activation-service.js +12 -2
- package/lib/browser/ai-activation-service.js.map +1 -1
- package/lib/browser/ai-core-command-contribution.d.ts +8 -0
- package/lib/browser/ai-core-command-contribution.d.ts.map +1 -0
- package/lib/browser/ai-core-command-contribution.js +44 -0
- package/lib/browser/ai-core-command-contribution.js.map +1 -0
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +8 -1
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
- package/lib/browser/prompttemplate-contribution.js +4 -4
- package/lib/browser/prompttemplate-contribution.js.map +1 -1
- package/lib/common/prompt-service.d.ts +42 -1
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +68 -24
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +59 -1
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/prompt-variable-contribution.d.ts +16 -0
- package/lib/common/prompt-variable-contribution.d.ts.map +1 -0
- package/lib/common/prompt-variable-contribution.js +122 -0
- package/lib/common/prompt-variable-contribution.js.map +1 -0
- package/lib/common/variable-service.d.ts +26 -4
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js +77 -16
- package/lib/common/variable-service.js.map +1 -1
- package/lib/common/variable-service.spec.d.ts +2 -0
- package/lib/common/variable-service.spec.d.ts.map +1 -0
- package/lib/common/variable-service.spec.js +237 -0
- package/lib/common/variable-service.spec.js.map +1 -0
- package/package.json +9 -9
- package/src/browser/agent-preferences.ts +74 -0
- package/src/browser/ai-activation-service.ts +13 -2
- package/src/browser/ai-core-command-contribution.ts +37 -0
- package/src/browser/ai-core-frontend-module.ts +10 -2
- package/src/browser/ai-settings-service.ts +1 -1
- package/src/browser/index.ts +1 -0
- package/src/browser/prompttemplate-contribution.ts +4 -4
- package/src/common/prompt-service.spec.ts +68 -1
- package/src/common/prompt-service.ts +109 -24
- package/src/common/prompt-variable-contribution.ts +138 -0
- package/src/common/variable-service.spec.ts +289 -0
- package/src/common/variable-service.ts +110 -13
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2024 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as sinon from 'sinon';
|
|
18
|
+
import { ContributionProvider, Logger } from '@theia/core';
|
|
19
|
+
import { expect } from 'chai';
|
|
20
|
+
import {
|
|
21
|
+
DefaultAIVariableService,
|
|
22
|
+
AIVariable,
|
|
23
|
+
AIVariableContribution,
|
|
24
|
+
AIVariableResolverWithVariableDependencies,
|
|
25
|
+
ResolvedAIVariable,
|
|
26
|
+
createAIResolveVariableCache,
|
|
27
|
+
AIVariableArg
|
|
28
|
+
} from './variable-service';
|
|
29
|
+
|
|
30
|
+
describe('DefaultAIVariableService', () => {
|
|
31
|
+
let variableService: DefaultAIVariableService;
|
|
32
|
+
let contributionProvider: sinon.SinonStubbedInstance<ContributionProvider<AIVariableContribution>>;
|
|
33
|
+
let logger: sinon.SinonStubbedInstance<Logger>;
|
|
34
|
+
|
|
35
|
+
const varA: AIVariable = {
|
|
36
|
+
id: 'provider.a',
|
|
37
|
+
name: 'a',
|
|
38
|
+
description: 'Variable A'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const varB: AIVariable = {
|
|
42
|
+
id: 'provider.b',
|
|
43
|
+
name: 'b',
|
|
44
|
+
description: 'Variable B'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const varC: AIVariable = {
|
|
48
|
+
id: 'provider.c',
|
|
49
|
+
name: 'c',
|
|
50
|
+
description: 'Variable C'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const varD: AIVariable = {
|
|
54
|
+
id: 'provider.d',
|
|
55
|
+
name: 'd',
|
|
56
|
+
description: 'Variable D'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Create resolvers for our variables
|
|
60
|
+
const resolverA: AIVariableResolverWithVariableDependencies = {
|
|
61
|
+
canResolve: sinon.stub().returns(1),
|
|
62
|
+
resolve: async (_request, _context, resolveDependency?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>) => {
|
|
63
|
+
if (resolveDependency) {
|
|
64
|
+
// Variable A depends on both B and C
|
|
65
|
+
const dependencyB = await resolveDependency({ variable: varB });
|
|
66
|
+
const dependencyC = await resolveDependency({ variable: varC });
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
variable: varA,
|
|
70
|
+
value: `A resolved with B: ${dependencyB?.value ?? 'undefined'} and C: ${dependencyC?.value ?? 'undefined'}`,
|
|
71
|
+
allResolvedDependencies: [
|
|
72
|
+
...(dependencyB ? [dependencyB] : []),
|
|
73
|
+
...(dependencyC ? [dependencyC] : [])
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { variable: varA, value: 'A value' };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resolverB: AIVariableResolverWithVariableDependencies = {
|
|
82
|
+
canResolve: sinon.stub().returns(1),
|
|
83
|
+
resolve: async (_request, _context, resolveDependency?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>) => {
|
|
84
|
+
if (resolveDependency) {
|
|
85
|
+
// Variable B depends on A, creating a cycle
|
|
86
|
+
const dependencyA = await resolveDependency({ variable: varA });
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
variable: varB,
|
|
90
|
+
value: `B resolved with A: ${dependencyA?.value ?? 'undefined (cycle detected)'}`,
|
|
91
|
+
allResolvedDependencies: dependencyA ? [dependencyA] : []
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { variable: varB, value: 'B value' };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const resolverC: AIVariableResolverWithVariableDependencies = {
|
|
99
|
+
canResolve: sinon.stub().returns(1),
|
|
100
|
+
resolve: async (_request, _context, resolveDependency?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>) => {
|
|
101
|
+
if (resolveDependency) {
|
|
102
|
+
// Variable C depends on D with two different arguments
|
|
103
|
+
const dependencyD1 = await resolveDependency({ variable: varD, arg: 'arg1' });
|
|
104
|
+
const dependencyD2 = await resolveDependency({ variable: varD, arg: 'arg2' });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
variable: varC,
|
|
108
|
+
value: `C resolved with D(arg1): ${dependencyD1?.value ?? 'undefined'} and D(arg2): ${dependencyD2?.value ?? 'undefined'}`,
|
|
109
|
+
allResolvedDependencies: [
|
|
110
|
+
...(dependencyD1 ? [dependencyD1] : []),
|
|
111
|
+
...(dependencyD2 ? [dependencyD2] : [])
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { variable: varC, value: 'C value' };
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const resolverD: AIVariableResolverWithVariableDependencies = {
|
|
120
|
+
canResolve: sinon.stub().returns(1),
|
|
121
|
+
resolve: async request => {
|
|
122
|
+
const arg = request.arg;
|
|
123
|
+
return {
|
|
124
|
+
variable: varD,
|
|
125
|
+
value: arg ? `D value with ${arg}` : 'D value'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
// Create stub for the contribution provider
|
|
132
|
+
contributionProvider = {
|
|
133
|
+
getContributions: sinon.stub().returns([])
|
|
134
|
+
} as sinon.SinonStubbedInstance<ContributionProvider<AIVariableContribution>>;
|
|
135
|
+
|
|
136
|
+
// Create stub for logger
|
|
137
|
+
logger = sinon.createStubInstance(Logger);
|
|
138
|
+
|
|
139
|
+
// Create the service under test
|
|
140
|
+
variableService = new DefaultAIVariableService(
|
|
141
|
+
contributionProvider,
|
|
142
|
+
logger
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Register the variables and resolvers
|
|
146
|
+
variableService.registerResolver(varA, resolverA);
|
|
147
|
+
variableService.registerResolver(varB, resolverB);
|
|
148
|
+
variableService.registerResolver(varC, resolverC);
|
|
149
|
+
variableService.registerResolver(varD, resolverD);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('resolveVariable', () => {
|
|
153
|
+
it('should handle recursive variable resolution and detect cycles', async () => {
|
|
154
|
+
// Try to resolve variable A, which has a cycle with B and also depends on C which depends on D
|
|
155
|
+
const result = await variableService.resolveVariable('a', {});
|
|
156
|
+
|
|
157
|
+
// Verify the result
|
|
158
|
+
expect(result).to.not.be.undefined;
|
|
159
|
+
expect(result!.variable).to.deep.equal(varA);
|
|
160
|
+
|
|
161
|
+
// The value should contain B's value (with a cycle detection) and C's value
|
|
162
|
+
expect(result!.value).to.include('B resolved with A: undefined (cycle detected)');
|
|
163
|
+
expect(result!.value).to.include('C resolved with D(arg1): D value with arg1 and D(arg2): D value with arg2');
|
|
164
|
+
|
|
165
|
+
// Verify that we logged a warning about the cycle
|
|
166
|
+
expect(logger.warn.calledOnce).to.be.true;
|
|
167
|
+
expect(logger.warn.firstCall.args[0]).to.include('Cycle detected for variable: a');
|
|
168
|
+
|
|
169
|
+
// Verify dependencies are tracked
|
|
170
|
+
expect(result!.allResolvedDependencies).to.have.lengthOf(2);
|
|
171
|
+
|
|
172
|
+
// Find the B dependency and verify it doesn't have A in its dependencies (due to cycle detection)
|
|
173
|
+
const bDependency = result!.allResolvedDependencies!.find(d => d.variable.name === 'b');
|
|
174
|
+
expect(bDependency).to.not.be.undefined;
|
|
175
|
+
expect(bDependency!.allResolvedDependencies).to.be.empty;
|
|
176
|
+
|
|
177
|
+
// Find the C dependency and its D dependencies
|
|
178
|
+
const cDependency = result!.allResolvedDependencies!.find(d => d.variable.name === 'c');
|
|
179
|
+
expect(cDependency).to.not.be.undefined;
|
|
180
|
+
expect(cDependency!.value).to.equal('C resolved with D(arg1): D value with arg1 and D(arg2): D value with arg2');
|
|
181
|
+
expect(cDependency!.allResolvedDependencies).to.have.lengthOf(2);
|
|
182
|
+
|
|
183
|
+
const dDependency1 = cDependency!.allResolvedDependencies![0];
|
|
184
|
+
expect(dDependency1.variable.name).to.equal('d');
|
|
185
|
+
expect(dDependency1.value).to.equal('D value with arg1');
|
|
186
|
+
|
|
187
|
+
const dDependency2 = cDependency!.allResolvedDependencies![1];
|
|
188
|
+
expect(dDependency2.variable.name).to.equal('d');
|
|
189
|
+
expect(dDependency2.value).to.equal('D value with arg2');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle variables with a simple chain of dependencies', async () => {
|
|
193
|
+
// Variable C depends on D with two different arguments
|
|
194
|
+
const result = await variableService.resolveVariable('c', {});
|
|
195
|
+
|
|
196
|
+
expect(result).to.not.be.undefined;
|
|
197
|
+
expect(result!.variable).to.deep.equal(varC);
|
|
198
|
+
expect(result!.value).to.equal('C resolved with D(arg1): D value with arg1 and D(arg2): D value with arg2');
|
|
199
|
+
|
|
200
|
+
// Verify dependency chain
|
|
201
|
+
expect(result!.allResolvedDependencies).to.have.lengthOf(2);
|
|
202
|
+
|
|
203
|
+
const dDependency1 = result!.allResolvedDependencies![0];
|
|
204
|
+
expect(dDependency1.variable.name).to.equal('d');
|
|
205
|
+
expect(dDependency1.value).to.equal('D value with arg1');
|
|
206
|
+
|
|
207
|
+
const dDependency2 = result!.allResolvedDependencies![1];
|
|
208
|
+
expect(dDependency2.variable.name).to.equal('d');
|
|
209
|
+
expect(dDependency2.value).to.equal('D value with arg2');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle variables without dependencies', async () => {
|
|
213
|
+
// D has no dependencies
|
|
214
|
+
const result = await variableService.resolveVariable('d', {});
|
|
215
|
+
|
|
216
|
+
expect(result).to.not.be.undefined;
|
|
217
|
+
expect(result!.variable).to.deep.equal(varD);
|
|
218
|
+
expect(result!.value).to.equal('D value');
|
|
219
|
+
expect(result!.allResolvedDependencies).to.be.undefined;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle variables with arguments', async () => {
|
|
223
|
+
// Test D with an argument
|
|
224
|
+
const result = await variableService.resolveVariable({ variable: 'd', arg: 'test-arg' }, {}, undefined);
|
|
225
|
+
|
|
226
|
+
expect(result).to.not.be.undefined;
|
|
227
|
+
expect(result!.variable).to.deep.equal(varD);
|
|
228
|
+
expect(result!.value).to.equal('D value with test-arg');
|
|
229
|
+
expect(result!.allResolvedDependencies).to.be.undefined;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return undefined for non-existent variables', async () => {
|
|
233
|
+
const result = await variableService.resolveVariable('nonexistent', {});
|
|
234
|
+
expect(result).to.be.undefined;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should properly populate cache when resolving variables with dependencies', async () => {
|
|
238
|
+
// Create a cache to pass into the resolver
|
|
239
|
+
const cache = createAIResolveVariableCache();
|
|
240
|
+
|
|
241
|
+
// Resolve variable A, which depends on B and C, and C depends on D with two arguments
|
|
242
|
+
const result = await variableService.resolveVariable('a', {}, cache);
|
|
243
|
+
|
|
244
|
+
// Verify that the result is correct
|
|
245
|
+
expect(result).to.not.be.undefined;
|
|
246
|
+
expect(result!.variable).to.deep.equal(varA);
|
|
247
|
+
|
|
248
|
+
// Verify that the cache has entries for all variables
|
|
249
|
+
expect(cache.size).to.equal(5); // A, B, C, D(arg1), and D(arg2)
|
|
250
|
+
|
|
251
|
+
// Verify that all variables have entries in the cache
|
|
252
|
+
expect(cache.has('a:')).to.be.true; // 'a:' key format is variableName + separator + arg (empty in this case)
|
|
253
|
+
expect(cache.has('b:')).to.be.true;
|
|
254
|
+
expect(cache.has('c:')).to.be.true;
|
|
255
|
+
expect(cache.has('d:arg1')).to.be.true;
|
|
256
|
+
expect(cache.has('d:arg2')).to.be.true;
|
|
257
|
+
|
|
258
|
+
// Verify that all promises in the cache are resolved
|
|
259
|
+
for (const entry of cache.values()) {
|
|
260
|
+
const resolvedVar = await entry.promise;
|
|
261
|
+
expect(resolvedVar).to.not.be.undefined;
|
|
262
|
+
expect(entry.inProgress).to.be.false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check specific variable results from cache
|
|
266
|
+
const aEntry = cache.get('a:');
|
|
267
|
+
const aResult = await aEntry!.promise;
|
|
268
|
+
expect(aResult!.variable.name).to.equal('a');
|
|
269
|
+
|
|
270
|
+
const bEntry = cache.get('b:');
|
|
271
|
+
const bResult = await bEntry!.promise;
|
|
272
|
+
expect(bResult!.variable.name).to.equal('b');
|
|
273
|
+
|
|
274
|
+
const cEntry = cache.get('c:');
|
|
275
|
+
const cResult = await cEntry!.promise;
|
|
276
|
+
expect(cResult!.variable.name).to.equal('c');
|
|
277
|
+
|
|
278
|
+
const dEntry1 = cache.get('d:arg1');
|
|
279
|
+
const dResult1 = await dEntry1!.promise;
|
|
280
|
+
expect(dResult1!.variable.name).to.equal('d');
|
|
281
|
+
expect(dResult1!.value).to.equal('D value with arg1');
|
|
282
|
+
|
|
283
|
+
const dEntry2 = cache.get('d:arg2');
|
|
284
|
+
const dResult2 = await dEntry2!.promise;
|
|
285
|
+
expect(dResult2!.variable.name).to.equal('d');
|
|
286
|
+
expect(dResult2!.value).to.equal('D value with arg2');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { ContributionProvider, Disposable, Emitter, ILogger, MaybePromise, Prioritizeable, Event } from '@theia/core';
|
|
23
23
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
24
24
|
import * as monaco from '@theia/monaco-editor-core';
|
|
25
|
+
import { PromptText } from './prompt-text';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* A variable is a short string that is used to reference a value that is resolved and replaced in the user prompt at request-time.
|
|
@@ -75,6 +76,8 @@ export interface ResolvedAIVariable {
|
|
|
75
76
|
arg?: string;
|
|
76
77
|
/** value that is inserted into the prompt at the position of the variable usage */
|
|
77
78
|
value: string;
|
|
79
|
+
/** Flat list of all variables that have been (recursively) resolved while resolving this variable. */
|
|
80
|
+
allResolvedDependencies?: ResolvedAIVariable[];
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
export namespace ResolvedAIVariable {
|
|
@@ -132,6 +135,24 @@ export interface AIVariableResolver {
|
|
|
132
135
|
resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined>;
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
export interface AIVariableResolverWithVariableDependencies extends AIVariableResolver {
|
|
139
|
+
resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined>;
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the given AI variable resolution request. When resolving dependencies with `resolveDependency`,
|
|
142
|
+
* add the resolved dependencies to the result's `allResolvedDependencies` list
|
|
143
|
+
* to enable consumers of the resolved variable to inspect dependencies.
|
|
144
|
+
*/
|
|
145
|
+
resolve(
|
|
146
|
+
request: AIVariableResolutionRequest,
|
|
147
|
+
context: AIVariableContext,
|
|
148
|
+
resolveDependency: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
|
|
149
|
+
): Promise<ResolvedAIVariable | undefined>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isResolverWithDependencies(resolver: AIVariableResolver | undefined): resolver is AIVariableResolverWithVariableDependencies {
|
|
153
|
+
return resolver !== undefined && resolver.resolve.length >= 3;
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
export const AIVariableService = Symbol('AIVariableService');
|
|
136
157
|
export interface AIVariableService {
|
|
137
158
|
hasVariable(name: string): boolean;
|
|
@@ -153,7 +174,7 @@ export interface AIVariableService {
|
|
|
153
174
|
unregisterArgumentCompletionProvider(variable: AIVariable, argPicker: AIVariableArgCompletionProvider): void;
|
|
154
175
|
getArgumentCompletionProvider(name: string): Promise<AIVariableArgCompletionProvider | undefined>;
|
|
155
176
|
|
|
156
|
-
resolveVariable(variable: AIVariableArg, context: AIVariableContext): Promise<ResolvedAIVariable | undefined>;
|
|
177
|
+
resolveVariable(variable: AIVariableArg, context: AIVariableContext, cache?: Map<string, ResolveAIVariableCacheEntry>): Promise<ResolvedAIVariable | undefined>;
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
/** Contributions on the frontend can optionally implement `FrontendVariableContribution`. */
|
|
@@ -162,6 +183,33 @@ export interface AIVariableContribution {
|
|
|
162
183
|
registerVariables(service: AIVariableService): void;
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
export interface ResolveAIVariableCacheEntry {
|
|
187
|
+
promise: Promise<ResolvedAIVariable | undefined>;
|
|
188
|
+
inProgress: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type ResolveAIVariableCache = Map<string, ResolveAIVariableCacheEntry>;
|
|
192
|
+
/**
|
|
193
|
+
* Creates a new, empty cache for AI variable resolvement to hand into `AIVariableService.resolveVariable`.
|
|
194
|
+
*/
|
|
195
|
+
export function createAIResolveVariableCache(): Map<string, ResolveAIVariableCacheEntry> {
|
|
196
|
+
return new Map();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Utility function to get all resolved AI variables from a {@link ResolveAIVariableCache} */
|
|
200
|
+
export async function getAllResolvedAIVariables(cache: ResolveAIVariableCache): Promise<ResolvedAIVariable[]> {
|
|
201
|
+
const resolvedVariables: ResolvedAIVariable[] = [];
|
|
202
|
+
for (const cacheEntry of cache.values()) {
|
|
203
|
+
if (!cacheEntry.inProgress) {
|
|
204
|
+
const resolvedVariable = await cacheEntry.promise;
|
|
205
|
+
if (resolvedVariable) {
|
|
206
|
+
resolvedVariables.push(resolvedVariable);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return resolvedVariables;
|
|
211
|
+
}
|
|
212
|
+
|
|
165
213
|
@injectable()
|
|
166
214
|
export class DefaultAIVariableService implements AIVariableService {
|
|
167
215
|
protected variables = new Map<string, AIVariable>();
|
|
@@ -172,11 +220,10 @@ export class DefaultAIVariableService implements AIVariableService {
|
|
|
172
220
|
protected readonly onDidChangeVariablesEmitter = new Emitter<void>();
|
|
173
221
|
readonly onDidChangeVariables: Event<void> = this.onDidChangeVariablesEmitter.event;
|
|
174
222
|
|
|
175
|
-
@inject(ILogger) protected logger: ILogger;
|
|
176
|
-
|
|
177
223
|
constructor(
|
|
178
224
|
@inject(ContributionProvider) @named(AIVariableContribution)
|
|
179
|
-
protected readonly contributionProvider: ContributionProvider<AIVariableContribution
|
|
225
|
+
protected readonly contributionProvider: ContributionProvider<AIVariableContribution>,
|
|
226
|
+
@inject(ILogger) protected readonly logger: ILogger
|
|
180
227
|
) {
|
|
181
228
|
}
|
|
182
229
|
|
|
@@ -291,15 +338,65 @@ export class DefaultAIVariableService implements AIVariableService {
|
|
|
291
338
|
return this.argCompletionProviders.get(this.getKey(name)) ?? undefined;
|
|
292
339
|
}
|
|
293
340
|
|
|
294
|
-
async resolveVariable(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
341
|
+
async resolveVariable(
|
|
342
|
+
request: AIVariableArg,
|
|
343
|
+
context: AIVariableContext,
|
|
344
|
+
cache: ResolveAIVariableCache = createAIResolveVariableCache()
|
|
345
|
+
): Promise<ResolvedAIVariable | undefined> {
|
|
346
|
+
// Calculate unique variable cache key from variable name and argument
|
|
347
|
+
const variableName = typeof request === 'string'
|
|
348
|
+
? request
|
|
349
|
+
: typeof request.variable === 'string'
|
|
350
|
+
? request.variable
|
|
351
|
+
: request.variable.name;
|
|
300
352
|
const arg = typeof request === 'string' ? undefined : request.arg;
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
353
|
+
const cacheKey = `${variableName}${PromptText.VARIABLE_SEPARATOR_CHAR}${arg ?? ''}`;
|
|
354
|
+
|
|
355
|
+
// If the current cache key exists and is still in progress, we reached a cycle.
|
|
356
|
+
// If we reach it but it has been resolved, it was part of another resolvement branch and we can simply return it.
|
|
357
|
+
if (cache.has(cacheKey)) {
|
|
358
|
+
const existingEntry = cache.get(cacheKey)!;
|
|
359
|
+
if (existingEntry.inProgress) {
|
|
360
|
+
this.logger.warn(`Cycle detected for variable: ${variableName} with arg: ${arg}. Skipping resolution.`);
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
return existingEntry.promise;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const entry: ResolveAIVariableCacheEntry = { promise: Promise.resolve(undefined), inProgress: true };
|
|
367
|
+
cache.set(cacheKey, entry);
|
|
368
|
+
|
|
369
|
+
// Asynchronously resolves a variable, handling its dependencies while preventing cyclical resolution.
|
|
370
|
+
// Selects the appropriate resolver and resolution strategy based on whether nested dependency resolution is supported.
|
|
371
|
+
const promise = (async () => {
|
|
372
|
+
const variable = this.getVariable(variableName);
|
|
373
|
+
if (!variable) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
const resolver = await this.getResolver(variableName, arg, context);
|
|
377
|
+
let resolved: ResolvedAIVariable | undefined;
|
|
378
|
+
if (isResolverWithDependencies(resolver)) {
|
|
379
|
+
// Explicit cast needed because Typescript does not consider the method parameter length of the type guard at compile time
|
|
380
|
+
resolved = await (resolver as AIVariableResolverWithVariableDependencies).resolve(
|
|
381
|
+
{ variable, arg },
|
|
382
|
+
context,
|
|
383
|
+
async (depRequest: AIVariableResolutionRequest) =>
|
|
384
|
+
this.resolveVariable(depRequest, context, cache)
|
|
385
|
+
);
|
|
386
|
+
} else if (resolver) {
|
|
387
|
+
// Explicit cast needed because Typescript does not consider the method parameter length of the type guard at compile time
|
|
388
|
+
resolved = await (resolver as AIVariableResolver).resolve({ variable, arg }, context);
|
|
389
|
+
} else {
|
|
390
|
+
resolved = undefined;
|
|
391
|
+
}
|
|
392
|
+
return resolved ? { ...resolved, arg } : undefined;
|
|
393
|
+
})();
|
|
394
|
+
|
|
395
|
+
entry.promise = promise;
|
|
396
|
+
promise.finally(() => {
|
|
397
|
+
entry.inProgress = false;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return promise;
|
|
304
401
|
}
|
|
305
402
|
}
|