Skip to content

Commit

Permalink
Add prompt specific request settings
Browse files Browse the repository at this point in the history
fixed #14505

Signed-off-by: Jonas Helming <[email protected]>
  • Loading branch information
JonasHelming committed Nov 23, 2024
1 parent c47e894 commit d791d1b
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 22 deletions.
3 changes: 2 additions & 1 deletion packages/ai-code-completion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"@theia/output": "1.55.0",
"@theia/workspace": "1.55.0",
"minimatch": "^5.1.0",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"js-yaml": "^4.1.0"
},
"main": "lib/common",
"publishConfig": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FrontendApplicationContribution, KeybindingContribution, PreferenceCont
import { Agent } from '@theia/ai-core';
import { AICodeCompletionPreferencesSchema } from './ai-code-completion-preference';
import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider';
import { CodeCompletionPromptParserService, DefaultCodeCompletionPromptParserService } from '../common/prompt-metadata-parsing-service';

export default new ContainerModule(bind => {
bind(ILogger).toDynamicValue(ctx => {
Expand All @@ -36,4 +37,5 @@ export default new ContainerModule(bind => {
bind(FrontendApplicationContribution).to(AIFrontendApplicationContribution);
bind(KeybindingContribution).toService(AIFrontendApplicationContribution);
bind(PreferenceContribution).toConstantValue({ schema: AICodeCompletionPreferencesSchema });
bind(CodeCompletionPromptParserService).to(DefaultCodeCompletionPromptParserService).inSingletonScope();
});
31 changes: 25 additions & 6 deletions packages/ai-code-completion/src/common/code-completion-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { generateUuid, ILogger } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { CodeCompletionPromptMetaData, CodeCompletionPromptParserService } from './prompt-metadata-parsing-service';

export const CodeCompletionAgent = Symbol('CodeCompletionAgent');
export interface CodeCompletionAgent extends Agent {
Expand All @@ -30,6 +31,11 @@ export interface CodeCompletionAgent extends Agent {

@injectable()
export class CodeCompletionAgentImpl implements CodeCompletionAgent {
@inject(CodeCompletionPromptParserService)
protected readonly codeCodeCompletionsPromptParserService: CodeCompletionPromptParserService;

private readonly CODE_COMPLETION_MAIN_PROMPT_ID = 'code-completion-prompt';

async provideInlineCompletions(
model: monaco.editor.ITextModel,
position: monaco.Position,
Expand All @@ -48,6 +54,16 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
return undefined;
}

const unresolvedPrompt = await this.promptService.getUnresolvedPrompt(this.CODE_COMPLETION_MAIN_PROMPT_ID);
if (!unresolvedPrompt) {
this.logger.error('No prompt found for code-completion-agent');
return undefined;
}
const { metadata, prompt: modifiedPrompt } = this.getPromptMetaData(unresolvedPrompt);
unresolvedPrompt.template = modifiedPrompt;

console.log('metadata', metadata);

// Get text until the given position
const prefix = model.getValueInRange({
startLineNumber: 1,
Expand All @@ -71,18 +87,15 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
return undefined;
}
const prompt = await this.promptService
.getPrompt('code-completion-prompt', { prefix, suffix, file, language })
.resolvePrompt(unresolvedPrompt, { prefix, suffix, file, language })
.then(p => p?.text);
if (!prompt) {
this.logger.error('No prompt found for code-completion-agent');
return undefined;
}

// since we do not actually hold complete conversions, the request/response pair is considered a session
const sessionId = generateUuid();
const requestId = generateUuid();
const request: LanguageModelRequest = {
messages: [{ type: 'text', actor: 'user', query: prompt }],
settings: metadata?.requestSettings || {}
};
if (token.isCancellationRequested) {
return undefined;
Expand Down Expand Up @@ -113,6 +126,12 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
enableForwardStability: true,
};
}
protected getPromptMetaData(unresolvedPrompt: PromptTemplate | undefined): { metadata: CodeCompletionPromptMetaData | undefined; prompt: string } {
if (!unresolvedPrompt || !unresolvedPrompt.template) {
return { metadata: undefined, prompt: unresolvedPrompt?.template || '' };
}
return this.codeCodeCompletionsPromptParserService.parse(unresolvedPrompt.template);
}

@inject(ILogger)
@named('code-completion-agent')
Expand All @@ -133,7 +152,7 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
'This agent provides inline code completion in the code editor in the Theia IDE.';
promptTemplates: PromptTemplate[] = [
{
id: 'code-completion-prompt',
id: this.CODE_COMPLETION_MAIN_PROMPT_ID,
template: `{{!-- Made improvements or adaptations to this prompt template? We’d love for you to share it with the community! Contribute back here:
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
You are a code completion agent. The current file you have to complete is named {{file}}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// *****************************************************************************
// 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 'reflect-metadata';
import { expect } from 'chai';
import { CodeCompletionPromptMetaData, DefaultCodeCompletionPromptParserService } from './prompt-metadata-parsing-service';

describe('DefaultCodeCompletionPromptParserService', () => {
let parserService: DefaultCodeCompletionPromptParserService;

beforeEach(() => {
parserService = new DefaultCodeCompletionPromptParserService();
});

it('should parse valid metadata with requestSettings and return the modified prompt', () => {
const input = `---
requestSettings:
max_new_tokens: 2024
stop: ['<|im_end|>']
---
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: {
requestSettings: {
max_new_tokens: 2024,
stop: ['<|im_end|>']
}
},
prompt: 'Some other content.'
});
});

it('should return undefined metadata and original prompt if metadata does not start on the first line', () => {
const input = `
---
requestSettings:
max_new_tokens: 2024
---
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should return undefined metadata and original prompt if metadata is not closed correctly', () => {
const input = `---
requestSettings:
max_new_tokens: 2024
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should return undefined metadata and original prompt if metadata is invalid YAML', () => {
const input = `---
requestSettings:
max_new_tokens: 2024
stop: ['<|im_end|>
---
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should return undefined metadata and original prompt if there is no metadata', () => {
const input = 'Some other content.';
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should return undefined metadata and original prompt for valid but empty metadata', () => {
const input = `---
---
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should return undefined metadata and original prompt if metadata contains non-object content', () => {
const input = `---
- some
- list
---
Some other content.`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: undefined,
prompt: input
});
});

it('should correctly handle metadata followed by an empty prompt', () => {
const input = `---
requestSettings:
max_new_tokens: 2024
---
`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: {
requestSettings: {
max_new_tokens: 2024
}
},
prompt: ''
});
});

it('should correctly handle a prompt with no content after valid metadata', () => {
const input = `---
requestSettings:
max_new_tokens: 2024
---`;
const result = parserService.parse(input);
expect(result).to.deep.equal({
metadata: {
requestSettings: {
max_new_tokens: 2024
}
},
prompt: ''
});
});
});
describe('CodeCompletionPromptMetaData', () => {
it('should return true for a valid metadata object with requestSettings', () => {
const validObject = {
requestSettings: { key1: 'value1', key2: 42 },
};
expect(CodeCompletionPromptMetaData.isStrict(validObject)).to.be.true;
});

it('should return true for a valid metadata object without requestSettings', () => {
const validObject = {};
expect(CodeCompletionPromptMetaData.isStrict(validObject)).to.be.true;
});

it('should return false for an object with additional unknown properties', () => {
const invalidObject = {
requestSettings: { key1: 'value1' },
extraKey: 'unexpected',
};
expect(CodeCompletionPromptMetaData.isStrict(invalidObject)).to.be.false;
});

it('should return false for an object where requestSettings is not an object', () => {
const invalidObject = {
requestSettings: 'invalid',
};
expect(CodeCompletionPromptMetaData.isStrict(invalidObject)).to.be.false;
});

it('should return false for an object where requestSettings is null', () => {
const invalidObject = {
// eslint-disable-next-line no-null/no-null
requestSettings: null,
};
expect(CodeCompletionPromptMetaData.isStrict(invalidObject)).to.be.false;
});

it('should return false for an object where requestSettings is an array', () => {
const invalidObject = {
requestSettings: ['invalid'],
};
expect(CodeCompletionPromptMetaData.isStrict(invalidObject)).to.be.false;
});

it('should return false for null', () => {
// eslint-disable-next-line no-null/no-null
expect(CodeCompletionPromptMetaData.isStrict(null)).to.be.false;
});

it('should return false for undefined', () => {
expect(CodeCompletionPromptMetaData.isStrict(undefined)).to.be.false;
});

it('should return false for a number', () => {
expect(CodeCompletionPromptMetaData.isStrict(42)).to.be.false;
});

it('should return false for a string', () => {
expect(CodeCompletionPromptMetaData.isStrict('string')).to.be.false;
});

it('should return false for an array', () => {
expect(CodeCompletionPromptMetaData.isStrict([])).to.be.false;
});

it('should return false for an object wit only unknown properties', () => {
const invalidObject = {
unknownKey: 'invalid',
};
expect(CodeCompletionPromptMetaData.isStrict(invalidObject)).to.be.false;
});

});
Loading

0 comments on commit d791d1b

Please sign in to comment.