@theia/ai-core 1.59.0 → 1.60.0-next.47

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.
Files changed (53) hide show
  1. package/lib/browser/agent-preferences.d.ts +4 -0
  2. package/lib/browser/agent-preferences.d.ts.map +1 -0
  3. package/lib/browser/agent-preferences.js +74 -0
  4. package/lib/browser/agent-preferences.js.map +1 -0
  5. package/lib/browser/ai-activation-service.d.ts +1 -0
  6. package/lib/browser/ai-activation-service.d.ts.map +1 -1
  7. package/lib/browser/ai-activation-service.js +12 -2
  8. package/lib/browser/ai-activation-service.js.map +1 -1
  9. package/lib/browser/ai-core-command-contribution.d.ts +8 -0
  10. package/lib/browser/ai-core-command-contribution.d.ts.map +1 -0
  11. package/lib/browser/ai-core-command-contribution.js +44 -0
  12. package/lib/browser/ai-core-command-contribution.js.map +1 -0
  13. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  14. package/lib/browser/ai-core-frontend-module.js +8 -1
  15. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  16. package/lib/browser/index.d.ts +1 -0
  17. package/lib/browser/index.d.ts.map +1 -1
  18. package/lib/browser/index.js +1 -0
  19. package/lib/browser/index.js.map +1 -1
  20. package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
  21. package/lib/browser/prompttemplate-contribution.js +4 -4
  22. package/lib/browser/prompttemplate-contribution.js.map +1 -1
  23. package/lib/common/prompt-service.d.ts +42 -1
  24. package/lib/common/prompt-service.d.ts.map +1 -1
  25. package/lib/common/prompt-service.js +68 -24
  26. package/lib/common/prompt-service.js.map +1 -1
  27. package/lib/common/prompt-service.spec.js +59 -1
  28. package/lib/common/prompt-service.spec.js.map +1 -1
  29. package/lib/common/prompt-variable-contribution.d.ts +16 -0
  30. package/lib/common/prompt-variable-contribution.d.ts.map +1 -0
  31. package/lib/common/prompt-variable-contribution.js +122 -0
  32. package/lib/common/prompt-variable-contribution.js.map +1 -0
  33. package/lib/common/variable-service.d.ts +26 -4
  34. package/lib/common/variable-service.d.ts.map +1 -1
  35. package/lib/common/variable-service.js +77 -16
  36. package/lib/common/variable-service.js.map +1 -1
  37. package/lib/common/variable-service.spec.d.ts +2 -0
  38. package/lib/common/variable-service.spec.d.ts.map +1 -0
  39. package/lib/common/variable-service.spec.js +237 -0
  40. package/lib/common/variable-service.spec.js.map +1 -0
  41. package/package.json +9 -9
  42. package/src/browser/agent-preferences.ts +74 -0
  43. package/src/browser/ai-activation-service.ts +13 -2
  44. package/src/browser/ai-core-command-contribution.ts +37 -0
  45. package/src/browser/ai-core-frontend-module.ts +10 -2
  46. package/src/browser/ai-settings-service.ts +1 -1
  47. package/src/browser/index.ts +1 -0
  48. package/src/browser/prompttemplate-contribution.ts +4 -4
  49. package/src/common/prompt-service.spec.ts +68 -1
  50. package/src/common/prompt-service.ts +109 -24
  51. package/src/common/prompt-variable-contribution.ts +138 -0
  52. package/src/common/variable-service.spec.ts +289 -0
  53. package/src/common/variable-service.ts +110 -13
@@ -20,6 +20,10 @@ import { expect } from 'chai';
20
20
  import { Container } from 'inversify';
21
21
  import { PromptService, PromptServiceImpl } from './prompt-service';
22
22
  import { DefaultAIVariableService, AIVariableService } from './variable-service';
23
+ import { ToolInvocationRegistry } from './tool-invocation-registry';
24
+ import { ToolRequest } from './language-model';
25
+ import { Logger } from '@theia/core';
26
+ import * as sinon from 'sinon';
23
27
 
24
28
  describe('PromptService', () => {
25
29
  let promptService: PromptService;
@@ -27,8 +31,9 @@ describe('PromptService', () => {
27
31
  beforeEach(() => {
28
32
  const container = new Container();
29
33
  container.bind<PromptService>(PromptService).to(PromptServiceImpl).inSingletonScope();
34
+ const logger = sinon.createStubInstance(Logger);
30
35
 
31
- const variableService = new DefaultAIVariableService({ getContributions: () => [] });
36
+ const variableService = new DefaultAIVariableService({ getContributions: () => [] }, logger);
32
37
  const nameVariable = { id: 'test', name: 'name', description: 'Test name ' };
33
38
  variableService.registerResolver(nameVariable, {
34
39
  canResolve: () => 100,
@@ -298,4 +303,66 @@ describe('PromptService', () => {
298
303
  expect(variantsForMainWithVariants).to.deep.equal(['variant1', 'variant2']);
299
304
  expect(variantsForMainWithoutVariants).to.deep.equal([]);
300
305
  });
306
+
307
+ it('should resolve function references within resolved variable replacements', async () => {
308
+ // Mock the tool invocation registry
309
+ const toolInvocationRegistry = {
310
+ getFunction: sinon.stub()
311
+ };
312
+
313
+ // Create a test tool request that will be returned by the registry
314
+ const testFunction: ToolRequest = {
315
+ id: 'testFunction',
316
+ name: 'Test Function',
317
+ description: 'A test function',
318
+ parameters: {
319
+ type: 'object',
320
+ properties: {
321
+ param1: {
322
+ type: 'string',
323
+ description: 'Test parameter'
324
+ }
325
+ }
326
+ },
327
+ providerName: 'test-provider',
328
+ handler: sinon.stub()
329
+ };
330
+ toolInvocationRegistry.getFunction.withArgs('testFunction').returns(testFunction);
331
+
332
+ // Create a container with our mocked registry
333
+ const container = new Container();
334
+ container.bind<PromptService>(PromptService).to(PromptServiceImpl).inSingletonScope();
335
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
336
+ container.bind<ToolInvocationRegistry>(ToolInvocationRegistry).toConstantValue(toolInvocationRegistry as any);
337
+
338
+ // Set up a variable service that returns a fragment with a function reference
339
+ const variableService = new DefaultAIVariableService({ getContributions: () => [] }, sinon.createStubInstance(Logger));
340
+ const fragmentVariable = { id: 'test', name: 'fragment', description: 'Test fragment with function' };
341
+ variableService.registerResolver(fragmentVariable, {
342
+ canResolve: () => 100,
343
+ resolve: async () => ({
344
+ variable: fragmentVariable,
345
+ value: 'This fragment contains a function reference: ~{testFunction}'
346
+ })
347
+ });
348
+ container.bind<AIVariableService>(AIVariableService).toConstantValue(variableService);
349
+
350
+ const testPromptService = container.get<PromptService>(PromptService);
351
+ testPromptService.storePromptTemplate({ id: 'testPrompt', template: 'Template with fragment: {{fragment}}' });
352
+
353
+ // Get the resolved prompt
354
+ const resolvedPrompt = await testPromptService.getPrompt('testPrompt');
355
+
356
+ // Verify that the function was resolved
357
+ expect(resolvedPrompt).to.not.be.undefined;
358
+ expect(resolvedPrompt?.text).to.include('This fragment contains a function reference:');
359
+ expect(resolvedPrompt?.text).to.not.include('~{testFunction}');
360
+
361
+ // Verify that the function description was added to functionDescriptions
362
+ expect(resolvedPrompt?.functionDescriptions?.size).to.equal(1);
363
+ expect(resolvedPrompt?.functionDescriptions?.get('testFunction')).to.deep.equal(testFunction);
364
+
365
+ // Verify that the tool invocation registry was called
366
+ expect(toolInvocationRegistry.getFunction.calledWith('testFunction')).to.be.true;
367
+ });
301
368
  });
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { URI, Event } from '@theia/core';
18
18
  import { inject, injectable, optional } from '@theia/core/shared/inversify';
19
- import { AIVariableContext, AIVariableService } from './variable-service';
19
+ import { AIVariableArg, AIVariableContext, AIVariableService, createAIResolveVariableCache, ResolvedAIVariable } from './variable-service';
20
20
  import { ToolInvocationRegistry } from './tool-invocation-registry';
21
21
  import { toolRequestToPromptText } from './language-model-util';
22
22
  import { ToolRequest } from './language-model';
@@ -41,6 +41,8 @@ export interface ResolvedPromptTemplate {
41
41
  text: string;
42
42
  /** All functions referenced in the prompt template. */
43
43
  functionDescriptions?: Map<string, ToolRequest>;
44
+ /** All variables resolved in the prompt template */
45
+ variables?: ResolvedAIVariable[];
44
46
  }
45
47
 
46
48
  export const PromptService = Symbol('PromptService');
@@ -64,10 +66,34 @@ export interface PromptService {
64
66
  * Allows to directly replace placeholders in the prompt. The supported format is 'Hi {{name}}!'.
65
67
  * The placeholder is then searched inside the args object and replaced.
66
68
  * Function references are also supported via format '~{functionId}'.
69
+ *
70
+ * All placeholders are replaced before function references are resolved.
71
+ * This allows to resolve function references contained in placeholders.
72
+ *
67
73
  * @param id the id of the prompt
68
74
  * @param args the object with placeholders, mapping the placeholder key to the value
69
75
  */
70
76
  getPrompt(id: string, args?: { [key: string]: unknown }, context?: AIVariableContext): Promise<ResolvedPromptTemplate | undefined>;
77
+
78
+ /**
79
+ * Allows to directly replace placeholders in the prompt. The supported format is 'Hi {{name}}!'.
80
+ * The placeholder is then searched inside the args object and replaced.
81
+ *
82
+ * In contrast to {@link getPrompt}, this method does not resolve function references but leaves them as is.
83
+ * This allows resolving them later as part of the prompt or chat message containing the fragment.
84
+ *
85
+ * @param id the id of the prompt
86
+ * @param args the object with placeholders, mapping the placeholder key to the value
87
+ * @param context the {@link AIVariableContext} to use during variable resolvement
88
+ * @param resolveVariable the variable resolving method. Fall back to using the {@link AIVariableService} if not given.
89
+ */
90
+ getPromptFragment(
91
+ id: string,
92
+ args?: { [key: string]: unknown },
93
+ context?: AIVariableContext,
94
+ resolveVariable?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
95
+ ): Promise<Omit<ResolvedPromptTemplate, 'functionDescriptions'> | undefined>;
96
+
71
97
  /**
72
98
  * Adds a {@link PromptTemplate} to the list of prompts.
73
99
  * @param promptTemplate the prompt template to store
@@ -246,27 +272,14 @@ export class PromptServiceImpl implements PromptService {
246
272
  return undefined;
247
273
  }
248
274
 
249
- const matches = matchVariablesRegEx(prompt.template);
250
- const variableAndArgReplacements = await Promise.all(matches.map(async match => {
251
- const completeText = match[0];
252
- const variableAndArg = match[1];
253
- let variableName = variableAndArg;
254
- let argument: string | undefined;
255
- const parts = variableAndArg.split(':', 2);
256
- if (parts.length > 1) {
257
- variableName = parts[0];
258
- argument = parts[1];
259
- }
260
- return {
261
- placeholder: completeText,
262
- value: String(args?.[variableAndArg] ?? (await this.variableService?.resolveVariable({
263
- variable: variableName,
264
- arg: argument
265
- }, context ?? {}))?.value ?? completeText)
266
- };
267
- }));
275
+ // First resolve variables and arguments
276
+ let resolvedTemplate = prompt.template;
277
+ const variableAndArgReplacements = await this.getVariableAndArgReplacements(prompt.template, args, context);
278
+ variableAndArgReplacements.replacements.forEach(replacement => resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
268
279
 
269
- const functionMatches = matchFunctionsRegEx(prompt.template);
280
+ // Then resolve function references with already resolved variables and arguments
281
+ // This allows to resolve function references contained in resolved variables (e.g. prompt fragments)
282
+ const functionMatches = matchFunctionsRegEx(resolvedTemplate);
270
283
  const functions = new Map<string, ToolRequest>();
271
284
  const functionReplacements = functionMatches.map(match => {
272
285
  const completeText = match[0];
@@ -280,16 +293,88 @@ export class PromptServiceImpl implements PromptService {
280
293
  value: toolRequest ? toolRequestToPromptText(toolRequest) : completeText
281
294
  };
282
295
  });
296
+ functionReplacements.forEach(replacement => resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
297
+
298
+ return {
299
+ id,
300
+ text: resolvedTemplate,
301
+ functionDescriptions: functions.size > 0 ? functions : undefined,
302
+ variables: variableAndArgReplacements.resolvedVariables
303
+ };
304
+ }
283
305
 
306
+ async getPromptFragment(
307
+ id: string,
308
+ args?: { [key: string]: unknown },
309
+ context?: AIVariableContext,
310
+ resolveVariable?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
311
+ ): Promise<Omit<ResolvedPromptTemplate, 'functionDescriptions'> | undefined> {
312
+ const variantId = await this.getVariantId(id);
313
+ const prompt = this.getUnresolvedPrompt(variantId);
314
+ if (prompt === undefined) {
315
+ return undefined;
316
+ }
317
+
318
+ const replacements = await this.getVariableAndArgReplacements(prompt.template, args, context, resolveVariable);
284
319
  let resolvedTemplate = prompt.template;
285
- const replacements = [...variableAndArgReplacements, ...functionReplacements];
286
- replacements.forEach(replacement => resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
320
+ replacements.replacements.forEach(replacement => resolvedTemplate = resolvedTemplate.replace(replacement.placeholder, replacement.value));
321
+
287
322
  return {
288
323
  id,
289
324
  text: resolvedTemplate,
290
- functionDescriptions: functions.size > 0 ? functions : undefined
325
+ variables: replacements.resolvedVariables
291
326
  };
292
327
  }
328
+
329
+ /**
330
+ * Calculates all variable and argument replacements for an unresolved template.
331
+ *
332
+ * @param template the unresolved template text
333
+ * @param args the object with placeholders, mapping the placeholder key to the value
334
+ * @param context the {@link AIVariableContext} to use during variable resolvement
335
+ * @param resolveVariable the variable resolving method. Fall back to using the {@link AIVariableService} if not given.
336
+ */
337
+ protected async getVariableAndArgReplacements(
338
+ template: string,
339
+ args?: { [key: string]: unknown },
340
+ context?: AIVariableContext,
341
+ resolveVariable?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
342
+ ): Promise<{ replacements: { placeholder: string; value: string }[], resolvedVariables: ResolvedAIVariable[] }> {
343
+ const matches = matchVariablesRegEx(template);
344
+ const variableCache = createAIResolveVariableCache();
345
+ const variableAndArgReplacements: { placeholder: string; value: string }[] = [];
346
+ const resolvedVariables: Set<ResolvedAIVariable> = new Set();
347
+ for (const match of matches) {
348
+ const completeText = match[0];
349
+ const variableAndArg = match[1];
350
+ let variableName = variableAndArg;
351
+ let argument: string | undefined;
352
+ const parts = variableAndArg.split(':', 2);
353
+ if (parts.length > 1) {
354
+ variableName = parts[0];
355
+ argument = parts[1];
356
+ }
357
+ let value: string;
358
+ if (args && args[variableAndArg] !== undefined) {
359
+ value = String(args[variableAndArg]);
360
+ } else {
361
+ const toResolve = { variable: variableName, arg: argument };
362
+ const resolved = resolveVariable
363
+ ? await resolveVariable(toResolve)
364
+ : await this.variableService?.resolveVariable(toResolve, context ?? {}, variableCache);
365
+ // Track resolved variable and its dependencies in all resolved variables
366
+ if (resolved) {
367
+ resolvedVariables.add(resolved);
368
+ resolved.allResolvedDependencies?.forEach(v => resolvedVariables.add(v));
369
+ }
370
+ value = String(resolved?.value ?? completeText);
371
+ }
372
+ variableAndArgReplacements.push({ placeholder: completeText, value });
373
+ }
374
+
375
+ return { replacements: variableAndArgReplacements, resolvedVariables: Array.from(resolvedVariables) };
376
+ }
377
+
293
378
  getAllPrompts(): PromptMap {
294
379
  if (this.customizationService !== undefined) {
295
380
  const myCustomization = this.customizationService;
@@ -0,0 +1,138 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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
+ import { CommandService, nls } from '@theia/core';
17
+ import { injectable, inject, optional } from '@theia/core/shared/inversify';
18
+ import * as monaco from '@theia/monaco-editor-core';
19
+ import {
20
+ AIVariable,
21
+ AIVariableContribution,
22
+ AIVariableService,
23
+ AIVariableResolutionRequest,
24
+ AIVariableContext,
25
+ ResolvedAIVariable,
26
+ AIVariableResolverWithVariableDependencies,
27
+ AIVariableArg
28
+ } from './variable-service';
29
+ import { PromptCustomizationService, PromptService } from './prompt-service';
30
+ import { PromptText } from './prompt-text';
31
+
32
+ export const PROMPT_VARIABLE: AIVariable = {
33
+ id: 'prompt-provider',
34
+ description: nls.localize('theia/ai/core/promptVariable/description', 'Resolves prompt templates via the prompt service'),
35
+ name: 'prompt',
36
+ args: [
37
+ { name: 'id', description: nls.localize('theia/ai/core/promptVariable/argDescription', 'The prompt template id to resolve') }
38
+ ]
39
+ };
40
+
41
+ @injectable()
42
+ export class PromptVariableContribution implements AIVariableContribution, AIVariableResolverWithVariableDependencies {
43
+
44
+ @inject(CommandService)
45
+ protected readonly commandService: CommandService;
46
+
47
+ @inject(PromptService)
48
+ protected readonly promptService: PromptService;
49
+
50
+ @inject(PromptCustomizationService) @optional()
51
+ protected readonly promptCustomizationService: PromptCustomizationService;
52
+
53
+ registerVariables(service: AIVariableService): void {
54
+ service.registerResolver(PROMPT_VARIABLE, this);
55
+ service.registerArgumentPicker(PROMPT_VARIABLE, this.triggerArgumentPicker.bind(this));
56
+ service.registerArgumentCompletionProvider(PROMPT_VARIABLE, this.provideArgumentCompletionItems.bind(this));
57
+ }
58
+
59
+ canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): number {
60
+ if (request.variable.name === PROMPT_VARIABLE.name) {
61
+ return 1;
62
+ }
63
+ return -1;
64
+ }
65
+
66
+ async resolve(
67
+ request: AIVariableResolutionRequest,
68
+ context: AIVariableContext,
69
+ resolveDependency?: (variable: AIVariableArg) => Promise<ResolvedAIVariable | undefined>
70
+ ): Promise<ResolvedAIVariable | undefined> {
71
+ if (request.variable.name === PROMPT_VARIABLE.name) {
72
+ const promptId = request.arg?.trim();
73
+ if (promptId) {
74
+ const resolvedPrompt = await this.promptService.getPromptFragment(promptId, undefined, context, resolveDependency);
75
+ if (resolvedPrompt) {
76
+ return {
77
+ variable: request.variable,
78
+ value: resolvedPrompt.text,
79
+ allResolvedDependencies: resolvedPrompt.variables
80
+ };
81
+ }
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ protected async triggerArgumentPicker(): Promise<string | undefined> {
88
+ // Trigger the suggestion command to show argument completions
89
+ this.commandService.executeCommand('editor.action.triggerSuggest');
90
+ // Return undefined because we don't actually pick the argument here.
91
+ // The argument is selected and inserted by the monaco editor's completion mechanism.
92
+ return undefined;
93
+ }
94
+
95
+ protected async provideArgumentCompletionItems(
96
+ model: monaco.editor.ITextModel,
97
+ position: monaco.Position
98
+ ): Promise<monaco.languages.CompletionItem[] | undefined> {
99
+ const lineContent = model.getLineContent(position.lineNumber);
100
+
101
+ // Only provide completions once the variable argument separator is typed
102
+ const triggerCharIndex = lineContent.lastIndexOf(PromptText.VARIABLE_SEPARATOR_CHAR, position.column - 1);
103
+ if (triggerCharIndex === -1) {
104
+ return undefined;
105
+ }
106
+
107
+ // Check if the text immediately before the trigger is the prompt variable, i.e #prompt
108
+ const requiredVariable = `${PromptText.VARIABLE_CHAR}${PROMPT_VARIABLE.name}`;
109
+ if (triggerCharIndex < requiredVariable.length ||
110
+ lineContent.substring(triggerCharIndex - requiredVariable.length, triggerCharIndex) !== requiredVariable) {
111
+ return undefined;
112
+ }
113
+
114
+ const range = new monaco.Range(position.lineNumber, triggerCharIndex + 2, position.lineNumber, position.column);
115
+
116
+ const customPromptIds = this.promptCustomizationService?.getCustomPromptTemplateIDs() ?? [];
117
+ const builtinPromptIds = Object.keys(this.promptService.getAllPrompts());
118
+
119
+ const customPromptCompletions = customPromptIds.map(promptId => ({
120
+ label: promptId,
121
+ kind: monaco.languages.CompletionItemKind.Enum,
122
+ insertText: promptId,
123
+ range,
124
+ detail: nls.localize('theia/ai/core/promptVariable/completions/detail/custom', 'Custom prompt template'),
125
+ sortText: `AAA${promptId}` // Sort before everything else including all built-in prompts
126
+ }));
127
+ const builtinPromptCompletions = builtinPromptIds.map(promptId => ({
128
+ label: promptId,
129
+ kind: monaco.languages.CompletionItemKind.Variable,
130
+ insertText: promptId,
131
+ range,
132
+ detail: nls.localize('theia/ai/core/promptVariable/completions/detail/builtin', 'Built-in prompt template'),
133
+ sortText: `AAB${promptId}` // Sort after all custom prompts but before others
134
+ }));
135
+
136
+ return [...customPromptCompletions, ...builtinPromptCompletions];
137
+ }
138
+ }