diff --git a/README.md b/README.md index 3b45a8f..f2b0d4c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Start watching all packages, including `browser-app`, of your application with *or* watch only specific packages with - cd theia-ai-exerciser + cd exerciser yarn watch and the browser example. @@ -69,7 +69,7 @@ Start watching all packages, including `electron-app`, of your application with *or* watch only specific packages with - cd theia-ai-exerciser + cd exerciser yarn watch and the Electron example. @@ -79,7 +79,7 @@ and the Electron example. Run the example as [described above](#Running-the-Electron-example) -## Publishing theia-ai-exerciser +## Publishing exerciser Create a npm user and login to the npm registry, [more on npm publishing](https://docs.npmjs.com/getting-started/publishing-npm-packages). diff --git a/browser-app/package.json b/browser-app/package.json index ed9fdf2..7701562 100644 --- a/browser-app/package.json +++ b/browser-app/package.json @@ -58,7 +58,7 @@ "@theia/variable-resolver": "1.55.0", "@theia/vsx-registry": "1.55.0", "@theia/workspace": "1.55.0", - "theia-ai-exerciser": "0.0.0" + "exerciser": "0.0.0" }, "devDependencies": { "@theia/cli": "1.55.0" diff --git a/electron-app/package.json b/electron-app/package.json index 77c21f3..85a8f7f 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -16,7 +16,7 @@ "@theia/process": "1.55.0", "@theia/terminal": "1.55.0", "@theia/workspace": "1.55.0", - "theia-ai-exerciser": "0.0.0" + "exerciser": "0.0.0" }, "devDependencies": { "@theia/cli": "1.55.0", diff --git a/theia-ai-exerciser/README.md b/exerciser/README.md similarity index 100% rename from theia-ai-exerciser/README.md rename to exerciser/README.md diff --git a/theia-ai-exerciser/package.json b/exerciser/package.json similarity index 65% rename from theia-ai-exerciser/package.json rename to exerciser/package.json index 614ba77..6ec9c0d 100644 --- a/theia-ai-exerciser/package.json +++ b/exerciser/package.json @@ -1,5 +1,5 @@ { - "name": "theia-ai-exerciser", + "name": "exerciser", "keywords": [ "theia-extension" ], @@ -10,7 +10,10 @@ "src" ], "dependencies": { - "@theia/core": "1.55.0" + "@theia/core": "1.55.0", + "@theia/filesystem": "1.55.0", + "@theia/ai-chat": "1.55.0", + "@theia/ai-terminal": "1.55.0" }, "devDependencies": { "rimraf": "^5.0.0", @@ -24,7 +27,8 @@ }, "theiaExtensions": [ { - "frontend": "lib/browser/theia-ai-exerciser-frontend-module" + "frontend": "lib/browser/exerciser-frontend-module" + } ] } \ No newline at end of file diff --git a/exerciser/src/browser/exercise-conductor/exercise-conductor.ts b/exerciser/src/browser/exercise-conductor/exercise-conductor.ts new file mode 100644 index 0000000..077485f --- /dev/null +++ b/exerciser/src/browser/exercise-conductor/exercise-conductor.ts @@ -0,0 +1,58 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// 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 { AgentSpecificVariables, PromptTemplate, ToolInvocationRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { exerciseConductorTemplate } from "./template"; +import { CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID } from './../utils/tool-functions/function-names'; + +@injectable() +export class ExerciseConductorAgent extends AbstractStreamParsingChatAgent implements ChatAgent { + name: string; + description: string; + promptTemplates: PromptTemplate[]; + variables: never[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + readonly functions: string[]; + + @inject(ToolInvocationRegistry) + protected toolInvocationRegistry: ToolInvocationRegistry; + + constructor() { + super('ExerciseConductor', [{ + purpose: 'chat', + identifier: 'openai/gpt-4o', + }], 'chat'); + + this.name = 'ExerciseConductor'; + this.description = 'This agent assists with coding exercises by providing code snippets, explanations, and guidance. \ + It can execute code snippets, evaluate results, and answer questions related to coding challenges.'; + + // Define the prompt template and variables specific to coding exercises + this.promptTemplates = [exerciseConductorTemplate]; + this.variables = []; + this.agentSpecificVariables = []; + + // Register functions relevant for coding exercises, including file access and code execution + this.functions = [CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID]; + } + + protected override async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(exerciseConductorTemplate.id); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } +} diff --git a/exerciser/src/browser/exercise-conductor/template.ts b/exerciser/src/browser/exercise-conductor/template.ts new file mode 100644 index 0000000..9227d38 --- /dev/null +++ b/exerciser/src/browser/exercise-conductor/template.ts @@ -0,0 +1,54 @@ +import { PromptTemplate } from '@theia/ai-core/lib/common'; +import { CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID } from '../utils/tool-functions/function-names'; + +export const exerciseConductorTemplate = { + id: 'coding-exercise-conductor', + template: ` + # Coding Exercise Conductor + + You are an AI assistant in the Theia IDE tasked with guiding users through coding exercises interactively. Your primary goal is to generate and manage a conduction file (e.g., an '_conductor' file) that enables users to work through coding exercises step-by-step. + + ## Key Functions + - Use ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to list all files and directories in the workspace. + - Use ~{${GET_FILE_CONTENT_FUNCTION_ID}} to read specific files for exercise content, instructions, or answers. + - Use ~{${CREATE_FILE_FUNCTION_ID}} to create conductor files, but only after user confirmation. + + ## Guidelines + + 1. **Initiate Conductor File Creation Based on User Hint**: + - When the user provides a hint to create a conductor file, use ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to search for the actual exercise file. The hint may not match the exact name of the exercise, so identify the correct file and directory by analyzing the listed files. + - Verify the filename and directory to ensure the correct file. + - Generate a conductor file by copying the exercise's instructions while blanking out the answers. This file will guide the user to complete the exercise themselves. + - Name the conductor file as ‘[exercise_name]_conductor’ and prompt the user to confirm its creation before proceeding. + - Create the conductor file in the same directory as the exercise file. If no directory is specified, the default location is the workspace root. + + 2. **Provide User-Directed Guidance and Feedback**: + - In the conductor file, present the exercise instructions, omitting answers, so users can work through each part themselves. + - As users complete part or all of the exercise and request feedback, first call ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to confirm the correct path and filename of the initial exercise and the conductor exercise. Then, use ~{${GET_FILE_CONTENT_FUNCTION_ID}} to retrieve the content of these files for comparison. This ensures accurate feedback by checking the user’s current input in the conductor file against the original answers in the initial exercise file. + - Focus feedback only on parts where the user has made an attempt and their solution differs from the correct answer. For correct solutions, acknowledge briefly that they are correct. Overlook parts/steps where the user has left the answer blank, and keep the feedback concise, emphasizing key issues to help the user improve. + + + 3. **Encourage Incremental Progress and Learning**: + - For each user question, check the relevant parts of the initial exercise file for comparison. + - Help the user focus on core concepts within the exercise, prompting further exploration when appropriate. + - Provide supportive feedback that encourages confidence and understanding. + + 4. **Iterative Feedback Loop**: + - Continue the feedback process until the user is satisfied with their progress. + - Keep responses concise and relevant, focusing on guiding users toward accurate solutions and understanding. + + 5. **Professional and Encouraging Tone**: + - Maintain a professional and supportive tone in all interactions. + - Use precise technical language when appropriate, while making instructions accessible. + - Offer positive reinforcement and insights to support the user’s learning experience. + + + ## Example Workflow: + + - **Step 1**: Receive the user’s hint for creating a conductor file for an existing exercise (e.g., “I want to condunct 'Java_FileIO' exercise”). + - **Step 2**: Use ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to locate the actual file based on the user’s hint, confirming the correct exercise file and directory. + - **Step 3**: Generate a 'Java_FileIO_conductor' file that includes the instructions but blanks out answers, asking the user to fill in solutions. + - **Step 4**: Upon user request for feedback, call ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to get confirm the correct path and filename of 'Java_FileIO' exercise file and the 'Java_FileIO_conductor' file. Then use ~{${GET_FILE_CONTENT_FUNCTION_ID}} to compare user input in the conductor file with the initial exercise's solutions. Focus feedback on changed or incorrect parts, confirming correct solutions briefly and highlighting key problems. + - **Step 5**: Provide iterative guidance based on their input, helping users correct mistakes or confirming correct steps. + ` +}; diff --git a/exerciser/src/browser/exercise-creator/exercise-creator.ts b/exerciser/src/browser/exercise-creator/exercise-creator.ts new file mode 100644 index 0000000..52eddb9 --- /dev/null +++ b/exerciser/src/browser/exercise-creator/exercise-creator.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// 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 { AgentSpecificVariables, PromptTemplate, ToolInvocationRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { exerciseCreatorTemplate } from "./template"; +import { CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID } from '../utils/tool-functions/function-names'; + +@injectable() +export class ExerciseCreatorAgent extends AbstractStreamParsingChatAgent implements ChatAgent { + name: string; + description: string; + promptTemplates: PromptTemplate[]; + variables: never[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + readonly functions: string[]; + + @inject(ToolInvocationRegistry) + protected toolInvocationRegistry: ToolInvocationRegistry; + + constructor() { + super('ExerciseCreator', [{ + purpose: 'chat', + identifier: 'openai/gpt-4o', + }], 'chat'); + + // Set the agent name and description for coding exercises + this.name = 'ExerciseCreator'; + this.description = 'This agent assists with creating custom coding exercises on user requests. It previews file structures, confirms exercise design, and generates instructional files upon approval. \ + The assistant provides clear guidance, explanations, and encourages hands-on coding and experimentation.'; + + // Define the prompt template and variables specific to coding exercises + this.promptTemplates = [exerciseCreatorTemplate]; + this.variables = []; + this.agentSpecificVariables = []; + + // Register functions relevant for coding exercises, including file access and code execution + this.functions = [CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID]; + } + + protected override async getSystemMessageDescription(): Promise { + const resolvedPrompt = await this.promptService.getPrompt(exerciseCreatorTemplate.id); + return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined; + } +} diff --git a/exerciser/src/browser/exercise-creator/template.ts b/exerciser/src/browser/exercise-creator/template.ts new file mode 100644 index 0000000..69138b3 --- /dev/null +++ b/exerciser/src/browser/exercise-creator/template.ts @@ -0,0 +1,64 @@ +import { PromptTemplate } from '@theia/ai-core/lib/common'; +import { CREATE_FILE_FUNCTION_ID, GET_FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILES_FUNCTION_ID } from '../utils/tool-functions/function-names'; + +export const exerciseCreatorTemplate = { + id: 'coding-exercise-system', + template: ` + # Coding Exercise Assistant + + You are an AI assistant integrated into the Theia IDE, designed to provide interactive coding exercises based on the user’s needs. Your primary role is to generate exercises, preview files to be created, confirm with the user, and finally create the files once the user approves. + + You have access to several functions for navigating, reading, and creating files in the workspace: + - Use ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} to list all files and directories in the workspace. This function returns both file names and folder names; infer which entries are folders as needed. + - Use ~{${GET_FILE_CONTENT_FUNCTION_ID}} to read specific files to understand current code context. **Always use this to avoid assumptions about the workspace.** + - Use ~{${CREATE_FILE_FUNCTION_ID}} to create files for exercises, but only after user confirmation. The **fileDir** parameter can be left as an empty string if exercises are stored by default in the workspace. If specified, the directory path should exactly match what’s returned from the workspace list. + + ## Guidelines + + 1. **Exercise Generation and Contextual Awareness:** + - Generate exercises based on the language, framework, or tools specified by the user (e.g., Java, Python). + - Determine appropriate filenames, directories, and content for each file in the exercise. If **fileDir** is empty, inform users that the exercises are stored in the workspace by default. Specify directories only if user input or workspace structure suggests it. + - Confirm file names, structures, and existing content using provided functions to avoid assumptions about the workspace. + + + 2. **Present Proposed Design for User Approval**: + - Display a clear, concise summary of the whole exercise. + - Display the proposed filename, file directory, and a brief description of the content or purpose of each file to the user for review in bullet-point format. + - Allow the user to request adjustments to the filenames, directory paths, or file contents. This process can repeat iteratively until the user approves the design. + + + 3. **Create Files for Exercises (Post-Confirmation):** + -Once the design is approved, use ~{${CREATE_FILE_FUNCTION_ID}} to create all required files in the specified directories within the workspace. + -The primary file should contain the main code or skeleton structure for the exercise, and call the CREATE_FILE_FUNCTION multiple times to generate any additional files with logical names and paths, as needed. + -Except for the main code, each file should include instructions to guide the user and make the exercise standalone and self-contained. + -Ensure that all files specified are created and that each has a clear, unique purpose within the exercise. + + 4. **Provide Step-by-Step Guidance and Explanations:** + - Include clear instructions and objectives for each exercise, helping users understand the goals. + - For complex exercises, break down tasks into manageable steps, outlining each clearly. + - Explain coding patterns, techniques, or language features used in the exercise to enhance learning. + + 5. **Facilitate Learning and Encourage Exploration:** + - Provide tips or explanations to help users understand the reasoning behind each part of the exercise. + - Encourage users to experiment with code and explore alternative solutions where appropriate. + + 6. **Use a Professional and Supportive Tone:** + - Maintain a clear and encouraging tone in all instructions and explanations. + - Use technical language when necessary, but keep instructions accessible and clear. + - Be always consistent with the structure of your responses. + + 7. **Stay Relevant to Coding and Development:** + - Ensure exercises focus strictly on programming topics, tools, and frameworks used in development. + - For non-programming-related questions, respond politely with, "I'm here to assist with coding and development exercises. For other topics, please consult a specialized source." + + ## Example Flow: + + - **Step 1**: Receive user request for a coding exercise (e.g., "Create a Java exercise for file I/O operations"). + - **Step 2**: Use ~{${GET_WORKSPACE_FILES_FUNCTION_ID}} and ~{${GET_FILE_CONTENT_FUNCTION_ID}} functions to understand the current workspace structure and existing files. + - **Step 3**: Determine the number of files, file names, directories, and necessary content for the exercise based on the workspace list. + - **Step 4**: Generate a preview of the files to be created, including file names, directories, and a short description of each file's content or purpose. + - **Step 5**: Present this preview to the user and ask if they want to proceed with creating these files. + - **Step 6**: If the user confirms, use ~{${CREATE_FILE_FUNCTION_ID}} to create all required files. + - **Step 7**: Minimize instructions in the chat, guiding users to the exercise file for full details and code examples. + ` +}; \ No newline at end of file diff --git a/exerciser/src/browser/exerciser-frontend-module.ts b/exerciser/src/browser/exerciser-frontend-module.ts new file mode 100644 index 0000000..4feb57e --- /dev/null +++ b/exerciser/src/browser/exerciser-frontend-module.ts @@ -0,0 +1,31 @@ +import { ContainerModule } from 'inversify'; +import { ExerciseCreatorAgent } from './exercise-creator/exercise-creator'; +import { ChatAgent } from '@theia/ai-chat/lib/common'; +import { ExerciseConductorAgent } from './exercise-conductor/exercise-conductor'; +import { TerminalChatAgent } from './terminal-chat-agent/terminal-chat-agent'; + +import { Agent, ToolProvider } from '@theia/ai-core/lib/common'; +import { CreateFile } from './utils/tool-functions/create-file'; +import { GetFileContent } from './utils/tool-functions/get-file-content'; +import { GetWorkspaceFiles } from './utils/tool-functions/get-workspace-files'; + +export default new ContainerModule(bind => { + bind(ExerciseCreatorAgent).toSelf().inSingletonScope; + bind(Agent).toService(ExerciseCreatorAgent); + bind(ChatAgent).toService(ExerciseCreatorAgent); + + bind(ExerciseConductorAgent).toSelf().inSingletonScope; + bind(Agent).toService(ExerciseConductorAgent); + bind(ChatAgent).toService(ExerciseConductorAgent); + + bind(TerminalChatAgent).toSelf().inSingletonScope; + bind(Agent).toService(TerminalChatAgent); + bind(ChatAgent).toService(TerminalChatAgent); + + bind(ToolProvider).to(CreateFile); + bind(ToolProvider).to(GetFileContent); + bind(ToolProvider).to(GetWorkspaceFiles); + + +}); + diff --git a/exerciser/src/browser/terminal-chat-agent/template.ts b/exerciser/src/browser/terminal-chat-agent/template.ts new file mode 100644 index 0000000..9aa66f2 --- /dev/null +++ b/exerciser/src/browser/terminal-chat-agent/template.ts @@ -0,0 +1,158 @@ +import { PromptTemplate } from '@theia/ai-core/lib/common'; + +export const terminalChatAgentTemplate: PromptTemplate = { + id: 'terminal-chat-agent', + template: `# Terminal Chat Agent + +You are a terminal-based chat agent designed to assist users with terminal commands and interactions. Your responses should help users understand and execute commands directly in a terminal environment. + +# Format and Responses + +- Provide commands within JSON-formatted responses to ensure clarity and structure. +- When the user requests command suggestions, respond with either individual JSON entries or a list of JSON objects, each representing a possible command. +- Avoid excessive detail; keep responses concise and informative to support quick terminal interaction. +- Provide step-by-step guidance when needed but limit to essential information only. +- Do not execute commands or produce output; only instruct the user. + +# Examples + +The following examples demonstrate how to format responses for different types of interactions: + +## Example 1: Basic Command + +This response provides a basic command for listing files in the current directory, using a single JSON object. + +\`\`\`json +{ + "command": "ls -al", + "description": "Lists all files in the directory with details." +} +\`\`\` + +## Example 2: Command Suggestions with Explanations + +When the user requests a command but does not specify exact details, you may provide a list of JSON-formatted command options. + +For example, if the user asks, “How do I view files?” + +\`\`\`json +[ + { + "command": "ls", + "description": "Lists files and directories in the current directory." + }, + { + "command": "ls -a", + "description": "Lists all files, including hidden files, in the current directory." + }, + { + "command": "ls -l", + "description": "Lists files in long format, showing details like permissions." + } +] +\`\`\` + +Or you may also respond with individual JSON objects if the list is not necessary: + +\`\`\`json +{ + "command": "ls", + "description": "Lists files and directories in the current directory." +} +\`\`\` + +## Example 3: Error Handling + +If a user asks for a command that cannot be executed or an error is likely, respond with an explanation. + +\`\`\`json +{ + "message": "You do not have permission to access this directory. Please try as a superuser or check your permissions." +} +\`\`\` + +## Example 4: Custom Commands and Guidance + +If the command involves a specific or custom environment setup, explain the setup first, then provide the command in JSON format. + +For example, to activate a Python virtual environment and run a script: + +1. Activate the virtual environment: + + \`\`\`json + { + "command": "source ./venv/bin/activate", + "description": "Activates the Python virtual environment." + } + \`\`\` + +2. Then run the Python script: + + \`\`\`json + { + "command": "python my_script.py", + "description": "Runs the Python script after activating the virtual environment." + } + \`\`\` + +# Guidelines + +1. **Verify Commands Based on Context**: + - Identify the command context based on user input. If the user asks about file navigation, focus on directory and file commands like \`ls\`, \`cd\`, etc. + - If the request involves package management or environment setup, suggest commands such as \`pip\`, \`npm\`, or others relevant to their context. + +2. **Provide Command Variants if Needed**: + - For commands with multiple options, include the most common variant, either as a single JSON object or a list. For example: + \`\`\`json + { + "command": "rm -rf my_folder", + "description": "Removes the directory and its contents forcefully." + } + \`\`\` + - Explain potential risks if the command is potentially destructive, such as removing files or directories. + +3. **Encourage User Confirmation for Critical Commands**: + - If a command involves deleting files, modifying permissions, or altering the system state, recommend that the user double-checks before executing. + +4. **Error Scenarios**: + - If there are common errors or issues associated with a command, suggest troubleshooting steps or commands to check system status, such as: + \`\`\`json + { + "command": "ps aux | grep my_process", + "description": "Searches for 'my_process' among running processes." + } + \`\`\` + +# Example Workflow for User Interaction + +- **Step 1**: User requests help with creating a directory. + - Respond with a single JSON object: + \`\`\`json + { + "command": "mkdir my_directory", + "description": "Creates a new directory named 'my_directory'." + } + \`\`\` + +- **Step 2**: User requests to view file permissions in the directory. + - Respond with: + \`\`\`json + { + "command": "ls -l", + "description": "Lists files in the directory with their permissions." + } + \`\`\` + +- **Step 3**: User requests help with deleting a directory. + - Advise caution, then provide the command: + \`\`\`json + { + "command": "rm -rf my_directory", + "description": "Forcefully removes 'my_directory' and its contents. Use with caution as this is irreversible." + } + \`\`\` + +Use the above guidelines to respond in a supportive, concise, and informative manner tailored to terminal interaction. Be precise with command syntax and assume the user is following along in a terminal environment. + +` +}; diff --git a/exerciser/src/browser/terminal-chat-agent/terminal-chat-agent.ts b/exerciser/src/browser/terminal-chat-agent/terminal-chat-agent.ts new file mode 100644 index 0000000..9b6102c --- /dev/null +++ b/exerciser/src/browser/terminal-chat-agent/terminal-chat-agent.ts @@ -0,0 +1,133 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { AbstractTextToModelParsingChatAgent, ChatAgent, SystemMessageDescription } from '@theia/ai-chat/lib/common'; +import { + PromptTemplate, + AgentSpecificVariables +} from '@theia/ai-core'; + +import { terminalChatAgentTemplate } from "./template"; + +import { + ChatRequestModelImpl, + ChatResponseContent, + CommandChatResponseContentImpl, + CustomCallback, + HorizontalLayoutChatResponseContentImpl, + MarkdownChatResponseContentImpl, +} from '@theia/ai-chat'; +import { + CommandRegistry, + MessageService, + // generateUuid +} from '@theia/core'; + + +interface ParsedCommand { + type: 'terminal-command' | 'no-command' + commandId: string; + arguments?: string[]; + message?: string; +} + +@injectable() +export class TerminalChatAgent extends AbstractTextToModelParsingChatAgent implements ChatAgent { + @inject(CommandRegistry) + protected commandRegistry: CommandRegistry; + @inject(MessageService) + protected messageService: MessageService; + readonly name: string; + readonly description: string; + readonly variables: string[]; + readonly promptTemplates: PromptTemplate[]; + readonly functions: string[]; + readonly agentSpecificVariables: AgentSpecificVariables[]; + + constructor( + ) { + super('TerminalChatAgent', [{ + purpose: 'command', + identifier: 'openai/gpt-4o', + }], 'command'); + this.name = 'TerminalChatAgent'; + this.description = 'This agent is aware of all commands that the user can execute within the Theia IDE, the tool that the user is currently working with. \ + Based on the user request, it can find the right command and then let the user execute it.'; + this.variables = []; + this.promptTemplates = [terminalChatAgentTemplate]; + this.functions = []; + this.agentSpecificVariables = [{ + name: 'command-ids', + description: 'The list of available commands in Theia.', + usedInPrompt: true + }]; + } + + protected async getSystemMessageDescription(): Promise { + + const systemPrompt = await this.promptService.getPrompt(terminalChatAgentTemplate.id); + if (systemPrompt === undefined) { + throw new Error('Couldn\'t get system prompt '); + } + return SystemMessageDescription.fromResolvedPromptTemplate(systemPrompt); + } + + /** + * @param text the text received from the language model + * @returns the parsed command if the text contained a valid command. + * If there was no json in the text, return a no-command response. + */ + protected async parseTextResponse(text: string): Promise { + const jsonMatch = text.match(/(\{[\s\S]*\})/); + const jsonString = jsonMatch ? jsonMatch[1] : `[]`; + const parsedCommand = JSON.parse(jsonString); + return parsedCommand; + } + + + protected createResponseContent(parsedCommand: any, request: ChatRequestModelImpl): ChatResponseContent { + + if (parsedCommand) { + // const args = parsedCommand.arguments !== undefined && + // parsedCommand.arguments.length > 0 + // ? parsedCommand.arguments + // : undefined; + // const id = `ai-command-${generateUuid()}`; + //const commandArgs = Array.isArray(parsedCommand.arguments) ? parsedCommand.arguments : []; + // const commandArgs = parsedCommand.arguments !== undefined && parsedCommand.arguments.length > 0 ? parsedCommand.arguments : []; + //const args = [id, ...commandArgs]; + + const customCallback: CustomCallback = { + label: 'Copy the command to terminal', + callback: () => this.commandCallback(), + }; + return new HorizontalLayoutChatResponseContentImpl([ + new MarkdownChatResponseContentImpl( + 'I found this command that might help you: test terminal command' + ), + new CommandChatResponseContentImpl(undefined, customCallback) + ]); + } else { + return new MarkdownChatResponseContentImpl('Sorry, I can\'t find a suitable command for you'); + } + } + + protected async commandCallback(): Promise { + console.log("***** test *****") + } +} + diff --git a/exerciser/src/browser/utils/tool-functions/compare-file.ts b/exerciser/src/browser/utils/tool-functions/compare-file.ts new file mode 100644 index 0000000..62fd768 --- /dev/null +++ b/exerciser/src/browser/utils/tool-functions/compare-file.ts @@ -0,0 +1,66 @@ +// import { inject, injectable } from "@theia/core/shared/inversify"; +// import { URI } from '@theia/core'; +// import { ToolProvider, ToolRequest } from '@theia/ai-core'; +// import { FileService } from '@theia/filesystem/lib/browser/file-service'; +// import { COMPARE_FILES_CONTENT_FUNCTION_ID } from "./function-names"; + +// @injectable() +// export class CompareFilesContent implements ToolProvider { +// static ID = COMPARE_FILES_CONTENT_FUNCTION_ID; + +// getTool(): ToolRequest { +// return { +// id: CompareFilesContent.ID, +// name: CompareFilesContent.ID, +// description: 'Compare the content of two files', +// parameters: { +// type: 'object', +// properties: { +// filePath1: { +// type: 'string', +// description: 'Path of the first file' +// }, +// filePath2: { +// type: 'string', +// description: 'Path of the second file' +// } +// }, + +// }, +// handler: (arg_string: string) => { +// const { filePath1, filePath2 } = this.parseArgs(arg_string); +// return this.compareFilesContent(filePath1, filePath2); +// } +// }; +// } + +// @inject(FileService) +// protected readonly fileService: FileService; + +// private parseArgs(arg_string: string): { filePath1: string; filePath2: string } { +// return JSON.parse(arg_string); +// } + +// private async compareFilesContent(filePath1: string, filePath2: string): Promise { +// const uri1 = new URI(filePath1); +// const uri2 = new URI(filePath2); + +// try { +// const fileStat1 = await this.fileService.read(uri1); +// const content1 = new TextDecoder().decode(fileStat1.value); + + +// const fileStat2 = await this.fileService.read(uri2); +// const content2 = new TextDecoder().decode(fileStat2.value); + + +// if (content1 === content2) { +// return `Files ${filePath1} and ${filePath2} have identical content.`; +// } else { +// return `Files ${filePath1} and ${filePath2} have different content.`; +// } +// } catch (error) { +// return `Failed to compare files: ${error.message}`; +// } +// } +// } diff --git a/exerciser/src/browser/utils/tool-functions/create-file.ts b/exerciser/src/browser/utils/tool-functions/create-file.ts new file mode 100644 index 0000000..be7a87c --- /dev/null +++ b/exerciser/src/browser/utils/tool-functions/create-file.ts @@ -0,0 +1,72 @@ +import { inject, injectable } from "@theia/core/shared/inversify"; +import { URI } from '@theia/core'; +import { ToolProvider, ToolRequest } from '@theia/ai-core'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { CREATE_FILE_FUNCTION_ID } from "./function-names"; + + +@injectable() +export class CreateFile implements ToolProvider { + static ID = CREATE_FILE_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: CreateFile.ID, + name: CreateFile.ID, + description: 'Create a file with specified content', + parameters: { + type: 'object', + properties: { + fileName: { + type: 'string', + description: 'The name of the file to create or overwrite', + }, + fileDir: { + type: 'string', + description: 'The directory where the file should be created', + }, + content: { + type: 'string', + description: 'The content to write to the file', + } + }, + }, + handler: (arg_string: string) => { + const { fileName, fileDir, content } = this.parseArgs(arg_string); + return this.createFile(fileName, fileDir, content); + } + }; + } + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + private parseArgs(arg_string: string): { fileName: string; fileDir: string; content: string } { + return JSON.parse(arg_string); + } + + private async createFile(fileName: string, fileDir: string, content: string): Promise { + + const wsRoots = await this.workspaceService.roots; + if(!fileDir){ + if(wsRoots.length !== 0) { + const rootUri = wsRoots[0].resource; + const fileUri = rootUri.resolve(fileName); + await this.fileService.write(fileUri, content); + return `File created at ${fileUri.toString()}`; + }else{ + return `No workspace found to create file`; + } + }else{ + + const fileDirUri = new URI(fileDir); + const fileUri = fileDirUri.resolve(fileName); + await this.fileService.write(fileUri, content); + return `File created or updated at ${fileUri.toString()}`; + } + } +} diff --git a/exerciser/src/browser/utils/tool-functions/function-names.ts b/exerciser/src/browser/utils/tool-functions/function-names.ts new file mode 100644 index 0000000..6d94334 --- /dev/null +++ b/exerciser/src/browser/utils/tool-functions/function-names.ts @@ -0,0 +1,4 @@ +export const CREATE_FILE_FUNCTION_ID = 'createFile'; +export const GET_FILE_CONTENT_FUNCTION_ID = 'getFileContent'; +export const GET_WORKSPACE_FILES_FUNCTION_ID = 'getWorkspaceFileList'; +export const COMPARE_FILES_CONTENT_FUNCTION_ID = 'compareFilesContent' \ No newline at end of file diff --git a/exerciser/src/browser/utils/tool-functions/get-file-content.ts b/exerciser/src/browser/utils/tool-functions/get-file-content.ts new file mode 100644 index 0000000..52fcf7a --- /dev/null +++ b/exerciser/src/browser/utils/tool-functions/get-file-content.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from "@theia/core/shared/inversify"; +import { URI } from '@theia/core'; +import { ToolProvider, ToolRequest } from '@theia/ai-core'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { GET_FILE_CONTENT_FUNCTION_ID } from "./function-names"; + +@injectable() +export class GetFileContent implements ToolProvider { + static ID = GET_FILE_CONTENT_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: GetFileContent.ID, + name: GetFileContent.ID, + description: 'Get the content of the file', + parameters: { + type: 'object', + properties: { + file: { + type: 'string', + description: 'The path of the file to retrieve content for', + } + } + }, + handler: (arg_string: string) => { + const file = this.parseArg(arg_string); + return this.getFileContent(file); + } + }; + } + + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + private parseArg(arg_string: string): string { + const result = JSON.parse(arg_string); + return result.file; + } + + private async getFileContent(file: string): Promise { + const uri = new URI(file); + const fileContent = await this.fileService.read(uri); + return fileContent.value; + } +} diff --git a/exerciser/src/browser/utils/tool-functions/get-workspace-files.ts b/exerciser/src/browser/utils/tool-functions/get-workspace-files.ts new file mode 100644 index 0000000..61ec90c --- /dev/null +++ b/exerciser/src/browser/utils/tool-functions/get-workspace-files.ts @@ -0,0 +1,70 @@ +import { inject, injectable } from "@theia/core/shared/inversify"; +import { URI } from '@theia/core'; +import { ToolProvider, ToolRequest } from '@theia/ai-core'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { GET_WORKSPACE_FILES_FUNCTION_ID } from "./function-names"; + +@injectable() +export class GetWorkspaceFiles implements ToolProvider { + static ID = GET_WORKSPACE_FILES_FUNCTION_ID; + + getTool(): ToolRequest { + return { + id: GetWorkspaceFiles.ID, + name: GetWorkspaceFiles.ID, + description: 'List all files in the workspace', + + handler: () => this.getProjectFileList() + }; + } + + @inject(WorkspaceService) + protected workspaceService: WorkspaceService; + + @inject(FileService) + protected readonly fileService: FileService; + + async getProjectFileList(): Promise { + // Get all files from the workspace service as a flat list of qualified file names + const wsRoots = await this.workspaceService.roots; + const result: string[] = []; + for (const root of wsRoots) { + result.push(...await this.listFilesRecursively(root.resource)); + } + return result; + } + + private async listFilesRecursively(uri: URI): Promise { + const stat = await this.fileService.resolve(uri); + const result: string[] = []; + if (stat && stat.isDirectory) { + if (this.exclude(stat)) { + return result; + } + const children = await this.fileService.resolve(uri); + if (children.children) { + for (const child of children.children) { + result.push(child.resource.toString()); + result.push(...await this.listFilesRecursively(child.resource)); + } + } + } + return result; + } + + // Exclude folders which are not relevant to the AI Agent + private exclude(stat: FileStat): boolean { + if (stat.resource.path.base.startsWith('.')) { + return true; + } + if (stat.resource.path.base === 'node_modules') { + return true; + } + if (stat.resource.path.base === 'lib') { + return true; + } + return false; + } +} diff --git a/theia-ai-exerciser/tsconfig.json b/exerciser/tsconfig.json similarity index 100% rename from theia-ai-exerciser/tsconfig.json rename to exerciser/tsconfig.json diff --git a/package.json b/package.json index c09b59c..bea851a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,6 @@ "lerna": "2.4.0" }, "workspaces": [ - "theia-ai-exerciser", "browser-app", "electron-app" + "exerciser", "browser-app", "electron-app" ] } diff --git a/theia-ai-exerciser/src/browser/theia-ai-exerciser-contribution.ts b/theia-ai-exerciser/src/browser/theia-ai-exerciser-contribution.ts deleted file mode 100644 index 719dd3a..0000000 --- a/theia-ai-exerciser/src/browser/theia-ai-exerciser-contribution.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { injectable } from '@theia/core/shared/inversify'; - -@injectable() -// Add contribution interface to be implemented, e.g. "TheiaAiExerciserContribution implements CommandContribution" -export class TheiaAiExerciserContribution{ - -} \ No newline at end of file diff --git a/theia-ai-exerciser/src/browser/theia-ai-exerciser-frontend-module.ts b/theia-ai-exerciser/src/browser/theia-ai-exerciser-frontend-module.ts deleted file mode 100644 index 9653d2a..0000000 --- a/theia-ai-exerciser/src/browser/theia-ai-exerciser-frontend-module.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated using theia-extension-generator - */ -import { ContainerModule } from '@theia/core/shared/inversify'; -import { TheiaAiExerciserContribution } from './theia-ai-exerciser-contribution'; - - -export default new ContainerModule(bind => { - - // Replace this line with the desired binding, e.g. "bind(CommandContribution).to(TheiaAiExerciserContribution) - bind(TheiaAiExerciserContribution).toSelf(); -});