diff --git a/packages/ai-chat/src/common/command-chat-agents.ts b/packages/ai-chat/src/common/command-chat-agents.ts index d291ef17eca1c..7ab6608f049de 100644 --- a/packages/ai-chat/src/common/command-chat-agents.ts +++ b/packages/ai-chat/src/common/command-chat-agents.ts @@ -18,6 +18,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { AbstractTextToModelParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents'; import { PromptTemplate, + AgentSpecificVariables } from '@theia/ai-core'; import { ChatRequestModelImpl, @@ -252,11 +253,13 @@ export class CommandChatAgent extends AbstractTextToModelParsingChatAgent { diff --git a/packages/ai-chat/src/common/orchestrator-chat-agent.ts b/packages/ai-chat/src/common/orchestrator-chat-agent.ts index 139bdf9535cd4..9beec41a5cc30 100644 --- a/packages/ai-chat/src/common/orchestrator-chat-agent.ts +++ b/packages/ai-chat/src/common/orchestrator-chat-agent.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { getJsonOfResponse, LanguageModelResponse } from '@theia/ai-core'; +import { AgentSpecificVariables, getJsonOfResponse, LanguageModelResponse } from '@theia/ai-core'; import { PromptTemplate } from '@theia/ai-core/lib/common'; @@ -64,9 +64,12 @@ export const OrchestratorChatAgentId = 'Orchestrator'; export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent { name: string; description: string; - variables: string[]; + readonly variables: string[]; promptTemplates: PromptTemplate[]; fallBackChatAgentId: string; + readonly functions: string[] = []; + readonly agentSpecificVariables: AgentSpecificVariables[] = []; + constructor() { super(OrchestratorChatAgentId, [{ purpose: 'agent-selection', @@ -78,6 +81,8 @@ export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implem this.variables = ['chatAgents']; this.promptTemplates = [orchestratorTemplate]; this.fallBackChatAgentId = 'Universal'; + this.functions = []; + this.agentSpecificVariables = []; } @inject(ChatAgentService) diff --git a/packages/ai-chat/src/common/universal-chat-agent.ts b/packages/ai-chat/src/common/universal-chat-agent.ts index 32706aedf802b..ab8838d7578e6 100644 --- a/packages/ai-chat/src/common/universal-chat-agent.ts +++ b/packages/ai-chat/src/common/universal-chat-agent.ts @@ -14,6 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { AgentSpecificVariables } from '@theia/ai-core'; import { PromptTemplate } from '@theia/ai-core/lib/common'; @@ -81,6 +82,8 @@ export class UniversalChatAgent extends AbstractStreamParsingChatAgent implement description: string; variables: string[]; promptTemplates: PromptTemplate[]; + readonly functions: string[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; constructor() { super('Universal', [{ @@ -94,6 +97,8 @@ export class UniversalChatAgent extends AbstractStreamParsingChatAgent implement + 'access the current user context or the workspace.'; this.variables = []; this.promptTemplates = [universalTemplate]; + this.functions = []; + this.agentSpecificVariables = []; } protected override async getSystemMessageDescription(): Promise { diff --git a/packages/ai-code-completion/src/common/code-completion-agent.ts b/packages/ai-code-completion/src/common/code-completion-agent.ts index 1487f37b914f5..15724190118f0 100644 --- a/packages/ai-code-completion/src/common/code-completion-agent.ts +++ b/packages/ai-code-completion/src/common/code-completion-agent.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { - Agent, CommunicationHistoryEntry, CommunicationRecordingService, getTextOfResponse, + Agent, AgentSpecificVariables, CommunicationHistoryEntry, CommunicationRecordingService, getTextOfResponse, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequirement, PromptService, PromptTemplate } from '@theia/ai-core/lib/common'; import { generateUuid, ILogger } from '@theia/core'; @@ -116,8 +116,6 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent { enableForwardStability: true, }; } - tags?: String[] | undefined; - variables: string[] = []; @inject(ILogger) @named('code-completion-agent') @@ -154,4 +152,13 @@ Only return the exact replacement for [[MARKER]] to complete the snippet.`, identifier: 'openai/gpt-4o', }, ]; + readonly variables: string[] = []; + readonly functions: string[] = []; + readonly agentSpecificVariables: AgentSpecificVariables[] = [ + { name: 'file', usedInPrompt: true, description: 'The uri of the file being edited.' }, + { name: 'language', usedInPrompt: true, description: 'The languageId of the file being edited.' }, + { name: 'textUntilCurrentPosition', usedInPrompt: true, description: 'The code before the current position of the cursor.' }, + { name: 'textAfterCurrentPosition', usedInPrompt: true, description: 'The code after the current position of the cursor.' } + ]; + readonly tags?: String[] | undefined; } diff --git a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx index 9dc5640a37adf..8d5b06c5ccc41 100644 --- a/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx +++ b/packages/ai-core/src/browser/ai-configuration/agent-configuration-widget.tsx @@ -17,7 +17,16 @@ import { codicon, ReactWidget } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; -import { Agent, LanguageModel, LanguageModelRegistry, PromptCustomizationService } from '../../common'; +import { + Agent, + AIVariableService, + LanguageModel, + LanguageModelRegistry, + PROMPT_FUNCTION_REGEX, + PROMPT_VARIABLE_REGEX, + PromptCustomizationService, + PromptService, +} from '../../common'; import { AISettingsService } from '../ai-settings-service'; import { LanguageModelRenderer } from './language-model-renderer'; import { TemplateRenderer } from './template-settings-renderer'; @@ -25,6 +34,12 @@ import { AIConfigurationSelectionService } from './ai-configuration-service'; import { AIVariableConfigurationWidget } from './variable-configuration-widget'; import { AgentService } from '../../common/agent-service'; +interface ParsedPrompt { + functions: string[]; + globalVariables: string[]; + agentSpecificVariables: string[]; +}; + @injectable() export class AIAgentConfigurationWidget extends ReactWidget { @@ -46,6 +61,12 @@ export class AIAgentConfigurationWidget extends ReactWidget { @inject(AIConfigurationSelectionService) protected readonly aiConfigurationSelectionService: AIConfigurationSelectionService; + @inject(AIVariableService) + protected readonly variableService: AIVariableService; + + @inject(PromptService) + protected promptService: PromptService; + protected languageModels: LanguageModel[] | undefined; @postConstruct() @@ -62,6 +83,7 @@ export class AIAgentConfigurationWidget extends ReactWidget { this.languageModels = models; this.update(); })); + this.toDispose.push(this.promptCustomizationService.onDidChangePrompt(() => this.update())); this.aiSettingsService.onDidChange(() => this.update()); this.aiConfigurationSelectionService.onDidAgentChange(() => this.update()); @@ -98,6 +120,10 @@ export class AIAgentConfigurationWidget extends ReactWidget { const enabled = this.agentService.isEnabled(agent.id); + const parsedPromptParts = this.parsePromptTemplatesForVariableAndFunction(agent); + const globalVariables = Array.from(new Set([...parsedPromptParts.globalVariables, ...agent.variables])); + const functions = Array.from(new Set([...parsedPromptParts.functions, ...agent.functions])); + return
{this.renderAgentName(agent)}
{agent.description}
@@ -107,16 +133,6 @@ export class AIAgentConfigurationWidget extends ReactWidget { Enable Agent
-
- Variables: -
    - {agent.variables.map(variableId =>
  • -
    { this.showVariableConfigurationTab(); }} className='variable-reference'> - {variableId} - -
  • )} -
-
{agent.promptTemplates?.map(template =>
+
+ Used Global Variables: +
    + +
+
+
+ Used agent-specific Variables: +
    + +
+
+
+ Used Functions: +
    + +
+
; } + private parsePromptTemplatesForVariableAndFunction(agent: Agent): ParsedPrompt { + const promptTemplates = agent.promptTemplates; + const result: ParsedPrompt = { functions: [], globalVariables: [], agentSpecificVariables: [] }; + promptTemplates.forEach(template => { + const storedPrompt = this.promptService.getRawPrompt(template.id); + const prompt = storedPrompt?.template ?? template.template; + const variableMatches = [...prompt.matchAll(PROMPT_VARIABLE_REGEX)]; + + variableMatches.forEach(match => { + const variableId = match[1]; + // if the variable is part of the variable service and not part of the agent specific variables then it is a global variable + if (this.variableService.hasVariable(variableId) && + agent.agentSpecificVariables.find(v => v.name === variableId) === undefined) { + result.globalVariables.push(variableId); + } else { + result.agentSpecificVariables.push(variableId); + } + }); + + const functionMatches = [...prompt.matchAll(PROMPT_FUNCTION_REGEX)]; + functionMatches.forEach(match => { + const functionId = match[1]; + result.functions.push(functionId); + }); + + }); + return result; + } + protected showVariableConfigurationTab(): void { this.aiConfigurationSelectionService.selectConfigurationTab(AIVariableConfigurationWidget.ID); } @@ -159,3 +225,75 @@ export class AIAgentConfigurationWidget extends ReactWidget { }; } +interface AgentGlobalVariablesProps { + variables: string[]; + showVariableConfigurationTab: () => void; +} +const AgentGlobalVariables = ({ variables: globalVariables, showVariableConfigurationTab }: AgentGlobalVariablesProps) => { + if (globalVariables.length === 0) { + return <>None; + } + return <> + {globalVariables.map(variableId =>
  • +
    { showVariableConfigurationTab(); }} className='variable-reference'> + {variableId} + +
  • )} + + ; +}; + +interface AgentFunctionsProps { + functions: string[]; +} +const AgentFunctions = ({ functions }: AgentFunctionsProps) => { + if (functions.length === 0) { + return <>None; + } + return <> + {functions.map(functionId =>
  • + {functionId} +
  • )} + ; +}; + +interface AgentSpecificVariablesProps { + promptVariables: string[]; + agent: Agent; +} +const AgentSpecificVariables = ({ promptVariables, agent }: AgentSpecificVariablesProps) => { + const agentDefinedVariablesName = agent.agentSpecificVariables.map(v => v.name); + const variables = Array.from(new Set([...promptVariables, ...agentDefinedVariablesName])); + if (variables.length === 0) { + return <>None; + } + return <> + {variables.map(variableId => + + + )} + ; +}; +interface AgentSpecifcVariableProps { + variableId: string; + agent: Agent; + promptVariables: string[]; +} +const AgentSpecifcVariable = ({ variableId, agent, promptVariables }: AgentSpecifcVariableProps) => { + const agentDefinedVariable = agent.agentSpecificVariables.find(v => v.name === variableId); + const undeclared = agentDefinedVariable === undefined; + const notUsed = !promptVariables.includes(variableId) && agentDefinedVariable?.usedInPrompt === true; + return
  • +
    Name: {variableId}
    + {undeclared ?
    Undeclared
    : + (<> +
    Description: {agentDefinedVariable.description}
    + {notUsed &&
    Not used in prompt
    } + )} +
    +
  • ; +}; diff --git a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts index 2f3d5525f2bca..aff47785cb315 100644 --- a/packages/ai-core/src/browser/frontend-prompt-customization-service.ts +++ b/packages/ai-core/src/browser/frontend-prompt-customization-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { DisposableCollection, URI } from '@theia/core'; +import { DisposableCollection, URI, Event, Emitter } from '@theia/core'; import { OpenerService } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { PromptCustomizationService, PromptTemplate } from '../common'; @@ -48,6 +48,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati protected toDispose = new DisposableCollection(); + private readonly onDidChangePromptEmitter = new Emitter(); + readonly onDidChangePrompt: Event = this.onDidChangePromptEmitter.event; + @postConstruct() protected init(): void { this.preferences.onPreferenceChanged(event => { @@ -69,31 +72,32 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati this.toDispose.push(this.fileService.watch(templateURI, { recursive: true, excludes: [] })); this.toDispose.push(this.fileService.onDidFilesChange(async (event: FileChangesEvent) => { - - for (const child of this.trackedTemplateURIs) { - // check deletion and updates - if (event.contains(new URI(child))) { - for (const deletedFile of event.getDeleted()) { - if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) { - this.trackedTemplateURIs.delete(deletedFile.resource.toString()); - _templates.delete(deletedFile.resource.path.name); - } - } - for (const updatedFile of event.getUpdated()) { - if (this.trackedTemplateURIs.has(updatedFile.resource.toString())) { - const filecontent = await this.fileService.read(updatedFile.resource); - _templates.set(this.removePromptTemplateSuffix(updatedFile.resource.path.name), filecontent.value); - } - } + // check deleted templates + for (const deletedFile of event.getDeleted()) { + if (this.trackedTemplateURIs.has(deletedFile.resource.toString())) { + this.trackedTemplateURIs.delete(deletedFile.resource.toString()); + const templateId = this.removePromptTemplateSuffix(deletedFile.resource.path.name); + _templates.delete(templateId); + this.onDidChangePromptEmitter.fire(templateId); + } + } + // check updated templates + for (const updatedFile of event.getUpdated()) { + if (this.trackedTemplateURIs.has(updatedFile.resource.toString())) { + const filecontent = await this.fileService.read(updatedFile.resource); + const templateId = this.removePromptTemplateSuffix(updatedFile.resource.path.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); } } - // check new templates for (const addedFile of event.getAdded()) { if (addedFile.resource.parent.toString() === templateURI.toString() && addedFile.resource.path.ext === '.prompttemplate') { this.trackedTemplateURIs.add(addedFile.resource.toString()); const filecontent = await this.fileService.read(addedFile.resource); - _templates.set(this.removePromptTemplateSuffix(addedFile.resource.path.name), filecontent.value); + const templateId = this.removePromptTemplateSuffix(addedFile.resource.path.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); } } @@ -112,7 +116,9 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati if (fileURI.path.ext === '.prompttemplate') { this.trackedTemplateURIs.add(fileURI.toString()); const filecontent = await this.fileService.read(fileURI); - _templates.set(this.removePromptTemplateSuffix(file.name), filecontent.value); + const templateId = this.removePromptTemplateSuffix(file.name); + _templates.set(templateId, filecontent.value); + this.onDidChangePromptEmitter.fire(templateId); } } } diff --git a/packages/ai-core/src/browser/style/index.css b/packages/ai-core/src/browser/style/index.css index b17cac722d42c..b325058b20541 100644 --- a/packages/ai-core/src/browser/style/index.css +++ b/packages/ai-core/src/browser/style/index.css @@ -66,14 +66,16 @@ } #ai-variable-configuration-container-widget .variable-references, -#ai-agent-configuration-container-widget .variable-references { +#ai-agent-configuration-container-widget .variable-references, +#ai-agent-configuration-container-widget .function-references { margin-left: 0.5rem; padding: 0.5rem; border-left: solid 1px var(--theia-tree-indentGuidesStroke); } #ai-variable-configuration-container-widget .variable-reference, -#ai-agent-configuration-container-widget .variable-reference { +#ai-agent-configuration-container-widget .variable-reference, +#ai-agent-configuration-container-widget .function-reference { display: flex; flex-direction: row; align-items: center; diff --git a/packages/ai-core/src/common/agent.ts b/packages/ai-core/src/common/agent.ts index 652eea799142e..bd37c46f72cd8 100644 --- a/packages/ai-core/src/common/agent.ts +++ b/packages/ai-core/src/common/agent.ts @@ -17,6 +17,12 @@ import { LanguageModelRequirement } from './language-model'; import { PromptTemplate } from './prompt-service'; +export interface AgentSpecificVariables { + name: string; + description: string; + usedInPrompt: boolean; +} + export const Agent = Symbol('Agent'); /** * Agents represent the main functionality of the AI system. They are responsible for processing user input, collecting information from the environment, @@ -44,7 +50,7 @@ export interface Agent { /** A markdown description of its functionality and its privacy-relevant requirements, including function call handlers that access some data autonomously. */ readonly description: string; - /** The list of variable identifiers this agent needs to clarify its context requirements. See #39. */ + /** The list of global variable identifiers this agent needs to clarify its context requirements. See #39. */ readonly variables: string[]; /** The prompt templates introduced and used by this agent. */ @@ -55,4 +61,10 @@ export interface Agent { /** A list of tags to filter agents and to display capabilities in the UI */ readonly tags?: String[]; + + /** The list of local variable identifiers this agent needs to clarify its context requirements. */ + readonly agentSpecificVariables: AgentSpecificVariables[]; + + /** The list of global function identifiers this agent needs to clarify its context requirements. */ + readonly functions: string[]; } diff --git a/packages/ai-core/src/common/prompt-service.ts b/packages/ai-core/src/common/prompt-service.ts index 119373f0402a7..8bc55bedebd8b 100644 --- a/packages/ai-core/src/common/prompt-service.ts +++ b/packages/ai-core/src/common/prompt-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { URI } from '@theia/core'; +import { URI, Event } from '@theia/core'; import { inject, injectable, optional } from '@theia/core/shared/inversify'; import { AIVariableService } from './variable-service'; import { ToolInvocationRegistry } from './tool-invocation-registry'; @@ -104,6 +104,11 @@ export interface PromptCustomizationService { * @param uri the uri of the template file */ getTemplateIDFromURI(uri: URI): string | undefined; + + /** + * Event which is fired when the prompt template is changed. + */ + readonly onDidChangePrompt: Event; } @injectable() diff --git a/packages/ai-terminal/src/browser/ai-terminal-agent.ts b/packages/ai-terminal/src/browser/ai-terminal-agent.ts index fdc911c50a3ed..f68f73eeb3424 100644 --- a/packages/ai-terminal/src/browser/ai-terminal-agent.ts +++ b/packages/ai-terminal/src/browser/ai-terminal-agent.ts @@ -40,6 +40,13 @@ export class AiTerminalAgent implements Agent { Based on the user\'s request, it suggests commands and allows the user to directly paste and execute them in the terminal. \ It accesses the current directory, environment and the recent terminal output of the terminal session to provide context-aware assistance'; variables = []; + functions = []; + agentSpecificVariables = [ + { name: 'userRequest', usedInPrompt: true, description: 'The user\'s question or request.' }, + { name: 'shell', usedInPrompt: true, description: 'The shell being used, e.g., /usr/bin/zsh.' }, + { name: 'cwd', usedInPrompt: true, description: 'The current working directory.' }, + { name: 'recentTerminalContents', usedInPrompt: true, description: 'The last 0 to 50 recent lines visible in the terminal.' } + ]; promptTemplates = [ { id: 'terminal-system', diff --git a/packages/ai-workspace-agent/src/browser/workspace-agent.ts b/packages/ai-workspace-agent/src/browser/workspace-agent.ts index 2fd47fbd260e1..3d05487a16d9a 100644 --- a/packages/ai-workspace-agent/src/browser/workspace-agent.ts +++ b/packages/ai-workspace-agent/src/browser/workspace-agent.ts @@ -14,9 +14,10 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from '@theia/ai-chat/lib/common'; -import { PromptTemplate, ToolInvocationRegistry } from '@theia/ai-core'; +import { AgentSpecificVariables, PromptTemplate, ToolInvocationRegistry } from '@theia/ai-core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { workspaceTemplate } from '../common/template'; +import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID } from '../common/functions'; @injectable() export class WorkspaceAgent extends AbstractStreamParsingChatAgent implements ChatAgent { @@ -24,6 +25,8 @@ export class WorkspaceAgent extends AbstractStreamParsingChatAgent implements Ch description: string; promptTemplates: PromptTemplate[]; variables: never[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + readonly functions: string[]; @inject(ToolInvocationRegistry) protected toolInvocationRegistry: ToolInvocationRegistry; @@ -39,6 +42,8 @@ export class WorkspaceAgent extends AbstractStreamParsingChatAgent implements Ch where to put source code, where to find specific code or configurations, etc.'; this.promptTemplates = [workspaceTemplate]; this.variables = []; + this.agentSpecificVariables = []; + this.functions = [GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID]; } protected override async getSystemMessageDescription(): Promise {