generated from ubiquity-os/plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from ubq-testing/feat/ask
Feat/ask
- Loading branch information
Showing
21 changed files
with
388 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.