Skip to content
This repository was archived by the owner on Sep 18, 2024. It is now read-only.

Feature/assistants UI #133

Merged
merged 2 commits into from
Jun 11, 2024
Merged
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
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions packages/akeru-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"dependencies": {
"@elysiajs/bearer": "^1.0.2",
"@elysiajs/cors": "^1.0.2",
"@elysiajs/stream": "^1.0.2",
"@elysiajs/swagger": "^1.0.4",
"dayjs": "^1.11.10",
Expand Down
51 changes: 51 additions & 0 deletions packages/akeru-server/scripts/createHumanUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
createApiToken,
createHumanUser,
} from "@/core/application/controllers/userController";
import { createAssistant } from "@/core/application/services/assistantService";
import { createThread } from "@/core/application/services/threadService";
import { ulid } from "ulid";

async function createHumanUserWithApiKey() {
const userData = {
name: "Mr. Akeru Human User",
email: "[email protected]",
};

// Creating the human user for id
const userId = await createHumanUser(userData);
if (!userId) {
console.error("Failed to create super admin user");
return;
}

// creating user bearer token
const apiKey = await createApiToken(userId);
if (!apiKey) {
console.error("Failed to create API key for super admin user");
return;
}

// creating assistants associated with the user, creating three assistants
const { id: id_1 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Homework assistant", fileIds: [], tools: [], instruction: "You are a homework helper. Do not answer irrelevant questions." });
const { id: id_2 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Tropical Plant Expert", fileIds: [], tools: [], instruction: "You specialise in tropical plants. Do not answer irrelevant questions." });
const { id: id_3 } = await createAssistant({ userId, id: ulid(), model: "gpt-4", name: "Marine Biologist", fileIds: [], tools: [], instruction: "You specialise in Marine Biology. Do not answer irrelevant questions." });

// creating a few threads associated with said assistants
await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'Homework on Science' })
await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'What are fishes?' })
await createThread({ id: ulid(), createdBy: userId, participants: [], messageIds: [], name: 'Why do plants commonly die?' })

console.log(`human user created with ID: ${userId}`);
console.log(`API key for human user: ${apiKey}`);

console.log(`assistant_id 1 created with ID: ${id_1}`);
console.log(`assistant_id 2 created with ID: ${id_2}`);
console.log(`assistant_id 3 created with ID: ${id_3}`);

process.exit();
}

createHumanUserWithApiKey().catch((err) => {
console.error("Error creating super admin user with API key:", err);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
assignRole,
createUser,
} from "@/core/application/services/userService";
import { createAssistant } from "@/core/application/services/assistantService";
import {
createAssistant,
getAssistantData,
} from "@/core/application/services/assistantService";
import { AuthMiddleware } from "../../middlewares/authorizationMiddleware";
import bearer from "@elysiajs/bearer";

Expand Down Expand Up @@ -35,7 +38,7 @@ assistants.post(
name,
fileIds: [],
tools: [],
instruction: body.instruction
instruction: body.instruction,
});

return {
Expand All @@ -48,8 +51,36 @@ assistants.post(
body: t.Object({
name: t.String(),
model: t.Literal("gpt-4"), // add more models here
instruction: t.String()
instruction: t.String(),
}),
beforeHandle: AuthMiddleware(["create_assistant", "*"]),
}
);

/**
* Based on the query for the endpoint fetch the appropriate details about the users assistants
*/
assistants.get(
"/assistant",
async ({ query, bearer }) => {
console.info("hit")
// only support ALL query for now
const decodedToken = await parseToken(bearer!);
if (!decodedToken) return;

const { userId } = decodedToken;

const assistantData = await getAssistantData({
query: query.query,
userId: userId,
});

return assistantData;
},
{
query: t.Object({
query: t.Literal("ALL"),
}),
beforeHandle: AuthMiddleware(["view_assistants", "*"]),
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export const threads = new Elysia<"/thread">().use(bearer());

threads.post(
"/thread",
async ({ bearer }) => {
async ({ bearer, body }) => {
const decodedToken = await parseToken(bearer!);

if (decodedToken) {
const { userId } = decodedToken;
// // Create a new thread
const threadId = await createThread({
name: body.thread_name,
id: ulid(), // Generate a unique ID for the thread
createdBy: userId,
participants: [], // Initialize the participants list
Expand All @@ -44,6 +45,9 @@ threads.post(
}
},
{
body: t.Object({
thread_name: t.String()
}),
beforeHandle: AuthMiddleware(["create_thread", "*"]),
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ describe.only("threadController", async () => {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
thread_name: "Akeru thread",
}),
});

const response = await app.handle(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ export async function createSuperAdmin(
}
}

/**
* Creates a new user with a random ID and assigns them the "super admin" role.
* Also creates the user in Redis.
* WARNING: This function should only be used in seeding scripts.
* @param {Object} userData - The data of the user to create.
* @returns {Promise<string>} A promise that resolves to the ID of the new user if the user was created and assigned the role successfully, or null otherwise.
*/
export async function createHumanUser(
userData: Object
): Promise<string | null> {
try {
const userId = ulid();
const userCreated = await createUser(userId, userData);
if (!userCreated) {
return null;
}

const roleAssigned = await assignRole(userId, "user");
if (!roleAssigned) {
return null;
}

// Create the user in Redis
await redis.set(`user:${userId}`, JSON.stringify(userData));

return userId;
} catch (err) {
console.error("Error creating super admin:", err);
return null;
}
}

/**
* Creates a new API token for a user.
* WARNING: This function should only be used in seeding scripts.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Assistant } from "@/core/domain/assistant";
import { User } from "@/core/domain/user";
import { redis } from "@/infrastructure/adapters/redisAdapter";

/**
Expand All @@ -15,7 +16,10 @@ export async function createAssistant(args: Assistant & { userId: string }) {
const pipeline = redis.pipeline();

// Store the assistant data
pipeline.set(`assistant:${id}`, JSON.stringify({ tools, model, name, instruction }));
pipeline.set(
`assistant:${id}`,
JSON.stringify({ tools, model, name, instruction, id })
);

// Store the relationship between the assistant and the user
pipeline.sadd(`user:${userId}:assistants`, id);
Expand All @@ -31,11 +35,30 @@ export async function createAssistant(args: Assistant & { userId: string }) {
}

// Parse the assistant data from JSON
return JSON.parse(assistantData);
return JSON.parse(assistantData) as Assistant;
}

export async function getAssistantData(assistant_id: Assistant["id"]) {
const assistantData = await redis.get(`assistant:${assistant_id}`);
export async function getAssistantData(args: {
query: Assistant["id"] | "ALL";
userId: User["id"];
}) {
const { query, userId } = args;

// if query is ALL then fetch all relevant assistant data
if (query === "ALL") {
const allAssistantData = await redis.smembers(`user:${userId}:assistants`);
return Promise.all(
allAssistantData.map(async (assistantId) => {
const assistantData = await redis.get(`assistant:${assistantId}`);
if (!assistantData) {
throw new Error("Failed to get assistant");
}
return JSON.parse(assistantData) as Assistant;
})
);
}

const assistantData = await redis.get(`user:${userId}:assistant:${query}`);
if (!assistantData) {
throw new Error("Failed to get assistant");
}
Expand Down
1 change: 1 addition & 0 deletions packages/akeru-server/src/core/domain/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const roles: Record<Role, { permissions: Permission[] }> = {
"view_own_threads",
"create_message_in_own_thread",
"create_assistant",
"view_assistants"
],
},
assistant: {
Expand Down
1 change: 1 addition & 0 deletions packages/akeru-server/src/core/domain/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { User } from "./user";

export type Thread = {
id: string; // The ID of the thread
name: string;
createdBy: string; // The ID of the user who created the thread
startDate?: Date; // The start date of the thread
participants: User['id'][]; // The IDs of the users participating in the thread
Expand Down
2 changes: 2 additions & 0 deletions packages/akeru-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Elysia } from "elysia";
import { threads } from "@/core/application/controllers/thread/threadController";
import { assistants } from "@/core/application/controllers/assistant/assistantController";
import { users } from "./core/application/controllers/user/userController";
import cors from "@elysiajs/cors";

export const name = "Akeru";

Expand All @@ -17,6 +18,7 @@ export const healthCheck = async () => {
};

export const app = new Elysia()
.use(cors())
.use(assistants)
.use(threads)
.use(users)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export const redisConfig = {
? { password: process.env.REDIS_PASSWORD }
: {}), // Redis password
db: process.env.REDIS_DB ? parseInt(process.env.REDIS_DB) : 0, // Redis DB
tls: {},
// tls: {},
};
7 changes: 6 additions & 1 deletion packages/app-akeru/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"geist": "^1.3.0",
"lucide-react": "^0.378.0",
"next": "14.2.3",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"sonner": "^1.4.41",
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
Loading
Loading