Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ask before generate #26

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions exerciser/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# Empty Template Extension
# Exerciser

This exerciser is an agent integrated in the THEIA-IDE, which can be used to do programming exercises

---
## System design

```mermaid
flowchart TD
subgraph UserModule["User Module"]
UI["User Interface"]
end
subgraph CreatorModule["Creator Module"]
Creator["Exercise Creator Agent"]
ExerciseObj["Exercise Objects (Exercise, Instructions, Solution)"]
end
subgraph ConductorModule["Conductor Module"]
Conductor["Conductor Agent"]
Support["Support Module (Feedback + Hints)"]
end
subgraph SharingModule["Sharing Module"]
Sharing["Sharing Service"]
OtherUser["Other User Interface"]
end
subgraph PersistenceLayer["Persistence Layer"]
ExerciseService["Exercise Service (Centralized Storage)"]
end
subgraph InitService["Initialization Module"]
Initialization["Initialization Service"]
end
UI --> Creator & Conductor & Sharing
Creator --> ExerciseObj
ExerciseObj --> ExerciseService
ExerciseService --> Initialization
Initialization --> Conductor
Conductor --> Support
Support --> ExerciseService
Sharing --> ExerciseService & OtherUser

```


This template extension does not contain any features, but provides an empty stub including a frontend module and a generic contribution. It can be used as a starting point to implement any custom extension.
6 changes: 4 additions & 2 deletions exerciser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
],
"dependencies": {
"@theia/core": "1.55.0",
"@theia/ai-core": "1.55.0",
"@theia/filesystem": "1.55.0",
"@theia/ai-chat": "1.55.0",
"@theia/ai-chat-ui": "1.55.0",
"@theia/ai-terminal": "1.55.0"
},
"devDependencies": {
Expand All @@ -28,7 +30,7 @@
"theiaExtensions": [
{
"frontend": "lib/browser/exerciser-frontend-module"

}
]
}
}
37 changes: 37 additions & 0 deletions exerciser/src/browser/chat-response-renderer/FileItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from '@theia/core/shared/react';
import {fileToBeGenerated} from "../exercise-creator/types";

export type Props = {
file: fileToBeGenerated
}

export const FileItem: React.FC<Props> = ({file}) => {
const [isOpen, setIsOpen] = React.useState(false)

const showFileContent = () => {
setIsOpen(prevState => !prevState);
}

return (
<div style={{
display: "flex",
flexDirection: "column"
}}>
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}>
<p>{file.fileName}</p>
<button onClick={showFileContent}>{isOpen ? "collapse" : "expand"}</button>
</div>
{isOpen && (
<div>
{file.content.split("\n").map(line => {
return <p>{line}</p>
})}
</div>
)}
</div>
)
}
17 changes: 17 additions & 0 deletions exerciser/src/browser/chat-response-renderer/FileList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from '@theia/core/shared/react';
import {fileToBeGenerated} from "../exercise-creator/types";
import {FileItem} from "./FileItem";

export type Props = {
files: fileToBeGenerated[]
}

export const FileList: React.FC<Props> = ({files}) => {
return (
<div>
{files.map(file => {
return <FileItem key={file.fileName} file={file}/>
})}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// *****************************************************************************
// 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 {injectable} from "@theia/core/shared/inversify";
import {ChatResponsePartRenderer} from "@theia/ai-chat-ui/lib/browser/chat-response-part-renderer";
import {ChatResponseContent} from "@theia/ai-chat";
import * as React from '@theia/core/shared/react';
import {ExerciseCreatorResponse} from "../exercise-creator/types";
import {FileList} from "./FileList";

export interface CreateExerciseFileChatResponseContent
extends ChatResponseContent {
kind: 'createExerciseFile';
content: ExerciseCreatorResponse;
}

export class CreateExerciseFileChatResponseContentImpl implements CreateExerciseFileChatResponseContent {
readonly kind = 'createExerciseFile';
protected _content: ExerciseCreatorResponse;

constructor(content: ExerciseCreatorResponse) {
this._content = content;
}

get content(): ExerciseCreatorResponse {
return this._content;
}

asString(): string {
return JSON.stringify(this._content);
}

merge(nextChatResponseContent: CreateExerciseFileChatResponseContent): boolean {
this._content = {...this._content, ...nextChatResponseContent.content};
return true;
}
}

export namespace CreateExerciseFileChatResponseContent {
export function is(obj: unknown): obj is CreateExerciseFileChatResponseContent {
return (
ChatResponseContent.is(obj) &&
obj.kind === 'createExerciseFile' &&
'content' in obj &&
typeof (obj as { content: ExerciseCreatorResponse }).content === "object"
);
}
}

@injectable()
export class CreateExerciseFileRenderer implements ChatResponsePartRenderer<CreateExerciseFileChatResponseContent> {
canHandle(response: ChatResponseContent): number {
if (CreateExerciseFileChatResponseContent.is(response)) {
return 10;
}
return -1;
}

render(response: CreateExerciseFileChatResponseContent): React.ReactNode {
return (
<div style={{display: "flex", flexDirection: "column"}}>
<FileList files={response.content.exerciseFiles}/>
</div>
)
}
}

83 changes: 81 additions & 2 deletions exerciser/src/browser/exercise-creator/exercise-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,34 @@
// 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
ErrorChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat/lib/common';
import {
AgentSpecificVariables,
getTextOfResponse,
LanguageModelResponse,
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';
import {ILogger} from "@theia/core";
import {
ChatRequestModelImpl, CommandChatResponseContentImpl,
CustomCallback,
MarkdownChatResponseContentImpl
} from "@theia/ai-chat";
import {ExerciseCreatorResponse} from "./types";
import {WorkspaceService} from "@theia/workspace/lib/browser";
import {FileService} from "@theia/filesystem/lib/browser/file-service";
import {
CreateExerciseFileChatResponseContentImpl
} from "../chat-response-renderer/create-exercise-file-renderer";

@injectable()
export class ExerciseCreatorAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
Expand All @@ -32,6 +55,15 @@ export class ExerciseCreatorAgent extends AbstractStreamParsingChatAgent impleme
@inject(ToolInvocationRegistry)
protected toolInvocationRegistry: ToolInvocationRegistry;

@inject(ILogger)
protected readonly logger: ILogger;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(FileService)
protected readonly fileService: FileService;

constructor() {
super('ExerciseCreator', [{
purpose: 'chat',
Expand All @@ -56,4 +88,51 @@ export class ExerciseCreatorAgent extends AbstractStreamParsingChatAgent impleme
const resolvedPrompt = await this.promptService.getPrompt(exerciseCreatorTemplate.id);
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined;
}

protected override async addContentsToResponse(response: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
const responseText = await getTextOfResponse(response);
const jsonRegex = /{[\s\S]*}/;
const jsonMatch = responseText.match(jsonRegex);
if (jsonMatch) {
const jsonString = jsonMatch[0];
const beforeJson = responseText.slice(0, jsonMatch.index!);
request.response.response.addContent(new MarkdownChatResponseContentImpl(beforeJson));
try {
const exerciseCreatorResponse: ExerciseCreatorResponse = JSON.parse(jsonString);
/*
exerciseCreatorResponse.exerciseFiles.forEach(exerciseFile => {
request.response.response.addContent(new CodeChatResponseContentImpl(exerciseFile.content));
})
*/
request.response.response.addContent(new CreateExerciseFileChatResponseContentImpl(exerciseCreatorResponse));

const generateFileChatResponse = this.filesToBeGenerated(exerciseCreatorResponse);
generateFileChatResponse && request.response.response.addContent(generateFileChatResponse);
} catch (error) {
request.response.response.addContent(new ErrorChatResponseContentImpl(new Error("Error while parsing files")));
}
} else {
request.response.response.addContent(new MarkdownChatResponseContentImpl(responseText));
}
}

filesToBeGenerated(exerciseCreatorResponse: ExerciseCreatorResponse) {
const customCallback: CustomCallback = {
label: 'Create Files',
callback: async () => {
const wsRoots = await this.workspaceService.roots;
for (const fileToBeGenerated of exerciseCreatorResponse.exerciseFiles) {
const rootUri = wsRoots[0].resource;
const fileUri = rootUri.resolve(fileToBeGenerated.fileName);
await this.fileService.write(fileUri, fileToBeGenerated.content);
}
for (const fileToBeGenerated of exerciseCreatorResponse.conductorFiles) {
const rootUri = wsRoots[0].resource;
const fileUri = rootUri.resolve(fileToBeGenerated.fileName);
await this.fileService.write(fileUri, fileToBeGenerated.content);
}
}
};
return new CommandChatResponseContentImpl({id: 'custom-command'}, customCallback);
}
}
Loading
Loading