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

Perplexity ask #875

Draft
wants to merge 6 commits into
base: development
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"exponential-backoff": "^3.1.1",
"husky": "^8.0.2",
"jimp": "^0.22.4",
"js-tiktoken": "^1.0.7",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"libsodium-wrappers": "^0.7.11",
Expand All @@ -63,6 +64,7 @@
"parse5": "^7.1.2",
"prettier": "^2.7.1",
"probot": "^12.2.4",
"sentencepiece-js": "^1.1.0",
"telegraf": "^4.11.2",
"tsx": "^3.12.7",
"yaml": "^2.2.2"
Expand Down
6 changes: 2 additions & 4 deletions src/bindings/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ export const loadConfig = async (context: Context): Promise<BotConfig> => {
permitBaseUrl: process.env.PERMIT_BASE_URL || permitBaseUrl,
},
unassign: {
timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE
? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE)
: timeRangeForMaxIssue,
timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE ? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) : timeRangeForMaxIssue,
timeRangeForMaxIssueEnabled: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED
? process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED == "true"
: timeRangeForMaxIssueEnabled,
Expand Down Expand Up @@ -108,7 +106,7 @@ export const loadConfig = async (context: Context): Promise<BotConfig> => {
registerWalletWithVerification: registerWalletWithVerification,
},
ask: {
apiKey: openAIKey,
apiKey: process.env.OPENAI_API_KEY || openAIKey,
tokenLimit: openAITokenLimit || 0,
},
accessControl: enableAccessControl,
Expand Down
1 change: 1 addition & 0 deletions src/configs/ubiquibot-config-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const DefaultConfig: MergedConfig = {
disableAnalytics: false,
commentIncentives: false,
registerWalletWithVerification: false,
openAIKey: process.env.OPENAI_API_KEY,
promotionComment:
"\n<h6>If you enjoy the DevPool experience, please follow <a href='https://github.com/ubiquity'>Ubiquity on GitHub</a> and star <a href='https://github.com/ubiquity/devpool-directory'>this repo</a> to show your support. It helps a lot!</h6>",
defaultLabels: [],
Expand Down
Binary file added src/declarations/tokenizer.model
Binary file not shown.
202 changes: 153 additions & 49 deletions src/handlers/comment/handlers/ask.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { getBotContext, getLogger } from "../../../bindings";
import { Payload, StreamlinedComment, UserType } from "../../../types";
import { getAllIssueComments, getAllLinkedIssuesAndPullsInBody } from "../../../helpers";
import { CreateChatCompletionRequestMessage } from "openai/resources/chat";
import { askGPT, decideContextGPT, sysMsg } from "../../../helpers/gpt";
import { ChatCompletionMessageParam } from "openai/resources/chat";
import { askGPT, gptContextTemplate, sysMsg } from "../../../helpers/gpt";
import { ErrorDiff } from "../../../utils/helpers";
import fetch from "node-fetch";
import { SentencePieceProcessor, cleanText } from "sentencepiece-js";

Check failure on line 8 in src/handlers/comment/handlers/ask.ts

View workflow job for this annotation

GitHub Actions / build

Could not find a declaration file for module 'sentencepiece-js'. '/home/runner/work/ubiquibot/ubiquibot/node_modules/sentencepiece-js/dist/index.js' implicitly has an 'any' type.

/**
* @param body The question to ask
Expand All @@ -13,7 +15,6 @@
const logger = getLogger();

const payload = context.payload as Payload;
const sender = payload.sender.login;
const issue = payload.issue;

if (!body) {
Expand All @@ -24,34 +25,56 @@
return `This command can only be used on issues`;
}

const chatHistory: CreateChatCompletionRequestMessage[] = [];
let chatHistory: ChatCompletionMessageParam[] = [];
const streamlined: StreamlinedComment[] = [];
let linkedPRStreamlined: StreamlinedComment[] = [];
let linkedIssueStreamlined: StreamlinedComment[] = [];

const regex = /^\/ask\s(.+)$/;
const regex = /^\/ask\s*([\s\S]*)$/;
const matches = body.match(regex);

if (matches) {
const [, body] = matches;

// standard comments
const sp = new SentencePieceProcessor();
try {
await sp.load(process.cwd() + "/src/declarations/tokenizer.model");
await sp.loadVocabulary(process.cwd() + "/src/declarations/tokenizer.model");
} catch (err) {
console.log("====================================");
console.log("err", err);
console.log("====================================");
}

const encodee = (s: string, bos = true) => {
const bosID = sp.encodeIds("<s>")[0];
const eosID = sp.encodeIds("</s>")[0];

if (typeof s !== "string") {
throw new Error("encodee only accepts strings");
}
let t = sp.encodeIds(s);

if (bos) {
t = [bosID, ...t];
}
t = [...t, eosID];
return t;
};

const comments = await getAllIssueComments(issue.number);
// raw so we can grab the <!--- { 'UbiquityAI': 'answer' } ---> tag
const commentsRaw = await getAllIssueComments(issue.number, "raw");

if (!comments) {
logger.info(`Error getting issue comments`);
return ErrorDiff(`Error getting issue comments`);
}

// add the first comment of the issue/pull request
streamlined.push({
login: issue.user.login,
body: issue.body,
});

// add the rest
comments.forEach(async (comment, i) => {
if (comment.user.type == UserType.User || commentsRaw[i].body.includes("<!--- { 'UbiquityAI': 'answer' } --->")) {
streamlined.push({
Expand All @@ -71,49 +94,130 @@
linkedPRStreamlined = links.linkedPrs;
}

// let chatgpt deduce what is the most relevant context
const gptDecidedContext = await decideContextGPT(chatHistory, streamlined, linkedPRStreamlined, linkedIssueStreamlined);

if (linkedIssueStreamlined.length == 0 && linkedPRStreamlined.length == 0) {
// No external context to add
chatHistory.push(
{
role: "system",
content: sysMsg,
name: "UbiquityAI",
} as CreateChatCompletionRequestMessage,
{
role: "user",
content: body,
name: sender,
} as CreateChatCompletionRequestMessage
);
} else {
chatHistory.push(
{
role: "system",
content: sysMsg, // provide the answer template
name: "UbiquityAI",
} as CreateChatCompletionRequestMessage,
{
role: "system",
content: "Original Context: " + JSON.stringify(gptDecidedContext), // provide the context
name: "system",
} as CreateChatCompletionRequestMessage,
{
role: "user",
content: "Question: " + JSON.stringify(body), // provide the question
name: "user",
} as CreateChatCompletionRequestMessage
);
const formatChat = (chat: { role?: string; content?: string; login?: string; body?: string }[]) => {
if (chat.length === 0) return "";
let chatString = "";
chat.reduce((acc, message) => {
if (!message) return acc;
const role = acc.role || acc.login;
const content = acc.content || acc.body;

chatString += `${cleanText(role)}: ${cleanText(content)}\n\n`;

acc = {
role,
content,
};

return acc;
});
console.log("chatString", chatString);
return chatString;
};

chatHistory.push(
{
role: "system",
content: gptContextTemplate,
},
{
role: "user",
content: `This issue/Pr context: \n ${JSON.stringify(streamlined)}`,
}
);

if (linkedIssueStreamlined.length > 0) {
chatHistory.push({
role: "user",
content: `Linked issue(s) context: \n ${JSON.stringify(linkedIssueStreamlined)}`,
});
} else if (linkedPRStreamlined.length > 0) {
chatHistory.push({
role: "user",
content: `Linked Pr(s) context: \n ${JSON.stringify(linkedPRStreamlined)}`,
});
}

const gptResponse = await askGPT(body, chatHistory);
const gptDecidedContext = await askGPT("ContextCall", chatHistory);

const gptAnswer = typeof gptDecidedContext === "string" ? gptDecidedContext : gptDecidedContext.answer || "";
const contextTokens = encodee(cleanText(gptAnswer));

chatHistory = [];

const tokenSize = contextTokens.length + encodee(body).length;

if (tokenSize > 4096) {
return "Your question is too long. Please ask a shorter question.";
}

chatHistory.push(
{
role: "system",
content: `${sysMsg}`,
},
{
role: "user",
content: `Context: ${cleanText(gptAnswer)} \n Question: ${body}`,
}
);

const chats = chatHistory.map((chat) => {
return {
role: chat.role,
content: chat.content ? cleanText(chat.content) : "",
};
});

if (typeof gptResponse === "string") {
return gptResponse;
} else if (gptResponse.answer) {
return gptResponse.answer;
const finalTokens = encodee(formatChat(chats), false);

const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
authorization: "Bearer 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",
},
body: JSON.stringify({
model: "mistral-7b-instruct",
messages: chatHistory,
}),
};

const ans = await fetch("https://api.perplexity.ai/chat/completions", options).then((response) => response.json().catch((err) => console.log(err)));
const answer = { tokens: ans.usage, text: ans.choices[0].message.content };
const gptRes = await askGPT(body, chatHistory);

const gptAns = typeof gptRes === "string" ? gptRes : gptRes.answer || "";
const gptTokens = typeof gptRes === "string" ? [] : gptRes.tokenUsage || [];

const comment = `
### Perp Tokens
\`\`\`json
${JSON.stringify(answer.tokens)}
\`\`\`

### GPT Tokens
\`\`\`json
${JSON.stringify(gptTokens)}
\`\`\

### SPP Tokens
\`\`\`json
Note: JSON in responses are throwing this off rn: ${finalTokens.length + contextTokens.length} tokens
\`\`\`

### Perp Response
${answer.text}

</hr>

### GPT Response
${gptAns}
`;

if (answer) {
return comment;
} else {
return ErrorDiff(`Error getting response from GPT`);
}
Expand Down
76 changes: 2 additions & 74 deletions src/helpers/gpt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { getBotConfig, getBotContext, getLogger } from "../bindings";
import { Payload, StreamlinedComment, UserType } from "../types";
import { getAllIssueComments, getAllLinkedIssuesAndPullsInBody } from "../helpers";
import { getBotConfig, getLogger } from "../bindings";
import OpenAI from "openai";
import { CreateChatCompletionRequestMessage } from "openai/resources/chat";
import { ErrorDiff } from "../utils/helpers";
Expand Down Expand Up @@ -61,77 +59,7 @@ Example:[
* @param linkedPRStreamlined an array of comments in the form of { login: string, body: string }
* @param linkedIssueStreamlined an array of comments in the form of { login: string, body: string }
*/
export const decideContextGPT = async (
chatHistory: CreateChatCompletionRequestMessage[],
streamlined: StreamlinedComment[],
linkedPRStreamlined: StreamlinedComment[],
linkedIssueStreamlined: StreamlinedComment[]
) => {
const context = getBotContext();
const logger = getLogger();

const payload = context.payload as Payload;
const issue = payload.issue;

if (!issue) {
return `Payload issue is undefined`;
}

// standard comments
const comments = await getAllIssueComments(issue.number);
// raw so we can grab the <!--- { 'UbiquityAI': 'answer' } ---> tag
const commentsRaw = await getAllIssueComments(issue.number, "raw");

if (!comments) {
logger.info(`Error getting issue comments`);
return `Error getting issue comments`;
}

// add the first comment of the issue/pull request
streamlined.push({
login: issue.user.login,
body: issue.body,
});

// add the rest
comments.forEach(async (comment, i) => {
if (comment.user.type == UserType.User || commentsRaw[i].body.includes("<!--- { 'UbiquityAI': 'answer' } --->")) {
streamlined.push({
login: comment.user.login,
body: comment.body,
});
}
});

// returns the conversational context from all linked issues and prs
const links = await getAllLinkedIssuesAndPullsInBody(issue.number);

if (typeof links === "string") {
logger.info(`Error getting linked issues or prs: ${links}`);
return `Error getting linked issues or prs: ${links}`;
}

linkedIssueStreamlined = links.linkedIssues;
linkedPRStreamlined = links.linkedPrs;

chatHistory.push(
{
role: "system",
content: "This issue/Pr context: \n" + JSON.stringify(streamlined),
name: "UbiquityAI",
} as CreateChatCompletionRequestMessage,
{
role: "system",
content: "Linked issue(s) context: \n" + JSON.stringify(linkedIssueStreamlined),
name: "UbiquityAI",
} as CreateChatCompletionRequestMessage,
{
role: "system",
content: "Linked Pr(s) context: \n" + JSON.stringify(linkedPRStreamlined),
name: "UbiquityAI",
} as CreateChatCompletionRequestMessage
);

export const decideContextGPT = async (chatHistory: CreateChatCompletionRequestMessage[]) => {
// we'll use the first response to determine the context of future calls
const res = await askGPT("", chatHistory);

Expand Down
Loading
Loading