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

feat: dynamic ground truths #14

Merged
Show file tree
Hide file tree
Changes from 17 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
5 changes: 4 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
"nemo",
"Reranking",
"mistralai",
"OPENROUTER_API_KEY"
"Typeguard",
"typeguards",
"OPENROUTER_API_KEY",
"Openrouter"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/compute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ jobs:
run: yarn tsx ./src/main.ts
id: command-ask
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
VOYAGEAI_API_KEY: ${{ secrets.VOYAGEAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
UBIQUITY_OS_APP_NAME: ${{ secrets.UBIQUITY_OS_APP_NAME }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
VOYAGEAI_API_KEY: ${{ secrets.VOYAGEAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
UBIQUITY_OS_APP_NAME: ${{ secrets.UBIQUITY_OS_APP_NAME }}
2 changes: 1 addition & 1 deletion .github/workflows/update-configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
commitMessage: "chore: updated manifest.json and dist build"
nodeVersion: "20.10.0"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ cypress/screenshots
script.ts
.wrangler
test-dashboard.md
t.ts
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"prepare": "husky install",
"test": "jest --setupFiles dotenv/config --coverage",
"worker": "wrangler dev --env dev --port 5000"
"worker": "wrangler dev --env dev --port 4000"
},
"keywords": [
"typescript",
Expand Down Expand Up @@ -66,7 +66,7 @@
"prettier": "3.3.2",
"ts-jest": "29.1.5",
"tsx": "4.15.6",
"typescript": "5.4.5",
"typescript": "^5.6.3",
"typescript-eslint": "7.13.1",
"wrangler": "^3.81.0"
},
Expand Down
20 changes: 12 additions & 8 deletions src/handlers/ask-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { IssueSimilaritySearchResult } from "../adapters/supabase/helpers/issues
import { recursivelyFetchLinkedIssues } from "../helpers/issue-fetching";
import { formatChatHistory } from "../helpers/format-chat-history";
import { optimizeContext } from "../helpers/issue";
import { fetchRepoDependencies, fetchRepoLanguageStats } from "./ground-truths/chat-bot";
import { findGroundTruths } from "./ground-truths/find-ground-truths";

/**
* Asks a question to GPT and returns the response
Expand Down Expand Up @@ -62,12 +64,14 @@ export async function askGpt(context: Context, question: string, formattedChat:
// const reRankedChat = formattedChat.length > 0 ? await context.adapters.voyage.reranker.reRankResults(formattedChat.filter(text => text !== ""), question, 300) : [];
similarText = similarText.filter((text) => text !== "");
const rerankedText = similarText.length > 0 ? await context.adapters.voyage.reranker.reRankResults(similarText, question) : [];
return context.adapters.openai.completions.createCompletion(
question,
model,
rerankedText,
formattedChat,
["typescript", "github", "cloudflare worker", "actions", "jest", "supabase", "openai"],
UBIQUITY_OS_APP_NAME
);

const languages = await fetchRepoLanguageStats(context);
const { dependencies, devDependencies } = await fetchRepoDependencies(context);
const groundTruths = await findGroundTruths(context, "chat-bot", {
languages,
dependencies,
devDependencies,
});

return context.adapters.openai.completions.createCompletion(question, model, rerankedText, formattedChat, groundTruths, UBIQUITY_OS_APP_NAME);
}
64 changes: 64 additions & 0 deletions src/handlers/ground-truths/chat-bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Context } from "../../types";
import { logger } from "../../helpers/errors";

export async function fetchRepoDependencies(context: Context) {
const {
octokit,
payload: {
repository: {
owner: { login: owner },
name: repo,
},
},
} = context;

const { data: packageJson } = await octokit.repos.getContent({
owner,
repo,
path: "package.json",
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
});

if ("content" in packageJson) {
return extractDependencies(JSON.parse(Buffer.from(packageJson.content, "base64").toString()));
} else {
throw logger.error(`No package.json found in ${owner}/${repo}`);
}
}

export function extractDependencies(packageJson: Record<string, Record<string, string>>) {
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
const { dependencies, devDependencies } = packageJson;

return {
dependencies,
devDependencies,
};
}

export async function fetchRepoLanguageStats(context: Context) {
const {
octokit,
payload: {
repository: {
owner: { login: owner },
name: repo,
},
},
} = context;

const { data: languages } = await octokit.repos.listLanguages({
owner,
repo,
});

const totalBytes = Object.values(languages).reduce((acc, bytes) => acc + bytes, 0);

const stats = Object.entries(languages).reduce(
(acc, [language, bytes]) => {
acc[language] = bytes / totalBytes;
return acc;
},
{} as Record<string, number>
);

return Array.from(Object.entries(stats)).sort((a, b) => b[1] - a[1]);
}
38 changes: 38 additions & 0 deletions src/handlers/ground-truths/create-ground-truth-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import OpenAI from "openai";
import { Context } from "../../types";
import { CompletionsModelHelper, ModelApplications } from "../../types/llm";

export async function createGroundTruthCompletion<TApp extends ModelApplications>(
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
context: Context,
groundTruthSource: string,
systemMsg: string,
model: CompletionsModelHelper<TApp>
): Promise<string | null> {
const {
env: { OPENAI_API_KEY },
config: { openAiBaseUrl },
} = context;

const openAi = new OpenAI({
apiKey: OPENAI_API_KEY,
...(openAiBaseUrl && { baseURL: openAiBaseUrl }),
});

const msgs = [
{
role: "system",
content: systemMsg,
},
{
role: "user",
content: groundTruthSource,
},
] as OpenAI.Chat.Completions.ChatCompletionMessageParam[];

const res = await openAi.chat.completions.create({
messages: msgs,
model: model,
});

return res.choices[0].message.content;
}
17 changes: 17 additions & 0 deletions src/handlers/ground-truths/create-system-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function createGroundTruthSysMsg({ truthRules, example, conditions }: { truthRules: string[]; example: string[]; conditions?: string[] }) {
return `
Using the input provided, your goal is to produce an array of strings that represent "Ground Truths."
These ground truths are high-level abstractions that encapsulate the tech stack and dependencies of the repository.

Each ground truth should:
- ${truthRules.join("\n- ")}

Example:
${example.join("\n")}

${conditions ? `Conditions:\n${conditions.join("\n")}` : ""}

Generate similar ground truths adhering to a maximum of 10.

Return a JSON parsable array of strings representing the ground truths, without comment or directive.`;
}
48 changes: 48 additions & 0 deletions src/handlers/ground-truths/find-ground-truths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Context } from "../../types";
import { AppParamsHelper, GroundTruthsSystemMessage, ModelApplications } from "../../types/llm";
import { GROUND_TRUTHS_SYSTEM_MESSAGES } from "./prompts";
import { chatBotPayloadTypeguard, codeReviewPayloadTypeguard } from "../../types/typeguards";
import { validateGroundTruths } from "./validate";
import { logger } from "../../helpers/errors";
import { createGroundTruthCompletion } from "./create-ground-truth-completion";
import { createGroundTruthSysMsg } from "./create-system-message";

export async function findGroundTruths<TApp extends ModelApplications = ModelApplications>(
context: Context,
application: TApp,
params: AppParamsHelper<TApp>
): Promise<string[]> {
const systemMsgObj = GROUND_TRUTHS_SYSTEM_MESSAGES[application];

// params are deconstructed to show quickly what's being passed to the function

if (chatBotPayloadTypeguard(params)) {
const { dependencies, devDependencies, languages } = params;
return findChatBotTruths(context, { dependencies, devDependencies, languages }, systemMsgObj);
} else if (codeReviewPayloadTypeguard(params)) {
const { taskSpecification } = params;
return findCodeReviewTruths(context, { taskSpecification }, systemMsgObj);
} else {
throw logger.error("Invalid payload type for ground truths");
}
}

async function findChatBotTruths(
context: Context,
params: AppParamsHelper<"chat-bot">,
systemMsgObj: GroundTruthsSystemMessage<"chat-bot">
): Promise<string[]> {
const systemMsg = createGroundTruthSysMsg(systemMsgObj);
const truths = await createGroundTruthCompletion<"chat-bot">(context, JSON.stringify(params), systemMsg, "o1-mini");
return validateGroundTruths(truths);
}

async function findCodeReviewTruths(
context: Context,
params: AppParamsHelper<"code-review">,
systemMsgObj: GroundTruthsSystemMessage<"code-review">
): Promise<string[]> {
const systemMsg = createGroundTruthSysMsg(systemMsgObj);
const truths = await createGroundTruthCompletion<"code-review">(context, params.taskSpecification, systemMsg, "gpt-4o");
return validateGroundTruths(truths);
}
57 changes: 57 additions & 0 deletions src/handlers/ground-truths/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { GroundTruthsSystemMessageTemplate, ModelApplications } from "../../types/llm";

const CODE_REVIEW_GROUND_TRUTHS_SYSTEM_MESSAGE = {
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
example: [
`Using the input provided, your goal is to produce an array of strings that represent "Ground Truths."
These ground truths are high-level abstractions that encapsulate the key aspects of the task.
They serve to guide and inform our code review model's interpretation of the task by providing clear, concise, and explicit insights.

Each ground truth should:
- Be succinct and easy to understand.
- Directly pertain to the task at hand.
- Focus on essential requirements, behaviors, or assumptions involved in the task.

Example:
Task: Implement a function that adds two numbers.
Ground Truths:
- The function should accept two numerical inputs.
- The function should return the sum of the two inputs.
- Inputs must be validated to ensure they are numbers.

Based on the given task, generate similar ground truths adhering to a maximum of 10.

Return a JSON parsable array of strings representing the ground truths, without comment or directive.`,
],
truthRules: [],
conditions: [],
};

const CHAT_BOT_GROUND_TRUTHS_SYSTEM_MESSAGE = {
truthRules: [
"Be succinct and easy to understand.",
"Use only the information provided in the input.",
"Focus on essential requirements, behaviors, or assumptions involved in the repository.",
],
example: [
"Languages: { TypeScript: 60%, JavaScript: 15%, HTML: 10%, CSS: 5%, ... }",
"Dependencies: Esbuild, Wrangler, React, Tailwind CSS, ms, React-carousel, React-icons, ...",
"Dev Dependencies: @types/node, @types/jest, @mswjs, @testing-library/react, @testing-library/jest-dom, @Cypress ...",
"Ground Truths:",
"- The repo predominantly uses TypeScript, with JavaScript, HTML, and CSS also present.",
"- The repo is a React project that uses Tailwind CSS.",
"- The project is built with Esbuild and deployed with Wrangler, indicating a Cloudflare Workers project.",
"- The repo tests use Jest, Cypress, mswjs, and React Testing Library.",
],
conditions: [
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
"Assume your output builds the foundation for a chatbot to understand the repository when asked an arbitrary query.",
"Do not list every language or dependency, focus on the most prevalent ones.",
"Focus on what is essential to understand the repository at a high level.",
"Brevity is key. Use zero formatting. Do not wrap in quotes, backticks, or other characters.",
`response === ["some", "array", "of", "strings"]`,
],
};

export const GROUND_TRUTHS_SYSTEM_MESSAGES: Record<ModelApplications, GroundTruthsSystemMessageTemplate> = {
"code-review": CODE_REVIEW_GROUND_TRUTHS_SYSTEM_MESSAGE,
"chat-bot": CHAT_BOT_GROUND_TRUTHS_SYSTEM_MESSAGE,
} as const;
29 changes: 29 additions & 0 deletions src/handlers/ground-truths/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { logger } from "../../helpers/errors";

export function validateGroundTruths(truthsString: string | null): string[] {
let truths;
if (!truthsString) {
throw logger.error("Failed to generate ground truths");
}

try {
truths = JSON.parse(truthsString);
} catch (err) {
throw logger.error("Failed to parse ground truths");
}
if (!Array.isArray(truths)) {
throw logger.error("Ground truths must be an array");
}

if (truths.length > 10) {
throw logger.error("Ground truths must not exceed 10");
}

truths.forEach((truth: string) => {
if (typeof truth !== "string") {
throw logger.error("Each ground truth must be a string");
}
});

return truths;
}
3 changes: 3 additions & 0 deletions src/helpers/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Logs } from "@ubiquity-dao/ubiquibot-logger"; // import is fixed in #13

export const logger = new Logs("debug");
19 changes: 0 additions & 19 deletions src/types/llm.d.ts

This file was deleted.

Loading