Skip to content

Commit

Permalink
Merge pull request #35 from ubq-testing/feat/ask
Browse files Browse the repository at this point in the history
Feat/ask
  • Loading branch information
Keyrxng authored Nov 27, 2024
2 parents 6e92de7 + ec20f06 commit cf3d666
Show file tree
Hide file tree
Showing 21 changed files with 388 additions and 57 deletions.
10 changes: 10 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
"SUPABASE",
"CODEOWNER",
"nosniff",
"voyageai",
"OPENROUTER",
"reranked",
"rerank",
"Reranked",
"ftse",
"Reranking",
"VOYAGEAI",
"supergroup",
"ubiquityos",
"newtask",
"supergroup"
],
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"knip": "knip --config .github/knip.ts",
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"prepare": "husky install",
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --setupFiles dotenv/config --coverage",
"test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --setupFiles dotenv/config --coverage",
"worker": "wrangler dev --env dev --port 3000",
"deploy": "wrangler deploy --env dev",
"sms-auth": "npx tsx src/bot/mtproto-api/bot/scripts/sms-auth/sms-auth.ts",
Expand Down Expand Up @@ -51,7 +51,8 @@
"octokit": "^4.0.2",
"openai": "^4.70.2",
"telegram": "^2.24.11",
"typebox-validators": "0.3.5"
"typebox-validators": "0.3.5",
"voyageai": "^0.0.1-5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240529.0",
Expand All @@ -65,6 +66,7 @@
"@mswjs/data": "0.16.1",
"@types/jest": "^29.5.12",
"@types/node": "20.14.5",
"cross-env": "^7.0.3",
"cspell": "8.14.2",
"eslint": "9.9.1",
"eslint-config-prettier": "9.1.0",
Expand Down
12 changes: 6 additions & 6 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Context } from "../types";
import { SessionManagerFactory } from "../bot/mtproto-api/bot/session/session-manager";
import { UserBaseStorage, ChatAction, HandleChatParams, StorageTypes, RetrievalHelper, Chat } from "../types/storage";
import { Completions } from "./openai/openai";
import { Embeddings } from "./supabase/embeddings";
import { VoyageAIClient } from "voyageai";

export interface Storage {
userSnapshot(chatId: number, userIds: number[]): Promise<void>;
Expand All @@ -19,12 +21,10 @@ export interface Storage {
}

export function createAdapters(ctx: Context) {
const {
config: { shouldUseGithubStorage },
env: { OPENAI_API_KEY },
} = ctx;
const sessionManager = SessionManagerFactory.createSessionManager(ctx);
return {
storage: SessionManagerFactory.createSessionManager(shouldUseGithubStorage, ctx).storage,
ai: new Completions(OPENAI_API_KEY),
storage: sessionManager.storage,
ai: new Completions(ctx),
embeddings: new Embeddings(sessionManager.getClient(), new VoyageAIClient({ apiKey: ctx.env.VOYAGEAI_API_KEY })),
};
}
59 changes: 34 additions & 25 deletions src/adapters/openai/openai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import OpenAI from "openai";
import { logger } from "../../utils/logger";
import { Context } from "../../types";

export interface ResponseFromLlm {
answer: string;
Expand All @@ -12,8 +14,23 @@ export interface ResponseFromLlm {
export class Completions {
protected client: OpenAI;

constructor(apiKey: string) {
this.client = new OpenAI({ apiKey: apiKey });
constructor(context: Context) {
const {
env,
config: {
aiConfig: { baseUrl, kind },
},
} = context;
const apiKey = kind === "OpenAi" ? env.OPENAI_API_KEY : env.OPENROUTER_API_KEY;

if (!apiKey) {
throw new Error(`Plugin is configured to use ${kind} but ${kind === "OpenAi" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"} is not set in the environment`);
}

this.client = new OpenAI({
baseURL: baseUrl,
apiKey,
});
}

createSystemMessage({
Expand All @@ -33,7 +50,7 @@ export class Completions {
}): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
return [
{
role: "system",
role: "user",
content: `You are UbiquityOS, a Telegram-integrated GitHub-first assistant for UbiquityDAO.
# Directives
Expand All @@ -59,41 +76,33 @@ export class Completions {
];
}

async createCompletion({
directives,
constraints,
additionalContext,
embeddingsSearch,
outputStyle,
query,
model,
}: {
async createCompletion(params: {
directives: string[];
constraints: string[];
additionalContext: string[];
embeddingsSearch: string[];
outputStyle: string;
query: string;
model: string;
}): Promise<ResponseFromLlm | undefined> {
}): Promise<string> {
const ctxWindow = this.createSystemMessage(params);

logger.info("ctxWindow:\n\n", { ctxWindow });

const res: OpenAI.Chat.Completions.ChatCompletion = await this.client.chat.completions.create({
model: model,
messages: this.createSystemMessage({ directives, constraints, query, embeddingsSearch, additionalContext, outputStyle }),
temperature: 0.2,
top_p: 0.5,
frequency_penalty: 0,
presence_penalty: 0,
model: params.model,
messages: ctxWindow,
response_format: {
type: "text",
},
});

const answer = res.choices[0].message;
if (answer?.content && res.usage) {
const { prompt_tokens, completion_tokens, total_tokens } = res.usage;
return {
answer: answer.content,
tokenUsage: { input: prompt_tokens, output: completion_tokens, total: total_tokens },
};
if (answer?.content) {
return answer.content;
}

logger.error("No answer found", { res });
return `There was an error processing your request. Please try again later.`;
}
}
101 changes: 101 additions & 0 deletions src/adapters/supabase/embeddings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { logger } from "../../utils/logger";
import { VoyageAIClient } from "voyageai";
import { CommentSimilaritySearchResult, DatabaseItem, IssueSimilaritySearchResult } from "../../types/ai";

export class Embeddings {
protected supabase: SupabaseClient;
protected voyage: VoyageAIClient;

constructor(supabase: SupabaseClient | void, client: VoyageAIClient) {
if (!supabase) {
throw new Error("Supabase client is required to use Embeddings");
}
this.supabase = supabase;
this.voyage = client;
}

async getIssue(issueNodeId: string): Promise<DatabaseItem[] | null> {
const { data, error } = await this.supabase.from("issues").select("*").eq("id", issueNodeId).returns<DatabaseItem[]>();
if (error) {
logger.error("Error getting issue", { error });
return null;
}
return data;
}

async getComment(commentNodeId: string): Promise<DatabaseItem[] | null> {
const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentNodeId);
if (error) {
logger.error("Error getting comment", { error });
}
return data;
}

async findSimilarIssues(plaintext: string, threshold: number): Promise<IssueSimilaritySearchResult[] | null> {
const embedding = await this.createEmbedding({ text: plaintext, prompt: "This is a query for the stored documents:" });
plaintext = plaintext.replace(/'/g, "''").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "\\%").replace(/_/g, "\\_");
const { data, error } = await this.supabase.rpc("find_similar_issue_ftse", {
current_id: "",
query_text: plaintext,
query_embedding: embedding,
threshold: threshold,
max_results: 10,
});
if (error) {
logger.error("Error finding similar issues", { error });
throw new Error("Error finding similar issues");
}
return data;
}

async findSimilarComments(query: string, threshold: number): Promise<CommentSimilaritySearchResult[] | null> {
const embedding = await this.createEmbedding({ text: query, prompt: "This is a query for the stored documents:" });
query = query.replace(/'/g, "''").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "\\%").replace(/_/g, "\\_");
logger.info(`Query: ${query}`);
const { data, error } = await this.supabase.rpc("find_similar_comments", {
current_id: "",
query_text: query,
query_embedding: embedding,
threshold: threshold,
max_results: 10,
});
if (error) {
logger.error("Error finding similar comments", { error });
throw new Error("Error finding similar comments");
}
return data;
}

async createEmbedding(input: { text?: string; prompt?: string } = {}): Promise<number[]> {
const VECTOR_SIZE = 1024;
const { text = null, prompt = null } = input;
if (text === null) {
return new Array(VECTOR_SIZE).fill(0);
} else {
const response = await this.voyage.embed({
input: prompt ? `${prompt} ${text}` : text,
model: "voyage-large-2-instruct",
});
return response.data?.[0]?.embedding || [];
}
}

async reRankResults(results: string[], query: string, topK: number = 5): Promise<string[]> {
let response;
try {
response = await this.voyage.rerank({
query,
documents: results,
model: "rerank-2",
returnDocuments: true,
topK,
});
} catch (e: unknown) {
logger.error("Reranking failed!", { e });
return results;
}
const rerankedResults = response.data || [];
return rerankedResults.map((result) => result.document).filter((document): document is string => document !== undefined);
}
}
69 changes: 69 additions & 0 deletions src/bot/features/commands/shared/ask-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { chatAction } from "@grammyjs/auto-chat-action";
import { Composer } from "grammy";
import { GrammyContext } from "../../../helpers/grammy-context";
import { logHandle } from "../../../helpers/logging";
import { logger } from "../../../../utils/logger";
import { PluginContext } from "../../../../types/plugin-context-single";
import { CommentSimilaritySearchResult, IssueSimilaritySearchResult } from "../../../../types/ai";

const composer = new Composer<GrammyContext>();
const feature = composer.chatType(["group", "private", "supergroup", "channel"]);

feature.command("ubiquityos", logHandle("command-ubiquityos"), chatAction("typing"), async (ctx) => {
const {
adapters: { ai, embeddings },
} = ctx;

const directives = [
"Extract Relevant Information: Identify key pieces of information, even if they are incomplete, from the available corpus.",
"Apply Knowledge: Use the extracted information and relevant documentation to construct an informed response.",
"Draft Response: Compile the gathered insights into a coherent and concise response, ensuring it's clear and directly addresses the user's query.",
"Review and Refine: Check for accuracy and completeness, filling any gaps with logical assumptions where necessary.",
];

const constraints = [
"Ensure the response is crafted from the corpus provided, without introducing information outside of what's available or relevant to the query.",
"Consider edge cases where the corpus might lack explicit answers, and justify responses with logical reasoning based on the existing information.",
"Replies MUST be in Markdown V1 format but do not wrap in code blocks.",
];

const outputStyle = "Concise and coherent responses in paragraphs that directly address the user's question.";

const question = ctx.message?.text.replace("/ubiquityos", "").trim();

if (!question) {
return ctx.reply("Please provide a question to ask UbiquityOS.");
}

const { similarityThreshold, model } = PluginContext.getInstance().config.aiConfig;
const similarText = await Promise.all([
embeddings.findSimilarComments(question, 1 - similarityThreshold),
embeddings.findSimilarIssues(question, 1 - similarityThreshold),
]).then(([comments, issues]) => {
return [
...(comments?.map((comment: CommentSimilaritySearchResult) => comment.comment_plaintext) || []),
...(issues?.map((issue: IssueSimilaritySearchResult) => issue.issue_plaintext) || []),
];
});

logger.info("Similar Text:\n\n", { similarText });
const rerankedText = similarText.length > 0 ? await embeddings.reRankResults(similarText, question) : [];
logger.info("Reranked Text:\n\n", { rerankedText });

return ctx.reply(
await ai.createCompletion({
directives,
constraints,
query: question,
embeddingsSearch: rerankedText,
additionalContext: [],
outputStyle,
model,
}),
{
parse_mode: "Markdown",
}
);
});

export { composer as askFeature };
6 changes: 2 additions & 4 deletions src/bot/features/commands/shared/task-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async function createTask(taskToCreate: string, ctx: GrammyContext, { owner, rep

const outputStyle = `{ "title": "Task Title", "body": "Task Body" }`;

const llmResponse = await ctx.adapters.ai.createCompletion({
const taskFromLlm = await ctx.adapters.ai.createCompletion({
embeddingsSearch: [],
directives,
constraints,
Expand All @@ -144,12 +144,10 @@ async function createTask(taskToCreate: string, ctx: GrammyContext, { owner, rep
query: taskToCreate,
});

if (!llmResponse) {
if (!taskFromLlm) {
return await ctx.reply("Failed to create task");
}

const taskFromLlm = llmResponse.answer;

let taskDetails;

try {
Expand Down
9 changes: 8 additions & 1 deletion src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { welcomeFeature } from "./features/start-command";
import { unhandledFeature } from "./features/helpers/unhandled";
import { Context } from "../types";
import { session } from "./middlewares/session";
import { askFeature } from "./features/commands/shared/ask-command";
import { newTaskFeature } from "./features/commands/shared/task-creation";

interface Dependencies {
Expand All @@ -46,6 +47,9 @@ export async function createBot(token: string, dependencies: Dependencies, optio
const bot = new TelegramBot(token, {
...options.botConfig,
ContextConstructor: await createContextConstructor(dependencies),
client: {
timeoutSeconds: 500,
},
});

// Error handling
Expand Down Expand Up @@ -81,7 +85,6 @@ export async function createBot(token: string, dependencies: Dependencies, optio
bot.use(userIdFeature);
bot.use(chatIdFeature);
bot.use(botIdFeature);
bot.use(newTaskFeature);

// Private chat commands
bot.use(registerFeature);
Expand All @@ -91,6 +94,10 @@ export async function createBot(token: string, dependencies: Dependencies, optio
// Group commands
bot.use(banCommand);

// shared commands
bot.use(askFeature);
bot.use(newTaskFeature);

// Unhandled command handler
bot.use(unhandledFeature);

Expand Down
Loading

0 comments on commit cf3d666

Please sign in to comment.