Skip to content

Commit

Permalink
Merge pull request #31 from ubq-testing/development
Browse files Browse the repository at this point in the history
`/newtask`
  • Loading branch information
Keyrxng authored Nov 27, 2024
2 parents b29d383 + c38c0d1 commit 6e92de7
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 53 deletions.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"Superbase",
"SUPABASE",
"CODEOWNER",
"nosniff"
"nosniff",
"newtask",
"supergroup"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/update-configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
steps:
- uses: ubiquity-os/action-deploy-plugin@main
with:
pluginEntry: '${{ github.workspace }}/src/workflow-entry.ts'
schemaPath: '${{ github.workspace }}/src/types/plugin-inputs.ts'
pluginEntry: "${{ github.workspace }}/src/workflow-entry.ts"
schemaPath: "${{ github.workspace }}/src/types/plugin-inputs.ts"
env:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
36 changes: 17 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,25 @@

## 1.0.0 (2024-11-01)


### Features

* callback proxy ([0673806](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/067380691740ae30f26834cb6caec496e088ea5d))
* create room on assignment, skip if exists ([390e3ed](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/390e3ed5c4ea26e3bae5371991c7c19f04187321))
* disqualification trigger ([8e5ace4](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/8e5ace47770c6c363801e6dddb3345da05ce5d61))
* GitHub storage adapter ([db459b6](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/db459b6cdd38dd64aaadd8441bac5ff6c7301050))
* payment notifications ([14b0843](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/14b0843a48a0f9ad5d91e50e2ef3ad71210417ce))
* reminder trigger ([c72012f](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c72012fc07c98d9d2ace6f474d9c5c1bfa8bf5c5))
* repo secret saving, guided env setup ([00d9d42](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/00d9d422fd69aaf3dd605c351643480c589afa9a))
* review trigger ([bf3a904](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/bf3a9048f503fea64c315edf90811f67c94da041))
* sesson & storage manager ([50976ed](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/50976ed8373762a2765aebc205e8fd6132939de0))
* storage app ([c471790](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c4717907eb5a1cc584eded77015e815a24cca066))
* subscribe command ([3afcea7](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/3afcea74d779885ec12e81162210f090c99803cd))
* targetBranch config item ([0947e07](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/0947e076e2365b4f3d31ddade203843d967e0863))
* telegram register command ([c46e204](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c46e204893e5c1677e8632841cc1cf9989bcf61e))

- callback proxy ([0673806](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/067380691740ae30f26834cb6caec496e088ea5d))
- create room on assignment, skip if exists ([390e3ed](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/390e3ed5c4ea26e3bae5371991c7c19f04187321))
- disqualification trigger ([8e5ace4](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/8e5ace47770c6c363801e6dddb3345da05ce5d61))
- GitHub storage adapter ([db459b6](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/db459b6cdd38dd64aaadd8441bac5ff6c7301050))
- payment notifications ([14b0843](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/14b0843a48a0f9ad5d91e50e2ef3ad71210417ce))
- reminder trigger ([c72012f](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c72012fc07c98d9d2ace6f474d9c5c1bfa8bf5c5))
- repo secret saving, guided env setup ([00d9d42](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/00d9d422fd69aaf3dd605c351643480c589afa9a))
- review trigger ([bf3a904](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/bf3a9048f503fea64c315edf90811f67c94da041))
- session & storage manager ([50976ed](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/50976ed8373762a2765aebc205e8fd6132939de0))
- storage app ([c471790](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c4717907eb5a1cc584eded77015e815a24cca066))
- subscribe command ([3afcea7](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/3afcea74d779885ec12e81162210f090c99803cd))
- targetBranch config item ([0947e07](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/0947e076e2365b4f3d31ddade203843d967e0863))
- telegram register command ([c46e204](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/c46e204893e5c1677e8632841cc1cf9989bcf61e))

### Bug Fixes

* add Telegram Room action and update workflows ([fcc10b1](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/fcc10b10e00329f8907ae224a5b4e9352b5b6a0e))
* configure Jest to support ES modules in TypeScript ([47403a0](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/47403a02b98198a601615c44684b6b36102b2ee5))
* proxy usage, event support for labeled/close/reopen with storage ([bf8dee4](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/bf8dee418132a1f58ff6a9ab59734d2f2436de93))
* update logger and add support for multiple Octokit types ([71d2836](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/71d283650ddda609868578902906196cf41b1df5))
- add Telegram Room action and update workflows ([fcc10b1](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/fcc10b10e00329f8907ae224a5b4e9352b5b6a0e))
- configure Jest to support ES modules in TypeScript ([47403a0](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/47403a02b98198a601615c44684b6b36102b2ee5))
- proxy usage, event support for labeled/close/reopen with storage ([bf8dee4](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/bf8dee418132a1f58ff6a9ab59734d2f2436de93))
- update logger and add support for multiple Octokit types ([71d2836](https://github.com/ubiquity-os-marketplace/ubiquity-os-kernel-telegram/commit/71d283650ddda609868578902906196cf41b1df5))
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default tsEslint.config({
"@typescript-eslint": tsEslint.plugin,
"check-file": checkFile,
},
ignores: [".github/knip.ts", "tests/**/*.ts", "eslint.config.mjs", ".wrangler/**/*.{js,ts}", "coverage/**/*.js"],
ignores: [".github/knip.ts", "tests/**/*.ts", "eslint.config.mjs", ".wrangler/**/*.{js,ts}", "coverage/**/*.js", "dist/**/*.js"],
extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended],
languageOptions: {
parser: tsEslint.parser,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"big-integer": "^1.6.52",
"dotenv": "16.4.5",
"fuse.js": "^7.0.0",
"grammy": "^1.29.0",
"grammy-guard": "0.5.0",
"hono": "^4.5.9",
"octokit": "^4.0.2",
"openai": "^4.70.2",
"telegram": "^2.24.11",
"typebox-validators": "0.3.5"
},
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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";

export interface Storage {
userSnapshot(chatId: number, userIds: number[]): Promise<void>;
Expand All @@ -20,8 +21,10 @@ export interface Storage {
export function createAdapters(ctx: Context) {
const {
config: { shouldUseGithubStorage },
env: { OPENAI_API_KEY },
} = ctx;
return {
storage: SessionManagerFactory.createSessionManager(shouldUseGithubStorage, ctx).storage,
ai: new Completions(OPENAI_API_KEY),
};
}
99 changes: 99 additions & 0 deletions src/adapters/openai/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import OpenAI from "openai";

export interface ResponseFromLlm {
answer: string;
tokenUsage: {

Check failure on line 5 in src/adapters/openai/openai.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

src/adapters/openai/openai.ts#L5

'ResponseFromLlm' is an unused type
input: number;
output: number;
total: number;
};
}

export class Completions {
protected client: OpenAI;

constructor(apiKey: string) {
this.client = new OpenAI({ apiKey: apiKey });
}

createSystemMessage({
additionalContext,
constraints,
directives,
embeddingsSearch,
outputStyle,
query,
}: {
directives: string[];
constraints: string[];
query: string;
embeddingsSearch: string[];
additionalContext: string[];
outputStyle: string;
}): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
return [
{
role: "system",
content: `You are UbiquityOS, a Telegram-integrated GitHub-first assistant for UbiquityDAO.
# Directives
${directives.join("\n- ")}
# Constraints
${constraints.join("\n- ")}
${embeddingsSearch.length > 0 ? `## Embeddings Search Results\n${embeddingsSearch.join("\n- ")}` : ""}
${additionalContext.length > 0 ? `### Additional Context\n${additionalContext.join("\n- ")}` : ""}
# Output Style
${outputStyle}
`
.replace(/ {16}/g, "")
.trim(),
},
{
role: "user",
content: query,
},
];
}

async createCompletion({
directives,
constraints,
additionalContext,
embeddingsSearch,
outputStyle,
query,
model,
}: {
directives: string[];
constraints: string[];
additionalContext: string[];
embeddingsSearch: string[];
outputStyle: string;
query: string;
model: string;
}): Promise<ResponseFromLlm | undefined> {
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,
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 },
};
}
}
}
191 changes: 191 additions & 0 deletions src/bot/features/commands/shared/task-creation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { chatAction } from "@grammyjs/auto-chat-action";
import { Composer } from "grammy";
import { GrammyContext } from "../../../helpers/grammy-context";
import { logHandle } from "../../../helpers/logging";
import { isAdmin } from "../../../filters/is-admin";
import { logger } from "../../../../utils/logger";
import { RestEndpointMethodTypes } from "@octokit/rest";
import Fuse from "fuse.js";
import { PluginContext } from "../../../../types/plugin-context-single";

const composer = new Composer<GrammyContext>();

const feature = composer.chatType(["group", "private", "supergroup", "channel"]);

/**
* This is responsible for creating a task on GitHub. It's going to be a direct reply
* callback to the user who wrote the comment that we'll turn into a fully featured github
* task specification.
*/

feature.command("newtask", logHandle("task-creation"), chatAction("typing"), async (ctx: GrammyContext) => {
if (!ctx.message || !ctx.message.reply_to_message) {
logger.info(`No message or reply to message`);
return await ctx.reply("To create a new task, reply to the message with `/newtask <repo>`");
}

const taskToCreate = ctx.message.reply_to_message.text;

if (!taskToCreate || taskToCreate.length < 10) {
return await ctx.reply("A new task needs substantially more content than that");
}

const repoToCreateIn = ctx.message.text?.split(" ")[1];

if (!repoToCreateIn) {
logger.info(`No repo to create task in`);
return await ctx.reply("To create a new task, reply to the message with `/newtask <repo>`");
}

const fromId = ctx.message.from.id;
const isReplierAdmin = isAdmin([fromId])(ctx);

/**
* a cheap workaround for ctx being inferred as never if not an admin fsr, needs looked into.
* ctx types are complex here with mixins and such and the grammy ctx is highly dynamic.
* my assumption is that the ctx returned by isAdmin is replacing the initial ctx type.
*/
const replyFn = ctx.reply;
if (!isReplierAdmin) {
logger.info(`User ${fromId} is not an admin`);
return await replyFn("Only admins can create tasks");
}

const response = await fetch("https://raw.githubusercontent.com/ubiquity/devpool-directory/__STORAGE__/devpool-issues.json");
const devPoolIssues = (await response.json()) as RestEndpointMethodTypes["issues"]["get"]["response"]["data"][];

const repoNames = Array.from(
new Set(
devPoolIssues.map((issue) => {
return ownerRepoFromUrl(issue.html_url)?.repo ?? "";
})
)
);

const context = await PluginContext.getInstance().getContext();

const options = {
includeScore: true,
threshold: context.config.fuzzySearchThreshold,
};

const fuse = new Fuse(repoNames, options);
const results = fuse.search(repoToCreateIn);

if (results.length > 0) {
const bestMatchRepoName = results[0].item;
const foundIssue = devPoolIssues.find((issue) => {
const repoName = ownerRepoFromUrl(issue.html_url)?.repo ?? "";
return repoName === bestMatchRepoName;
});

if (!foundIssue) {
return await ctx.reply("No issue found");
}

const found = ownerRepoFromUrl(foundIssue?.html_url);

if (!found) {
return await ctx.reply("No repo found");
}

return await createTask(taskToCreate, ctx, found, fromId);
}
return await ctx.reply("No repo found");
});

function ownerRepoFromUrl(url: string) {
const namedGroups = /https:\/\/github.com\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<issue>\d+)/.exec(url)?.groups;

if (!namedGroups) {
return null;
}

return {
owner: namedGroups.owner,
repo: namedGroups.repo,
};
}

async function createTask(taskToCreate: string, ctx: GrammyContext, { owner, repo }: { owner: string; repo: string }, fromId: number) {
const directives = [
"Consume the user's message and begin to transform it into a GitHub task specification",
"Include a relevant short title for opening the task with",
"Include the task's description based on the user's message",
"Include any relevant context or constraints",
"Use a structured approach to writing the task",
"Do so without comment or directive, just the requested 'outputStyle'",
];

const constraints = [
"Never hallucinate details into the specification",
"Ensure the task is clear and actionable",
"Use GitHub flavoured markdown by default",
"Return the markdown within a code block to maintain formatting on GitHub",
"DO NOT use backticks in the markdown",
];

const additionalContext = [
"The task will be created via the GitHub app under your name; UbiquityOS",
"The correct repository will be selected by the admin who invoked this intent",
"Your output will be JSON parsed for the 'title' and 'body' keys",
"The user credit will be injected into the footer of your spec, so always leave it blank following a '---' separator",
];

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

const llmResponse = await ctx.adapters.ai.createCompletion({
embeddingsSearch: [],
directives,
constraints,
additionalContext,
outputStyle,
model: "gpt-4o",
query: taskToCreate,
});

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

const taskFromLlm = llmResponse.answer;

let taskDetails;

try {
taskDetails = JSON.parse(taskFromLlm);
} catch {
return await ctx.reply("Failed to parse task");
}

const userCredits = await ctx.adapters.storage.retrieveUserByTelegramId(fromId);

const username = userCredits?.github_username ?? "Anonymous";
const chatLink = ctx.chat?.type !== "private" && (await ctx.createChatInviteLink());

const chatLinkText = chatLink ? ` [here](${chatLink.invite_link})` : "";
const fullSpec = `${taskDetails.body}\n\n_Originally created by @${username} via Telegram${chatLinkText}_`;

logger.info("creating task", {
taskDetails,
fullSpec,
owner,
repo,
author: username,
});

const task = await ctx.octokit.rest.issues.create({
owner,
repo,
title: taskDetails.title,
body: fullSpec,
});

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

return await ctx.reply(`${fullSpec}\n\n [View on GitHub](${task.data.html_url})`);
}

export { composer as newTaskFeature };
Loading

0 comments on commit 6e92de7

Please sign in to comment.