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

feat: create assistant #13

Merged
merged 1 commit into from
Jan 16, 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createSuperAdminForTesting } from "@/__tests__/utils";
import { app } from "@/index";
import { getNeo4jSession } from "@/infrastructure/adaptaters/neo4jAdapter";
import { test, expect, describe } from "bun:test";
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues";

describe("assistantController", async () => {
const token = await createSuperAdminForTesting();

test("allows creating a assistant and the assistant is saved in the database", async () => {
const name = "test assistant";
const request = new Request("http://localhost:8080/assistant", {
headers: {
authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
name,
}),
});

const response = await app.handle(request);
const responseJson: any = await response.json();

expect(responseJson).toHaveProperty("id");

const id = responseJson.id;

const session = getNeo4jSession();

const result = await session.run("MATCH (t:Assistant {id: $id}) RETURN t", {
id,
});

const singleRecord = result.records[0];
const node = singleRecord.get(0);
const idInDb = node.properties.id;
const nameInDb = node.properties.name;

expect(idInDb).toEqual(id);
expect(nameInDb).toBe(name);
});

test("prevents from creating a test if there is no api token present", async () => {
const request = new Request("http://localhost:3000/assistant", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "test assistant",
}),
});

const response: any = await app
.handle(request)
.then((response) => response.json());

expect(response.message).toBe(UNAUTHORIZED_MISSING_TOKEN.message);
});
});
73 changes: 73 additions & 0 deletions src/core/application/controllers/assistant/assistantController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Elysia, t } from "elysia";
import { ulid } from "ulid";

import { getTokenPermissions, parseToken } from "../../services/tokenService";
import { UNAUTHORIZED_NO_PERMISSION_CREATE_ASSISTANT } from "./returnValues";
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues";
import {
assignRole,
createUser,
} from "@/core/application/services/userService";
import { createAssistant } from "@/core/application/services/assistantService";

type AssistantDecorator = {
request: {
bearer: string | undefined;
};
store: {};
derive: {};
resolve: {};
};

export const assistants = new Elysia<"/assistant", AssistantDecorator>();

assistants.post(
"/assistant",
async ({ bearer, set, body }) => {
if (!bearer) {
set.status = 401;
return UNAUTHORIZED_MISSING_TOKEN;
}
const permissions = await getTokenPermissions(bearer!);
const decodedToken = await parseToken(bearer!);

if (
!permissions?.some((p) => p.key === "create_assistant" || p.key === "*")
) {
set.status = 403;
return UNAUTHORIZED_NO_PERMISSION_CREATE_ASSISTANT;
}

if (decodedToken) {
const { userId } = decodedToken;
const assistantId = ulid();

const { name } = body;

// create user for assistant
await createUser(assistantId, {});
// give user the proper role
await assignRole(assistantId, "agent");

// creat assistant in db
await createAssistant({
userId,
id: assistantId,
model: "gpt-4",
name,
fileIds: [],
tools: [],
});

return {
id: assistantId,
name,
};
}
},
{
body: t.Object({
name: t.String(),
}),
}
);
4 changes: 4 additions & 0 deletions src/core/application/controllers/assistant/returnValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const UNAUTHORIZED_NO_PERMISSION_CREATE_ASSISTANT = {
message: "Unauthorized: User does not have permission to create assistants",
code: 403,
};
4 changes: 0 additions & 4 deletions src/core/application/controllers/thread/returnValues.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export const UNAUTHORIZED_MISSING_TOKEN = {
message: "Unauthorized: Missing token",
code: 401,
};
export const UNAUTHORIZED_USER_NOT_PARTICIPANT = {
message: "Unauthorized: User is not a participant in the thread",
code: 403,
Expand Down
20 changes: 12 additions & 8 deletions src/core/application/controllers/thread/threadController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import {
} from "@/core/application/services/threadService";
import {
THREAD_DELETED_SUCCESSFULLY,
UNAUTHORIZED_MISSING_TOKEN,
UNAUTHORIZED_NO_PERMISSION_CREATE,
UNAUTHORIZED_NO_PERMISSION_DELETE,
UNAUTHORIZED_NO_PERMISSION_READ,
UNAUTHORIZED_USER_NOT_OWNER,
UNAUTHORIZED_USER_NOT_PARTICIPANT,
} from "./returnValues";
import { UNAUTHORIZED_MISSING_TOKEN } from "@/core/application/ports/returnValues";

type ThreadDecorator = {
request: {
Expand All @@ -37,6 +37,11 @@ threads.post("/thread", async ({ bearer, set }) => {
const permissions = await getTokenPermissions(bearer!);
const decodedToken = await parseToken(bearer!);

if (!permissions?.some((p) => p.key === "create_thread" || p.key === "*")) {
set.status = 403;
return UNAUTHORIZED_NO_PERMISSION_CREATE;
}

if (decodedToken) {
const { userId } = decodedToken;
// // Create a new thread
Expand All @@ -54,11 +59,6 @@ threads.post("/thread", async ({ bearer, set }) => {
};
}
}

if (!permissions?.some((p) => p.key === "create_thread" || p.key === "*")) {
set.status = 403;
return UNAUTHORIZED_NO_PERMISSION_CREATE;
}
});

threads.delete("/thread/:id", async ({ params, bearer, set }) => {
Expand All @@ -70,7 +70,9 @@ threads.delete("/thread/:id", async ({ params, bearer, set }) => {
const threadId = params.id;

// Check if the user has the permission to delete their own thread or * permission
if (permissions?.some((p) => p.key === "delete_thread" || p.key === "*")) {
if (
permissions?.some((p) => p.key === "delete_own_thread" || p.key === "*")
) {
// If the user has * permission, delete the thread without checking ownership
if (permissions.some((p) => p.key === "*")) {
await deleteThread(threadId, userId);
Expand Down Expand Up @@ -102,7 +104,9 @@ threads.get("/thread/:id", async ({ params, bearer, set }) => {
const threadId = params.id;

// Check if the user has the permission to see their own threads or * permission
if (permissions?.some((p) => p.key === "read_thread" || p.key === "*")) {
if (
permissions?.some((p) => p.key === "view_own_threads" || p.key === "*")
) {
// If the user has * permission or is a participant in the thread, get the thread
if (
permissions.some((p) => p.key === "*") ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createSuperAdminForTesting } from "@/__tests__/utils";
import { app } from "@/index";
import { getNeo4jSession } from "@/infrastructure/adaptaters/neo4jAdapter";
import { test, expect, describe } from "vitest";
import { UNAUTHORIZED_MISSING_TOKEN } from "./returnValues";
import { test, expect, describe } from "bun:test";
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues";

describe("threadController", async () => {
const token = await createSuperAdminForTesting();

test("allows creating a thread and the thread is saved in the database", async () => {
const request = new Request("http://localhost:3000/thread", {
const request = new Request("http://localhost:8080/thread", {
headers: {
authorization: `Bearer ${token}`,
},
Expand Down Expand Up @@ -36,7 +36,7 @@ describe("threadController", async () => {
});

test("prevents from creating a test if there is no api token present", async () => {
const request = new Request("http://localhost:3000/thread", {
const request = new Request("http://localhost:8080/thread", {
method: "POST",
});

Expand Down
17 changes: 0 additions & 17 deletions src/core/application/controllers/threadController.http

This file was deleted.

4 changes: 4 additions & 0 deletions src/core/application/ports/returnValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const UNAUTHORIZED_MISSING_TOKEN = {
message: "Unauthorized: Missing token",
code: 401,
};
40 changes: 40 additions & 0 deletions src/core/application/services/assistantService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Assistant } from "@/core/domain/assistant";
import { getNeo4jSession } from "@/infrastructure/adaptaters/neo4jAdapter";

/**
* Creates an assistant in Neo4j if it does not exist and adds a 'CREATED_BY' relationship to the user.
*
* @param {Assistant & { userId: string }} args - The assistant details and the user ID.
* @returns {Promise<Record<string, any>>} The properties of the created assistant.
* @throws {Error} If there is an error creating the assistant or adding the relationship.
*/
export async function createAssistant(args: Assistant & { userId: string }) {
const { id, fileIds, tools, userId, model, name } = args;

const session = getNeo4jSession();

// implenting file ids and tools will happen later. They should be relations
try {
const result = await session.run(
`
MERGE (a:Assistant {id: $assistantId})
ON CREATE SET a.tools = $assistantTools, a.model = $assistantModel, a.name = $assistantName
WITH a
MATCH (u:User {id: $userId})
MERGE (a)-[:CREATED_BY]->(u)
RETURN a
`,
{
assistantId: id,
assistantTools: tools,
assistantModel: model,
userId: userId,
assistantName: name,
}
);

return result.records[0].get("a").properties;
} finally {
await session.close();
}
}
3 changes: 2 additions & 1 deletion src/core/application/services/tokenService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redis } from "@/infrastructure/adaptaters/redisAdapter";
import { getUserPermissions } from "@/core/application/services/userService";
import jwt from "jsonwebtoken";
import { PermissionDetailArray } from "@/core/domain/permissions";

/**
* Creates a new API token for a user with their associated permissions.
Expand All @@ -25,7 +26,7 @@ export async function createToken(userId: string): Promise<string> {
*/
export async function getTokenPermissions(
token: string
): Promise<{ key: string; description: string }[] | null> {
): Promise<PermissionDetailArray | null> {
const tokenData = await redis.hget("api_tokens", token);
if (!tokenData) {
return null;
Expand Down
7 changes: 7 additions & 0 deletions src/core/domain/assistant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type Assistant = {
id: string;
name: string;
model: "gpt-4";
tools: { type: string }[];
fileIds: string[];
};
19 changes: 18 additions & 1 deletion src/core/domain/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ export type Permission =
| "create_document"
| "view_own_documents"
| "edit_own_documents"
| "link_own_documents";
| "link_own_documents"
| "create_assistant"
| "delete_assistant"
| "view_assistants";

export type PermissionDetails = {
description: string;
};

export type PermissionDetailArray = {
key: keyof typeof permissions;
description: string;
}[];

/**
* An object that defines a set of permissions.
* @type {Object.<string, {description: string}>}
Expand Down Expand Up @@ -75,6 +83,15 @@ export const permissions: Record<Permission, PermissionDetails> = {
link_own_documents: {
description: "Allows the user to link their own documents to a thread.",
},
create_assistant: {
description: "Allows the user to create an assistant.",
},
delete_assistant: {
description: "Allows the user to delete an assistant.",
},
view_assistants: {
description: "Allows the user to view all assistants.",
},
};

/**
Expand Down
5 changes: 5 additions & 0 deletions src/core/domain/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type User = {
id: string;
name: string;
description: string;
};
2 changes: 1 addition & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// test/index.test.ts
import { describe, expect, it } from "vitest";
import { describe, expect, it } from "bun:test";
import { app } from ".";

describe("Elysia", () => {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Elysia } from "elysia";
import { bearer } from "@elysiajs/bearer";

import { threads } from "@/core/application/controllers/thread/threadController";
import { assistants } from "@/core/application/controllers/assistant/assistantController";

export const name = "Sprout";

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

export const app = new Elysia()
.use(bearer())
.use(assistants)
.use(threads)
.get("/", healthCheck)
.listen(process.env.PORT || 8080);
Expand Down