Skip to content

Commit

Permalink
Merge branch 'main' into mock-llm
Browse files Browse the repository at this point in the history
  • Loading branch information
tpaulshippy committed Jun 10, 2024
2 parents b57c544 + 25ba826 commit dfb58db
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 66 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
**/*.prompt.md
**/*.prompt.md
.next
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [0.0.30](https://github.com/BuilderIO/micro-agent/compare/v0.0.29...v0.0.30) (2024-06-10)

### [0.0.29](https://github.com/BuilderIO/micro-agent/compare/v0.0.28...v0.0.29) (2024-06-10)

### [0.0.28](https://github.com/BuilderIO/micro-agent/compare/v0.0.27...v0.0.28) (2024-06-10)

### [0.0.26](https://github.com/BuilderIO/micro-agent/compare/v0.0.25...v0.0.26) (2024-06-06)

### [0.0.24](https://github.com/BuilderIO/micro-agent/compare/v0.0.23...v0.0.24) (2024-06-06)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ app/about
OpenAI is simply just not good at visual matching. We recommend using [Anthropic](https://anthropic.com/) for visual matching. To use Anthropic, you need to add your API key to the CLI:

```bash
micro-agent config set ANTHROPIC_KEY <your token>
micro-agent config set ANTHROPIC_KEY=<your token>
```

Visual matching uses a multi-agent approach where Anthropic Claude Opus will do the visual matching and feedback, and then OpenAI will generate the code to match the design and address the feedback.
Expand Down
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@builder.io/micro-agent",
"description": "An AI CLI that writes code for you.",
"version": "0.0.26",
"version": "0.0.30",
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "^0.21.1",
Expand Down Expand Up @@ -46,9 +46,10 @@
"typecheck": "tsc",
"build": "pkgroll",
"release:patch": "npm run build && npm version patch && npm run build && npm publish && git push --follow-tags && standard-version --release-as patch",
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"standard-version:release": "standard-version",
"standard-version:release:minor": "standard-version --release-as minor",
"standard-version:release:major": "standard-version --release-as major",
"standard-version:release:patch": "standard-version --release-as patch",
"postinstall": "npx playwright install",
"prepare": "husky install"
},
Expand Down
39 changes: 8 additions & 31 deletions src/helpers/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { intro, log, spinner, text } from '@clack/prompts';

import { glob } from 'glob';
import { RunOptions, runAll } from './run';
import { getSimpleCompletion } from './llm';
import { getFileSuggestion, getSimpleCompletion } from './llm';
import { getConfig, setConfigs } from './config';
import { readFile, writeFile } from 'fs/promises';
import { readFile } from 'fs/promises';
import dedent from 'dedent';
import { removeBackticks } from './remove-backticks';
import { formatMessage } from './test';
import { gray, green } from 'kolorist';
import { exitOnCancel } from './exit-on-cancel';
import { iterateOnTest } from './iterate-on-test';
import { outputFile } from './output-file';

export async function interactiveMode(options: Partial<RunOptions>) {
console.log('');
Expand Down Expand Up @@ -44,33 +45,7 @@ export async function interactiveMode(options: Partial<RunOptions>) {
const loading = spinner();
loading.start();

const recommendedFilePath = removeBackticks(
await getSimpleCompletion({
messages: [
{
role: 'system',
content:
'You return a file path only. No other words, just one file path',
},
{
role: 'user',
content: dedent`
Please give me a recommended file path for the following prompt:
<prompt>
${prompt}
</prompt>
Here is a preview of the files in the current directory for reference. Please
use these as a reference as to what a good file name and path would be:
<files>
${fileString}
</files>
`,
},
],
})
);
const recommendedFilePath = await getFileSuggestion(prompt, fileString);
loading.stop();

filePath = exitOnCancel(
Expand Down Expand Up @@ -122,7 +97,7 @@ export async function interactiveMode(options: Partial<RunOptions>) {
${prompt}
</prompt>
The test will be located at \`${testFilePath}\` and the code to test will be located at
The test will be located at \`${testFilePath}\` and the code to test will be located at
\`${filePath}\`.
${
Expand All @@ -140,6 +115,8 @@ export async function interactiveMode(options: Partial<RunOptions>) {
`
: ''
}
Only output the test code. No other words, just the code.
`,
},
],
Expand Down Expand Up @@ -171,7 +148,7 @@ export async function interactiveMode(options: Partial<RunOptions>) {
}

// TODO: generate dir if one doesn't exist yet
await writeFile(testFilePath, testContents);
await outputFile(testFilePath, testContents);
log.success(`${green('Test file generated!')} ${gray(`${testFilePath}`)}`);
const testCommand = exitOnCancel(
await text({
Expand Down
58 changes: 57 additions & 1 deletion src/helpers/openai.test.ts → src/helpers/llm.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getOpenAi, getSimpleCompletion } from './llm';
import { getCompletion, getOpenAi, getSimpleCompletion } from './llm';
import { KnownError } from './error';
import { expect, describe, it, vi } from 'vitest';
import OpenAI from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources';
import { RunOptions } from './run';
import { gray } from 'kolorist';

const mocks = vi.hoisted(() => {
return {
Expand Down Expand Up @@ -119,3 +121,57 @@ describe('getSimpleCompletion', () => {
expect(onChunk).toHaveBeenCalledWith(' World');
});
});

describe('getCompletion', () => {
it('should call openai.chat.completions.create with the correct parameters', async () => {
mocks.getConfig
.mockResolvedValueOnce(defaultConfig)
.mockResolvedValueOnce(defaultConfig);
const openaiInstance = new OpenAI();
mocks.create.mockResolvedValueOnce([]);

const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: 'Hello' },
];
const options = {
messages,
options: {} as RunOptions,
useAssistant: false,
};
await getCompletion(options);

expect(openaiInstance.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-4o',
messages,
stream: true,
});
});

it('should write to stdout and stderr', async () => {
mocks.getConfig
.mockResolvedValueOnce(defaultConfig)
.mockResolvedValueOnce(defaultConfig);
const openaiInstance = new OpenAI();
mocks.create.mockResolvedValueOnce([
{ choices: [{ delta: { content: 'Hello' } }] },
{ choices: [{ delta: { content: 'World' } }] },
]);
const stdOutWriteMock = vi.spyOn(process.stdout, 'write');
const stdErrWriteMock = vi.spyOn(process.stderr, 'write');

const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: 'Hello' },
];
const options = {
messages,
options: {} as RunOptions,
useAssistant: false,
};
await getCompletion(options);

expect(stdOutWriteMock).toHaveBeenNthCalledWith(1, gray('\n│ '));
expect(stdOutWriteMock).toHaveBeenNthCalledWith(2, '\n');
expect(stdErrWriteMock).toHaveBeenCalledWith(gray('Hello'));
expect(stdErrWriteMock).toHaveBeenCalledWith(gray('World'));
});
});
85 changes: 85 additions & 0 deletions src/helpers/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { green } from 'kolorist';
import { formatMessage } from './test';
import { removeBackticks } from './remove-backticks';
import ollama from 'ollama';
import dedent from 'dedent';
import { removeInitialSlash } from './remove-initial-slash';
import { readFile, writeFile } from 'fs/promises';

const defaultModel = 'gpt-4o';
Expand All @@ -36,6 +38,89 @@ export const getOpenAi = async function () {
return openai;
};

export const getFileSuggestion = async function (
prompt: string,
fileString: string
) {
const message = {
role: 'user' as const,
content: dedent`
Please give me a recommended file path for the following prompt:
<prompt>
${prompt}
</prompt>
Here is a preview of the files in the current directory for reference. Please
use these as a reference as to what a good file name and path would be:
<files>
${fileString}
</files>
`,
};
const { MODEL: model } = await getConfig();
if (useOllama(model)) {
return removeInitialSlash(
removeBackticks(
await getSimpleCompletion({
messages: [
{
role: 'system' as const,
content:
'You are an assistant that given a snapshot of the current filesystem suggests a relative file path for the code algorithm mentioned in the prompt. No other words, just one file path',
},
message,
],
})
)
);
}
const openai = await getOpenAi();
const completion = await openai.chat.completions.create({
model: model || defaultModel,
tool_choice: {
type: 'function',
function: { name: 'file_suggestion' },
},
tools: [
{
type: 'function',
function: {
name: 'file_suggestion',
description:
'Given a prompt and a list of files, suggest a file path',
parameters: {
type: 'object',
properties: {
filePath: {
type: 'string',
description:
'Relative file path to the file that the code algorithm should be written in, in case of doubt the extension should be .js',
},
},
required: ['filePath'],
},
},
},
],
messages: [
{
role: 'system' as const,
content:
'You are an assistant that given a snapshot of the current filesystem suggests a relative file path for the code algorithm mentioned in the prompt.',
},
message,
],
response_format: { type: 'json_object' },
});
const jsonStr =
completion.choices[0]?.message.tool_calls?.[0]?.function.arguments;
if (!jsonStr) {
return 'src/algorithm.js';
}
return removeInitialSlash(JSON.parse(jsonStr).filePath);
};

export const getSimpleCompletion = async function (options: {
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
onChunk?: (chunk: string) => void;
Expand Down
12 changes: 12 additions & 0 deletions src/helpers/output-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { mkdir, writeFile } from 'fs/promises';
import { dirname } from 'path';

export async function outputFile(
filePath: string,
content: string
): Promise<void> {
const dir = dirname(filePath);

await mkdir(dir, { recursive: true });
await writeFile(filePath, content);
}
24 changes: 24 additions & 0 deletions src/helpers/remove-initial-slash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from 'vitest';
import { removeInitialSlash } from './remove-initial-slash';

test('removes initial slash from an absolute path', () => {
expect(removeInitialSlash('/absolute/path')).toBe('absolute/path');
expect(removeInitialSlash('/')).toBe('');
expect(removeInitialSlash('/another/path/with/multiple/segments')).toBe(
'another/path/with/multiple/segments'
);
expect(removeInitialSlash('/singlefolder')).toBe('singlefolder');
});

test('returns the same string if the path is already relative', () => {
expect(removeInitialSlash('relative/path')).toBe('relative/path');
expect(removeInitialSlash('another/relative/path')).toBe(
'another/relative/path'
);
expect(removeInitialSlash('justonefolder')).toBe('justonefolder');
expect(removeInitialSlash('relative')).toBe('relative');
});

test('handles empty string input', () => {
expect(removeInitialSlash('')).toBe('');
});
8 changes: 8 additions & 0 deletions src/helpers/remove-initial-slash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function removeInitialSlash(path: string): string {
if (path.startsWith('/')) {
return path.slice(1);
}
return path;
}

export { removeInitialSlash };
Loading

0 comments on commit dfb58db

Please sign in to comment.