Skip to content

Commit

Permalink
Implement ChatGPT API calling
Browse files Browse the repository at this point in the history
  • Loading branch information
baseballyama committed Nov 3, 2023
1 parent 13e475e commit 1890ed4
Show file tree
Hide file tree
Showing 8 changed files with 507 additions and 5 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 20
cache: "pnpm"

- name: prebuild
run: pnpm install --frozen-lockfile

- name: Build
run: cd packages/ai-craftsman && pnpm build

- name: AI Review
run: node packages/ai-craftsman/dist/ci.js
env:
BASE_REF: ${{ github.base_ref }}
6 changes: 5 additions & 1 deletion packages/ai-craftsman/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"author": "baseballyama",
"license": "MIT",
"devDependencies": {
"@types/node": "20.8.9"
"@types/node": "20.8.10",
"tiktoken-node": "0.0.6"
},
"dependencies": {
"openai": "4.14.2"
}
}
33 changes: 33 additions & 0 deletions packages/ai-craftsman/src/ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getDiff } from "./git.js";
import { chunk } from "./diff.js";
import { getTokenCount, chat } from "./openai.js";

const main = async () => {
const baseref = process.env["BASE_REF"];
if (baseref == null || baseref === "") {
throw new Error("BASE_REF is not set");
}

for (const { diff, file } of getDiff(baseref)) {
const chunked = chunk({
source: diff,
counter: getTokenCount,
maxCount: 500,
duplicateLines: 2,
});
for (const { source, startRow, endRow } of chunked) {
await chat([
{
content: "次のコードをレビューしてください",
role: "system",
},
{
content: source,
role: "user",
},
]);
}
}
};

void main();
112 changes: 112 additions & 0 deletions packages/ai-craftsman/src/diff.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, test, expect } from "vitest";
import { parse, chunk } from "./diff";

describe("source", () => {
const source = `\
const PORT = 8080;
const HOME_PATH = '/home';
const ABOUT_PATH = '/about';
const SUBMIT_PATH = '/submit';
function isPortAvailable(port) {
return port == PORT;
}
function listen(port, callback) {
callback({ type: 'GET', path: HOME_PATH });
}
function routeToController(method, path, callback) {
callback();
}
function renderTemplate(template, callback) {
callback();
}
function fetchDataFromDB(key, callback) {
callback({ name: 'John', age: 30 });
}
function validateData(data) {
return data != null;
}
function saveToDB(data) {
print("Data saved.");
}
function log(message) {
print("Log: " + message);
}
function respondWithError(message) {
print("Error: " + message);
}
function isNotEmpty(data) {
return data != null;
}
function insertIntoTemplate(data) {
print("Inserting data into template.");
}
function print(message) {
// Simulate printing a message to the console
return "Printed: " + message;
}
function onRequest(req) {
routeToController(req.type, req.path, () => {
if (req.path == HOME_PATH) {
renderTemplate('home.html', () => {
populateData(() => {
fetchDataFromDB('home_data', (data) => {
if (isNotEmpty(data)) {
insertIntoTemplate(data);
} else {
log('No data to display');
}
});
});
});
} else if (req.path == ABOUT_PATH) {
renderTemplate('about.html', () => {});
} else {
renderTemplate('404.html', () => {});
}
});
}
function main() {
if (isPortAvailable(PORT)) {
listen(PORT, onRequest);
} else {
log(\`Port \${PORT} is not available\`);
}
}
main();`;

test("parse", async () => {
expect(parse(source).length).toEqual(85);
});

test("chunk", async () => {
const result = chunk({
source,
counter: (str: string) => str.length,
maxCount: 200,
duplicateLines: 2,
});

expect(result.length).toEqual(11);
expect(result[0].startRow).toEqual(0);
expect(result[0].endRow).toEqual(9);
expect(result[1].startRow).toEqual(8);
expect(result[1].endRow).toEqual(18);
expect(result[10].startRow).toEqual(78);
expect(result[10].endRow).toEqual(84);
});
});
63 changes: 63 additions & 0 deletions packages/ai-craftsman/src/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
interface ChunkedSource {
source: string;
startRow: number;
endRow: number;
}

export interface SourceRow {
source: string;
level: number;
}

export const parse = (source: string): SourceRow[] => {
const rows: SourceRow[] = [];
const lines = source.split("\n");
for (const line of lines) {
const level = line.match(/^[\s\t]*/)?.[0]?.length ?? 0;
rows.push({ source: line, level });
}
return rows;
};

export const chunk = ({
source,
counter,
maxCount,
duplicateLines = 0,
}: {
source: string;
counter: (str: string) => number;
maxCount: number;
duplicateLines?: number;
}): ChunkedSource[] => {
const getChunkedSource = (currentIndex: number): ChunkedSource => {
const currentLine = parsed[currentIndex]?.source;
const prevLine = parsed[currentIndex - 1]?.source ?? "";
const prevPrevLine = parsed[currentIndex - 2]?.source ?? "";
let source = prevPrevLine;
if (source.length > 0) source += "\n";
source += prevLine;
if (source.length > 0) source += "\n";
source += currentLine;
const row = Math.max(0, currentIndex - duplicateLines);
return { source, startRow: row, endRow: row };
};

const chunks: ChunkedSource[] = [];
let cur: ChunkedSource = { source: "", startRow: 0, endRow: 0 };
const parsed = parse(source);
parsed.forEach((line, index) => {
if (counter(`${cur.source}${line.source.length}`) > maxCount) {
chunks.push(cur);
cur = getChunkedSource(index);
} else {
cur.source += line.source + "\n";
cur.endRow = index;
}
});
if (cur.source.length > 0) {
chunks.push(cur);
}

return chunks;
};
33 changes: 33 additions & 0 deletions packages/ai-craftsman/src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* reference: https://gist.github.com/drwpow/86b11688babd6d1251b90e22ef7354ba
*/

import { readFileSync } from "node:fs";
import { execSync } from "node:child_process";

export const getDiff = (targetBranch: string) => {
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD")
.toString()
.trim();

const branch =
currentBranch === targetBranch
? `HEAD~1..HEAD`
: `origin/${targetBranch}..${currentBranch}`;

const diffFiles = execSync(
`git --no-pager diff --minimal --name-only ${branch}`
)
.toString()
.split("\n")
.map((ln) => ln.trim())
.filter((ln) => !!ln);

return diffFiles.map((diffFile) => {
const file = readFileSync(diffFile, "utf-8");
const diff = execSync(`git --no-pager diff --minimal ${branch} ${diffFile}`)
.toString()
.trim();
return { file, diff };
});
};
29 changes: 29 additions & 0 deletions packages/ai-craftsman/src/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getEncoding } from "tiktoken-node";
import OpenAI from "openai";

const openai = new OpenAI({
apiKey: String(process.env["OPENAI_API_KEY"]),
});

const encoding = getEncoding("cl100k_base");

export const getTokenCount = (text: string): number => {
return encoding.encode(text).length;
};

export const chat = async (messages: OpenAI.ChatCompletionMessageParam[]) => {
const tokenCount = messages.reduce((acc, message) => {
return acc + getTokenCount(message.content ?? "");
}, 0);

const model =
tokenCount * 1.1 + 1024 < 4 * 1024 ? "gpt-3.5-turbo" : "gpt-3.5-turbo-16k";
const response = await openai.chat.completions.create({
model,
messages,
temperature: 0,
max_tokens: Math.min(1024, (16 * 1024 - tokenCount) * 0.9),
});

console.log({ messages, response });
};
Loading

0 comments on commit 1890ed4

Please sign in to comment.