@theia/ai-core 1.59.0-next.72 → 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.
Files changed (68) 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/browser/theia-variable-contribution.d.ts +5 -1
  24. package/lib/browser/theia-variable-contribution.d.ts.map +1 -1
  25. package/lib/browser/theia-variable-contribution.js +48 -9
  26. package/lib/browser/theia-variable-contribution.js.map +1 -1
  27. package/lib/common/language-model.d.ts +5 -3
  28. package/lib/common/language-model.d.ts.map +1 -1
  29. package/lib/common/language-model.js +32 -7
  30. package/lib/common/language-model.js.map +1 -1
  31. package/lib/common/prompt-service.d.ts +42 -1
  32. package/lib/common/prompt-service.d.ts.map +1 -1
  33. package/lib/common/prompt-service.js +68 -24
  34. package/lib/common/prompt-service.js.map +1 -1
  35. package/lib/common/prompt-service.spec.js +59 -1
  36. package/lib/common/prompt-service.spec.js.map +1 -1
  37. package/lib/common/prompt-variable-contribution.d.ts +16 -0
  38. package/lib/common/prompt-variable-contribution.d.ts.map +1 -0
  39. package/lib/common/prompt-variable-contribution.js +122 -0
  40. package/lib/common/prompt-variable-contribution.js.map +1 -0
  41. package/lib/common/variable-service.d.ts +26 -4
  42. package/lib/common/variable-service.d.ts.map +1 -1
  43. package/lib/common/variable-service.js +77 -16
  44. package/lib/common/variable-service.js.map +1 -1
  45. package/lib/common/variable-service.spec.d.ts +2 -0
  46. package/lib/common/variable-service.spec.d.ts.map +1 -0
  47. package/lib/common/variable-service.spec.js +237 -0
  48. package/lib/common/variable-service.spec.js.map +1 -0
  49. package/lib/node/tool-request-parameters.spec.d.ts +2 -0
  50. package/lib/node/tool-request-parameters.spec.d.ts.map +1 -0
  51. package/lib/node/tool-request-parameters.spec.js +116 -0
  52. package/lib/node/tool-request-parameters.spec.js.map +1 -0
  53. package/package.json +10 -10
  54. package/src/browser/agent-preferences.ts +74 -0
  55. package/src/browser/ai-activation-service.ts +13 -2
  56. package/src/browser/ai-core-command-contribution.ts +37 -0
  57. package/src/browser/ai-core-frontend-module.ts +10 -2
  58. package/src/browser/ai-settings-service.ts +1 -1
  59. package/src/browser/index.ts +1 -0
  60. package/src/browser/prompttemplate-contribution.ts +4 -4
  61. package/src/browser/theia-variable-contribution.ts +49 -8
  62. package/src/common/language-model.ts +46 -11
  63. package/src/common/prompt-service.spec.ts +68 -1
  64. package/src/common/prompt-service.ts +109 -24
  65. package/src/common/prompt-variable-contribution.ts +138 -0
  66. package/src/common/variable-service.spec.ts +289 -0
  67. package/src/common/variable-service.ts +110 -13
  68. package/src/node/tool-request-parameters.spec.ts +121 -0
@@ -30,19 +30,19 @@ const PROMPT_TEMPLATE_TEXTMATE_SCOPE = 'source.prompttemplate';
30
30
 
31
31
  export const PROMPT_TEMPLATE_EXTENSION = '.prompttemplate';
32
32
 
33
- export const DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS: Command = {
33
+ export const DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS: Command = Command.toLocalizedCommand({
34
34
  id: 'theia-ai-prompt-template:discard',
35
35
  iconClass: codicon('discard'),
36
36
  category: 'Theia AI Prompt Templates'
37
- };
37
+ }, '', 'theia/ai/core/prompts/category');
38
38
 
39
39
  // TODO this command is mainly for testing purposes
40
- export const SHOW_ALL_PROMPTS_COMMAND: Command = {
40
+ export const SHOW_ALL_PROMPTS_COMMAND: Command = Command.toLocalizedCommand({
41
41
  id: 'theia-ai-prompt-template:show-prompts-command',
42
42
  label: 'Show all prompts',
43
43
  iconClass: codicon('beaker'),
44
44
  category: 'Theia AI Prompt Templates',
45
- };
45
+ }, 'theia/ai/core/showAllPrompts/label', 'theia/ai/core/prompts/category');
46
46
 
47
47
  @injectable()
48
48
  export class PromptTemplateContribution implements LanguageGrammarDefinitionContribution, CommandContribution, TabBarToolbarContribution {
@@ -24,6 +24,9 @@ import { AIVariableContribution, AIVariableResolver, AIVariableService, AIVariab
24
24
  */
25
25
  @injectable()
26
26
  export class TheiaVariableContribution implements AIVariableContribution, AIVariableResolver {
27
+
28
+ private static readonly THEIA_PREFIX = 'theia-';
29
+
27
30
  @inject(VariableResolverService)
28
31
  protected readonly variableResolverService: VariableResolverService;
29
32
 
@@ -33,8 +36,33 @@ export class TheiaVariableContribution implements AIVariableContribution, AIVari
33
36
  @inject(FrontendApplicationStateService)
34
37
  protected readonly stateService: FrontendApplicationStateService;
35
38
 
36
- protected variableRenameMap: Map<string, string> = new Map([
37
- ['file', 'currentFilePath'],
39
+ // Map original variable name to new name and description. If a mapped value is not present then the original will be kept.
40
+ // Only variables present in this map are registered.
41
+ protected variableRenameMap: Map<string, { name?: string, description?: string }> = new Map([
42
+ ['file', {
43
+ name: 'currentAbsoluteFilePath', description: nls.localize('theia/ai/core/variable-contribution/currentAbsoluteFilePath', 'The absolute path of the \
44
+ currently opened file. Please note that most agents will expect a relative file path (relative to the current workspace).')
45
+ }],
46
+ ['selectedText', {
47
+ description: nls.localize('theia/ai/core/variable-contribution/currentSelectedText', 'The plain text that is currently selected in the \
48
+ opened file. This excludes the information where the content is coming from. Please note that most agents will work better with a relative file path \
49
+ (relative to the current workspace).')
50
+ }],
51
+ ['currentText', {
52
+ name: 'currentFileContent', description: nls.localize('theia/ai/core/variable-contribution/currentFileContent', 'The plain content of the \
53
+ currently opened file. This excludes the information where the content is coming from. Please note that most agents will work better with a relative file path \
54
+ (relative to the current workspace).')
55
+ }],
56
+ ['relativeFile', {
57
+ name: 'currentRelativeFilePath', description: nls.localize('theia/ai/core/variable-contribution/currentRelativeFilePath', 'The relative path of the \
58
+ currently opened file.')
59
+ }],
60
+ ['relativeFileDirname', {
61
+ name: 'currentRelativeDirPath', description: nls.localize('theia/ai/core/variable-contribution/currentRelativeDirPath', 'The relative path of the directory \
62
+ containing the currently opened file.')
63
+ }],
64
+ ['lineNumber', {}],
65
+ ['workspaceFolder', {}]
38
66
  ]);
39
67
 
40
68
  registerVariables(service: AIVariableService): void {
@@ -42,21 +70,35 @@ export class TheiaVariableContribution implements AIVariableContribution, AIVari
42
70
  // some variable contributions in Theia are done as part of the onStart, same as our AI variable contributions
43
71
  // we therefore wait for all of them to be registered before we register we map them to our own
44
72
  this.variableRegistry.getVariables().forEach(variable => {
45
- const variableName = this.variableRenameMap.has(variable.name) ? this.variableRenameMap.get(variable.name)! : variable.name;
73
+ if (!this.variableRenameMap.has(variable.name)) {
74
+ return; // Do not register variables not part of the map
75
+ }
76
+ const mapping = this.variableRenameMap.get(variable.name)!;
77
+ const newName = (mapping.name && mapping.name.trim() !== '') ? mapping.name : variable.name;
78
+ const newDescription = (mapping.description && mapping.description.trim() !== '') ? mapping.description
79
+ : (variable.description && variable.description.trim() !== '' ? variable.description
80
+ : nls.localize('theia/ai/core/variable-contribution/builtInVariable', 'Theia Built-in Variable'));
81
+
46
82
  service.registerResolver({
47
- id: `theia-${variable.name}`,
48
- name: variableName,
49
- description: variable.description ?? nls.localize('theia/ai/core/variable-contribution/builtInVariable', 'Theia Built-in Variable')
83
+ id: `${TheiaVariableContribution.THEIA_PREFIX}${variable.name}`,
84
+ name: newName,
85
+ description: newDescription
50
86
  }, this);
51
87
  });
52
88
  });
53
89
  }
54
90
 
55
91
  protected toTheiaVariable(request: AIVariableResolutionRequest): string {
56
- return `$\{${request.variable.name}${request.arg ? ':' + request.arg : ''}}`;
92
+ // Remove the THEIA_PREFIX if present before constructing the variable string
93
+ const variableId = request.variable.id.startsWith(TheiaVariableContribution.THEIA_PREFIX) ? request.variable.id.slice(TheiaVariableContribution.THEIA_PREFIX.length) :
94
+ request.variable.id;
95
+ return `\${${variableId}${request.arg ? ':' + request.arg : ''}}`;
57
96
  }
58
97
 
59
98
  async canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<number> {
99
+ if (!request.variable.id.startsWith(TheiaVariableContribution.THEIA_PREFIX)) {
100
+ return 0;
101
+ }
60
102
  // some variables are not resolvable without providing a specific context
61
103
  // this may be expensive but was not a problem for Theia's built-in variables
62
104
  const resolved = await this.variableResolverService.resolve(this.toTheiaVariable(request), context);
@@ -68,4 +110,3 @@ export class TheiaVariableContribution implements AIVariableContribution, AIVari
68
110
  return resolved ? { value: resolved, variable: request.variable } : undefined;
69
111
  }
70
112
  }
71
-
@@ -32,7 +32,14 @@ export const isLanguageModelRequestMessage = (obj: unknown): obj is LanguageMode
32
32
  'query' in obj &&
33
33
  typeof (obj as { query: unknown }).query === 'string'
34
34
  );
35
- export type ToolRequestParametersProperties = Record<string, { type: string, [key: string]: unknown }>;
35
+
36
+ export interface ToolRequestParameterProperty {
37
+ type?: string;
38
+ anyOf?: ToolRequestParameterProperty[];
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ export type ToolRequestParametersProperties = Record<string, ToolRequestParameterProperty>;
36
43
  export interface ToolRequestParameters {
37
44
  type?: 'object';
38
45
  properties: ToolRequestParametersProperties;
@@ -48,17 +55,45 @@ export interface ToolRequest {
48
55
  }
49
56
 
50
57
  export namespace ToolRequest {
58
+ function isToolRequestParameterProperty(obj: unknown): obj is ToolRequestParameterProperty {
59
+ if (!obj || typeof obj !== 'object') {
60
+ return false;
61
+ }
62
+ const record = obj as Record<string, unknown>;
63
+
64
+ // Check that at least one of "type" or "anyOf" exists
65
+ if (!('type' in record) && !('anyOf' in record)) {
66
+ return false;
67
+ }
68
+
69
+ // If an "anyOf" field is present, it must be an array where each item is also a valid property.
70
+ if ('anyOf' in record) {
71
+ if (!Array.isArray(record.anyOf)) {
72
+ return false;
73
+ }
74
+ for (const item of record.anyOf) {
75
+ if (!isToolRequestParameterProperty(item)) {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ if ('type' in record && typeof record.type !== 'string') {
81
+ return false;
82
+ }
83
+
84
+ // No further checks required for additional properties.
85
+ return true;
86
+ }
51
87
  export function isToolRequestParametersProperties(obj: unknown): obj is ToolRequestParametersProperties {
52
- if (!obj || typeof obj !== 'object') { return false; };
53
-
54
- return Object.entries(obj).every(([key, value]) =>
55
- typeof key === 'string' &&
56
- value &&
57
- typeof value === 'object' &&
58
- 'type' in value &&
59
- typeof value.type === 'string' &&
60
- Object.keys(value).every(k => typeof k === 'string')
61
- );
88
+ if (!obj || typeof obj !== 'object') {
89
+ return false;
90
+ }
91
+ return Object.entries(obj).every(([key, value]) => {
92
+ if (typeof key !== 'string') {
93
+ return false;
94
+ }
95
+ return isToolRequestParameterProperty(value);
96
+ });
62
97
  }
63
98
  export function isToolRequestParameters(obj: unknown): obj is ToolRequestParameters {
64
99
  return !!obj && typeof obj === 'object' &&
@@ -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
+ }