├── .env-example ├── .gitignore ├── Procfile ├── __tests__ ├── cron │ ├── index.test.ts │ └── runningTasksAndAgents.test.ts ├── server │ └── api │ │ ├── agent │ │ └── tools │ │ │ └── findFile.tool.test.ts │ │ ├── aiCreatedCode │ │ └── aiCreatedCode.route.test.ts │ │ ├── codeCompletion │ │ └── codeCompletion.route.test.ts │ │ ├── codeDirectory │ │ ├── codeDirectory.route.test.ts │ │ └── codeDirectory.service.test.ts │ │ ├── codeFile │ │ └── codeFile.route.test.ts │ │ ├── codeSnippet │ │ └── codeSnippet.route.test.ts │ │ ├── message │ │ └── message.route.test.ts │ │ └── search │ │ └── search.route.test.ts └── utils │ ├── fileOperations.service.test.ts │ └── getFileName.test.ts ├── createServer.ts ├── cron ├── index.ts ├── runningTasksAndAgents.ts └── updatingDbCode.ts ├── docker-compose.yml ├── exo-config.json ├── jest.config.js ├── package.json ├── readme.md ├── schema.sql ├── server.ts ├── server ├── api │ ├── agent │ │ ├── agent.act.ts │ │ ├── agent.context.ts │ │ ├── agent.controller.ts │ │ ├── agent.prompt.ts │ │ ├── agent.routes.ts │ │ ├── agent.service.ts │ │ ├── autoAgents │ │ │ ├── addTests.agent.ts │ │ │ ├── handleLoopAgent.ts │ │ │ └── improveCodeAgent.ts │ │ ├── prompt.json │ │ └── tools │ │ │ ├── askUserAQuestion.prompt.ts │ │ │ ├── askUserAQuestion.tool.ts │ │ │ ├── finalAnswer.prompt.ts │ │ │ ├── finalAnswerTool.ts │ │ │ ├── findDirectory.prompt.ts │ │ │ ├── findDirectory.tool.ts │ │ │ ├── findFile.prompt.ts │ │ │ ├── findFile.tool.ts │ │ │ ├── generateNewCode.prompt.ts │ │ │ ├── generateNewCode.tool.ts │ │ │ ├── generateTestCode.tool.ts │ │ │ ├── getCode.prompt.ts │ │ │ ├── getCode.tool.ts │ │ │ ├── index.ts │ │ │ ├── retrieveMemory.tool.ts │ │ │ ├── searchCode.prompt.ts │ │ │ ├── searchCode.tool.ts │ │ │ ├── searchDirectory.prompt.ts │ │ │ ├── searchDirectory.tool.ts │ │ │ ├── searchFiles.tool.ts │ │ │ ├── searchTests.tool.ts │ │ │ ├── setLocationToWriteCode.prompt.ts │ │ │ ├── setLocationToWriteCode.tool.ts │ │ │ ├── storeMemory.tool.ts │ │ │ ├── updateExistingCode.prompt.ts │ │ │ ├── updateExistingCode.tool.ts │ │ │ ├── writeCodeToScarchPad.tool.ts │ │ │ └── writeCompletedCode.tool.ts │ ├── aiCreatedCode │ │ ├── aiCreatedCode.controller.ts │ │ ├── aiCreatedCode.repository.ts │ │ ├── aiCreatedCode.routes.ts │ │ └── aiCreatedCode.service.ts │ ├── codeCompletion │ │ ├── codeCompletion.classifier.ts │ │ ├── codeCompletion.controller.ts │ │ ├── codeCompletion.prompts.ts │ │ ├── codeCompletion.routes.ts │ │ ├── codeCompletion.rules.ts │ │ ├── codeCompletion.service.ts │ │ ├── codeCompletion.types.ts │ │ └── scenerios │ │ │ ├── codeCompletion.knowFuncAndLoc.ts │ │ │ ├── codeCompletion.knowLocNotFunc.ts │ │ │ ├── codeCompletion.knownNextAction.ts │ │ │ └── codeCompletion.updateExisting.ts │ ├── codeDirectory │ │ ├── codeDirectory.controller.ts │ │ ├── codeDirectory.repository.ts │ │ ├── codeDirectory.routes.ts │ │ ├── codeDirectory.service.ts │ │ └── codeDirectory.types.ts │ ├── codeFile │ │ ├── codeFile.controller.ts │ │ ├── codeFile.repository.ts │ │ ├── codeFile.routes.ts │ │ ├── codeFile.service.ts │ │ └── codeFile.type.ts │ ├── codeSnippet │ │ ├── codeSnippet.controller.ts │ │ ├── codeSnippet.repository.ts │ │ ├── codeSnippet.routes.ts │ │ ├── codeSnippet.service.ts │ │ └── codeSnippet.type.ts │ ├── exoConfig │ │ ├── example.md │ │ ├── exoConfig.controller.ts │ │ ├── exoConfig.routes.ts │ │ ├── exoConfig.service.ts │ │ └── templateExoConfig.json │ ├── exportImportMap │ │ ├── exportImportMap.controller.ts │ │ ├── exportImportMap.repository.ts │ │ ├── exportImportMap.routes.ts │ │ └── exportImportMap.service.ts │ ├── github │ │ ├── github.controller.ts │ │ ├── github.repository.ts │ │ ├── github.routes.ts │ │ └── github.service.ts │ ├── memory │ │ ├── memory.controller.ts │ │ ├── memory.routes.ts │ │ └── memory.service.ts │ ├── message │ │ ├── message.controller.ts │ │ ├── message.routes.ts │ │ └── message.service.ts │ ├── objective │ │ ├── objective.controller.ts │ │ ├── objective.repository.ts │ │ ├── objective.routes.ts │ │ ├── objective.service.ts │ │ └── objective.types.ts │ ├── openAi │ │ ├── openAi.controller.ts │ │ ├── openAi.repository.ts │ │ └── openai.service.ts │ ├── prompt │ │ ├── prompt.controller.ts │ │ ├── prompt.routes.ts │ │ └── prompt.service.ts │ ├── search │ │ ├── search.controller.ts │ │ ├── search.repository.ts │ │ ├── search.routes.ts │ │ └── search.service.ts │ ├── session │ │ ├── session.controller.ts │ │ ├── session.repository.ts │ │ ├── session.routes.ts │ │ ├── session.service.ts │ │ └── session.type.ts │ ├── supabase │ │ ├── account.service.ts │ │ └── supabase.service.ts │ └── task │ │ ├── task.controller.ts │ │ ├── task.repository.ts │ │ ├── task.routes.ts │ │ ├── task.service.ts │ │ └── task.types.ts └── middleware │ ├── githubWebhookAuth.ts │ └── isAuthenticated.ts ├── side-by-side.png ├── tsconfig.json ├── types ├── chatMessage.type.ts ├── openAiTypes │ ├── openAiBaseModel.ts │ ├── openAiCompletionReqRes.ts │ └── openAiEngine.ts ├── parseCode.types.ts ├── supabase.ts └── treeSitterTypes.json ├── utils ├── appendFile.ts ├── chunkString.ts ├── commandLineColors.ts ├── commandLineLoadingl.ts ├── createfile.ts ├── dates.ts ├── deserializeJson.ts ├── envVariable.ts ├── fileOperations.service.ts ├── findArrayInAString.ts ├── findTextAfterComment.ts ├── funnyErrorMessage.ts ├── generateCode.ts ├── generateTestFileName.ts ├── getFileName.ts ├── getMethodName.ts ├── getSubstringFromMultilineCode.ts ├── getTimestamp.ts ├── getUniqueNumbers.ts ├── git.service.ts ├── iterateOverFolders.ts ├── openAi.ts ├── removeTextAfterString.ts └── treeSitter.ts └── yarn.lock /.env-example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | SUPABASE_ANON=Your-supabase-key-here 3 | SUPABASE_URL=Your-supabase-url-here 4 | SUPABASE_DB_ID=Your-supabase-db-id-here 5 | SUPABASE_DATABASE_PASSWORD=Your-supabase-db-password-here 6 | OPENAI_API_KEY=Your-openai-api-key-here 7 | PORT=8081 8 | GOOGLE_API_KEY=Optional 9 | CUSTOM_SEARCH_ENGINE_ID=Optional 10 | SLACK_BOT_TOKEN=Optional 11 | SLACK_SIGNING_SECRET=Optional 12 | EXO_GITHUB_APP_WEBHOOK_SECRET=Optional 13 | GITHUB_EXO_APP_ID=Optional 14 | GITHUB_PRIVATE_KEY=Optional -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .env -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server.js -------------------------------------------------------------------------------- /__tests__/cron/index.test.ts: -------------------------------------------------------------------------------- 1 | import { runScheduledTasks } from "../../cron"; 2 | import { runTasksAndAgents } from "../../cron/runningTasksAndAgents"; 3 | import { runUpdateCodeDbEntries } from "../../cron/updatingDbCode"; 4 | 5 | jest.mock("../../cron/runningTasksAndAgents", () => ({ 6 | runTasksAndAgents: jest.fn(), 7 | })); 8 | jest.mock("../../cron/updatingDbCode", () => ({ 9 | runUpdateCodeDbEntries: jest.fn(), 10 | })); 11 | 12 | describe("Scheduled tasks", () => { 13 | beforeEach(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | 17 | it("should call runUpdateCodeDbEntries and runTasksAndAgents on start", () => { 18 | runScheduledTasks(); 19 | 20 | expect(runUpdateCodeDbEntries).toHaveBeenCalled(); 21 | expect(runTasksAndAgents).toHaveBeenCalled(); 22 | }); 23 | }); 24 | // ``` 25 | 26 | // Replace `'../path/to/your/destination'` with the actual path to your file containing `runScheduledTasks`. The tests for the cron schedules have been commented out since they will require you to implement the test functionality based on the cron library you are using. You can uncomment and implement those tests based on your requirements for testing cron schedules. 27 | -------------------------------------------------------------------------------- /__tests__/cron/runningTasksAndAgents.test.ts: -------------------------------------------------------------------------------- 1 | import { findCompletedTasksThatNeedLoops } from "../../server/api/agent/agent.context"; 2 | import { findAndExecuteTasks } from "../../server/api/task/task.service"; 3 | import cron from "node-cron"; 4 | import { runTasksAndAgents } from "../../cron/runningTasksAndAgents"; 5 | import { createServer } from "../../createServer"; 6 | 7 | jest.mock("../../server", () => ({ 8 | supabase: { 9 | rpc: jest.fn(), 10 | }, 11 | })); 12 | 13 | jest.mock("node-cron", () => ({ 14 | schedule: jest.fn(), 15 | })); 16 | jest.mock("../../server/api/task/task.service"); 17 | jest.mock("../../server/api/agent/agent.context"); 18 | 19 | const app = createServer(); 20 | 21 | describe("runTasksAndAgents", () => { 22 | afterEach(() => { 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | it("schedules a cron job to run every 10 seconds", () => { 27 | runTasksAndAgents(); 28 | 29 | expect(cron.schedule).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it("calls findAndExecuteTasks and findCompletedTasksThatNeedLoops inside the cron task", () => { 33 | const mockCronFunction = jest.fn(); 34 | // @ts-ignore 35 | cron.schedule.mockImplementation((schedule, fn) => { 36 | mockCronFunction.mockImplementation(fn); 37 | }); 38 | 39 | runTasksAndAgents(); 40 | expect(mockCronFunction).toHaveBeenCalledTimes(0); 41 | 42 | mockCronFunction(); 43 | expect(findAndExecuteTasks).toHaveBeenCalledTimes(1); 44 | expect(findCompletedTasksThatNeedLoops).toHaveBeenCalledTimes(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/server/api/agent/tools/findFile.tool.test.ts: -------------------------------------------------------------------------------- 1 | import { findFileTool } from "../../../../../server/api/agent/tools/findFile.tool"; 2 | import { findCodeByQuery } from "../../../../../server/api/search/search.service"; 3 | import { findOrUpdateAccount } from "../../../../../server/api/supabase/account.service"; 4 | import { createServer } from "../../../../../createServer"; 5 | 6 | jest.mock("../../../../../server/api/search/search.service"); 7 | jest.mock("../../../../../server/api/supabase/account.service"); 8 | jest.mock("../../../../../server/api/agent/tools/index", () => ({ 9 | ToolName: { 10 | searchCode: "searchCode", 11 | }, 12 | })); 13 | 14 | jest.mock("../../../../../server", () => ({ 15 | supabase: { 16 | rpc: jest.fn(), 17 | }, 18 | })); 19 | 20 | const app = createServer(); 21 | 22 | describe("findFileTool", () => { 23 | afterEach(() => { 24 | jest.resetAllMocks(); 25 | jest.clearAllTimers(); 26 | }); 27 | 28 | it("should return file name and location when a matching file is found", async () => { 29 | const mockedFindCodeByQuery = findCodeByQuery as jest.MockedFunction< 30 | typeof findCodeByQuery 31 | >; 32 | mockedFindCodeByQuery.mockResolvedValue([ 33 | { 34 | file_name: "testFile.js", 35 | relative_file_path: "/path/to/testFile.js", 36 | }, 37 | ]); 38 | const mockedFindOrUpdateAccount = 39 | findOrUpdateAccount as jest.MockedFunction; 40 | mockedFindOrUpdateAccount.mockResolvedValue({ 41 | id: "accountId123", 42 | created_at: "2021-08-01T00:00:00.000Z", 43 | user_id: "userId123", 44 | }); 45 | 46 | const tool = findFileTool(); 47 | const result = await tool.use("userId123", "sessionId123", "testFile.js"); 48 | 49 | expect(mockedFindOrUpdateAccount).toHaveBeenCalledWith("userId123"); 50 | expect(mockedFindCodeByQuery).toHaveBeenCalledWith( 51 | "testFile.js", 52 | "accountId123" 53 | ); 54 | expect(result.output).toEqual( 55 | "The file name that best matches this search is: testFile.js which is located at: /path/to/testFile.js." 56 | ); 57 | expect(result.metadata).toEqual([ 58 | { 59 | file_name: "testFile.js", 60 | relative_file_path: "/path/to/testFile.js", 61 | }, 62 | ]); 63 | }); 64 | 65 | it("should return 'No results found' when no matching file is found", async () => { 66 | const mockedFindCodeByQuery = findCodeByQuery as jest.MockedFunction< 67 | typeof findCodeByQuery 68 | >; 69 | mockedFindCodeByQuery.mockResolvedValue([]); 70 | const mockedFindOrUpdateAccount = 71 | findOrUpdateAccount as jest.MockedFunction; 72 | mockedFindOrUpdateAccount.mockResolvedValue({ 73 | id: "accountId123", 74 | created_at: "2021-08-01T00:00:00.000Z", 75 | user_id: "userId123", 76 | }); 77 | 78 | const tool = findFileTool(); 79 | const result = await tool.use( 80 | "userId123", 81 | "sessionId123", 82 | "nonExistent.js" 83 | ); 84 | 85 | expect(mockedFindOrUpdateAccount).toHaveBeenCalledWith("userId123"); 86 | expect(mockedFindCodeByQuery).toHaveBeenCalledWith( 87 | "nonExistent.js", 88 | "accountId123" 89 | ); 90 | 91 | expect(result.output).toEqual( 92 | "No results found. Try adapting your search query." 93 | ); 94 | expect(result.metadata).toEqual([]); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /__tests__/server/api/aiCreatedCode/aiCreatedCode.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe("GET /ai-completed-code", () => { 14 | describe("when requesting without a session", () => { 15 | it("returns 403", async () => { 16 | await supertest(app).get("/ai-completed-code").expect(403); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("Search API", () => { 23 | describe("POST /ai-completed-code", () => { 24 | describe("when uploading files without session", () => { 25 | it("returns 403", async () => { 26 | const { body, statusCode } = await supertest(app) 27 | .post("/ai-completed-code") 28 | .expect(403); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/server/api/codeCompletion/codeCompletion.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Code completion API", () => { 13 | describe("POST /code", () => { 14 | describe("when submittin without session", () => { 15 | it("returns 403", async () => { 16 | const { body, statusCode } = await supertest(app) 17 | .post("/code") 18 | .expect(403); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/server/api/codeDirectory/codeDirectory.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe("GET /code-directory", () => { 14 | describe("when requesting without a session", () => { 15 | it("returns 403", async () => { 16 | await supertest(app).get("/code-directory").expect(403); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("Search API", () => { 23 | describe("POST /code-directory", () => { 24 | describe("when uploading directories without session", () => { 25 | it("returns 403", async () => { 26 | const { body, statusCode } = await supertest(app) 27 | .post("/code-directory") 28 | .expect(403); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/server/api/codeDirectory/codeDirectory.service.test.ts: -------------------------------------------------------------------------------- 1 | // To write a test for the code above, you can use a testing framework like Jest. First, you need to install and set up Jest in your project. Once you have set up Jest, you can create a test file like `codeFile.test.ts`. Then you can write the test cases for the functions in the provided code. 2 | 3 | // Here is an example test for the `createDirectoryIfNotExists` function: 4 | 5 | // ```typescript 6 | 7 | import { 8 | createCodeDirectoryByUser, 9 | findCodeDirectoryByNameAndUser, 10 | } from "../../../../server/api/codeDirectory/codeDirectory.repository"; 11 | import { createDirectoryIfNotExists } from "../../../../server/api/codeDirectory/codeDirectory.service"; 12 | 13 | // Mock the functions from the repository 14 | jest.mock( 15 | "../../../../server/api/codeDirectory/codeDirectory.repository", 16 | () => ({ 17 | findCodeDirectoryByNameAndUser: jest.fn(), 18 | createCodeDirectoryByUser: jest.fn(), 19 | }) 20 | ); 21 | 22 | describe("codeFile", () => { 23 | afterEach(() => { 24 | jest.clearAllMocks(); // Clear mocks after each test 25 | }); 26 | 27 | describe("createDirectoryIfNotExists", () => { 28 | it("should create the code directory if not exists", async () => { 29 | (findCodeDirectoryByNameAndUser as jest.Mock).mockResolvedValueOnce(null); 30 | 31 | await createDirectoryIfNotExists( 32 | "testUserId", 33 | "testFilePath", 34 | "testDirectoryName", 35 | true 36 | ); 37 | 38 | expect(findCodeDirectoryByNameAndUser).toHaveBeenCalledWith( 39 | "testUserId", 40 | "testDirectoryName" 41 | ); 42 | expect(createCodeDirectoryByUser).toHaveBeenCalledWith( 43 | "testUserId", 44 | "testFilePath", 45 | "testDirectoryName", 46 | true 47 | ); 48 | }); 49 | 50 | it("should not create the code directory if it exists", async () => { 51 | (findCodeDirectoryByNameAndUser as jest.Mock).mockResolvedValueOnce({ 52 | id: 1, 53 | userId: "testUserId", 54 | filePath: "testFilePath", 55 | directoryName: "testDirectoryName", 56 | saved: true, 57 | }); 58 | 59 | await createDirectoryIfNotExists( 60 | "testUserId", 61 | "testFilePath", 62 | "testDirectoryName", 63 | true 64 | ); 65 | 66 | expect(findCodeDirectoryByNameAndUser).toHaveBeenCalledWith( 67 | "testUserId", 68 | "testDirectoryName" 69 | ); 70 | expect(createCodeDirectoryByUser).toHaveBeenCalledTimes(0); 71 | }); 72 | }); 73 | 74 | // Add more tests for the other functions 75 | }); 76 | // ``` 77 | 78 | // In this test, we create two test cases for the `createDirectoryIfNotExists` function. The first test checks if the function creates a new code directory when it doesn't exist. The second test checks if the function doesn't create a new code directory when it already exists. 79 | 80 | // Similarly, you can write tests for other functions like `findFilesForSavedDirectories` and `getSavedCodeDirectoriesByAccount`. To do this, you would mock the required dependencies and use Jest's assertions to verify the correct behavior. 81 | -------------------------------------------------------------------------------- /__tests__/server/api/codeFile/codeFile.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe("GET /code-file", () => { 14 | describe("when adding files without a session", () => { 15 | it("returns 403", async () => { 16 | await supertest(app).post("/code-file").expect(403); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("Search API", () => { 23 | describe("POST /code-file/add", () => { 24 | describe("when uploading files without session", () => { 25 | it("returns 403", async () => { 26 | const { body, statusCode } = await supertest(app) 27 | .post("/code-file/add") 28 | .expect(403); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/server/api/codeSnippet/codeSnippet.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe("GET /code-snippet", () => { 14 | describe("when requesting snippets without a session", () => { 15 | it("returns 403", async () => { 16 | await supertest(app).get("/code-snippet").expect(403); 17 | }); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/server/api/message/message.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { ApiRoutes, createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe(`GET ${ApiRoutes.MESSAGES}`, () => { 14 | describe("when getting a message without a session", () => { 15 | it("returns 403", async () => { 16 | await supertest(app).get(ApiRoutes.MESSAGES).expect(403); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("Search API", () => { 23 | describe(`POST ${ApiRoutes.MESSAGES}`, () => { 24 | describe("when posting a message without session", () => { 25 | it("returns 403", async () => { 26 | await supertest(app).post(ApiRoutes.MESSAGES).expect(403); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/server/api/search/search.route.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../../../../createServer"; 3 | 4 | jest.mock("../../../../server", () => ({ 5 | supabase: { 6 | rpc: jest.fn(), 7 | }, 8 | })); 9 | 10 | const app = createServer(); 11 | 12 | describe("Search API", () => { 13 | describe("GET /search", () => { 14 | describe("when the query is not present return 404", () => { 15 | it("returns 404", async () => { 16 | await supertest(app).get("/search").expect(404); 17 | }); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("Search API", () => { 23 | describe("POST /search", () => { 24 | describe("when uploading files without session", () => { 25 | it("returns 403", async () => { 26 | const { body, statusCode } = await supertest(app) 27 | .post("/search") 28 | .expect(403); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/utils/fileOperations.service.test.ts: -------------------------------------------------------------------------------- 1 | // First, you will need to install a testing library like Jest. After installing and setting up Jest, create a new test file with the test for the provided code. Here is an example of a test for the provided code: 2 | 3 | // ```javascript 4 | // Importing required modules and functions 5 | import * as fs from "fs"; 6 | import { findFileAndReturnContents } from "../../utils/fileOperations.service"; 7 | 8 | // Mocking the fs.readFileSync function 9 | jest.mock("fs"); 10 | (fs.readFileSync as jest.Mock).mockImplementation(() => "file contents"); 11 | 12 | describe("findFileAndReturnContents", () => { 13 | it("should find a file and return its contents", () => { 14 | // Test file path 15 | const filePath = "test-file.txt"; 16 | 17 | // Call the findFileAndReturnContents function with the test file path 18 | const result = findFileAndReturnContents(filePath); 19 | 20 | // Check if fs.readFileSync was called with the correct file path and encoding 21 | expect(fs.readFileSync).toHaveBeenCalledWith(filePath, "utf8"); 22 | 23 | // Check if the result is the expected file contents 24 | expect(result).toBe("file contents"); 25 | }); 26 | }); 27 | // ``` 28 | 29 | // Here we are using Jest to create a test for the `findFileAndReturnContents` function. We are mocking the `fs.readFileSync` function to avoid reading from an actual file and control the return value. 30 | 31 | // This test checks that the `findFileAndReturnContents` function calls the `fs.readFileSync` method with the correct parameters and returns the expected data. 32 | -------------------------------------------------------------------------------- /__tests__/utils/getFileName.test.ts: -------------------------------------------------------------------------------- 1 | // ```javascript 2 | 3 | import { convertToExoSuggestionFileName } from "../../utils/getFileName"; 4 | 5 | describe("convertToExoSuggestionFileName", () => { 6 | it("should correctly convert given file name to ExoSuggestion format", () => { 7 | const fileName = "sample-document.pdf"; 8 | const expectedResult = "sample-document.exo-suggestion.pdf"; 9 | 10 | const result = convertToExoSuggestionFileName(fileName); 11 | 12 | expect(result).toBe(expectedResult); 13 | }); 14 | 15 | it("should handle files with multiple dots in name", () => { 16 | const fileName = "example.file.json"; 17 | const expectedResult = "example.file.exo-suggestion.json"; 18 | 19 | const result = convertToExoSuggestionFileName(fileName); 20 | 21 | expect(result).toBe(expectedResult); 22 | }); 23 | 24 | it("should handle files without a file extension", () => { 25 | const fileName = "no-extension-file"; 26 | const expectedResult = "no-extension-file.exo-suggestion"; 27 | 28 | const result = convertToExoSuggestionFileName(fileName); 29 | 30 | expect(result).toBe(expectedResult); 31 | }); 32 | 33 | it("should handle empty strings", () => { 34 | const fileName = ""; 35 | const expectedResult = ""; 36 | 37 | const result = convertToExoSuggestionFileName(fileName); 38 | 39 | expect(result).toBe(expectedResult); 40 | }); 41 | }); 42 | // ``` 43 | -------------------------------------------------------------------------------- /createServer.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import cors from "cors"; 3 | import express, { Express, Request, Response, NextFunction } from "express"; 4 | import agentRouter from "./server/api/agent/agent.routes"; 5 | import aiCreatedCode from "./server/api/aiCreatedCode/aiCreatedCode.routes"; 6 | import codeCompletionRoutes from "./server/api/codeCompletion/codeCompletion.routes"; 7 | import codeDirectoryRoutes from "./server/api/codeDirectory/codeDirectory.routes"; 8 | import codeFileRoutes from "./server/api/codeFile/codeFile.routes"; 9 | import codeSnippetRoutes from "./server/api/codeSnippet/codeSnippet.routes"; 10 | import exoConfigRoutes from "./server/api/exoConfig/exoConfig.routes"; 11 | import messageRoutes from "./server/api/message/message.routes"; 12 | import promptRoutes from "./server/api/prompt/prompt.routes"; 13 | import searchRoutes from "./server/api/search/search.routes"; 14 | import { runScheduledTasks } from "./cron"; 15 | import taskRoutes from "./server/api/task/task.routes"; 16 | import githubRoutes from "./server/api/github/github.routes"; 17 | import { isProduction } from "./utils/envVariable"; 18 | const SmeeClient = require("smee-client"); 19 | // const { Client } = require("pg"); 20 | 21 | export enum ApiRoutes { 22 | CODE_DIRECTORY = "/code-directory", 23 | MESSAGES = "/messages", 24 | CODE_COMPLETION = "/code", 25 | AI_COMPLETED_CODE = "/ai-completed-code", 26 | AGENT = "/agent", 27 | SEARCH = "/search", 28 | PROMPT = "/prompt", 29 | EXO_CONFIG = "/exo-config", 30 | CODE_SNIPPET = "/code-snippet", 31 | CODE_FILE = "/code-file", 32 | TASK = "/task", 33 | GITHUB = "/github", 34 | } 35 | 36 | export function createServer() { 37 | const app: Express = express(); 38 | 39 | runScheduledTasks(); 40 | 41 | // TODO - Start of connection to postgres via docker 42 | // const client = new Client({ 43 | // host: "localhost", 44 | // port: 5432, 45 | // database: "exo", 46 | // user: "kg", 47 | // password: "password", 48 | // }); 49 | 50 | // client.connect((err: { stack: any }) => { 51 | // if (err) { 52 | // console.error("connection error", err.stack); 53 | // } else { 54 | // console.log("connected"); 55 | // } 56 | // }); 57 | 58 | // Will also need to add this to package.json 59 | // "predev": "docker-compose up -d" 60 | 61 | var corsOptions = { 62 | origin: "*", 63 | }; 64 | 65 | app.use( 66 | bodyParser.json({ 67 | limit: "50mb", 68 | verify: (req: Request, res: Response, buf: Buffer, encoding: string) => { 69 | if (buf && buf.length) { 70 | // @ts-ignore 71 | req.rawBody = buf.toString(encoding || "utf8"); 72 | } 73 | }, 74 | }) 75 | ); 76 | 77 | if (!isProduction && false) { 78 | const smee = new SmeeClient({ 79 | source: "https://smee.io/SfInyn7aN4zyGqPs", 80 | target: "http://localhost:8081/github", 81 | logger: console, 82 | }); 83 | 84 | const events = smee.start(); 85 | // // Stop forwarding events 86 | // events.close(); 87 | } 88 | 89 | app.use(cors(corsOptions)); 90 | 91 | app.use(ApiRoutes.AI_COMPLETED_CODE, aiCreatedCode); 92 | app.use(ApiRoutes.CODE_DIRECTORY, codeDirectoryRoutes); 93 | app.use(ApiRoutes.CODE_FILE, codeFileRoutes); 94 | app.use(ApiRoutes.CODE_SNIPPET, codeSnippetRoutes); 95 | app.use(ApiRoutes.MESSAGES, messageRoutes); 96 | app.use(ApiRoutes.CODE_COMPLETION, codeCompletionRoutes); 97 | app.use(ApiRoutes.AGENT, agentRouter); 98 | app.use(ApiRoutes.SEARCH, searchRoutes); 99 | app.use(ApiRoutes.PROMPT, promptRoutes); 100 | app.use(ApiRoutes.EXO_CONFIG, exoConfigRoutes); 101 | app.use(ApiRoutes.TASK, taskRoutes); 102 | app.use(ApiRoutes.GITHUB, githubRoutes); 103 | return app; 104 | } 105 | -------------------------------------------------------------------------------- /cron/index.ts: -------------------------------------------------------------------------------- 1 | import { findAndAddTestAgent } from "../server/api/agent/autoAgents/addTests.agent"; 2 | import { improveCodeAgent } from "../server/api/agent/autoAgents/improveCodeAgent"; 3 | import { 4 | findAllImportStatements, 5 | matchExportsInSnippetBody, 6 | updateCodeSnippetNames, 7 | } from "../server/api/codeSnippet/codeSnippet.service"; 8 | import { findCodeNodes } from "../server/api/exportImportMap/exportImportMap.service"; 9 | import { logInfo } from "../utils/commandLineColors"; 10 | import { runTasksAndAgents } from "./runningTasksAndAgents"; 11 | import { runUpdateCodeDbEntries } from "./updatingDbCode"; 12 | 13 | const cron = require("node-cron"); 14 | 15 | export const runScheduledTasks = () => { 16 | runUpdateCodeDbEntries(); 17 | runTasksAndAgents(); 18 | // Maps imports and exports (many to many) in the db. 19 | // cron.schedule("*/12 * * * * ", () => { 20 | // console.log("Running match imports with exports script"); 21 | // findAllImportStatements(); 22 | // }); 23 | // This gets the export snippets that are imported the most 24 | // cron.schedule("*/12 * * * * ", () => { 25 | // console.log("Running finding code nodes script"); 26 | // findCodeNodes(); 27 | // }); 28 | // cron.schedule("*/2 * * * * ", () => { 29 | // findAndAddTestAgent(); 30 | // }); 31 | // cron.schedule("*/10 * * * * * ", () => { 32 | // logInfo("Running improve code script"); 33 | // improveCodeAgent(); 34 | // }); 35 | // Looks at all code snippets code and finds all methods that are imported into the snippet. 36 | // cron.schedule("*/12 * * * *", () => { 37 | // console.log("Running match exports in snippet body script"); 38 | // matchExportsInSnippetBody(); 39 | // }); 40 | }; 41 | -------------------------------------------------------------------------------- /cron/runningTasksAndAgents.ts: -------------------------------------------------------------------------------- 1 | import { findCompletedTasksThatNeedLoops } from "../server/api/agent/agent.context"; 2 | import { findAndExecuteTasks } from "../server/api/task/task.service"; 3 | 4 | const cron = require("node-cron"); 5 | 6 | export function runTasksAndAgents() { 7 | cron.schedule("*/10 * * * * *", () => { 8 | // This find tasks that have been created by agents and executes them. 9 | findAndExecuteTasks(); 10 | 11 | // Find completed task and ask the user for next steps. 12 | findCompletedTasksThatNeedLoops(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /cron/updatingDbCode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | findFilesForSavedDirectories, 3 | findMissingDirectoryNodes, 4 | getFilesAndMapToDirectories, 5 | updateDirectoryExplaination, 6 | } from "../server/api/codeDirectory/codeDirectory.service"; 7 | import { 8 | findAndAddDependenciesPerFile, 9 | findFilesWithoutExplainationAndAssignExplaination, 10 | } from "../server/api/codeFile/codeFile.service"; 11 | import { 12 | findSnippetsWithoutFilesAndAssignFiles, 13 | updateCodeSnippetNames, 14 | } from "../server/api/codeSnippet/codeSnippet.service"; 15 | 16 | const cron = require("node-cron"); 17 | 18 | export const runUpdateCodeDbEntries = () => { 19 | cron.schedule("*/6 * * * *", () => { 20 | // Subdirectories are not uploaded to the database. This finds all subdirectories and uploads them to the database. This looks at the code files and finds any subdirectories that are not in the database. 21 | findMissingDirectoryNodes(); 22 | }); 23 | 24 | cron.schedule("*/5 * * * *", () => { 25 | // Finds all the files in a directory and uses the explaination to update the directory explaination. 26 | updateDirectoryExplaination(); 27 | }); 28 | 29 | cron.schedule("*/8 * * * * ", () => { 30 | // This finds all files without a directory id and assigns it 31 | getFilesAndMapToDirectories(); 32 | }); 33 | 34 | cron.schedule("*/10 * * * *", () => { 35 | console.log("Running find snippets without files and assign files"); 36 | // Often a code snippet and the code file are not linked together when they are created. This finds all snippets without files and assigns them to the correct file. It creates a new file if it doesn't exist. 37 | findSnippetsWithoutFilesAndAssignFiles(); 38 | }); 39 | 40 | cron.schedule("*/12 * * * *", () => { 41 | // When files are added the explanation is not run becuase it requires the list of snippets to have explanation first. This finds all files without explanation and assigns them an explanation. 42 | findFilesWithoutExplainationAndAssignExplaination(); 43 | }); 44 | 45 | cron.schedule("*/15 * * * *", () => { 46 | // This matches any files with directories that have been saved by the user. 47 | findFilesForSavedDirectories(); 48 | 49 | // Some snippets are created but the methods are not named. This finds all snippets without names and assigns them a name. 50 | //TODO - makes sure snippets are named when they are created 51 | updateCodeSnippetNames(); 52 | }); 53 | 54 | cron.schedule("*/5 * * * * ", () => { 55 | // This creates the d.ts contents for each file in the database - it stores the contents of the d.ts file in the database (vs actually creating the d.ts file). 56 | findAndAddDependenciesPerFile(); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # TODO - This is how you can run the db locally with docker. But this will not work without running the supabase client locally. 2 | 3 | version: '3' 4 | 5 | services: 6 | db: 7 | image: postgres:15 8 | restart: always 9 | ports: 10 | - "5432:5432" 11 | environment: 12 | - POSTGRES_USER=kg 13 | - POSTGRES_PASSWORD=password 14 | - POSTGRES_DB=exo 15 | volumes: 16 | - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql -------------------------------------------------------------------------------- /exo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "directoryName": "code-gen-server", 3 | "explanation": "This directory, code-gen-server, is an express server for the Exo project. The server has endpoints that handle CRUD methods for the various Exo database entities. The server also has a cron scheduler that handles async task such as running async agent tasks and updating code.", 4 | "codeStandards": [ 5 | "Use function composition: Function composition is the process of combining two or more functions to produce a new function. Function composition can simplify complex code by breaking it down into smaller, reusable functions.", 6 | "Use Typings: Use typings in your code to ensure that it is properly typed. This will help you in avoiding errors and also make your code more readable.", 7 | "Documentation: Include proper documentation in your TS files. This will help other developers to understand your code.", 8 | "Code Linting: Use a code linter to ensure that your code follows the coding standards. This will help you to write clean and consistent code\n", 9 | "Use Interfaces: Use interfaces to define the structure of data that is passed between functions in your code.", 10 | "Use TSLint: Use TSLint to lint your code and enforce coding standards.", 11 | "Use Modules and Dependencies Properly: Use modules and dependencies properly. This will help you to manage your code easily and also make it more maintainable.", 12 | "Avoid Using any: Avoid using any in your code as much as possible. Use the most specific type that you can instead.", 13 | "Use pure functions: A pure function is a function that always returns the same output for the same input, without any side effects. Pure functions are easier to reason about, test, and compose, making your code more functional and modular" 14 | ], 15 | "testFrameworks": [] 16 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: ['**/**/*.test.ts'], 6 | verbose: true, 7 | forceExit: true, 8 | clearMocks: true, 9 | maxWorkers: 2, 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exo-server", 3 | "version": "1.0.0", 4 | "description": "Server to support the Exo electron app", 5 | "main": "index.ts", 6 | "author": "Kevin Grassi", 7 | "license": "MIT", 8 | "private": true, 9 | "engines": { 10 | "node": "18.x", 11 | "npm": ">=6" 12 | }, 13 | "scripts": { 14 | "prestart": "yarn run build", 15 | "start": "node dist/server.js", 16 | "dev": "tsc --watch & nodemon dist/server.js", 17 | "generate-db-types": "npx supabase gen types typescript --project-id xexjtohvdexqxpomspdb --schema public > types/supabase.ts", 18 | "tsc": "./node_modules/typescript/bin/tsc", 19 | "build": "tsc", 20 | "postinstall": "npm run build", 21 | "test": "jest --watchAll --verbose --forceExit", 22 | "create-db-schema": "source .env && psql --single-transaction --variable ON_ERROR_STOP=1 --file schema.sql --dbname \"postgresql://postgres:\"$SUPABASE_DATABASE_PASSWORD\"@db.\"$SUPABASE_DB_ID\".supabase.co:5432/postgres\"", 23 | "export-db-schema": "source .env && npx supabase init && npx supabase link --project-ref \"$SUPABASE_DB_ID\" && npx supabase db dump -f supabase/schema.sql" 24 | }, 25 | "dependencies": { 26 | "@octokit/auth-app": "^4.0.10", 27 | "@octokit/core": "^4.2.0", 28 | "@octokit/rest": "^19.0.8", 29 | "@octokit/webhooks": "^11.0.0", 30 | "@octokit/webhooks-types": "^6.11.0", 31 | "@slack/bolt": "^3.13.0", 32 | "@slack/web-api": "^6.8.1", 33 | "@supabase/supabase-js": "^2.10.0", 34 | "@types/jest": "^29.5.1", 35 | "@types/supertest": "^2.0.12", 36 | "axios": "^1.3.5", 37 | "body-parser": "^1.20.2", 38 | "concurrently": "^7.6.0", 39 | "cors": "^2.8.5", 40 | "dotenv": "^16.0.3", 41 | "express": "^4.18.2", 42 | "gpt-3-encoder": "^1.1.4", 43 | "jest": "^29.5.0", 44 | "limiter": "^2.1.0", 45 | "lodash": "^4.17.21", 46 | "node-cron": "^3.0.2", 47 | "openai": "^3.2.1", 48 | "pg": "^8.11.0", 49 | "rimraf": "^4.4.0", 50 | "simple-git": "^3.17.0", 51 | "sinon": "^15.0.4", 52 | "smee-client": "^1.2.3", 53 | "supertest": "^6.3.3", 54 | "tree-sitter": "^0.20.1", 55 | "tree-sitter-java": "^0.19.1", 56 | "tree-sitter-python": "^0.20.1", 57 | "tree-sitter-typescript": "^0.20.1", 58 | "ts-jest": "^29.1.0", 59 | "typescript": "^4.9.5" 60 | }, 61 | "devDependencies": { 62 | "@types/cors": "^2.8.13", 63 | "@types/express": "^4.17.17", 64 | "@types/lodash": "^4.14.191", 65 | "@types/node": "^18.15.5", 66 | "@types/node-cron": "^3.0.7", 67 | "@types/sinon": "^10.0.14", 68 | "nodemon": "^2.0.21", 69 | "typescript": "^4.9.5" 70 | } 71 | } -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import { createServer } from "./createServer"; 3 | import { Database } from "./types/supabase"; 4 | import { port, supabaseKey, supabaseUrl } from "./utils/envVariable"; 5 | 6 | // Create a single supabase client for interacting with your database 7 | export const supabaseBaseServerClient = createClient( 8 | supabaseUrl, 9 | supabaseKey 10 | ); 11 | 12 | const app = createServer(); 13 | 14 | app.listen(process.env.PORT, () => { 15 | console.log(`[Server]: Running at https://localhost:${port}`); 16 | }); 17 | -------------------------------------------------------------------------------- /server/api/agent/agent.context.ts: -------------------------------------------------------------------------------- 1 | import { logInfo } from "../../../utils/commandLineColors"; 2 | import { getCompletedTasks, updateTaskById } from "../task/task.repository"; 3 | import { Task, TaskWithObjective } from "../task/task.types"; 4 | import { handleLoopAgent } from "./autoAgents/handleLoopAgent"; 5 | import { ToolName } from "./tools"; 6 | 7 | export function isSearchTool(taskName: string | null) { 8 | if (!taskName) return false; 9 | if ( 10 | taskName === ToolName.searchCode || 11 | taskName === ToolName.searchDirectory || 12 | taskName === ToolName.searchFiles || 13 | taskName === ToolName.searchTests 14 | ) 15 | return true; 16 | return false; 17 | } 18 | 19 | export function isGenerateCodeTool(task: TaskWithObjective | Task) { 20 | if (!task) return false; 21 | if ( 22 | task?.tool_name === ToolName.generateNewCode || 23 | task.tool_name === ToolName.generateTestCode || 24 | task.tool_name === ToolName.getExisitingCode 25 | ) 26 | return true; 27 | return false; 28 | } 29 | 30 | export async function findCompletedTasksThatNeedLoops() { 31 | const completed = await getCompletedTasks(); 32 | logInfo(`Tasks with output count: ${completed.length}`); 33 | 34 | if (completed.length === 0) return; 35 | for (let task of completed) { 36 | await updateTaskById(task.id, { 37 | loop_evaluated_at: new Date().toISOString(), 38 | }); 39 | 40 | // This is just one combo of where a loop agent is neede, there will likely be others and this method can be extrapolated to handle them 41 | if (task.tool_name === ToolName.searchFiles) { 42 | const otherTasks = task?.objective?.task; 43 | if (otherTasks.length === 0) { 44 | logInfo("No other tasks"); 45 | continue; 46 | } else { 47 | const found = otherTasks.find((task: any) => isGenerateCodeTool(task)); 48 | if (!found || !found.tool_name) { 49 | logInfo("No generate code tool"); 50 | continue; 51 | } else { 52 | // Loop agent will check if there are multiple files or code to create and then create the tasks 53 | await handleLoopAgent(task, found); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/api/agent/agent.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { testAgent, useAgent } from "./agent.controller"; 3 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 4 | 5 | const agentRouter = Router(); 6 | 7 | // Base route: /agent 8 | 9 | agentRouter.post("/", ensureAuthenticated, useAgent); 10 | agentRouter.get("/test", testAgent); 11 | export default agentRouter; 12 | -------------------------------------------------------------------------------- /server/api/agent/autoAgents/addTests.agent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertToTestFileName, 3 | getFilePrefix, 4 | } from "../../../../utils/getFileName"; 5 | import { createAiWritenCode } from "../../aiCreatedCode/aiCreatedCode.repository"; 6 | import { 7 | createCodeFile, 8 | findTestFile, 9 | updateFileById, 10 | } from "../../codeFile/codeFile.repository"; 11 | import { createTestBasedOnExistingCode } from "../../codeFile/codeFile.service"; 12 | import { getLongExportSnippets } from "../../codeSnippet/codeSnippet.repository"; 13 | import { compareAndUpdateSnippets } from "../../supabase/supabase.service"; 14 | 15 | export async function findAndAddTestAgent() { 16 | // This agent searches the code base for complex code snippets and makes sure there are tests for this code 17 | 18 | console.log("Running find and add test agent"); 19 | const longSnippets = await getLongExportSnippets(30); 20 | console.log(longSnippets.length); 21 | 22 | if (!longSnippets) { 23 | return; 24 | } 25 | 26 | const topTwo = longSnippets.slice(0, 3); 27 | 28 | for (let snippet of topTwo) { 29 | if ( 30 | !snippet || 31 | !snippet.code_file || 32 | !snippet.account_id || 33 | !snippet.code_string 34 | ) { 35 | continue; 36 | } 37 | if (snippet.code_file) { 38 | let codeFile; 39 | if (Array.isArray(snippet.code_file)) { 40 | codeFile = snippet.code_file[0]; 41 | } else { 42 | codeFile = snippet.code_file; 43 | } 44 | 45 | if ( 46 | codeFile.test_file_id || 47 | !codeFile.file_name || 48 | !codeFile.id || 49 | !codeFile.file_path || 50 | !codeFile.account_id 51 | ) { 52 | continue; 53 | // Consider updating the test file if the snippet has changed 54 | } else { 55 | // Look for a test file that hasn't been linked 56 | 57 | const filePrefix = getFilePrefix(codeFile.file_name); 58 | const found = await findTestFile(filePrefix, snippet.account_id); 59 | if (found) { 60 | console.log("Found test file"); 61 | 62 | await updateFileById(codeFile.id, { 63 | test_file_id: found.id, 64 | updated_at: new Date().toISOString(), 65 | }); 66 | } else { 67 | // Create a test file 68 | const test = await createTestBasedOnExistingCode(snippet.code_string); 69 | if (test) { 70 | const file_name = convertToTestFileName(codeFile.file_name); 71 | 72 | const newAiCode = await createAiWritenCode({ 73 | code: test, 74 | account_id: snippet.account_id, 75 | location: "newFile", 76 | path: codeFile.file_path, 77 | file_name, 78 | functionality: "test for existing code", 79 | completed_at: new Date().toISOString(), 80 | existing_code: snippet.code_string, 81 | }); 82 | 83 | if (newAiCode) { 84 | // Create new file 85 | // Create new snippet 86 | await createCodeFile(snippet.account_id, { 87 | file_name, 88 | file_path: codeFile.file_path, 89 | code_directory_id: codeFile.code_directory_id, 90 | code_directory_parent_id: codeFile.code_directory_parent_id, 91 | test_file_id: codeFile.id, 92 | }); 93 | 94 | await compareAndUpdateSnippets( 95 | { 96 | filePath: codeFile.file_path, 97 | contents: test, 98 | }, 99 | codeFile.account_id 100 | ); 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/api/agent/autoAgents/handleLoopAgent.ts: -------------------------------------------------------------------------------- 1 | import { ChatUserType } from "../../../../types/chatMessage.type"; 2 | import { logError, logInfo } from "../../../../utils/commandLineColors"; 3 | import { deserializeJson } from "../../../../utils/deserializeJson"; 4 | import { OpenAiChatCompletionResponse } from "../../codeCompletion/codeCompletion.types"; 5 | import { createChatCompletion } from "../../openAi/openai.service"; 6 | import { createTaskWithObjective } from "../../task/task.repository"; 7 | import { Task, TaskWithObjective } from "../../task/task.types"; 8 | import { getToolByNames } from "../agent.service"; 9 | import { allTools } from "../tools"; 10 | 11 | const loopPrompt = ( 12 | task: TaskWithObjective, 13 | generativeTask: Task, 14 | toolArgument: string 15 | ) => ` 16 | Here is your objective: ${generativeTask.description} 17 | 18 | Here is what you know so far: ${task.tool_output} 19 | 20 | Here are the tools you can use to complete tasks: 21 | 22 | toolName: ${generativeTask.tool_name} 23 | toolInput: ${toolArgument} 24 | 25 | You can complete this loop n number of times. 26 | 27 | Here is the json format of the output: 28 | [ 29 | { 30 | "toolName": ${generativeTask.tool_name}, 31 | "toolInput": ${toolArgument} 32 | } 33 | ] 34 | 35 | Create all the tasks you need to complete this objective using the above json format. 36 | 37 | `; 38 | 39 | async function runLoop(prompt: string) { 40 | let input: { toolName: string; toolInput: string }[] | null = null; 41 | try { 42 | logInfo(`Loop prompt: ${prompt}`); 43 | const response: string = await createChatCompletion([ 44 | { 45 | content: prompt, 46 | role: ChatUserType.user, 47 | }, 48 | ]); 49 | 50 | logInfo(`Loop response: ${response}`); 51 | input = JSON.parse(response); 52 | if (!input) { 53 | input = extractAllToolInfo(response); 54 | } 55 | if (!input) { 56 | logError(`Loop agent failed to run. Aborting...`); 57 | throw new Error("Loop agent failed to run. Aborting..."); 58 | } 59 | logInfo(`Input: ${JSON.stringify(input)}`); 60 | return input; 61 | } catch (e) { 62 | return null; 63 | } 64 | } 65 | 66 | export async function handleLoopAgent( 67 | task: TaskWithObjective, 68 | generativeTask: Task 69 | ) { 70 | let input: { toolName: string; toolInput: string }[] | null = null; 71 | 72 | const tools = getToolByNames(allTools); 73 | const toolArgument = tools[generativeTask.tool_name || ""].arguments[0]; 74 | const prompt = loopPrompt(task, generativeTask, toolArgument); 75 | 76 | let runCount = 0; 77 | input = await runLoop(prompt); 78 | 79 | if (!input && runCount < 5) { 80 | runCount++; 81 | input = await runLoop(prompt); 82 | } else if (runCount >= 5) { 83 | logError(`Loop agent failed to run 5 times. Aborting...`); 84 | return null; 85 | } 86 | if (!input || input.length === 0) return; 87 | 88 | for (let tool of input) { 89 | const res = await createTaskWithObjective( 90 | { 91 | tool_name: tool.toolName, 92 | tool_input: tool.toolInput, 93 | }, 94 | task.objective_id || "" 95 | ); 96 | logInfo(`Created task: ${JSON.stringify(res)}`); 97 | } 98 | 99 | return input; 100 | } 101 | 102 | function extractAllToolInfo( 103 | rawText: string 104 | ): { toolName: string; toolInput: string }[] { 105 | const regex = /toolName:\s*"([^"]+)"[\s\S]*?toolInput:\s*"([^"]+)"/g; 106 | const matches = []; 107 | let match; 108 | 109 | while ((match = regex.exec(rawText)) !== null) { 110 | matches.push({ 111 | toolName: match[1], 112 | toolInput: match[2], 113 | }); 114 | } 115 | 116 | return matches; 117 | } 118 | -------------------------------------------------------------------------------- /server/api/agent/autoAgents/improveCodeAgent.ts: -------------------------------------------------------------------------------- 1 | import { ChatUserType } from "../../../../types/chatMessage.type"; 2 | import { EngineName } from "../../../../types/openAiTypes/openAiEngine"; 3 | import { createAiWritenCode } from "../../aiCreatedCode/aiCreatedCode.repository"; 4 | import { getLongExportSnippets } from "../../codeSnippet/codeSnippet.repository"; 5 | import { createNewFileFromSnippets } from "../../codeSnippet/codeSnippet.service"; 6 | import { getOrCreateExoConfig } from "../../exoConfig/exoConfig.service"; 7 | import { createChatCompletion } from "../../openAi/openai.service"; 8 | import exoConfig from "../../../../exo-config.json"; 9 | import { convertToExoSuggestionFileName } from "../../../../utils/getFileName"; 10 | 11 | const updated: number[] = []; 12 | 13 | export async function improveCodeAgent() { 14 | const longSnippets = await getLongExportSnippets(30); 15 | console.log(longSnippets.length); 16 | 17 | if (!longSnippets) { 18 | return; 19 | } 20 | const topTwo = longSnippets.slice(9, 11); 21 | 22 | for (let snippet of topTwo) { 23 | console.log(snippet.name); 24 | if ( 25 | !snippet || 26 | !snippet.code_file || 27 | !snippet.account_id || 28 | !snippet.code_string || 29 | !snippet.code_file_id 30 | ) { 31 | continue; 32 | } 33 | if (snippet.code_file) { 34 | let codeFile; 35 | if (Array.isArray(snippet.code_file)) { 36 | codeFile = snippet.code_file[0]; 37 | } else { 38 | codeFile = snippet.code_file; 39 | } 40 | 41 | if ( 42 | !codeFile.file_name || 43 | !codeFile.id || 44 | !codeFile.file_path || 45 | !codeFile.account_id 46 | ) { 47 | continue; 48 | // Consider updating the test file if the snippet has changed 49 | } else { 50 | // Pass it to GPT4 and ask for improvements. 51 | // Will need to get the location of the code in the file 52 | console.log("Start row", snippet.start_row); 53 | console.log("End row", snippet.end_row); 54 | console.log("code id", snippet.id); 55 | 56 | // Find exo config 57 | // If not present create one 58 | const exoConfig = await getOrCreateExoConfig(snippet.code_file_id); 59 | if ( 60 | !exoConfig || 61 | // @ts-ignore 62 | !exoConfig.code_snippet || 63 | // @ts-ignore 64 | !exoConfig.code_snippet[0] || 65 | // @ts-ignore 66 | !exoConfig.code_snippet[0].code_string || 67 | updated.includes(snippet.id) 68 | ) { 69 | return; 70 | } 71 | const parsedExoConfig = JSON.parse( 72 | // @ts-ignore 73 | exoConfig.code_snippet[0].code_string 74 | ); 75 | if (!parsedExoConfig || !parsedExoConfig.codeStandards) { 76 | continue; 77 | } 78 | 79 | const prompt = ` 80 | Please improve the following code snippet to meet the following standards: 81 | ${parsedExoConfig.codeStandards.join("\n")} 82 | 83 | If you want to add a part of the code to a new file, please add the following comment to the code: // New file: 84 | 85 | You can assume that all the imports are already present in the file. You don't need to add, remove or change any imports. 86 | 87 | Here is the code: 88 | ${snippet.code_string} 89 | 90 | Improve the code: 91 | `; 92 | 93 | updated.push(snippet.id); 94 | 95 | const res = await createChatCompletion( 96 | [ 97 | { 98 | content: prompt, 99 | role: ChatUserType.user, 100 | }, 101 | ], 102 | EngineName.GPT4, 103 | 0.5, 104 | 4000 105 | ); 106 | if (!res) { 107 | continue; 108 | } else { 109 | await createAiWritenCode({ 110 | code: res, 111 | location: "newFile", 112 | path: codeFile.file_path, 113 | file_name: convertToExoSuggestionFileName(codeFile.file_name), 114 | account_id: codeFile.account_id, 115 | completed_at: new Date().toISOString(), 116 | }); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /server/api/agent/prompt.json: -------------------------------------------------------------------------------- 1 | { 2 | "Question": "", 3 | "Thought": "", 4 | "Reasoning": "", 5 | "Plan": ["example step one", "example step two"], 6 | "Critisism": "", 7 | "Action": "", 8 | "Observation": "" 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /server/api/agent/tools/askUserAQuestion.prompt.ts: -------------------------------------------------------------------------------- 1 | export const askUserAQuestionPrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "ask user" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/askUserAQuestion.tool.ts: -------------------------------------------------------------------------------- 1 | // session holds location, file name and file path 2 | // Once the file name and file path (or scatchpad) are known and the functionality is known you can write to the ai generated code table 3 | 4 | import { ToolName } from "."; 5 | import { ChatUserType } from "../../../../types/chatMessage.type"; 6 | import { createMessageWithUser } from "../../message/message.service"; 7 | import { ToolInterface } from "../agent.service"; 8 | import { askUserAQuestionPrompt } from "./askUserAQuestion.prompt"; 9 | 10 | export function askUserAQuestionTool(): ToolInterface { 11 | async function handleSearchDirectory( 12 | userId: string, 13 | sessionId: string, 14 | question: string 15 | ) { 16 | createMessageWithUser( 17 | { 18 | content: question, 19 | role: ChatUserType.assistant, 20 | }, 21 | sessionId 22 | ); 23 | 24 | let output = `I've asked the user the question: ${question}. Please wait for a response.`; 25 | 26 | return { 27 | output, 28 | }; 29 | } 30 | 31 | const name = ToolName.askUser; 32 | 33 | return { 34 | name, 35 | description: "Asks the user a question and waits for a response", 36 | use: async (userId, sessionId, question) => 37 | await handleSearchDirectory(userId, sessionId, question), 38 | arguments: ["question"], 39 | promptTemplate: askUserAQuestionPrompt, 40 | availableTools: [name], 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /server/api/agent/tools/finalAnswer.prompt.ts: -------------------------------------------------------------------------------- 1 | export const finalAnswerPrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "final answer" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | return the response 12 | """ 13 | `; 14 | -------------------------------------------------------------------------------- /server/api/agent/tools/finalAnswerTool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { ToolInterface } from "../agent.service"; 3 | import { finalAnswerPrompt } from "./finalAnswer.prompt"; 4 | 5 | export function finalAnswerTool(): ToolInterface { 6 | async function handleFinalAnser( 7 | userId: string, 8 | sessionId: string, 9 | text: string 10 | ) { 11 | return { 12 | output: text, 13 | metadata: "", 14 | }; 15 | } 16 | 17 | const name = ToolName.finalAnswer; 18 | 19 | return { 20 | name, 21 | description: "Tool to run when you have the final answer.", 22 | use: async (userId, sessionId, text) => 23 | await handleFinalAnser(userId, sessionId, text), 24 | arguments: ["final answer"], 25 | promptTemplate: finalAnswerPrompt, 26 | availableTools: [name], 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /server/api/agent/tools/findDirectory.prompt.ts: -------------------------------------------------------------------------------- 1 | export const findDirectoryPrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "find one directory" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/findDirectory.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { extractPath } from "../../../../utils/fileOperations.service"; 3 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 4 | import { codeDirectorySearch } from "../../search/search.repository"; 5 | 6 | import { findOrUpdateAccount } from "../../supabase/account.service"; 7 | import { ToolInterface } from "../agent.service"; 8 | import { findDirectoryPrompt } from "./findDirectory.prompt"; 9 | 10 | export function findDirectoryTool(): ToolInterface { 11 | async function handleSearchDirectory( 12 | userId: string, 13 | sessionId: string, 14 | query: string 15 | ) { 16 | // const searchResult = await handleSearch(userId, sessionId); 17 | const account = await findOrUpdateAccount(userId); 18 | const response = await codeDirectorySearch( 19 | query, 20 | account?.id ? account.id : "" 21 | ); 22 | 23 | let output = ""; 24 | if (response && response.length > 0) { 25 | output = `The directory name that best matches this search is: ${response[0].directory_name} which is located at: ${response[0].file_path}.`; 26 | } else { 27 | output = "No results found"; 28 | } 29 | 30 | return { 31 | output, 32 | metadata: response, 33 | }; 34 | } 35 | 36 | function outputFunction(output: string, sessionId: string) { 37 | findAndUpdateAiCodeBySession( 38 | sessionId, 39 | { 40 | location: output, 41 | path: extractPath(output), 42 | }, 43 | "location" 44 | ); 45 | 46 | return output; 47 | } 48 | 49 | const name = ToolName.findDirectory; 50 | 51 | return { 52 | name, 53 | description: "Finds one code file or directory given the query", 54 | use: async (userId, sessionId, query) => 55 | await handleSearchDirectory(userId, sessionId, query), 56 | arguments: ["search query"], 57 | promptTemplate: findDirectoryPrompt, 58 | availableTools: [name, ToolName.searchCode, ToolName.searchDirectory], 59 | outputFunction, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /server/api/agent/tools/findFile.prompt.ts: -------------------------------------------------------------------------------- 1 | export const findFilePrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "find one file" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/findFile.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { extractPath } from "../../../../utils/fileOperations.service"; 3 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 4 | import { findCodeByQuery } from "../../search/search.service"; 5 | import { findOrUpdateAccount } from "../../supabase/account.service"; 6 | import { ToolInterface } from "../agent.service"; 7 | import { findFilePrompt } from "./findFile.prompt"; 8 | 9 | export function findFileTool(): ToolInterface { 10 | async function handleSearchCode( 11 | userId: string, 12 | sessionId: string, 13 | text: string 14 | ) { 15 | // const searchResult = await handleSearch(userId, sessionId); 16 | const account = await findOrUpdateAccount(userId); 17 | const response = await findCodeByQuery(text, account?.id ? account.id : ""); 18 | 19 | if (!response || response.length === 0) { 20 | return { 21 | output: "No results found. Try adapting your search query.", 22 | metadata: response, 23 | }; 24 | } 25 | 26 | return { 27 | output: response[0].file_name 28 | ? `The file name that best matches this search is: ${response[0].file_name} which is located at: ${response[0].relative_file_path}.` 29 | : "No results found. Try adapting your search query.", 30 | metadata: response, 31 | }; 32 | } 33 | const name = ToolName.findFile; 34 | 35 | function outputFunction(output: string, sessionId: string) { 36 | findAndUpdateAiCodeBySession( 37 | sessionId, 38 | { 39 | location: output, 40 | path: extractPath(output), 41 | }, 42 | "location" 43 | ); 44 | 45 | return output; 46 | } 47 | 48 | return { 49 | name, 50 | description: 51 | "Finds the users code file for the given name or query and returns one code file result", 52 | use: async (userId, sessionId, text) => 53 | await handleSearchCode(userId, sessionId, text), 54 | arguments: ["query"], 55 | promptTemplate: findFilePrompt, 56 | availableTools: [ 57 | name, 58 | ToolName.searchCode, 59 | ToolName.searchDirectory, 60 | ToolName.findDirectory, 61 | ToolName.finalAnswer, 62 | ], 63 | outputFunction, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /server/api/agent/tools/generateNewCode.prompt.ts: -------------------------------------------------------------------------------- 1 | export const generateNewCodePrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "generate new code" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/generateTestCode.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { removeQuotes } from "../../../../utils/findArrayInAString"; 3 | import { convertToTestFileName } from "../../../../utils/getFileName"; 4 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 5 | import { 6 | findFileByAccountIdAndFullFilePath, 7 | findFileByFileNameAndAccount, 8 | } from "../../codeFile/codeFile.repository"; 9 | import { createTestBasedOnExistingCode } from "../../codeFile/codeFile.service"; 10 | import { updateSession } from "../../session/session.repository"; 11 | import { findOrUpdateAccount } from "../../supabase/account.service"; 12 | import { ToolInterface } from "../agent.service"; 13 | import { generateNewCodePrompt } from "./generateNewCode.prompt"; 14 | 15 | export function generateTestCodeTool(): ToolInterface { 16 | async function handleWriteTestCode( 17 | userId: string, 18 | sessionId: string, 19 | path: string 20 | ) { 21 | const account = await findOrUpdateAccount(userId); 22 | 23 | let file = await findFileByAccountIdAndFullFilePath(account.id, path); 24 | 25 | if (!file) { 26 | file = await findFileByFileNameAndAccount(removeQuotes(path), account.id); 27 | } 28 | 29 | if (!file || !file.file_name || !file.file_path) { 30 | return { 31 | output: `I found the file but it didn't contain all the info I need. Please make sure the file has code by indexing your repository again.`, 32 | }; 33 | } 34 | 35 | const { content } = file; 36 | 37 | if (!content) { 38 | return { 39 | output: `This file does not have any content. Please make sure the file has code by indexing your repository again.`, 40 | }; 41 | } 42 | 43 | const test = await createTestAndUpdateAiCodeAndSession( 44 | content, 45 | userId, 46 | sessionId, 47 | { 48 | file_name: file.file_name, 49 | file_path: file.file_path, 50 | } 51 | ); 52 | 53 | if (!test) { 54 | return { 55 | output: `I'm sorry I couldn't generate code for you. Please try again later.`, 56 | }; 57 | } else { 58 | return { 59 | output: `Here is the test code that I generated for you: ${test}. I've added this code to the session. When you are ready to write this code to the file use the write file tool.`, 60 | }; 61 | } 62 | } 63 | 64 | const name = ToolName.generateTestCode; 65 | 66 | return { 67 | name, 68 | description: 69 | "Generates test code based on the existing file path passed into the tool. The file you want to write a test for has to exisit and contain valid code.", 70 | use: async (userId, sessionId, path) => 71 | await handleWriteTestCode(userId, sessionId, path), 72 | arguments: ["/file/path"], 73 | promptTemplate: generateNewCodePrompt, 74 | availableTools: [ 75 | name, 76 | ToolName.searchCode, 77 | ToolName.finalAnswer, 78 | ToolName.searchDirectory, 79 | ], 80 | }; 81 | } 82 | 83 | export async function createTestAndUpdateAiCodeAndSession( 84 | content: string, 85 | userId: string, 86 | sessionId: string, 87 | file: { 88 | file_name: string; 89 | file_path: string; 90 | } 91 | ) { 92 | const test = await createTestBasedOnExistingCode(content); 93 | 94 | if (!test) { 95 | return null; 96 | } 97 | 98 | await updateSession(userId, sessionId, { 99 | code_content: test, 100 | }); 101 | 102 | await findAndUpdateAiCodeBySession( 103 | sessionId, 104 | { 105 | code: test, 106 | functionality: "Write test code based on existing code", 107 | file_name: convertToTestFileName(file.file_name), 108 | path: file.file_path, 109 | }, 110 | "code" 111 | ); 112 | 113 | return test; 114 | } 115 | -------------------------------------------------------------------------------- /server/api/agent/tools/getCode.prompt.ts: -------------------------------------------------------------------------------- 1 | export const getCodePrompt = (name: string) => ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the ${name} tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/getCode.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { findCodeByQuery } from "../../search/search.service"; 3 | import { findOrUpdateAccount } from "../../supabase/account.service"; 4 | import { ToolInterface } from "../agent.service"; 5 | import { getCodePrompt } from "./getCode.prompt"; 6 | 7 | export function getExisitingCodeTool(): ToolInterface { 8 | async function handleSearchCode( 9 | userId: string, 10 | sessionId: string, 11 | text: string 12 | ) { 13 | // const searchResult = await handleSearch(userId, sessionId); 14 | const account = await findOrUpdateAccount(userId); 15 | const response = await findCodeByQuery(text, account?.id ? account.id : ""); 16 | return { 17 | output: response[0].code_string 18 | ? response[0].code_string 19 | : "No results found", 20 | metadata: response, 21 | }; 22 | } 23 | 24 | const name = ToolName.getExisitingCode; 25 | 26 | return { 27 | name, 28 | description: 29 | "Finds the users code snippet given the code explaination. This is useful for when you want to reuse code you've already written or write tests for existing code.", 30 | use: async (userId, sessionId, text) => 31 | await handleSearchCode(userId, sessionId, text), 32 | arguments: ["search query"], 33 | promptTemplate: getCodePrompt(name), 34 | availableTools: [name], 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /server/api/agent/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { askUserAQuestionTool } from "./askUserAQuestion.tool"; 2 | import { finalAnswerTool } from "./finalAnswerTool"; 3 | import { findDirectoryTool } from "./findDirectory.tool"; 4 | import { findFileTool } from "./findFile.tool"; 5 | import { generateNewCodeTool } from "./generateNewCode.tool"; 6 | import { generateTestCodeTool } from "./generateTestCode.tool"; 7 | import { getExisitingCodeTool } from "./getCode.tool"; 8 | import { retrieveMemoryTool } from "./retrieveMemory.tool"; 9 | import { searchCodeTool } from "./searchCode.tool"; 10 | import { searchDirectoryTool } from "./searchDirectory.tool"; 11 | import { searchFilesTool } from "./searchFiles.tool"; 12 | import { searchTestsTool } from "./searchTests.tool"; 13 | import { writeCompletedCodeTool } from "./writeCompletedCode.tool"; 14 | 15 | export * from "./askUserAQuestion.tool"; 16 | export * from "./findDirectory.tool"; 17 | export * from "./findFile.tool"; 18 | export * from "./generateNewCode.tool"; 19 | export * from "./getCode.tool"; 20 | export * from "./retrieveMemory.tool"; 21 | export * from "./searchCode.tool"; 22 | export * from "./searchDirectory.tool"; 23 | export * from "./storeMemory.tool"; 24 | export * from "./writeCompletedCode.tool"; 25 | 26 | export enum ToolName { 27 | searchCode = "search code", 28 | searchDirectory = "search directory", 29 | searchTests = "search tests", 30 | findFile = "find one file", 31 | findDirectory = "find one directory", 32 | generateNewCode = "generate new code", 33 | generateTestCode = "generate test code", 34 | askUser = "ask user", 35 | storeText = "store text", 36 | retrieveText = "retrieve text", 37 | writeCompletedCode = "write completed code", 38 | getExisitingCode = "get exisiting code", 39 | finalAnswer = "final answer", 40 | updateExistingCode = "update existing code", 41 | writeCodeToScarchPad = "write code to scratch pad", 42 | searchFiles = "search files", 43 | } 44 | 45 | export const allTools = [ 46 | searchCodeTool(), 47 | searchDirectoryTool(), 48 | findFileTool(), 49 | generateNewCodeTool(), 50 | askUserAQuestionTool(), 51 | findDirectoryTool(), 52 | retrieveMemoryTool(), 53 | // writeCompletedCodeTool(), 54 | getExisitingCodeTool(), 55 | finalAnswerTool(), 56 | generateTestCodeTool(), 57 | searchTestsTool(), 58 | searchFilesTool(), 59 | generateTestCodeTool(), 60 | ]; 61 | -------------------------------------------------------------------------------- /server/api/agent/tools/retrieveMemory.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { getMemoriesById } from "../../memory/memory.service"; 3 | import { ToolInterface } from "../agent.service"; 4 | 5 | export function retrieveMemoryTool(): ToolInterface { 6 | async function handleStoreMemory( 7 | userId: string, 8 | sessionId: string, 9 | id: string 10 | ) { 11 | const memory = await getMemoriesById(id); 12 | 13 | return { 14 | output: `Here is the text you requested: ${ 15 | memory?.memory_text ? memory.memory_text : "No text found" 16 | }`, 17 | }; 18 | } 19 | 20 | const name = ToolName.retrieveText; 21 | 22 | return { 23 | name, 24 | description: 25 | "Retrieves stored text for longer tasks that require multiple steps or loops to complete.", 26 | use: async (userId, sessionId, id) => 27 | handleStoreMemory(userId, sessionId, id), 28 | arguments: ["text id"], 29 | promptTemplate: "", 30 | availableTools: [name], 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchCode.prompt.ts: -------------------------------------------------------------------------------- 1 | export const searchCodePrompt = (name: string) => ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the ${name} tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchCode.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { findCodeByQuery } from "../../search/search.service"; 3 | import { findOrUpdateAccount } from "../../supabase/account.service"; 4 | import { ToolInterface } from "../agent.service"; 5 | import { searchCodePrompt } from "./searchCode.prompt"; 6 | import { searchDirectoryTool } from "./searchDirectory.tool"; 7 | 8 | export function searchCodeTool(): ToolInterface { 9 | async function handleSearchCode( 10 | userId: string, 11 | sessionId: string, 12 | text: string 13 | ) { 14 | // const searchResult = await handleSearch(userId, sessionId); 15 | const account = await findOrUpdateAccount(userId); 16 | const response = await findCodeByQuery(text, account?.id ? account.id : ""); 17 | return { 18 | output: response 19 | .slice(0, 10) 20 | .map( 21 | (r: { file_name: any; relative_file_path: any }) => 22 | `Name: ${r.file_name}, Path: ${r.relative_file_path}` 23 | ) 24 | .join(", "), 25 | }; 26 | } 27 | 28 | const name = ToolName.searchCode; 29 | 30 | return { 31 | name, 32 | description: 33 | "Searches or finds the users code for the given query and return up to ten results for code locations.", 34 | use: async (userId, sessionId, text) => 35 | await handleSearchCode(userId, sessionId, text), 36 | arguments: ["search query"], 37 | promptTemplate: searchCodePrompt(name), 38 | availableTools: [ 39 | name, 40 | ToolName.searchDirectory, 41 | ToolName.findFile, 42 | ToolName.finalAnswer, 43 | ], 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchDirectory.prompt.ts: -------------------------------------------------------------------------------- 1 | export const searchDirectoryPrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "search directory" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | 33 | export const refinementPrompt = (description: string, response: string) => ` 34 | Here is the original task goal: ${description} 35 | Here is the new information gained from running the task: ${response} 36 | 37 | Please use the new information to better answer the original task goal. 38 | 39 | `; 40 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchDirectory.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { ChatUserType } from "../../../../types/chatMessage.type"; 3 | import { extractPath } from "../../../../utils/fileOperations.service"; 4 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 5 | import { createChatCompletion } from "../../openAi/openai.service"; 6 | import { codeDirectorySearch } from "../../search/search.repository"; 7 | 8 | import { findOrUpdateAccount } from "../../supabase/account.service"; 9 | import { TaskWithObjective } from "../../task/task.types"; 10 | import { ToolInterface } from "../agent.service"; 11 | import { 12 | refinementPrompt, 13 | searchDirectoryPrompt, 14 | } from "./searchDirectory.prompt"; 15 | 16 | export function searchDirectoryTool(): ToolInterface { 17 | async function handleSearchDirectory( 18 | userId: string, 19 | sessionId: string, 20 | query: string, 21 | task?: Partial 22 | ) { 23 | const account = await findOrUpdateAccount(userId); 24 | const response = await codeDirectorySearch( 25 | query, 26 | account?.id ? account.id : "" 27 | ); 28 | 29 | let output = ""; 30 | if (response && response.length > 0 && account) { 31 | if (response && response.length > 0) { 32 | output = response 33 | .slice(0, 10) 34 | .map( 35 | (r: { directory_name: any; file_path: any }) => 36 | `Name: ${r.directory_name}, Path: ${r.file_path}` 37 | ) 38 | .join(", "); 39 | 40 | if (task && task.description) { 41 | const refinement = await createChatCompletion([ 42 | { 43 | content: refinementPrompt(task.description, output), 44 | role: ChatUserType.user, 45 | }, 46 | ]); 47 | 48 | if (refinement) { 49 | output = refinement; 50 | } 51 | } 52 | } else { 53 | output = "No results found"; 54 | } 55 | } else { 56 | output = "No results found"; 57 | } 58 | 59 | return { 60 | output, 61 | metadata: { ...response }, 62 | }; 63 | } 64 | 65 | function outputFunction(output: string, sessionId: string) { 66 | findAndUpdateAiCodeBySession( 67 | sessionId, 68 | { 69 | location: output, 70 | path: extractPath(output), 71 | }, 72 | "location" 73 | ); 74 | 75 | return output; 76 | } 77 | 78 | const name = ToolName.searchDirectory; 79 | 80 | return { 81 | name, 82 | description: 83 | "Searches or finds the users directories for the given directory name or query and return up to ten directories. This does not return the contents of the directory. To find files use the 'search files' tool.", 84 | use: async (userId, sessionId, query, task) => 85 | await handleSearchDirectory(userId, sessionId, query, task), 86 | arguments: ["search query"], 87 | promptTemplate: searchDirectoryPrompt, 88 | availableTools: [ 89 | name, 90 | ToolName.findFile, 91 | ToolName.findDirectory, 92 | ToolName.finalAnswer, 93 | ToolName.searchFiles, 94 | ], 95 | outputFunction, 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchFiles.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { extractPath } from "../../../../utils/fileOperations.service"; 3 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 4 | import { findFileByExplainationEmbedding } from "../../search/search.repository"; 5 | import { findOrUpdateAccount } from "../../supabase/account.service"; 6 | import { ToolInterface } from "../agent.service"; 7 | import { searchDirectoryPrompt } from "./searchDirectory.prompt"; 8 | 9 | // Need toflesh this out 10 | 11 | export function searchFilesTool(): ToolInterface { 12 | async function handleSearchDirectory( 13 | userId: string, 14 | sessionId: string, 15 | query: string 16 | ) { 17 | const account = await findOrUpdateAccount(userId); 18 | const response = await findFileByExplainationEmbedding( 19 | query, 20 | account?.id ? account.id : "" 21 | ); 22 | 23 | let output = ""; 24 | if (response && response.length > 0 && account) { 25 | if (response && response.length > 0) { 26 | output = response 27 | .slice(0, 10) 28 | .map((r) => `Name: ${r.file_name}, Path: ${r.file_path}`) 29 | .join(", "); 30 | } else { 31 | output = "No results found"; 32 | } 33 | } else { 34 | output = "No results found"; 35 | } 36 | 37 | return { 38 | output, 39 | metadata: { ...response }, 40 | }; 41 | } 42 | 43 | function outputFunction(output: string, sessionId: string) { 44 | findAndUpdateAiCodeBySession( 45 | sessionId, 46 | { 47 | location: output, 48 | path: extractPath(output), 49 | }, 50 | "location" 51 | ); 52 | 53 | return output; 54 | } 55 | 56 | const name = ToolName.searchFiles; 57 | 58 | return { 59 | name, 60 | description: 61 | "Searches or finds the users files for the given file name or query and return up to ten files. This does not return the contents of the files. To find the contents of a file use the 'find one file' tool.", 62 | use: async (userId, sessionId, query) => 63 | await handleSearchDirectory(userId, sessionId, query), 64 | arguments: ["search query"], 65 | promptTemplate: searchDirectoryPrompt, 66 | availableTools: [ 67 | name, 68 | ToolName.findFile, 69 | ToolName.findDirectory, 70 | ToolName.finalAnswer, 71 | ], 72 | outputFunction, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /server/api/agent/tools/searchTests.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { findCodeByQuery } from "../../search/search.service"; 3 | import { findOrUpdateAccount } from "../../supabase/account.service"; 4 | import { ToolInterface } from "../agent.service"; 5 | import { searchCodeTool } from "./searchCode.tool"; 6 | 7 | export function searchTestsTool(): ToolInterface { 8 | async function handleSearchCode( 9 | userId: string, 10 | sessionId: string, 11 | text: string 12 | ) { 13 | // const searchResult = await handleSearch(userId, sessionId); 14 | const account = await findOrUpdateAccount(userId); 15 | const response = await findCodeByQuery(text, account?.id ? account.id : ""); 16 | return { 17 | output: response 18 | .slice(0, 10) 19 | .map( 20 | (r: { file_name: any; relative_file_path: any }) => 21 | `Name: ${r.file_name}, Path: ${r.relative_file_path}` 22 | ) 23 | .join(", "), 24 | }; 25 | } 26 | 27 | const name = ToolName.searchTests; 28 | 29 | return { 30 | name, 31 | description: 32 | "Searches for test files in a given directory. Returns all the test files in the directory and subdirectories.", 33 | use: async (userId, sessionId, text) => 34 | await handleSearchCode(userId, sessionId, text), 35 | arguments: ["directory path"], 36 | promptTemplate: "", 37 | availableTools: [name, ToolName.searchCode, ToolName.findFile], 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /server/api/agent/tools/setLocationToWriteCode.prompt.ts: -------------------------------------------------------------------------------- 1 | export const setLocationPrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "set location" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | 26 | Your goal is to make sure the "set location" tool set the location to the correct value. 27 | 28 | Begin! 29 | Question: {question} 30 | Thought: {previous_responses} 31 | """ 32 | `; 33 | -------------------------------------------------------------------------------- /server/api/agent/tools/setLocationToWriteCode.tool.ts: -------------------------------------------------------------------------------- 1 | // import { ToolName } from "."; 2 | // import { extractPath } from "../../../../utils/fileOperations.service"; 3 | // import { extractFileNameAndPathFromFullPath } from "../../../../utils/getFileName"; 4 | // import { getAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.repository"; 5 | // import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 6 | // import { updateSession } from "../../session/session.repository"; 7 | // import { ToolInterface } from "../agent.service"; 8 | // import { searchCodeTool } from "./searchCode.tool"; 9 | // import { setLocationPrompt } from "./setLocationToWriteCode.prompt"; 10 | 11 | // export function setLocationToWriteCodeTool(): ToolInterface { 12 | // async function handleGetLocation( 13 | // userId: string, 14 | // sessionId: string, 15 | // text: string 16 | // ) { 17 | // const doesTextIncludePath = text.includes("/"); 18 | 19 | // if (!doesTextIncludePath) { 20 | // return { 21 | // output: `It doesn't look like you've included a path. Please try again.`, 22 | // }; 23 | // } 24 | 25 | // const fullPath = extractPath(text); 26 | 27 | // const { fileName, extractedPath } = 28 | // extractFileNameAndPathFromFullPath(fullPath); 29 | 30 | // const maybeFileName = fileName.includes(".") ? fileName : null; 31 | 32 | // await updateSession(userId, sessionId, { 33 | // location: text, 34 | // file_path: fullPath, 35 | // file_name: maybeFileName, 36 | // }); 37 | 38 | // await findAndUpdateAiCodeBySession( 39 | // sessionId, 40 | // { 41 | // location: text, 42 | // path: fullPath, 43 | // file_name: maybeFileName, 44 | // }, 45 | // "location" 46 | // ); 47 | 48 | // return { 49 | // output: `I've set the location to write code to ${text}`, 50 | // }; 51 | // } 52 | 53 | // const name = ToolName.setLocationToWriteCode; 54 | 55 | // return { 56 | // name, 57 | // description: 58 | // "If you know the location to write code to you can set it here. Before using this tool you should decide if you should search for an exising code location or write new code to a new location.", 59 | // use: async (userId, sessionId, text) => 60 | // handleGetLocation(userId, sessionId, text), 61 | // arguments: ["location"], 62 | // promptTemplate: setLocationPrompt, 63 | // availableTools: [name, ToolName.searchCode, ToolName.finalAnswer], 64 | // }; 65 | // } 66 | -------------------------------------------------------------------------------- /server/api/agent/tools/storeMemory.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { createMemoryWithSession } from "../../memory/memory.service"; 3 | import { ToolInterface } from "../agent.service"; 4 | 5 | export function storeMemoryTool(): ToolInterface { 6 | async function handleStoreMemory( 7 | userId: string, 8 | sessionId: string, 9 | text: string 10 | ) { 11 | const memory = await createMemoryWithSession( 12 | { 13 | memory_text: text, 14 | }, 15 | sessionId 16 | ); 17 | 18 | return { 19 | output: `I've set the text: ${text}. To retrieve this text say 'retrieve text' with the id of ${memory.id} `, 20 | }; 21 | } 22 | 23 | const name = ToolName.storeText; 24 | 25 | return { 26 | name, 27 | description: 28 | "Stores text for longer tasks that require multiple steps or loops to complete.", 29 | use: async (userId, sessionId, memory) => 30 | handleStoreMemory(userId, sessionId, memory), 31 | arguments: ["memory text"], 32 | promptTemplate: "", 33 | availableTools: [name], 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /server/api/agent/tools/updateExistingCode.prompt.ts: -------------------------------------------------------------------------------- 1 | export const updateExistingCodePrompt = ` 2 | """Today is {today} and you can use tools to get new information. You can also write code to the users scratch pad and file system. 3 | 4 | Here is some additional context: 5 | {context} 6 | 7 | You used the "update existing code" tool to answer the following question: {question}. 8 | 9 | And you got this reponse: {response} 10 | 11 | Tools: 12 | The following tools are available to you: 13 | {tool_description} 14 | 15 | Use the following format: 16 | Question: the input question you must answer 17 | Thought: comment on what you want to do next 18 | Tool: the action to take, exactly one element of tool names: {tool_names} 19 | Tool Input: the input to the action 20 | Observation: the result of the action 21 | ... (this Thought/Tool/Tool Input/Observation repeats N times, use it until you are sure of the answer) 22 | Thought: I now know the final answer 23 | Final Answer: your final answer to the original input question 24 | 25 | Try to get to the final answer. You can use the tools to help you get there. 26 | 27 | Begin! 28 | Question: {question} 29 | Thought: {previous_responses} 30 | """ 31 | `; 32 | -------------------------------------------------------------------------------- /server/api/agent/tools/writeCodeToScarchPad.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { ChatUserType } from "../../../../types/chatMessage.type"; 3 | import { EngineName } from "../../../../types/openAiTypes/openAiEngine"; 4 | import { findAndUpdateAiCodeBySession } from "../../aiCreatedCode/aiCreatedCode.service"; 5 | import { 6 | createChatCompletion, 7 | createTextCompletion, 8 | } from "../../openAi/openai.service"; 9 | import { updateSession } from "../../session/session.repository"; 10 | import { ToolInterface } from "../agent.service"; 11 | import { generateNewCodePrompt } from "./generateNewCode.prompt"; 12 | 13 | export function writeCodeToScratchPadTool(): ToolInterface { 14 | async function handleWriteCode( 15 | userId: string, 16 | sessionId: string, 17 | functionality: string 18 | ) { 19 | // This is an expensive and time consuming operation. We should only do this when we are sure that the functionality is valid. 20 | 21 | const isPromptToGenerateCode = await createTextCompletion( 22 | `Is this an example of a prompt to generate code? 23 | 24 | Prompt: ${functionality}. 25 | 26 | Is that prompt an example of a prompt to generate code? 27 | 28 | Return yes or no`, 29 | 0.2 30 | ); 31 | 32 | console.log("isPromptToGenerateCode", isPromptToGenerateCode); 33 | if (isPromptToGenerateCode.toLowerCase().includes("yes")) { 34 | const response = await createChatCompletion( 35 | [ 36 | { 37 | role: ChatUserType.user, 38 | content: functionality, 39 | }, 40 | ], 41 | EngineName.GPT4 42 | ); 43 | 44 | const improvedCode = response ? response : null; 45 | 46 | console.log("improvedCode", improvedCode); 47 | 48 | if (!improvedCode) { 49 | return { 50 | output: `I'm sorry I couldn't generate code for you. Please try again later.`, 51 | }; 52 | } 53 | await updateSession(userId, sessionId, { 54 | code_content: improvedCode, 55 | }); 56 | 57 | await findAndUpdateAiCodeBySession( 58 | sessionId, 59 | { 60 | code: improvedCode, 61 | location: "scratchPad", 62 | }, 63 | "code" 64 | ); 65 | 66 | return { 67 | output: `Here is the code that I generated for you: ${improvedCode}. I've added this code to the session. When you are ready to write this code to the file use the write file tool.`, 68 | }; 69 | } else { 70 | return { 71 | output: `The functionality you provided is not a valid prompt to generate code. Please try again.`, 72 | }; 73 | } 74 | } 75 | 76 | const name = ToolName.writeCodeToScarchPad; 77 | 78 | return { 79 | name, 80 | description: 81 | "If the user wants to add code to the scrarch pad, this generates new code based on the functionality requested and adds the code to the scratch pad. Arguments should be as specific as possible, outlining what the code should do.", 82 | use: async (userId, sessionId, functionality) => 83 | await handleWriteCode(userId, sessionId, functionality), 84 | arguments: ["code functionality"], 85 | promptTemplate: generateNewCodePrompt, 86 | availableTools: [name, ToolName.finalAnswer], 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /server/api/agent/tools/writeCompletedCode.tool.ts: -------------------------------------------------------------------------------- 1 | import { ToolName } from "."; 2 | import { 3 | getAiCodeBySessionOrAccount, 4 | updateAiWritenCode, 5 | } from "../../aiCreatedCode/aiCreatedCode.repository"; 6 | import { resetSession } from "../../session/session.service"; 7 | 8 | import { ToolInterface } from "../agent.service"; 9 | 10 | export function writeCompletedCodeTool(): ToolInterface { 11 | async function handleWriteCode( 12 | userId: string, 13 | sessionId: string, 14 | text: string 15 | ) { 16 | const aiGeneratedCode = await getAiCodeBySessionOrAccount(sessionId); 17 | 18 | if (aiGeneratedCode.length > 0) { 19 | // Get the most recent ai generated code that the location is not set to 20 | const findCodeReadyToWrite = aiGeneratedCode.find( 21 | (aiCreatedCode) => 22 | aiCreatedCode.location !== null && 23 | aiCreatedCode.code !== null && 24 | aiCreatedCode.path !== null 25 | ); 26 | 27 | if (findCodeReadyToWrite) { 28 | await updateAiWritenCode(findCodeReadyToWrite.id, { 29 | completed_at: new Date().toISOString(), 30 | }); 31 | await resetSession(userId, sessionId); 32 | return { 33 | output: `I've written the code to the location you specified. I've also cleared the session of the code and location so you can write new code.`, 34 | }; 35 | } else { 36 | return { 37 | output: `I can't find any code to write. You must first generate the code and set the location to write the code to.`, 38 | }; 39 | } 40 | } else { 41 | return { 42 | output: `I can't find any code to write. You must first generate the code and set the location to write the code to.`, 43 | }; 44 | } 45 | } 46 | 47 | const name = ToolName.writeCompletedCode; 48 | 49 | return { 50 | name, 51 | description: 52 | "Writes the given code functionality to the the location specified by the `set location` tool. Before using this tool you must set the location to write code to and generate the code.", 53 | use: async (userId, sessionId, text) => 54 | await handleWriteCode(userId, sessionId, text), 55 | arguments: [], 56 | promptTemplate: "", 57 | availableTools: [name], 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /server/api/aiCreatedCode/aiCreatedCode.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from "express"; 2 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 3 | import { findOrUpdateAccount } from "../supabase/account.service"; 4 | import { findOrCreateSession } from "../session/session.service"; 5 | import { 6 | getAiCodeBySessionOrAccount, 7 | updateAiWritenCode, 8 | } from "./aiCreatedCode.repository"; 9 | 10 | export const getAiCompletedCode = async ( 11 | req: AuthenticatedRequest, 12 | res: Response, 13 | next: NextFunction 14 | ) => { 15 | try { 16 | const { userId } = req; 17 | debugger; 18 | 19 | const { session_id } = req.headers; 20 | 21 | const sessionId = session_id as string; 22 | 23 | const account = await findOrUpdateAccount(userId); 24 | 25 | const aiCreatedCode = await getAiCodeBySessionOrAccount( 26 | sessionId, 27 | account.id 28 | ); 29 | 30 | return res.status(200).json({ 31 | data: [...aiCreatedCode], 32 | }); 33 | } catch (error: any) { 34 | console.log(error); 35 | res.status(500).json({ message: error.message }); 36 | } 37 | }; 38 | 39 | export const updateAiCompletedCode = async ( 40 | req: AuthenticatedRequest, 41 | res: Response 42 | ) => { 43 | try { 44 | const { values, id, sessionId } = req.body; 45 | const { userId } = req; 46 | 47 | await findOrCreateSession(userId, sessionId); 48 | 49 | await updateAiWritenCode(id, { ...values }); 50 | 51 | return res.status(200).json({ 52 | data: "done", 53 | }); 54 | } catch (error: any) { 55 | console.log(error); 56 | res.status(500).json({ message: error.message }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /server/api/aiCreatedCode/aiCreatedCode.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 3 | import { 4 | getAiCompletedCode, 5 | updateAiCompletedCode, 6 | } from "./aiCreatedCode.controller"; 7 | 8 | const aiCreatedCode = Router(); 9 | 10 | // Base route: /ai-completed-code 11 | 12 | aiCreatedCode.get("/", ensureAuthenticated, getAiCompletedCode); 13 | aiCreatedCode.post("/", ensureAuthenticated, updateAiCompletedCode); 14 | 15 | export default aiCreatedCode; 16 | -------------------------------------------------------------------------------- /server/api/aiCreatedCode/aiCreatedCode.service.ts: -------------------------------------------------------------------------------- 1 | import { ChatUserType } from "../../../types/chatMessage.type"; 2 | import { EngineName } from "../../../types/openAiTypes/openAiEngine"; 3 | import { Database } from "../../../types/supabase"; 4 | import { deserializeJson } from "../../../utils/deserializeJson"; 5 | import { 6 | createNewCodePrompt, 7 | getFileNameAndFunctionalityPrompt, 8 | } from "../codeCompletion/codeCompletion.prompts"; 9 | import { 10 | createChatCompletion, 11 | createTextCompletion, 12 | } from "../openAi/openai.service"; 13 | import { getSessionById } from "../session/session.repository"; 14 | import { 15 | createAiWritenCode, 16 | getAiCodeBySessionOrAccount, 17 | updateAiWritenCode, 18 | } from "./aiCreatedCode.repository"; 19 | 20 | export const createAiCodeFromNewFilePrompt = async ( 21 | userId: string, 22 | content: string, 23 | sessionId: string, 24 | path: string 25 | ) => { 26 | // Parse the content into file name and functionality 27 | 28 | const fileNameAndFunc = await createTextCompletion( 29 | getFileNameAndFunctionalityPrompt(content), 30 | 0.2 31 | ); 32 | if (!fileNameAndFunc) { 33 | throw new Error("Could not parse file name and functionality"); 34 | } 35 | const { fileName, functionality } = deserializeJson(fileNameAndFunc); 36 | 37 | if (!fileName || !functionality) { 38 | throw new Error("Could not parse file name and functionality"); 39 | } 40 | console.log("fileNameAndFunc", fileNameAndFunc, fileName, functionality); 41 | 42 | const session = await getSessionById(sessionId); 43 | 44 | const codePrompt = createNewCodePrompt( 45 | functionality, 46 | "", 47 | session.scratch_pad_content || "" 48 | ); 49 | 50 | // Create the code 51 | const response = await createChatCompletion( 52 | [ 53 | { 54 | role: ChatUserType.user, 55 | content: codePrompt, 56 | }, 57 | ], 58 | EngineName.GPT4 59 | ); 60 | 61 | // Add it to ai created code 62 | 63 | createAiWritenCode({ 64 | code: response, 65 | functionality, 66 | session_id: sessionId, 67 | location: "newFile", 68 | completed_at: new Date().toISOString(), 69 | path, 70 | file_name: fileName, 71 | }); 72 | }; 73 | 74 | export const findAndUpdateAiCodeBySession = async ( 75 | sessionId: string, 76 | updates: Partial, 77 | property: 78 | | "code" 79 | | "completed_at" 80 | | "created_at" 81 | | "file_name" 82 | | "functionality" 83 | | "id" 84 | | "location" 85 | | "path" 86 | | "session_id" 87 | | "writen_to_file_at" 88 | ) => { 89 | const aiGeneratedCode = await getAiCodeBySessionOrAccount(sessionId); 90 | 91 | if (!aiGeneratedCode || aiGeneratedCode?.length > 0) { 92 | // Get the most recent ai generated code that the location is not set to 93 | const aiGeneratedCodeWithProperyNotSet = aiGeneratedCode.find( 94 | (aiCreatedCode) => aiCreatedCode[property] === null 95 | ); 96 | 97 | // If there is an ai generated code that has not been set to the property 98 | if (aiGeneratedCodeWithProperyNotSet) { 99 | await updateAiWritenCode(aiGeneratedCodeWithProperyNotSet.id, { 100 | ...updates, 101 | }); 102 | } else { 103 | // If there is not an ai generated code, create one 104 | await createAiWritenCode({ 105 | session_id: sessionId, 106 | ...updates, 107 | }); 108 | } 109 | } else { 110 | await createAiWritenCode({ 111 | session_id: sessionId, 112 | ...updates, 113 | }); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /server/api/codeCompletion/codeCompletion.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 3 | import { 4 | createMessageWithUser, 5 | getOnlyRoleAndContentMessagesByUserAndSession, 6 | } from "../message/message.service"; 7 | import { createTextCompletion } from "../openAi/openai.service"; 8 | import { handleSearch } from "../search/search.service"; 9 | 10 | import { ChatUserType } from "../../../types/chatMessage.type"; 11 | import { updateSession } from "../session/session.repository"; 12 | import { findOrCreateSession } from "../session/session.service"; 13 | import { 14 | createBaseClassificationPrompt, 15 | runBaseClassificaitonChatCompletion, 16 | } from "./codeCompletion.classifier"; 17 | import { checkDbSession } from "./codeCompletion.service"; 18 | import { CodeCompletionRequest } from "./codeCompletion.types"; 19 | import { handleKnownNextAction } from "./scenerios/codeCompletion.knownNextAction"; 20 | 21 | export const handleCodeCompletion = async ( 22 | req: AuthenticatedRequest, 23 | res: Response 24 | ) => { 25 | try { 26 | const { userId } = req; 27 | 28 | const { sessionId, scratchPadContent } = req.body as CodeCompletionRequest; 29 | 30 | const sessionMessages = await getOnlyRoleAndContentMessagesByUserAndSession( 31 | userId, 32 | sessionId 33 | ); 34 | 35 | const dbSession = await findOrCreateSession(userId, sessionId); 36 | 37 | await updateSession(userId, sessionId, { 38 | scratch_pad_content: scratchPadContent, 39 | }); 40 | 41 | if (dbSession.expected_next_action) { 42 | return await handleKnownNextAction( 43 | sessionMessages, 44 | dbSession, 45 | userId, 46 | sessionId, 47 | res 48 | ); 49 | } 50 | 51 | // if (scratchPadContent){ 52 | // return await handleScratchPadContent( 53 | // sessionMessages, 54 | // dbSession, 55 | // userId 56 | // sessionId, 57 | // scratchPadContent, 58 | // res 59 | // ); 60 | // } 61 | 62 | const classifyMessage = await createTextCompletion( 63 | createBaseClassificationPrompt(sessionMessages), 64 | 0.2 65 | ); 66 | 67 | const baseClassificaiton = classifyMessage.trim(); 68 | 69 | console.log("Base classificaiton", baseClassificaiton); 70 | 71 | let metadata = { 72 | projectDirectory: "", 73 | projectFile: "", 74 | newFile: null as null | boolean, 75 | requiredFunctionality: "", 76 | }; 77 | 78 | if (baseClassificaiton === "generalChat") { 79 | const choices = await await runBaseClassificaitonChatCompletion( 80 | sessionMessages 81 | ); 82 | 83 | await createMessageWithUser( 84 | { 85 | content: choices, 86 | role: ChatUserType.assistant, 87 | }, 88 | sessionId 89 | ); 90 | return res.status(200).json({ 91 | data: { 92 | choices, 93 | metadata, 94 | }, 95 | }); 96 | } else if (baseClassificaiton === "creatingCode") { 97 | const responseBasedOnDbSession = await checkDbSession( 98 | dbSession, 99 | sessionMessages, 100 | userId, 101 | sessionId 102 | ); 103 | 104 | return res.status(200).json({ 105 | data: responseBasedOnDbSession, 106 | }); 107 | } else if (baseClassificaiton === "search") { 108 | return res.status(200).json(await handleSearch(userId, sessionId)); 109 | } else { 110 | res 111 | .status(500) 112 | .json({ message: "Something went wrong with the classification" }); 113 | } 114 | } catch (error: any) { 115 | console.log(error); 116 | 117 | res.status(500).json({ message: error.message }); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /server/api/codeCompletion/codeCompletion.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 3 | import { handleCodeCompletion } from "./codeCompletion.controller"; 4 | 5 | const codeCompletionRoutes = Router(); 6 | 7 | // Base route: /code 8 | 9 | codeCompletionRoutes.post("/", ensureAuthenticated, handleCodeCompletion); 10 | 11 | export default codeCompletionRoutes; 12 | -------------------------------------------------------------------------------- /server/api/codeCompletion/codeCompletion.rules.ts: -------------------------------------------------------------------------------- 1 | import { EngineName } from "../../../types/openAiTypes/openAiEngine"; 2 | import { 3 | CodeCompletionRequest, 4 | CodeCompletionResponseMetadata, 5 | } from "./codeCompletion.types"; 6 | 7 | import { 8 | AllValues, 9 | directoryOnlyPrompt, 10 | fileNamePrompt, 11 | newFilePrompt, 12 | refactorCodePrompt, 13 | requiredFunctionalityOnlyPrompt, 14 | } from "./codeCompletion.prompts"; 15 | 16 | export type NeededValues = 17 | | "projectDirectory" 18 | | "newfile" 19 | | "projectFile" 20 | | "requiredFunctionality" 21 | | "writeCode" 22 | | "none"; 23 | 24 | export const whatValuesDoWeNeed = (request: AllValues): NeededValues => { 25 | const { projectFile, newFile, projectDirectory, requiredFunctionality } = 26 | request; 27 | if (!projectDirectory) { 28 | return "projectDirectory"; 29 | } else if (projectDirectory && newFile === null) { 30 | return "newfile"; 31 | } else if (projectDirectory && newFile !== null && !projectFile) { 32 | return "projectFile"; 33 | } else if ( 34 | projectDirectory && 35 | newFile !== null && 36 | projectFile && 37 | !requiredFunctionality 38 | ) { 39 | return "requiredFunctionality"; 40 | } else if ( 41 | projectDirectory && 42 | newFile !== null && 43 | projectFile && 44 | requiredFunctionality 45 | ) { 46 | return "none"; 47 | } else { 48 | return "projectDirectory"; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /server/api/codeCompletion/codeCompletion.types.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage } from "../../../types/chatMessage.type"; 2 | 3 | export interface CodeDirectory { 4 | projectDirectory: string; 5 | newFile: boolean; 6 | } 7 | 8 | export interface CodeCompletionRequest { 9 | messages: ChatMessage[]; 10 | codeContent: string; 11 | fullFilePathWithName: string; 12 | sessionId: string; 13 | scratchPadContent?: string; 14 | } 15 | 16 | export interface CodeCompletionDetails { 17 | projectFile: string; 18 | requiredFunctionality: string; 19 | } 20 | 21 | export interface CodeCompletionResponseMetadata { 22 | projectDirectory: string; 23 | projectFile: string; 24 | newFile: boolean | null; 25 | requiredFunctionality: string; 26 | } 27 | 28 | export interface Choice { 29 | message: ChatMessage; 30 | } 31 | 32 | export interface CodeCompletionResponse { 33 | choices: Choice[]; 34 | metadata: CodeCompletionResponseMetadata; 35 | completedCode?: string; 36 | } 37 | 38 | export interface OpenAiChatCompletionResponse { 39 | id: string; 40 | created: number; 41 | model: string; 42 | choices: Choice[]; 43 | } 44 | 45 | export type BaseClassification = "generalChat" | "creatingCode"; 46 | 47 | export type CodeOrMessage = "code" | "message"; 48 | -------------------------------------------------------------------------------- /server/api/codeCompletion/scenerios/codeCompletion.knowLocNotFunc.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage, ChatUserType } from "../../../../types/chatMessage.type"; 2 | import { EngineName } from "../../../../types/openAiTypes/openAiEngine"; 3 | import { createMessageWithUser } from "../../message/message.service"; 4 | import { createChatCompletion } from "../../openAi/openai.service"; 5 | import { updateSession } from "../../session/session.repository"; 6 | import { 7 | requiredFunctionalityOnlyPrompt, 8 | useChooseDirectory, 9 | useChooseFile, 10 | } from "../codeCompletion.prompts"; 11 | import { 12 | addSystemMessage, 13 | codeCompletionResponse, 14 | } from "../codeCompletion.service"; 15 | import { CodeCompletionResponse } from "../codeCompletion.types"; 16 | 17 | export async function handleKNnowLocButNotFunc( 18 | messages: ChatMessage[], 19 | userId: string, 20 | sessionId: string, 21 | classification: { location: string; functionality: any } 22 | ): Promise { 23 | const { location, functionality } = classification; 24 | 25 | updateSession(userId, sessionId, { 26 | location, 27 | functionality, 28 | }); 29 | 30 | if (classification.location === "existingFile") { 31 | const adaptedMessages = addSystemMessage(messages, useChooseFile()); 32 | const response = await createChatCompletion( 33 | adaptedMessages, 34 | EngineName.Turbo, 35 | 0.3 36 | ); 37 | 38 | codeCompletionResponse.metadata = { 39 | projectDirectory: "", 40 | projectFile: "", 41 | newFile: false, 42 | requiredFunctionality: "", 43 | }; 44 | 45 | return { 46 | choices: [ 47 | { 48 | message: { 49 | content: response, 50 | role: ChatUserType.assistant, 51 | }, 52 | }, 53 | ], 54 | metadata: { 55 | projectDirectory: "existingFile", 56 | projectFile: "", 57 | newFile: false, 58 | requiredFunctionality: "", 59 | }, 60 | }; 61 | } else if (classification.location === "newFile") { 62 | console.log("new file"); 63 | const adaptedMessages = addSystemMessage(messages, useChooseDirectory()); 64 | const response = await createChatCompletion( 65 | adaptedMessages, 66 | EngineName.Turbo, 67 | 0.3, 68 | 400 69 | ); 70 | 71 | codeCompletionResponse.metadata = { 72 | projectDirectory: "", 73 | projectFile: "", 74 | newFile: true, 75 | requiredFunctionality: "", 76 | }; 77 | return { 78 | choices: [ 79 | { 80 | message: { 81 | content: response, 82 | role: ChatUserType.assistant, 83 | }, 84 | }, 85 | ], 86 | metadata: { 87 | projectDirectory: "newFile", 88 | projectFile: "", 89 | newFile: true, 90 | requiredFunctionality: "", 91 | }, 92 | }; 93 | } else { 94 | // Write to the scratch pad 95 | 96 | const adaptedMessages = await addSystemMessage( 97 | messages, 98 | requiredFunctionalityOnlyPrompt(messages) 99 | ); 100 | const response = await createChatCompletion( 101 | adaptedMessages, 102 | EngineName.Turbo, 103 | 0.1 104 | ); 105 | 106 | await createMessageWithUser( 107 | { 108 | content: response, 109 | role: ChatUserType.assistant, 110 | }, 111 | sessionId 112 | ); 113 | 114 | return { 115 | choices: [ 116 | { 117 | message: { 118 | content: response, 119 | role: ChatUserType.assistant, 120 | }, 121 | }, 122 | ], 123 | metadata: { 124 | projectDirectory: "scratchPad", 125 | projectFile: "", 126 | newFile: false, 127 | requiredFunctionality: "", 128 | }, 129 | }; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /server/api/codeDirectory/codeDirectory.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 3 | import { 4 | createDirectoryByAccount, 5 | getCodeDirectoriesByAccount, 6 | setDirectoryToAddFile, 7 | updateDirectory, 8 | } from "./codeDirectory.controller"; 9 | 10 | const codeDirectoryRoutes = Router(); 11 | 12 | // Base route: /code-directory 13 | 14 | codeDirectoryRoutes.get("/", ensureAuthenticated, getCodeDirectoriesByAccount); 15 | codeDirectoryRoutes.put("/", ensureAuthenticated, updateDirectory); 16 | codeDirectoryRoutes.post("/", ensureAuthenticated, createDirectoryByAccount); 17 | codeDirectoryRoutes.post( 18 | "/add-file", 19 | ensureAuthenticated, 20 | setDirectoryToAddFile 21 | ); 22 | 23 | export default codeDirectoryRoutes; 24 | -------------------------------------------------------------------------------- /server/api/codeDirectory/codeDirectory.types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | 3 | export type Directories = Database["public"]["Tables"]["code_directory"]["Row"]; 4 | export type Files = Database["public"]["Tables"]["code_file"]["Row"][]; 5 | export type DirectoryWithFiles = Directories & { code_file: Files[] }; 6 | -------------------------------------------------------------------------------- /server/api/codeFile/codeFile.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { ParseCode } from "../../../types/parseCode.types"; 3 | import { CodeCompletionRequest } from "../codeCompletion/codeCompletion.types"; 4 | 5 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 6 | import { handleFileUploadWithSession } from "../codeCompletion/codeCompletion.service"; 7 | import { getOnlyRoleAndContentMessagesByUserAndSession } from "../message/message.service"; 8 | import { findOrUpdateAccount } from "../supabase/account.service"; 9 | import { findOrCreateSession } from "../session/session.service"; 10 | import { 11 | findDeletedFiles, 12 | findDuplicateFiles, 13 | handleAndFilesToDb, 14 | writeTestsForFiles, 15 | } from "./codeFile.service"; 16 | import { 17 | deleteMulipleFilesById, 18 | findFilesByDirectoryId, 19 | } from "./codeFile.repository"; 20 | import { logInfo } from "../../../utils/commandLineColors"; 21 | 22 | export interface CreateFilesRequest { 23 | files: ParseCode[]; 24 | directoryId: number; 25 | baseApiUrl: string; 26 | session: any; 27 | sessionId: string; 28 | } 29 | 30 | export const findAndUpdateFilesFromClient = async ( 31 | req: AuthenticatedRequest, 32 | res: Response 33 | ) => { 34 | try { 35 | const { userId } = req; 36 | 37 | const account = await findOrUpdateAccount(userId); 38 | 39 | if (!account) { 40 | return res.status(404).json({ message: "Can't find the user account" }); 41 | } 42 | 43 | const { files, directoryId } = req.body as CreateFilesRequest; 44 | 45 | // const dbFiles = await findFilesByDirectoryId(directoryId); 46 | // if (dbFiles) { 47 | // logInfo(`DB Files ${dbFiles.length}`); 48 | // logInfo(`Client Files ${files.length}`); 49 | 50 | // const duplicateFiles = findDuplicateFiles(dbFiles); 51 | 52 | // if (duplicateFiles && duplicateFiles.duplicateCount > 0) { 53 | // deleteMulipleFilesById( 54 | // duplicateFiles.duplicateFilePairs.map((f) => f?.oldestFile?.id) 55 | // ); 56 | // } 57 | 58 | // const deletedFiles = findDeletedFiles(files, dbFiles); 59 | // if (deletedFiles.length > 0) { 60 | // deleteMulipleFilesById(deletedFiles.map((f) => f.id)); 61 | // } 62 | // } 63 | 64 | handleAndFilesToDb(files, account, directoryId); 65 | 66 | return res.status(200).json({ data: files }); 67 | } catch (error: any) { 68 | res.status(500).json({ message: error.message }); 69 | } 70 | }; 71 | 72 | export const handleFileUpload = async ( 73 | req: AuthenticatedRequest, 74 | res: Response 75 | ) => { 76 | try { 77 | const { userId } = req; 78 | 79 | const { fullFilePathWithName, sessionId, codeContent } = 80 | req.body as CodeCompletionRequest; 81 | 82 | const sessionMessages = await getOnlyRoleAndContentMessagesByUserAndSession( 83 | userId, 84 | sessionId 85 | ); 86 | const dbSession = await findOrCreateSession(userId, sessionId); 87 | 88 | const response = await handleFileUploadWithSession( 89 | sessionMessages, 90 | fullFilePathWithName, 91 | userId, 92 | sessionId, 93 | codeContent, 94 | dbSession 95 | ); 96 | 97 | return res.status(200).json({ 98 | data: response, 99 | }); 100 | } catch (error: any) { 101 | console.log(error); 102 | res.status(500).json({ message: error.message }); 103 | } 104 | }; 105 | 106 | export interface WriteTestRequest { 107 | files: ParseCode[]; 108 | directoryPath: string; 109 | baseApiUrl: string; 110 | session: any; 111 | sessionId: string; 112 | } 113 | 114 | export const handleWriteTests = async ( 115 | req: AuthenticatedRequest, 116 | res: Response 117 | ) => { 118 | try { 119 | const { userId } = req; 120 | 121 | const { directoryPath, sessionId, files } = req.body as WriteTestRequest; 122 | 123 | const account = await findOrUpdateAccount(userId); 124 | 125 | if (!account) { 126 | return res.status(404).json({ message: "Can't find the user account" }); 127 | } 128 | 129 | const response = await writeTestsForFiles( 130 | files, 131 | directoryPath, 132 | account.id, 133 | userId, 134 | sessionId 135 | ); 136 | 137 | return res.status(200).json({ 138 | data: response, 139 | }); 140 | } catch (error: any) { 141 | console.log(error); 142 | res.status(405).json({ message: error.message }); 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /server/api/codeFile/codeFile.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 4 | import { 5 | findAndUpdateFilesFromClient, 6 | handleFileUpload, 7 | handleWriteTests, 8 | } from "./codeFile.controller"; 9 | 10 | const codeFileRoutes = Router(); 11 | 12 | // Base route: /code-file 13 | 14 | codeFileRoutes.post("/", ensureAuthenticated, findAndUpdateFilesFromClient); 15 | codeFileRoutes.post("/add", ensureAuthenticated, handleFileUpload); 16 | codeFileRoutes.post("/write-tests", ensureAuthenticated, handleWriteTests); 17 | 18 | export default codeFileRoutes; 19 | -------------------------------------------------------------------------------- /server/api/codeFile/codeFile.type.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | import { CodeSnippet } from "../codeSnippet/codeSnippet.type"; 3 | 4 | export type DbFile = Database["public"]["Tables"]["code_file"]["Row"]; 5 | 6 | export type FileWithSnippets = DbFile & { code_snippet: CodeSnippet[] }; 7 | 8 | export type FileWithConfirmedContentAndAccount = { 9 | id: number; 10 | file_name: string; 11 | content: string; 12 | account_id: string; 13 | }; 14 | -------------------------------------------------------------------------------- /server/api/codeSnippet/codeSnippet.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { supabaseBaseServerClient } from "../../../server"; 3 | 4 | export const getCodeSnippets = async (req: Request, res: Response) => { 5 | try { 6 | const { data, error } = await supabaseBaseServerClient 7 | .from("code_snippet") 8 | .select("*"); 9 | 10 | res.status(200).json({ data }); 11 | } catch (error: any) { 12 | res.status(500).json({ message: error.message }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /server/api/codeSnippet/codeSnippet.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getCodeSnippets } from "./codeSnippet.controller"; 3 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 4 | 5 | const codeSnippetRoutes = Router(); 6 | 7 | // Base route: /code-snippet 8 | 9 | codeSnippetRoutes.get("/", ensureAuthenticated, getCodeSnippets); 10 | 11 | export default codeSnippetRoutes; 12 | -------------------------------------------------------------------------------- /server/api/codeSnippet/codeSnippet.type.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | 3 | export type CodeSnippet = Database["public"]["Tables"]["code_snippet"]["Row"]; 4 | -------------------------------------------------------------------------------- /server/api/exoConfig/example.md: -------------------------------------------------------------------------------- 1 | This directory, code-gen-server, contains code files that are used to handle code completion, file upload, and search requests. The code files contain functions that check the session of the user making the request, extract data from the request body, create a classifier, handle the file upload, search, or create messages with users. If there is an error, the code files will log the error and return a response of status code 500 with an error message. 2 | 3 | // Use pure functions: A pure function is a function that always returns the same output for the same input, without any side effects. Pure functions are easier to reason about, test, and compose, making your code more functional and modular. 4 | 5 | // Use function composition: Function composition is the process of combining two or more functions to produce a new function. Function composition can simplify complex code by breaking it down into smaller, reusable functions. 6 | 7 | // Use immutability: Immutable data is data that cannot be changed after it is created. Immutable data makes your code more predictable and easier to reason about because you can be sure that the data will not change unexpectedly. 8 | 9 | // Use type inference: TypeScript has excellent type inference capabilities that can help you write more concise and expressive code. You can leverage TypeScript's type inference by using type aliases, type intersections, and type unions to create more precise and reusable types. 10 | 11 | // Use functional programming libraries: Functional programming libraries like Ramda, lodash/fp, and RxJS can help you write more functional and declarative code. These libraries provide many useful functions and utilities for working with functional programming concepts like currying, composition, and immutability. 12 | 13 | // Use algebraic data types: Algebraic data types like enums, unions, and interfaces can help you represent complex data structures in a more type-safe and expressive way. Algebraic data types can make your code more modular, reusable, and easier to reason about. 14 | 15 | // Use type guards: Type guards are functions that check the type of a value at runtime and return a boolean value that TypeScript can use to narrow the type of the value. Type guards can help you write more type-safe code and avoid runtime errors. 16 | -------------------------------------------------------------------------------- /server/api/exoConfig/exoConfig.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { createAiWritenCode } from "../aiCreatedCode/aiCreatedCode.repository"; 3 | import { updateFileById } from "../codeFile/codeFile.repository"; 4 | import { findOrUpdateAccount } from "../supabase/account.service"; 5 | import { checkSessionOrThrow } from "../session/session.service"; 6 | 7 | export const updateExoConfig = async (req: Request, res: Response) => { 8 | try { 9 | const session = await checkSessionOrThrow(req, res); 10 | 11 | const { user } = session.data; 12 | 13 | const account = await findOrUpdateAccount(user.id); 14 | 15 | const { exoConfig, fileId } = req.body; 16 | 17 | const file = await updateFileById(fileId, { 18 | content: JSON.stringify(exoConfig), 19 | updated_at: new Date().toISOString(), 20 | }); 21 | 22 | // Create the ai written code will update the file in the users repo next time they login to the app 23 | await createAiWritenCode({ 24 | account_id: account.id, 25 | location: "existingFile", 26 | completed_at: new Date().toISOString(), 27 | path: file?.file_path || "", 28 | file_name: file?.file_name, 29 | code: JSON.stringify(exoConfig), 30 | }); 31 | 32 | res.status(200).json({ data: "done" }); 33 | } catch (error: any) { 34 | res.status(500).json({ message: error.message }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /server/api/exoConfig/exoConfig.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { updateExoConfig } from "./exoConfig.controller"; 3 | 4 | const exoConfigRoutes = Router(); 5 | 6 | // Base route: /exo-config 7 | 8 | exoConfigRoutes.post("/", updateExoConfig); 9 | 10 | export default exoConfigRoutes; 11 | -------------------------------------------------------------------------------- /server/api/exoConfig/exoConfig.service.ts: -------------------------------------------------------------------------------- 1 | import { logError } from "../../../utils/commandLineColors"; 2 | import { 3 | createExoConfig, 4 | findExoConfigFileByCodeDirectoryId, 5 | findFileById, 6 | } from "../codeFile/codeFile.repository"; 7 | 8 | export const getOrCreateExoConfig = async (codeFileId: number) => { 9 | const codeFile = await findFileById(codeFileId); 10 | if (!codeFile?.code_directory_id) { 11 | logError("No code file found for exo config"); 12 | return null; 13 | } 14 | // Find the exo config for the code directory id 15 | const foundExoConfig = await findExoConfigFileByCodeDirectoryId( 16 | codeFile?.code_directory_id 17 | ); 18 | if (!foundExoConfig) { 19 | // Create a new exo config 20 | 21 | return await createExoConfig(codeFile?.code_directory_id); 22 | } else { 23 | return foundExoConfig; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /server/api/exoConfig/templateExoConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "directoryName": "", 3 | "explanation": "", 4 | "codeStandards": [], 5 | "testFramworks": [] 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /server/api/exportImportMap/exportImportMap.controller.ts: -------------------------------------------------------------------------------- 1 | // ```javascript 2 | import { Request, Response } from "express"; 3 | import { 4 | checkSessionOrThrow, 5 | findOrCreateSession, 6 | } from "../session/session.service"; 7 | 8 | export const handleGetExportImportMap = async (req: Request, res: Response) => { 9 | try { 10 | const { exportImportMapId, sessionId } = req.body; 11 | const session = await checkSessionOrThrow(req, res); 12 | 13 | const { user } = session.data; 14 | 15 | const dbSession = await findOrCreateSession(user.id, sessionId); 16 | 17 | // TODO - Add get map 18 | 19 | return res.status(200).json({ 20 | data: "done", 21 | }); 22 | } catch (error: any) { 23 | console.log(error); 24 | res.status(500).json({ message: error.message }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /server/api/exportImportMap/exportImportMap.repository.ts: -------------------------------------------------------------------------------- 1 | import { findAllSnippetsImportStatements } from "../codeSnippet/codeSnippet.repository"; 2 | import { supabaseBaseServerClient } from "../../../server"; 3 | 4 | export const getExportImportMaps = async () => { 5 | const { data, error } = await supabaseBaseServerClient 6 | .from("export_import_snippet_map") 7 | .select("*, code_snippet(file_name, name, id)"); 8 | 9 | const imports = await findAllSnippetsImportStatements(); 10 | 11 | let withExport = []; 12 | if (data && data.length > 0) { 13 | for (let map of data) { 14 | withExport.push({ 15 | ...map, 16 | import_snippet: imports.find( 17 | (i: { id: number | null }) => i.id === map.import_id 18 | ), 19 | }); 20 | } 21 | } else { 22 | return []; 23 | } 24 | return withExport; 25 | }; 26 | 27 | export const findImportExportMapByImportId = async (importId: number) => { 28 | const { data, error } = await supabaseBaseServerClient 29 | .from("export_import_snippet_map") 30 | .select("*") 31 | .eq("import_id", importId); 32 | 33 | if (error) { 34 | console.log("Error finding export import map by import id", error); 35 | return null; 36 | } 37 | 38 | if (!data || data.length === 0) { 39 | return null; 40 | } 41 | 42 | return data; 43 | }; 44 | -------------------------------------------------------------------------------- /server/api/exportImportMap/exportImportMap.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { handleGetExportImportMap } from "./exportImportMap.controller"; 3 | 4 | const exportImportMapRoutes = Router(); 5 | 6 | // Base route: /export-import-maps 7 | 8 | exportImportMapRoutes.get("/", handleGetExportImportMap); 9 | 10 | export default exportImportMapRoutes; 11 | -------------------------------------------------------------------------------- /server/api/exportImportMap/exportImportMap.service.ts: -------------------------------------------------------------------------------- 1 | import { getExportImportMaps } from "./exportImportMap.repository"; 2 | 3 | export async function findCodeNodes() { 4 | const allExportImportMaps = await getExportImportMaps(); 5 | console.log(allExportImportMaps.length); 6 | // console.log(allExportImportMaps); 7 | 8 | // const getCountsOfImportsById = (exportImportMaps: any) => { 9 | // const countsOfImportsById: any = {}; 10 | // for (let i = 0; i < exportImportMaps.length; i++) { 11 | // const exportImportMap = exportImportMaps[i]; 12 | // const { import_id, name } = exportImportMap; 13 | // if (!countsOfImportsById[import_id]) { 14 | // countsOfImportsById[import_id] = 0; 15 | // countsOfImportsById.name = name; 16 | // } 17 | // countsOfImportsById[import_id]++; 18 | // } 19 | // return countsOfImportsById; 20 | // }; 21 | 22 | const getCountsOfExportsById = (exportImportMaps: any) => { 23 | const countsOfExportsById: { 24 | export_id: number; 25 | name: string; 26 | count: number; 27 | }[] = []; 28 | for (let i = 0; i < exportImportMaps.length; i++) { 29 | const exportImportMap = exportImportMaps[i]; 30 | const { export_id, code_snippet } = exportImportMap; 31 | if (!countsOfExportsById[export_id]) { 32 | countsOfExportsById.push({ 33 | export_id, 34 | name: code_snippet.name, 35 | count: 0, 36 | }); 37 | } 38 | const toUpdate = countsOfExportsById.find( 39 | (item) => item.export_id === export_id 40 | ); 41 | if (toUpdate) { 42 | toUpdate.count++; 43 | } 44 | } 45 | return countsOfExportsById; 46 | }; 47 | 48 | // console.log("Imports:", getCountsOfImportsById(allExportImportMaps)); 49 | 50 | console.log( 51 | "Exports:", 52 | getCountsOfExportsById(allExportImportMaps).filter((item) => item.count > 5) 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /server/api/github/github.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyGithubWebhook } from "../../middleware/githubWebhookAuth"; 3 | import { acceptWebhook, addFileToRepo } from "./github.controller"; 4 | 5 | const githubRoutes = Router(); 6 | 7 | // Base route: /github 8 | 9 | githubRoutes.post("/", verifyGithubWebhook, acceptWebhook); 10 | githubRoutes.get("/test-add-file-to-repo", addFileToRepo); 11 | 12 | export default githubRoutes; 13 | -------------------------------------------------------------------------------- /server/api/github/github.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createBlobByRepo, 3 | createCommitByRepo, 4 | createNewTreeByRepo, 5 | getCommitByRepo, 6 | getRefByRepo, 7 | getTreeByRepo, 8 | initOctokit, 9 | updateRefByRepo, 10 | } from "./github.repository"; 11 | 12 | interface AddFileToCommitOptions { 13 | owner: string; 14 | repo: string; 15 | branch: string; 16 | filePath: string; 17 | content: string; 18 | commitMessage: string; 19 | installationId: string; 20 | } 21 | 22 | export async function addFileToCommit( 23 | options: AddFileToCommitOptions 24 | ): Promise { 25 | const { owner, repo, branch, filePath, content, commitMessage } = options; 26 | const octokit = initOctokit(options.installationId); 27 | 28 | const newBlob = await createBlobByRepo({ 29 | owner, 30 | repo, 31 | content, 32 | octokit, 33 | }); 34 | const branchRef = await getRefByRepo({ 35 | owner, 36 | repo, 37 | branch, 38 | octokit, 39 | }); 40 | const latestCommit = await getCommitByRepo({ 41 | owner, 42 | repo, 43 | commitSha: branchRef.object.sha, 44 | octokit, 45 | }); 46 | const tree = await getTreeByRepo({ 47 | owner, 48 | repo, 49 | treeSha: latestCommit.tree.sha, 50 | octokit, 51 | }); 52 | // Create a new tree with the added file 53 | const newTree = tree.tree.concat({ 54 | path: filePath, 55 | mode: "100644", 56 | type: "blob", 57 | sha: newBlob.sha, 58 | }) as any; 59 | const newTreeResult = await createNewTreeByRepo({ 60 | owner, 61 | repo, 62 | newTree, 63 | baseTreeSha: latestCommit.tree.sha, 64 | octokit, 65 | }); 66 | const newCommit = await createCommitByRepo({ 67 | owner, 68 | repo, 69 | message: commitMessage, 70 | newTreeResultSha: newTreeResult.sha, 71 | octokit, 72 | latestCommitSha: latestCommit.sha, 73 | }); 74 | await updateRefByRepo({ 75 | owner, 76 | repo, 77 | branch, 78 | newCommit, 79 | octokit, 80 | }); 81 | } 82 | 83 | interface GetFilesInBranchOptions { 84 | owner: string; 85 | repo: string; 86 | commitSha: string; 87 | 88 | installationId: string; 89 | } 90 | 91 | export async function getFilesInBranch( 92 | options: GetFilesInBranchOptions 93 | ): Promise { 94 | const { owner, repo, commitSha, installationId } = options; 95 | const octokit = initOctokit(installationId); 96 | 97 | // Get the commit object for the specified SHA 98 | const commit = await getCommitByRepo({ 99 | owner, 100 | repo, 101 | commitSha, 102 | octokit, 103 | }); 104 | 105 | const parentCommitSha = commit.parents[0].sha; 106 | const parentCommit = await getCommitByRepo({ 107 | owner, 108 | repo, 109 | commitSha: parentCommitSha, 110 | octokit, 111 | }); 112 | console.log(parentCommit); 113 | 114 | const parentTree = await getTreeByRepo({ 115 | owner, 116 | repo, 117 | treeSha: parentCommit.tree.sha, 118 | recursive: true, 119 | octokit, 120 | }); 121 | 122 | // Get the tree object for the commit 123 | const tree = await getTreeByRepo({ 124 | owner, 125 | repo, 126 | treeSha: commit.tree.sha, 127 | recursive: true, 128 | octokit, 129 | }); 130 | 131 | // Filter the files based on change type (added or modified) 132 | const updatedOrAddedFiles: string[] = tree.tree 133 | .filter((item) => { 134 | const parentTreeEntry = parentTree.tree.find( 135 | (parentItem: { path: any }) => parentItem.path === item.path 136 | ); 137 | return !parentTreeEntry || parentTreeEntry.sha !== item.sha; 138 | }) 139 | .map((item) => item.path); 140 | 141 | console.log(updatedOrAddedFiles); 142 | 143 | return updatedOrAddedFiles; 144 | } 145 | -------------------------------------------------------------------------------- /server/api/memory/memory.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { checkSessionOrThrow } from "../session/session.service"; 3 | import { 4 | createMemoryWithSession, 5 | getMemoriesBySession, 6 | } from "./memory.service"; 7 | 8 | export const getMemories = async (req: Request, res: Response) => { 9 | try { 10 | const session = await checkSessionOrThrow(req, res); 11 | 12 | const { session_id } = req.headers; 13 | 14 | const sessionId = session_id as string; 15 | 16 | const memories = await getMemoriesBySession(sessionId); 17 | 18 | return res.status(200).json({ 19 | data: memories, 20 | }); 21 | } catch (error: any) { 22 | res.status(500).json({ message: error.message }); 23 | } 24 | }; 25 | 26 | export const createMemory = async (req: Request, res: Response) => { 27 | try { 28 | const session = await checkSessionOrThrow(req, res); 29 | 30 | const { session_id } = req.headers; 31 | 32 | const sessionId = session_id as string; 33 | 34 | const response = await createMemoryWithSession(req.body.memory, sessionId); 35 | 36 | if (!response) { 37 | return res.status(404).json({ message: "Error creating message" }); 38 | } 39 | 40 | return res.status(200).json({ 41 | data: response, 42 | }); 43 | } catch (error: any) { 44 | res.status(500).json({ message: error.message }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /server/api/memory/memory.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createMemory, getMemories } from "./memory.controller"; 3 | 4 | const memoryRoutes = Router(); 5 | 6 | // Base route: /messages 7 | 8 | memoryRoutes.get("/", getMemories); 9 | 10 | memoryRoutes.post("/", createMemory); 11 | 12 | export default memoryRoutes; 13 | -------------------------------------------------------------------------------- /server/api/memory/memory.service.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | import { supabaseBaseServerClient } from "../../../server"; 3 | import { createEmbeddings } from "../openAi/openAi.repository"; 4 | 5 | export const getMemoriesBySession = async ( 6 | sessionId: string 7 | ): Promise< 8 | Partial[] | [] 9 | > => { 10 | const { data, error } = await supabaseBaseServerClient 11 | .from("short_term_memory") 12 | .select("id, memory_text, created_at, session_id, memory_context") 13 | .order("created_at", { ascending: false }) 14 | .eq("session_id", sessionId); 15 | 16 | if (error || !data) { 17 | console.log("Error getting memories", error); 18 | return []; 19 | } 20 | console.log(data); 21 | 22 | return data || []; 23 | }; 24 | 25 | export const createMemoryWithSession = async ( 26 | memory: Database["public"]["Tables"]["short_term_memory"]["Insert"], 27 | sessionId: string 28 | ): Promise => { 29 | memory["session_id"] = sessionId; 30 | 31 | if (memory.memory_text) { 32 | memory.memory_embedding = await createEmbeddings([memory.memory_text]); 33 | } 34 | 35 | const { data, error } = await supabaseBaseServerClient 36 | .from("short_term_memory") 37 | // @ts-ignore 38 | .insert([memory]) 39 | .select("*"); 40 | 41 | if (error || !data || data.length === 0) { 42 | console.log("Error creating memory", error); 43 | return null; 44 | } 45 | 46 | return data[0]; 47 | }; 48 | 49 | export const getMemoriesById = async ( 50 | id: string 51 | ): Promise | null> => { 54 | const { data, error } = await supabaseBaseServerClient 55 | .from("short_term_memory") 56 | .select("id, memory_text, created_at, session_id, memory_context") 57 | .order("created_at", { ascending: false }) 58 | .eq("id", id); 59 | 60 | if (error || !data) { 61 | console.log("Error getting memories", error); 62 | return null; 63 | } 64 | console.log(data); 65 | 66 | return data[0] || []; 67 | }; 68 | -------------------------------------------------------------------------------- /server/api/message/message.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 3 | import { 4 | createMessageWithUser, 5 | getMessagesByUserAndSession, 6 | } from "./message.service"; 7 | 8 | export const getMessages = async (req: AuthenticatedRequest, res: Response) => { 9 | try { 10 | const { session_id } = req.headers; 11 | 12 | const sessionId = session_id as string; 13 | // TODO - need to find a better way to track views 14 | // await findUnseenHelperMessages(userId, sessionId); 15 | 16 | const messages = await getMessagesByUserAndSession(sessionId); 17 | 18 | return res.status(200).json({ 19 | data: messages, 20 | }); 21 | } catch (error: any) { 22 | res.status(500).json({ message: error.message }); 23 | } 24 | }; 25 | 26 | export const createMessages = async (req: Request, res: Response) => { 27 | try { 28 | const response = await createMessageWithUser( 29 | req.body.message, 30 | req.body.sessionId 31 | ); 32 | 33 | if (!response) { 34 | return res.status(404).json({ message: "Error creating message" }); 35 | } 36 | 37 | return res.status(200).json({ 38 | data: response, 39 | }); 40 | } catch (error: any) { 41 | res.status(500).json({ message: error.message }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /server/api/message/message.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createMessages, getMessages } from "./message.controller"; 3 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 4 | 5 | const messageRoutes = Router(); 6 | 7 | // Base route: /messages 8 | 9 | messageRoutes.get("/", ensureAuthenticated, getMessages); 10 | 11 | messageRoutes.post("/", ensureAuthenticated, createMessages); 12 | 13 | export default messageRoutes; 14 | -------------------------------------------------------------------------------- /server/api/objective/objective.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { 3 | createObjectiveWithSession, 4 | getObjectivesBySession, 5 | } from "./objective.repository"; 6 | 7 | export const getObjectives = async (req: Request, res: Response) => { 8 | try { 9 | const { session_id } = req.headers; 10 | 11 | const sessionId = session_id as string; 12 | 13 | const objectives = await getObjectivesBySession(sessionId); 14 | 15 | return res.status(200).json({ 16 | data: objectives, 17 | }); 18 | } catch (error: any) { 19 | res.status(500).json({ message: error.message }); 20 | } 21 | }; 22 | 23 | export const createObjective = async (req: Request, res: Response) => { 24 | try { 25 | const { session_id } = req.headers; 26 | 27 | const sessionId = session_id as string; 28 | 29 | const response = await createObjectiveWithSession( 30 | req.body.objective, 31 | sessionId 32 | ); 33 | 34 | if (!response) { 35 | return res.status(404).json({ message: "Error creating message" }); 36 | } 37 | 38 | return res.status(200).json({ 39 | data: response, 40 | }); 41 | } catch (error: any) { 42 | res.status(500).json({ message: error.message }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /server/api/objective/objective.repository.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | import { supabaseBaseServerClient } from "../../../server"; 3 | import { ObjectiveWithTasks } from "./objective.types"; 4 | import { logError } from "../../../utils/commandLineColors"; 5 | 6 | export const getObjectivesBySession = async ( 7 | sessionId: string 8 | ): Promise< 9 | Partial[] | [] 10 | > => { 11 | const { data, error } = await supabaseBaseServerClient 12 | .from("objective") 13 | .select("*") 14 | .order("created_at", { ascending: false }) 15 | .eq("session_id", sessionId); 16 | 17 | if (error || !data) { 18 | console.log("Error getting objectives", error); 19 | return []; 20 | } 21 | 22 | return data || []; 23 | }; 24 | 25 | export const createObjectiveWithSession = async ( 26 | objective: Database["public"]["Tables"]["objective"]["Insert"], 27 | sessionId: string 28 | ): Promise => { 29 | objective["session_id"] = sessionId; 30 | 31 | const { data, error } = await supabaseBaseServerClient 32 | .from("objective") 33 | // @ts-ignore 34 | .insert([objective]) 35 | .select("*"); 36 | 37 | if (error || !data || data.length === 0) { 38 | console.log("Error creating objective", error); 39 | return null; 40 | } 41 | 42 | return data[0]; 43 | }; 44 | 45 | export const getObjectiveById = async ( 46 | id: string 47 | ): Promise => { 48 | const { data, error } = await supabaseBaseServerClient 49 | .from("objective") 50 | .select("*, task(*)") 51 | .order("created_at", { ascending: false }) 52 | .eq("id", id); 53 | 54 | if (error || !data || data.length === 0) { 55 | logError(`Error getting objectives: ${error?.message}`); 56 | return null; 57 | } 58 | console.log(data); 59 | 60 | return (data[0] as ObjectiveWithTasks) || null; 61 | }; 62 | -------------------------------------------------------------------------------- /server/api/objective/objective.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createObjective, getObjectives } from "./objective.controller"; 3 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 4 | 5 | const objectiveRoutes = Router(); 6 | 7 | // Base route: /objective 8 | 9 | objectiveRoutes.get("/", ensureAuthenticated, getObjectives); 10 | objectiveRoutes.post("/", ensureAuthenticated, createObjective); 11 | 12 | export default objectiveRoutes; 13 | -------------------------------------------------------------------------------- /server/api/objective/objective.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exoaihq/exo-server/264be6371690883562f7ad4ff54d66c44746307f/server/api/objective/objective.service.ts -------------------------------------------------------------------------------- /server/api/objective/objective.types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | import { Task } from "../task/task.types"; 3 | 4 | export type Objective = Database["public"]["Tables"]["objective"]["Row"]; 5 | 6 | export type ObjectiveWithTasks = Objective & { task: Task[] }; 7 | -------------------------------------------------------------------------------- /server/api/openAi/openAi.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { updateOpenAiModels } from "../supabase/supabase.service"; 3 | import { getOpenAiModels } from "./openAi.repository"; 4 | 5 | export const getModels = async (req: Request, res: Response) => { 6 | try { 7 | const models = await getOpenAiModels(); 8 | 9 | await updateOpenAiModels( 10 | models.data.data.map((model: any) => { 11 | return { 12 | id: model.id, 13 | ready: model.ready, 14 | object: model.object, 15 | }; 16 | }) 17 | ); 18 | 19 | res.status(200).json({ data: models.data }); 20 | } catch (error: any) { 21 | res.status(500).json({ message: error.message }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /server/api/openAi/openAi.repository.ts: -------------------------------------------------------------------------------- 1 | // This is the lowest (or highest) level - it interacts with open ai api directly. 2 | 3 | import { RateLimiter } from "limiter"; 4 | import { ChatMessage, ChatUserType } from "../../../types/chatMessage.type"; 5 | import { EngineName } from "../../../types/openAiTypes/openAiEngine"; 6 | import { 7 | clearLoading, 8 | commandLineLoading, 9 | } from "../../../utils/commandLineLoadingl"; 10 | import { openAiApiKey } from "../../../utils/envVariable"; 11 | import { logError } from "../../../utils/commandLineColors"; 12 | 13 | const { Configuration, OpenAIApi } = require("openai"); 14 | const { encode, decode } = require("gpt-3-encoder"); 15 | 16 | const configuration = new Configuration({ 17 | apiKey: openAiApiKey, 18 | }); 19 | 20 | const openai = new OpenAIApi(configuration); 21 | 22 | export function truncateStringTokens(str: string, maxTokens = 2046) { 23 | let encoded = encode(str || ""); 24 | return decode(encoded.slice(0, maxTokens)); 25 | } 26 | 27 | // Allow 30 requests per min (the Open api limit for embeddings). Also understands 28 | // 'second', 'minute', 'day', or a number of milliseconds 29 | const limiter = new RateLimiter({ tokensPerInterval: 15, interval: "minute" }); 30 | 31 | export async function baseCreateChat( 32 | messages: ChatMessage[], 33 | model: EngineName = EngineName.Turbo, 34 | temperature: number = 1, 35 | maxTokens: number = 2048, 36 | stop: string[] | null = null, 37 | loadingMessage: string = "Loading create chat completion" 38 | ): Promise { 39 | const interval = commandLineLoading(loadingMessage); 40 | try { 41 | const remainingRequests = await limiter.removeTokens(1); 42 | const res = await openai.createChatCompletion({ 43 | model, 44 | messages, 45 | max_tokens: maxTokens, 46 | temperature, 47 | stop, 48 | }); 49 | if (res.status === 429) { 50 | console.log("Too many requests"); 51 | setTimeout(() => { 52 | baseCreateChat(messages, model, temperature, maxTokens); 53 | }, 2000); 54 | } 55 | clearLoading(interval, ` Query completed`); 56 | return res?.data?.choices[0]?.message?.content; 57 | } catch (error: any) { 58 | clearLoading(interval, `Query failed`); 59 | logError(error.response.data.error.message); 60 | return ""; 61 | } 62 | } 63 | 64 | export async function getOpenAiModels() { 65 | try { 66 | return await openai.listEngines(); 67 | } catch (error: any) { 68 | console.log(error); 69 | throw error; 70 | } 71 | } 72 | 73 | export async function createEmbeddings(documents: Array): Promise { 74 | const interval = commandLineLoading("Creating embeddings"); 75 | 76 | const response = await openai.createEmbedding({ 77 | model: "text-embedding-ada-002", 78 | input: documents.map((d) => { 79 | if (!d) return ""; 80 | return truncateStringTokens(d, 8191); 81 | }), 82 | }); 83 | const [{ embedding }] = response?.data?.data; 84 | clearLoading(interval, `Embedding completed`); 85 | return embedding; 86 | } 87 | 88 | export async function getTexCompletionUsingDavinci( 89 | prompt: string, 90 | temperature: number = 1, 91 | stop: string[] | null = null 92 | ): Promise { 93 | try { 94 | const res = await openai.createCompletion({ 95 | model: "text-davinci-003", 96 | prompt: truncateStringTokens(prompt, 2048), 97 | max_tokens: 2048, 98 | temperature, 99 | stop, 100 | }); 101 | 102 | return res?.data?.choices[0]?.text; 103 | } catch (error: any) { 104 | console.log(error); 105 | return error; 106 | } 107 | } 108 | 109 | export async function getCompletionDefaultStopToken( 110 | prompt: string, 111 | temperature: number = 0 112 | ): Promise { 113 | try { 114 | const res = await openai.createCompletion({ 115 | model: "text-davinci-002", 116 | prompt, 117 | max_tokens: 2048, 118 | temperature, 119 | }); 120 | return res?.data?.choices[0]?.text; 121 | } catch (error: any) { 122 | console.log(error); 123 | return ""; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /server/api/prompt/prompt.controller.ts: -------------------------------------------------------------------------------- 1 | // ```javascript 2 | import { Request, Response } from "express"; 3 | import { findOrUpdateAccount } from "../supabase/account.service"; 4 | 5 | import { 6 | getGlobalPromptsDb, 7 | getPromptById, 8 | handleUsingSelectedPrompt, 9 | } from "./prompt.service"; 10 | import { 11 | checkSessionOrThrow, 12 | findOrCreateSession, 13 | } from "../session/session.service"; 14 | 15 | export const getGlobalPrompts = async (req: Request, res: Response) => { 16 | try { 17 | const session = await checkSessionOrThrow(req, res); 18 | 19 | const { user } = session.data; 20 | 21 | const account = await findOrUpdateAccount(user.id); 22 | if (!account) { 23 | return res.status(404).json({ message: "Can't find the user account" }); 24 | } 25 | 26 | const response = await getGlobalPromptsDb(); 27 | 28 | res.status(200).json({ data: response }); 29 | } catch (error: any) { 30 | res.status(500).json({ message: error.message }); 31 | } 32 | }; 33 | 34 | export const handleSelectedPrompt = async (req: Request, res: Response) => { 35 | try { 36 | const { promptId, sessionId } = req.body; 37 | const session = await checkSessionOrThrow(req, res); 38 | 39 | const { user } = session.data; 40 | 41 | const dbSession = await findOrCreateSession(user.id, sessionId); 42 | 43 | const prompt = await getPromptById(promptId); 44 | 45 | if (!prompt) { 46 | return res.status(404).json({ message: "Can't find the prompt" }); 47 | } 48 | 49 | await handleUsingSelectedPrompt(dbSession, sessionId, user.id, prompt); 50 | 51 | return res.status(200).json({ 52 | data: "done", 53 | }); 54 | } catch (error: any) { 55 | console.log(error); 56 | res.status(500).json({ message: error.message }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /server/api/prompt/prompt.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getGlobalPrompts, handleSelectedPrompt } from "./prompt.controller"; 3 | 4 | const promptRoutes = Router(); 5 | 6 | // Base route: /prompts 7 | 8 | promptRoutes.get("/", getGlobalPrompts); 9 | promptRoutes.post("/", handleSelectedPrompt); 10 | 11 | export default promptRoutes; 12 | -------------------------------------------------------------------------------- /server/api/search/search.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 3 | import { findOrUpdateAccount } from "../supabase/account.service"; 4 | import { findCodeByQuery } from "./search.service"; 5 | 6 | export const searchCode = async (req: AuthenticatedRequest, res: Response) => { 7 | try { 8 | const { userId } = req; 9 | 10 | const { search } = req.body; 11 | 12 | const account = await findOrUpdateAccount(userId); 13 | if (!account) { 14 | return res.status(404).json({ message: "Can't find the user account" }); 15 | } 16 | 17 | const response = await findCodeByQuery(search, account.id); 18 | 19 | res.status(200).json({ data: response }); 20 | } catch (error: any) { 21 | res.status(405).json({ message: error.message }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /server/api/search/search.repository.ts: -------------------------------------------------------------------------------- 1 | import { supabaseBaseServerClient } from "../../../server"; 2 | import { Database } from "../../../types/supabase"; 3 | import { createEmbeddings } from "../openAi/openAi.repository"; 4 | 5 | export async function codeDirectorySearch( 6 | searchQuery: string, 7 | accountId: string, 8 | match_count: number = 10 9 | ) { 10 | const embedding = await createEmbeddings([searchQuery]); 11 | 12 | const query = { 13 | accountid: accountId, 14 | query_embedding: embedding, 15 | similarity_threshold: 0.7, 16 | match_count, 17 | }; 18 | 19 | const { data, error } = await supabaseBaseServerClient.rpc( 20 | "match_code_directory", 21 | query 22 | ); 23 | 24 | return data; 25 | } 26 | 27 | export async function findFileByExplainationEmbedding( 28 | searchQuery: string, 29 | accountId: string, 30 | match_count: number = 10 31 | ): Promise[] | []> { 32 | const embedding = await createEmbeddings([searchQuery]); 33 | const query = { 34 | accountid: accountId, 35 | query_embedding: embedding, 36 | similarity_threshold: 0.5, 37 | match_count, 38 | }; 39 | 40 | const { data, error } = await supabaseBaseServerClient.rpc( 41 | "match_code_file", 42 | query 43 | ); 44 | 45 | if (error) { 46 | console.log(error); 47 | return []; 48 | } 49 | if (!data) { 50 | return []; 51 | } 52 | return data; 53 | } 54 | -------------------------------------------------------------------------------- /server/api/search/search.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 3 | import { searchCode } from "./search.controller"; 4 | 5 | const searchRoutes = Router(); 6 | 7 | // Base route: /search 8 | 9 | searchRoutes.post("/", ensureAuthenticated, searchCode); 10 | 11 | export default searchRoutes; 12 | -------------------------------------------------------------------------------- /server/api/search/search.service.ts: -------------------------------------------------------------------------------- 1 | import { ChatUserType } from "../../../types/chatMessage.type"; 2 | import { 3 | createMessageWithUser, 4 | getOnlyRoleAndContentMessagesSession, 5 | } from "../message/message.service"; 6 | import { findOrUpdateAccount } from "../supabase/account.service"; 7 | import { findFileByExplainationEmbedding } from "./search.repository"; 8 | 9 | export async function handleSearch(userId: string, sessionId: string) { 10 | const sessionMessages = await getOnlyRoleAndContentMessagesSession(sessionId); 11 | 12 | if (!userId) throw new Error("User not found"); 13 | 14 | const userMessages = sessionMessages.filter( 15 | (message) => message.role === "user" 16 | ); 17 | console.log("User messages", userMessages[userMessages.length - 1].content); 18 | 19 | const account = await findOrUpdateAccount(userId); 20 | 21 | const response = await findCodeByQuery( 22 | userMessages[userMessages.length - 1].content, 23 | account?.id ? account.id : "" 24 | ); 25 | 26 | const templateResponse = { 27 | data: { 28 | choices: [ 29 | { 30 | message: { 31 | content: 32 | "We found these code snippets that might match your search:", 33 | role: "assistant", 34 | }, 35 | }, 36 | ], 37 | metadata: { 38 | projectDirectory: "", 39 | projectFile: "", 40 | newFile: null, 41 | requiredFunctionality: "", 42 | }, 43 | completedCode: "", 44 | search: response, 45 | }, 46 | }; 47 | 48 | await createMessageWithUser( 49 | { 50 | content: templateResponse.data.choices[0].message.content, 51 | role: ChatUserType.assistant, 52 | }, 53 | sessionId 54 | ); 55 | return templateResponse; 56 | } 57 | 58 | export async function findCodeByQuery( 59 | query: string, 60 | accountId: string, 61 | match_count: number = 10 62 | ): Promise { 63 | const response = await findFileByExplainationEmbedding( 64 | query, 65 | accountId, 66 | match_count 67 | ); 68 | return response; 69 | } 70 | -------------------------------------------------------------------------------- /server/api/session/session.controller.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 3 | import { findOrUpdateAccount } from "../supabase/account.service"; 4 | import { getSessionById } from "../session/session.repository"; 5 | 6 | export const sessionCode = async (req: AuthenticatedRequest, res: Response) => { 7 | try { 8 | const { userId } = req; 9 | 10 | const { session } = req.body; 11 | 12 | const { session_id } = req.headers; 13 | 14 | const sessionId = session_id as string; 15 | 16 | const account = await findOrUpdateAccount(userId); 17 | if (!account) { 18 | return res.status(404).json({ message: "Can't find the user account" }); 19 | } 20 | 21 | const response = await getSessionById(sessionId); 22 | 23 | res.status(200).json({ data: response }); 24 | } catch (error: any) { 25 | res.status(405).json({ message: error.message }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/api/session/session.repository.ts: -------------------------------------------------------------------------------- 1 | import { supabaseBaseServerClient } from "../../../server"; 2 | import { Database } from "../../../types/supabase"; 3 | import { logError } from "../../../utils/commandLineColors"; 4 | import { authenticatedSupabaseClient } from "../supabase/supabase.service"; 5 | import { SessionJustId } from "./session.type"; 6 | 7 | export const getSessionById = async ( 8 | sessionId: string 9 | ): Promise => { 10 | const { data, error } = await authenticatedSupabaseClient() 11 | .from("session") 12 | .select("*") 13 | .eq("id", sessionId); 14 | 15 | if (error || !data || data.length === 0) { 16 | throw new Error("Can't find the users session"); 17 | } 18 | 19 | return data[0]; 20 | }; 21 | 22 | // Most entities have a session id that ties them to a user. This gets all the sessions for a user so you can find all the entities for a user. For example find all messages for a user. 23 | export const getSessionsByUserId = async ( 24 | userId: string 25 | ): Promise => { 26 | const { data, error } = await authenticatedSupabaseClient() 27 | .from("session") 28 | .select("id") 29 | .eq("user_id", userId); 30 | 31 | if (error || !data || data.length === 0) { 32 | throw new Error("Can't find the users session"); 33 | } 34 | 35 | return data; 36 | }; 37 | 38 | export const updateSession = async ( 39 | userId: string, 40 | sessionId: string, 41 | session: Partial 42 | ): Promise => { 43 | const { data, error } = await authenticatedSupabaseClient() 44 | .from("session") 45 | .update({ ...session }) 46 | .eq("user_id", userId) 47 | .eq("id", sessionId) 48 | .select(); 49 | 50 | if (error || !data || data.length === 0) { 51 | logError("Can't find the users session"); 52 | } 53 | 54 | return data; 55 | }; 56 | 57 | export async function checkSession(session: { 58 | access_token: string; 59 | refresh_token: string; 60 | }) { 61 | return await supabaseBaseServerClient.auth.setSession(session); 62 | } 63 | -------------------------------------------------------------------------------- /server/api/session/session.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | const sessionRoutes = Router(); 4 | 5 | // Base route: /session 6 | 7 | export default sessionRoutes; 8 | -------------------------------------------------------------------------------- /server/api/session/session.service.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { Database } from "../../../types/supabase"; 3 | import { 4 | AuthResponseWithUser, 5 | authenticatedSupabaseClient, 6 | supabaseAuthenticatedServerClient, 7 | } from "../supabase/supabase.service"; 8 | import { checkSession, updateSession } from "./session.repository"; 9 | 10 | export const resetSession = (userId: string, sessionId: any) => { 11 | updateSession(userId, sessionId, { 12 | code_content: "", 13 | file_name: "", 14 | file_path: "", 15 | new_file: null, 16 | location: "", 17 | expected_next_action: null, 18 | }); 19 | }; 20 | 21 | export async function checkSessionOrThrow( 22 | req: Request, 23 | res: Response 24 | ): Promise { 25 | const { access_token, refresh_token } = req.headers; 26 | 27 | if (!access_token || !refresh_token) { 28 | throw new Error("403"); 29 | } 30 | 31 | const access = access_token as string; 32 | const refresh = refresh_token as string; 33 | 34 | const session = await checkSession({ 35 | access_token: access, 36 | refresh_token: refresh, 37 | }); 38 | 39 | if ( 40 | !session || 41 | !session.data || 42 | !session.data.user || 43 | !session.data.user.id || 44 | session.error 45 | ) { 46 | throw new Error("403"); 47 | } 48 | 49 | return session as AuthResponseWithUser; 50 | } 51 | 52 | export const findOrCreateSession = async ( 53 | userId: string, 54 | sessionId: string 55 | ): Promise => { 56 | if (!supabaseAuthenticatedServerClient) { 57 | throw new Error("No authenticated supabase client"); 58 | } 59 | 60 | const { data, error } = await authenticatedSupabaseClient() 61 | .from("session") 62 | .select("*") 63 | .eq("user_id", userId) 64 | .eq("id", sessionId); 65 | 66 | if (error) { 67 | throw new Error(error.message); 68 | } 69 | 70 | if (data && data.length > 0) { 71 | return data[0]; 72 | } else { 73 | const { data, error } = await authenticatedSupabaseClient() 74 | .from("session") 75 | .insert([{ user_id: userId, id: sessionId }]) 76 | .select(); 77 | 78 | if (error || !data) { 79 | throw new Error(error.message); 80 | } 81 | 82 | return data[0] as Database["public"]["Tables"]["session"]["Row"]; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /server/api/session/session.type.ts: -------------------------------------------------------------------------------- 1 | export type SessionJustId = { 2 | id: string; 3 | }; 4 | -------------------------------------------------------------------------------- /server/api/supabase/account.service.ts: -------------------------------------------------------------------------------- 1 | import { supabaseBaseServerClient } from "../../../server"; 2 | import { Database } from "../../../types/supabase"; 3 | 4 | export const findOrUpdateAccount = async ( 5 | userId: string 6 | ): Promise => { 7 | const { data, error } = await supabaseBaseServerClient 8 | .from("account") 9 | .select("*") 10 | .eq("user_id", userId) 11 | .select(); 12 | 13 | if (error) { 14 | console.log("finding account error", error); 15 | } 16 | 17 | if (data && data.length > 0) { 18 | return data[0]; 19 | } else { 20 | const { data, error } = await supabaseBaseServerClient 21 | .from("account") 22 | .insert([{ user_id: userId }]) 23 | .select(); 24 | 25 | if (!data || !data[0]) { 26 | console.log("Error creating session for user: ", userId); 27 | throw new Error("Error creating account for user: " + userId); 28 | } else { 29 | return data[0] as Database["public"]["Tables"]["account"]["Row"]; 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /server/api/task/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { 3 | getIncompleteTasks, 4 | getTaskById, 5 | getTasksBySession, 6 | updateTaskById, 7 | } from "./task.repository"; 8 | import { AuthenticatedRequest } from "../../middleware/isAuthenticated"; 9 | import { executeTask } from "../agent/agent.act"; 10 | 11 | export const getTasks = async (req: AuthenticatedRequest, res: Response) => { 12 | try { 13 | const { session_id } = req.headers; 14 | 15 | const sessionId = session_id as string; 16 | 17 | const tasks = await getTasksBySession(sessionId); 18 | 19 | return res.status(200).json({ 20 | data: tasks, 21 | }); 22 | } catch (error: any) { 23 | res.status(500).json({ message: error.message }); 24 | } 25 | }; 26 | 27 | export const updateTask = async (req: AuthenticatedRequest, res: Response) => { 28 | try { 29 | const { taskId, values } = req.body; 30 | 31 | await updateTaskById(taskId, values); 32 | 33 | return res.status(200).json({ 34 | data: "done", 35 | }); 36 | } catch (error: any) { 37 | console.log(error); 38 | res.status(500).json({ message: error.message }); 39 | } 40 | }; 41 | 42 | export const testTasks = async (req: Request, res: Response) => { 43 | try { 44 | const userId = "4ff416c9-4805-4adb-bfe7-ef315ae9536b"; 45 | 46 | const sessionId = "ad536466-86b0-4f6c-8c40-2118a30a68e1"; 47 | 48 | // console.log("Getting tasks"); 49 | // const tasks = await getIncompleteTasks(); 50 | const task = await getTaskById("838098d8-bc7e-4221-8867-5cf0e2c4f13f"); 51 | 52 | let tasks; 53 | if (task) { 54 | tasks = await executeTask(task); 55 | } 56 | 57 | return res.status(200).json({ 58 | data: tasks, 59 | }); 60 | } catch (error: any) { 61 | console.log(error); 62 | res.status(500).json({ message: error.message }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /server/api/task/task.repository.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import { Database } from "../../../types/supabase"; 3 | 4 | import { supabaseKey, supabaseUrl } from "../../../utils/envVariable"; 5 | import { TaskWithObjective } from "./task.types"; 6 | import { getObjectivesBySession } from "../objective/objective.repository"; 7 | 8 | const supabase = createClient(supabaseUrl, supabaseKey); 9 | 10 | export const getTasksBySession = async ( 11 | sessionId: string 12 | ): Promise[] | []> => { 13 | const objective = await getObjectivesBySession(sessionId); 14 | const objectionIds = objective.map((obj) => obj.id); 15 | 16 | if (!objective || objective.length === 0) return []; 17 | const { data, error } = await supabase 18 | .from("task") 19 | .select("*") 20 | .order("created_at", { ascending: false }) 21 | .in("objective_id", [...objectionIds]); 22 | 23 | if (error || !data) { 24 | console.log("Error getting tasks", error); 25 | return []; 26 | } 27 | 28 | return data || []; 29 | }; 30 | 31 | export const getIncompleteTasks = async (): Promise< 32 | [] | Database["public"]["Tables"]["task"]["Row"][] 33 | > => { 34 | const { data, error } = await supabase 35 | .from("task") 36 | .select("*, objective(*, task(*))") 37 | .is("completed_at", null) 38 | .is("started_eval_at", null) 39 | .is("tool_output", null) 40 | .eq("marked_ready", true) 41 | .not("tool_input", "is", null) 42 | .not("tool_name", "is", null); 43 | 44 | if (error || !data) { 45 | console.log("Error getting incomplete tasks", error); 46 | return []; 47 | } 48 | 49 | return data || []; 50 | }; 51 | 52 | export const getCompletedTasks = async (): Promise< 53 | [] | TaskWithObjective[] 54 | > => { 55 | const { data, error } = await supabase 56 | .from("task") 57 | .select("*, objective(*, session_id, task(*))") 58 | .is("loop_evaluated_at", null) 59 | .not("tool_output", "is", null) 60 | .not("tool_name", "is", null); 61 | 62 | if (error || !data) { 63 | console.log("Error getting completed tasks", error); 64 | return []; 65 | } 66 | 67 | return (data as TaskWithObjective[]) || []; 68 | }; 69 | 70 | export const createTaskWithObjective = async ( 71 | task: Database["public"]["Tables"]["task"]["Insert"], 72 | objectiveId: string 73 | ): Promise => { 74 | task["objective_id"] = objectiveId; 75 | 76 | const { data, error } = await supabase 77 | .from("task") 78 | .insert([task]) 79 | .select("*"); 80 | 81 | if (error || !data || data.length === 0) { 82 | console.log("Error creating task", error); 83 | return null; 84 | } 85 | 86 | return data[0]; 87 | }; 88 | 89 | export const getTaskById = async ( 90 | id: string 91 | ): Promise => { 92 | const { data, error } = await supabase 93 | .from("task") 94 | .select("*") 95 | .order("created_at", { ascending: false }) 96 | .eq("id", id); 97 | 98 | if (error || !data) { 99 | console.log("Error getting tasks", error); 100 | return null; 101 | } 102 | console.log(data); 103 | 104 | return data[0] || []; 105 | }; 106 | 107 | export const updateTaskById = async ( 108 | taskId: string, 109 | task: Partial 110 | ): Promise => { 111 | const { data } = await supabase 112 | .from("task") 113 | .update({ ...task }) 114 | .eq("id", taskId) 115 | .select(); 116 | 117 | return data; 118 | }; 119 | -------------------------------------------------------------------------------- /server/api/task/task.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { ensureAuthenticated } from "../../middleware/isAuthenticated"; 3 | import { getTasks, testTasks, updateTask } from "./task.controller"; 4 | 5 | const taskRoutes = Router(); 6 | 7 | // Base route: /task 8 | 9 | taskRoutes.get("/", ensureAuthenticated, getTasks); 10 | taskRoutes.put("/", ensureAuthenticated, updateTask); 11 | taskRoutes.get("/test", testTasks); 12 | 13 | export default taskRoutes; 14 | -------------------------------------------------------------------------------- /server/api/task/task.service.ts: -------------------------------------------------------------------------------- 1 | import { logInfo } from "../../../utils/commandLineColors"; 2 | import { executeTask } from "../agent/agent.act"; 3 | import { isSearchTool } from "../agent/agent.context"; 4 | import { getToolByNames } from "../agent/agent.service"; 5 | import { allTools } from "../agent/tools"; 6 | import { getObjectiveById } from "../objective/objective.repository"; 7 | import { getSessionById } from "../session/session.repository"; 8 | import { getIncompleteTasks, updateTaskById } from "./task.repository"; 9 | 10 | export async function findAndExecuteTasks() { 11 | // Find all tasks that are not completed, have an input and no output 12 | 13 | const tasks = await getIncompleteTasks(); 14 | logInfo(`Incomplete tasks: ${tasks.length}`); 15 | 16 | // Execute each task 17 | for (const task of tasks) { 18 | if (isSearchTool(task.tool_name ? task.tool_name : null)) { 19 | executeTask(task); 20 | } else { 21 | if (task && task.tool_name && task.objective_id && task.tool_input) { 22 | const objective = await getObjectiveById(task.objective_id); 23 | if (!objective || !objective.session_id) { 24 | console.log("Objective not found"); 25 | return; 26 | } 27 | const session = await getSessionById(objective.session_id); 28 | if (!session || !session.user_id || !session.id) { 29 | console.log("Session not found"); 30 | return; 31 | } 32 | logInfo(`Executing task: ${JSON.stringify(task)}`); 33 | const allToolsAvailable = getToolByNames(allTools); 34 | 35 | const selectedTool = allToolsAvailable[task.tool_name]; 36 | console.log("selectedTool", selectedTool); 37 | updateTaskById(task.id, { 38 | started_eval_at: new Date().toISOString(), 39 | }); 40 | const { output } = await allToolsAvailable[task.tool_name].use( 41 | session.user_id, 42 | session.id, 43 | task.tool_input 44 | ); 45 | console.log("output", output); 46 | 47 | updateTaskById(task.id, { 48 | tool_output: output, 49 | completed_at: new Date().toISOString(), 50 | }); 51 | } 52 | 53 | // Completes all the outstanding tasks - good for reseting the database 54 | await updateTaskById(task.id, { 55 | completed_at: new Date().toISOString(), 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/api/task/task.types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../../types/supabase"; 2 | import { ObjectiveWithTasks } from "../objective/objective.types"; 3 | 4 | export type Task = Database["public"]["Tables"]["task"]["Row"]; 5 | 6 | export type TaskWithObjective = Task & { objective: ObjectiveWithTasks }; 7 | -------------------------------------------------------------------------------- /server/middleware/githubWebhookAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { exoGithubAppWebhookSecret } from "../../utils/envVariable"; 3 | const crypto = require("crypto"); 4 | 5 | export type RequestWithRawBody = Request & Record; 6 | 7 | const sigHeaderName = "X-Hub-Signature-256"; 8 | const sigHashAlg = "sha256"; 9 | 10 | export function verifyGithubWebhook( 11 | req: RequestWithRawBody, 12 | res: Response, 13 | next: NextFunction 14 | ) { 15 | if (!req.rawBody) { 16 | return next("Request body empty"); 17 | } 18 | 19 | const sig = Buffer.from(req.get(sigHeaderName) || "", "utf8"); 20 | const hmac = crypto.createHmac(sigHashAlg, exoGithubAppWebhookSecret); 21 | const digest = Buffer.from( 22 | sigHashAlg + "=" + hmac.update(req.rawBody).digest("hex"), 23 | "utf8" 24 | ); 25 | if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) { 26 | return next( 27 | `Request body digest (${digest}) did not match ${sigHeaderName} (${sig})` 28 | ); 29 | } 30 | 31 | return next(); 32 | } 33 | -------------------------------------------------------------------------------- /server/middleware/isAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { checkSessionOrThrow } from "../api/session/session.service"; 3 | import { 4 | setSupabaseAuthenticatedServerClient, 5 | supabaseAuthenticatedServerClient, 6 | } from "../api/supabase/supabase.service"; 7 | 8 | export type AuthenticatedRequest = Request & Record; 9 | 10 | export async function ensureAuthenticated( 11 | req: AuthenticatedRequest, 12 | res: Response, 13 | next: NextFunction 14 | ) { 15 | try { 16 | const session = await checkSessionOrThrow(req, res); 17 | req.session = session.data.session.access_token; 18 | req.userId = session.data.user.id; 19 | 20 | await setSupabaseAuthenticatedServerClient( 21 | session.data.session.refresh_token, 22 | session.data.session.access_token 23 | ); 24 | 25 | if (!supabaseAuthenticatedServerClient) { 26 | throw new Error("Unable to create authenticated supabase client"); 27 | } 28 | 29 | return next(); 30 | } catch (error: any) { 31 | return res 32 | .status(403) 33 | .json({ message: "You have to be logged in to do that" }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /side-by-side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Exoaihq/exo-server/264be6371690883562f7ad4ff54d66c44746307f/side-by-side.png -------------------------------------------------------------------------------- /types/chatMessage.type.ts: -------------------------------------------------------------------------------- 1 | export enum ChatUserType { 2 | system = "system", 3 | user = "user", 4 | assistant = "assistant", 5 | } 6 | 7 | export interface ChatMessage { 8 | role: ChatUserType; 9 | content: string; 10 | } 11 | -------------------------------------------------------------------------------- /types/openAiTypes/openAiBaseModel.ts: -------------------------------------------------------------------------------- 1 | import { EngineName } from "./openAiEngine"; 2 | 3 | export interface BaseModel { 4 | /** 5 | * List of text pairs that will help steer the model towards the tone and answer format you'd like. 6 | * We recommend adding 2 to 3 examples. 7 | */ 8 | examples?: string[][]; 9 | /** 10 | * List of documents from which the answer for the input `question` should be derived. 11 | * If this is an empty list, the question will be answered based on the question-answer examples. 12 | * You should specify either `documents` or a `file`, but not both. 13 | */ 14 | documents?: string[]; 15 | /** 16 | * The ID of an uploaded file that contains documents to search over. 17 | * See upload file for how to upload a file of the desired format and purpose. 18 | * You should specify either `documents` or a `file`, but not both. 19 | */ 20 | file?: string; 21 | /** 22 | * ID of the engine to use for Search. 23 | */ 24 | search_model?: EngineName; 25 | /** 26 | * The maximum number of documents to be ranked by Search when using file. 27 | * Setting it to a higher value leads to improved accuracy but with increased latency and cost. 28 | */ 29 | max_rerank?: number; 30 | /** 31 | * What sampling temperature to use. 32 | * Higher values mean the model will take more risks and value 0 (argmax sampling) works better for scenarios with a well-defined answer. 33 | */ 34 | temperature?: number; 35 | /** 36 | * Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. 37 | * For example, if logprobs is 10, the API will return a list of the 10 most likely tokens. 38 | * the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. 39 | * When logprobs is set, completion will be automatically added into expand to get the logprobs. 40 | */ 41 | logprobs?: number | null; 42 | /** 43 | * The maximum number of tokens allowed for the generated answer 44 | */ 45 | max_tokens?: number; 46 | /** 47 | * Up to 4 sequences where the API will stop generating further tokens. 48 | * The returned text will not contain the stop sequence. 49 | */ 50 | stop?: string | string[] | null; 51 | /** 52 | * How many answers to generate for each question. 53 | */ 54 | n?: number; 55 | /** 56 | * Modify the likelihood of specified tokens appearing in the completion. 57 | * 58 | * Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. 59 | * You can use this tokenizer tool (which works for both GPT-2 and GPT-3) to convert text to token IDs. 60 | * Mathematically, the bias is added to the logits generated by the model prior to sampling. 61 | * The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; 62 | * values like -100 or 100 should result in a ban or exclusive selection of the relevant token. 63 | * 64 | * As an example, you can pass {50256: -100} to prevent the <|endoftext|> token from being generated. 65 | */ 66 | logit_bias?: Record | null; 67 | /** 68 | * A special boolean flag for showing metadata. 69 | * If set to true, each document entry in the returned JSON will contain a metadata field. 70 | * 71 | * This flag only takes effect when file is set. 72 | */ 73 | return_metadata?: boolean; 74 | /** 75 | * If set to true, the returned JSON will include a prompt field containing the final prompt that was used to request a completion. 76 | * This is mainly useful for debugging purposes. 77 | */ 78 | return_prompt?: boolean; 79 | /** 80 | * If an object name is in the list, we provide the full information of the object; otherwise, we only provide the object ID. 81 | * Currently we support completion and file objects for expansion. 82 | */ 83 | expand?: string[]; 84 | } 85 | export interface OpenAIError { 86 | error: { 87 | code: null | unknown; 88 | message: string; 89 | param: null | unknown; 90 | type: string; 91 | }; 92 | } -------------------------------------------------------------------------------- /types/openAiTypes/openAiCompletionReqRes.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from "./openAiBaseModel"; 2 | 3 | export interface CompletionRequest extends BaseModel { 4 | /** 5 | * The prompt(s) to generate completions for, encoded as a string, a list of strings, or a list of token lists. 6 | * Note that <|endoftext|> is the document separator that the model sees during training, 7 | * so if a prompt is not specified the model will generate as if from the beginning of a new document. 8 | */ 9 | prompt?: string | string[]; 10 | /** 11 | * An alternative to sampling with temperature, called nucleus sampling, 12 | * where the model considers the results of the tokens with top_p probability mass. 13 | * So 0.1 means only the tokens comprising the top 10% probability mass are considered. 14 | * We generally recommend altering this or temperature but not both. 15 | */ 16 | top_p?: number; 17 | /** 18 | * Whether to stream back partial progress. 19 | * If set, tokens will be sent as data-only server-sent events as they become available, 20 | * with the stream terminated by a data: [DONE] message. 21 | */ 22 | stream?: boolean; 23 | /** 24 | * Echo back the prompt in addition to the completion 25 | */ 26 | echo?: boolean; 27 | /** 28 | * Number between 0 and 1 that penalizes new tokens based on whether they appear in the text so far. 29 | * Increases the model's likelihood to talk about new topics. 30 | */ 31 | presence_penalty?: number; 32 | /** 33 | * Number between 0 and 1 that penalizes new tokens based on their existing frequency in the text so far. 34 | * Decreases the model's likelihood to repeat the same line verbatim. 35 | */ 36 | frequency_penalty?: number; 37 | /** 38 | * Generates best_of completions server-side and returns the "best" (the one with the lowest log probability per token). 39 | * Results cannot be streamed. When used with n, best_of controls the number of candidate completions and n specifies how many to return – best_of must be greater than n. 40 | */ 41 | best_of?: number; 42 | /** 43 | * Fine-tune model if possible 44 | */ 45 | model?: string; 46 | /** 47 | * Incorporate user for logging purposes 48 | */ 49 | user?: string; 50 | } 51 | export interface CompletionChoice { 52 | text: string; 53 | index: number; 54 | logprobs: null | { 55 | tokens: string[]; 56 | token_logprobs: number[]; 57 | top_logprobs: Record[]; 58 | text_offset: number[]; 59 | }; 60 | finish_reason: string; 61 | message?: { 62 | role: string; 63 | content: string; 64 | }; 65 | } 66 | 67 | export interface CompletionResponse { 68 | id: string; 69 | created: number; 70 | model: string; 71 | choices: CompletionChoice[]; 72 | } 73 | -------------------------------------------------------------------------------- /types/openAiTypes/openAiEngine.ts: -------------------------------------------------------------------------------- 1 | export enum EngineName { 2 | GPT4 = "gpt-4", 3 | Turbo = "gpt-3.5-turbo", 4 | TextDavinci = "text-davinci-003", 5 | TextCurie = "text-curie-001", 6 | TextBabbage = "text-babbage-001", 7 | TextAda = "text-ada-001", 8 | DavinciInstructBeta = "davinci-instruct-beta", 9 | CurieInstructBeta = "curie-instruct-beta", 10 | DavinciCodex = "code-davinci-002", 11 | CushmanCodex = "code-cushman-001", 12 | } 13 | export interface Engine { 14 | id: EngineName; 15 | object: "engine"; 16 | created: null | string; 17 | max_replicas: null | string; 18 | owner: "openai"; 19 | permissions: null | string; 20 | ready: boolean; 21 | ready_replicas: null | string; 22 | replicas: null | string; 23 | } 24 | export interface ListEngine { 25 | data: Engine[]; 26 | object: "list"; 27 | } 28 | 29 | export interface AddModel { 30 | object: string; 31 | id: string; 32 | ready: boolean; 33 | } 34 | -------------------------------------------------------------------------------- /types/parseCode.types.ts: -------------------------------------------------------------------------------- 1 | export interface ParseCode { 2 | contents: string; 3 | filePath: string; 4 | } 5 | 6 | export interface ParsedCodeMetadata { 7 | element: Element; 8 | filePath: string; 9 | type: string; 10 | fileName: string; 11 | } 12 | 13 | export interface ParsedCode { 14 | code: string; 15 | metadata: ParsedCodeMetadata; 16 | } 17 | 18 | export interface Position { 19 | row: number; 20 | column: number; 21 | } 22 | 23 | export interface Element { 24 | type: string; 25 | startPosition: Position; 26 | endPosition: Position; 27 | startIndex: number; 28 | endIndex: number; 29 | } 30 | 31 | export interface ParsedDirectory { 32 | directoryName: string; 33 | filePath: string; 34 | files: string[]; 35 | } 36 | 37 | export interface ParsedFile { 38 | fileName: string; 39 | filePath: string; 40 | } 41 | 42 | export const parseCodeTypes = [ 43 | { 44 | name: "function_declaration", 45 | prefix: "function", 46 | parse: true, 47 | }, 48 | { 49 | name: "lexical_declaration", 50 | prefix: "const", 51 | parse: true, 52 | }, 53 | { 54 | name: "export_statement", 55 | prefix: "export", 56 | parse: true, 57 | }, 58 | { 59 | name: "import_statement", 60 | prefix: null, 61 | parse: true, 62 | }, 63 | { 64 | name: "empty_statement", 65 | prefix: null, 66 | parse: false, 67 | }, 68 | ]; 69 | 70 | export interface SnippetByFileName { 71 | file_name: string | null; 72 | id: number; 73 | code_file_id: number | null; 74 | code_string: string | null; 75 | code_explaination: string | null; 76 | start_row: number | null; 77 | start_column: number | null; 78 | end_row: number | null; 79 | end_column: number | null; 80 | parsed_code_type: string | null; 81 | } 82 | -------------------------------------------------------------------------------- /utils/appendFile.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const fs = require('fs'); 4 | const { promisify } = require('util'); 5 | const appendFile = promisify(fs.appendFile); 6 | 7 | export async function addCodeToTheBottonOfFile(fileName: string, code: string) { 8 | try { 9 | await appendFile(fileName, code); 10 | console.log(`${fileName} has been updated with text.`); 11 | } catch (err) { 12 | throw err; 13 | } 14 | } 15 | 16 | export async function overwriteFile(filePath: string, code: string) { 17 | await fs.writeFile(filePath, code, (err: any) => err && console.log(err)); 18 | } 19 | 20 | 21 | 22 | export async function writeStringToFileAtLocation( 23 | filePath: string, 24 | content: string, 25 | lineNumber: number = 0 26 | ): Promise { 27 | try { 28 | if (lineNumber < 0) { 29 | throw new Error('Line number must be non-negative'); 30 | } 31 | 32 | // Read the file 33 | const fileContent = await fs.readFileSync(filePath, 'utf-8'); 34 | console.log(fileContent) 35 | 36 | // Split the contents into an array of lines 37 | const fileLines = fileContent.split('\n'); 38 | 39 | // If the line number is bigger than the total number of lines set it to the last line. 40 | if (lineNumber > fileLines.length) { 41 | lineNumber = fileLines.length; 42 | } 43 | 44 | // Insert the new content at the specified line number 45 | fileLines.splice(lineNumber, 0, content); 46 | 47 | // Join the lines back into a single string 48 | const newContent = fileLines.join('\n'); 49 | console.log(newContent) 50 | 51 | // Write the updated contents back to the file 52 | 53 | await fs.writeFile(filePath, newContent, (err: any) => err && console.log(err)); 54 | 55 | console.log(`Content added at line ${lineNumber} in ${filePath}`); 56 | } catch (error) { 57 | console.error(`Error inserting content at line ${lineNumber} in ${filePath}:`, error); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /utils/chunkString.ts: -------------------------------------------------------------------------------- 1 | // Here's a TypeScript function that will chunk a string into an array of strings of a specified length (n) without splitting on full words: 2 | 3 | // ```typescript 4 | export function chunkString(text: string, n: number): string[] { 5 | if (n < 1) { 6 | throw new Error("Chunk length must be greater than 0"); 7 | } 8 | 9 | const words = text.split(" "); 10 | const result: string[] = []; 11 | let chunk = ""; 12 | 13 | for (const word of words) { 14 | const potentialChunk = chunk ? `${chunk} ${word}` : word; 15 | if (potentialChunk.length <= n) { 16 | chunk = potentialChunk; 17 | } else { 18 | result.push(chunk); 19 | chunk = word; 20 | } 21 | } 22 | 23 | if (chunk) { 24 | result.push(chunk); 25 | } 26 | 27 | return result; 28 | } 29 | 30 | // const text = "This is an example sentence"; 31 | // const n = 7; 32 | 33 | // const chunks = chunkString(text, n); 34 | // console.log(chunks); 35 | // ``` 36 | 37 | // This function checks if the length of the concatenated chunk will be less than or equal to the specified length (n). If so, the word is added to the current chunk. Otherwise, a new chunk is started. 38 | 39 | // This script will output this for the example `text` and `n`: 40 | 41 | // ``` 42 | // [ 'This is', 'an', 'example', 'sentenc' ] 43 | // ``` 44 | -------------------------------------------------------------------------------- /utils/commandLineColors.ts: -------------------------------------------------------------------------------- 1 | export const commandLineColors = { 2 | Reset: "\x1b[0m", 3 | Bright: "\x1b[1m", 4 | Dim: "\x1b[2m", 5 | Underscore: "\x1b[4m", 6 | Blink: "\x1b[5m", 7 | Reverse: "\x1b[7m", 8 | Hidden: "\x1b[8m", 9 | 10 | FgBlack: "\x1b[30m", 11 | FgRed: "\x1b[31m", 12 | FgGreen: "\x1b[32m", 13 | FgYellow: "\x1b[33m", 14 | FgBlue: "\x1b[34m", 15 | FgMagenta: "\x1b[35m", 16 | FgCyan: "\x1b[36m", 17 | FgWhite: "\x1b[37m", 18 | FgGray: "\x1b[90m", 19 | 20 | BgBlack: "\x1b[40m", 21 | BgRed: "\x1b[41m", 22 | BgGreen: "\x1b[42m", 23 | BgYellow: "\x1b[43m", 24 | BgBlue: "\x1b[44m", 25 | BgMagenta: "\x1b[45m", 26 | BgCyan: "\x1b[46m", 27 | BgWhite: "\x1b[47m", 28 | BgGray: "\x1b[100m", 29 | }; 30 | 31 | export function logError(message: string) { 32 | console.log(commandLineColors.FgRed, message, commandLineColors.Reset); 33 | } 34 | 35 | export function logSuccess(message: string) { 36 | console.log(commandLineColors.FgGreen, message, commandLineColors.Reset); 37 | } 38 | 39 | export function logWarning(message: string) { 40 | console.log(commandLineColors.FgYellow, message, commandLineColors.Reset); 41 | } 42 | 43 | export function logInfo(message: string) { 44 | console.log(commandLineColors.FgBlue, message, commandLineColors.Reset); 45 | } 46 | 47 | export function logDebug(message: string) { 48 | console.log(commandLineColors.FgGray, message, commandLineColors.Reset); 49 | } 50 | -------------------------------------------------------------------------------- /utils/commandLineLoadingl.ts: -------------------------------------------------------------------------------- 1 | 2 | const orientations = ["\\", "|", "/", "-"]; 3 | 4 | 5 | const twirlTimer = function (message: string = "Loading") { 6 | let x = 0; 7 | return setInterval(function () { 8 | const spinners = orientations[x++] 9 | process.stdout.write("\r" + spinners + " " + message + " " + spinners); 10 | x &= 3; 11 | }, 250); 12 | }; 13 | 14 | 15 | export function commandLineLoading(message: string): NodeJS.Timeout { 16 | const intervalId = twirlTimer(message); 17 | return intervalId; 18 | } 19 | 20 | 21 | export function clearLoading(intervalId: NodeJS.Timeout, completedMessage: string = "Done! ") { 22 | clearInterval(intervalId); 23 | console.log("\n" + completedMessage + "\n") 24 | } 25 | -------------------------------------------------------------------------------- /utils/createfile.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | const fs = require('fs'); 4 | 5 | export function createFile(fileName: string, text: string, folder: string = './src') { 6 | const location = path.join(folder, fileName); 7 | fs.writeFile(location, text, (err: any) => { 8 | if (err) throw err; 9 | console.log(`${location} has been created and populated with text.`); 10 | }); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /utils/dates.ts: -------------------------------------------------------------------------------- 1 | export const isTodaysDate = (date: Date): boolean => { 2 | const todaysDate = new Date().getDate(); 3 | const todaysMonth = new Date().getMonth(); 4 | const todaysYear = new Date().getFullYear(); 5 | 6 | const updatedToday = 7 | date.getDate() === todaysDate && 8 | date.getMonth() === todaysMonth && 9 | date.getFullYear() === todaysYear; 10 | 11 | return updatedToday; 12 | }; 13 | 14 | export const isThisHour = (date: Date): boolean => { 15 | const todaysDate = new Date().getDate(); 16 | const todaysMonth = new Date().getMonth(); 17 | const todaysYear = new Date().getFullYear(); 18 | const todaysHour = new Date().getHours(); 19 | 20 | const updatedToday = 21 | date.getDate() === todaysDate && 22 | date.getMonth() === todaysMonth && 23 | date.getFullYear() === todaysYear && 24 | date.getHours() === todaysHour; 25 | 26 | return updatedToday; 27 | }; 28 | -------------------------------------------------------------------------------- /utils/deserializeJson.ts: -------------------------------------------------------------------------------- 1 | export function deserializeJson(input: string): any | null { 2 | const jsonString = input.match(/{[^}]*}/)?.[0]; 3 | if (!jsonString) { 4 | return null; 5 | } 6 | 7 | const deserializedObject = JSON.parse(jsonString); 8 | return deserializedObject; 9 | } 10 | 11 | export function deserializeFullJson(input: string): any | null { 12 | const jsonString = input.match(/{[^}]*}/)?.[0]; 13 | if (!jsonString) { 14 | return null; 15 | } 16 | 17 | const deserializedObject = JSON.parse(input); 18 | return deserializedObject; 19 | } 20 | -------------------------------------------------------------------------------- /utils/envVariable.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | export const { env } = process; 4 | const { 5 | SUPABASE_ANON, 6 | SUPABASE_URL, 7 | PWD, 8 | OPENAI_API_KEY, 9 | PORT, 10 | SLACK_BOT_TOKEN, 11 | SLACK_SIGNING_SECRET, 12 | EXO_GITHUB_APP_WEBHOOK_SECRET, 13 | GITHUB_PRIVATE_KEY, 14 | GITHUB_EXO_APP_ID, 15 | NODE_ENV, 16 | } = env; 17 | 18 | export const isProduction = 19 | NODE_ENV && NODE_ENV === "production" ? true : false; 20 | export const supabaseKey = SUPABASE_ANON ? SUPABASE_ANON : ""; 21 | export const supabaseUrl = SUPABASE_URL ? SUPABASE_URL : ""; 22 | export const rootProjectDirectory = PWD ? PWD : __dirname; 23 | export const openAiApiKey = OPENAI_API_KEY ? OPENAI_API_KEY : ""; 24 | export const port = PORT ? PORT : 8081; 25 | export const slackBotToken = SLACK_BOT_TOKEN ? SLACK_BOT_TOKEN : ""; 26 | export const slackSigningSecret = SLACK_SIGNING_SECRET 27 | ? SLACK_SIGNING_SECRET 28 | : ""; 29 | export const exoGithubAppWebhookSecret = EXO_GITHUB_APP_WEBHOOK_SECRET 30 | ? EXO_GITHUB_APP_WEBHOOK_SECRET 31 | : ""; 32 | export const githubPrivateKey = GITHUB_PRIVATE_KEY 33 | ? GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n") 34 | : ""; 35 | export const githubExoAppId = GITHUB_EXO_APP_ID || ""; 36 | -------------------------------------------------------------------------------- /utils/fileOperations.service.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | export function findFileAndReturnContents(fullFilePathAndName: string) { 4 | return fs.readFileSync(fullFilePathAndName, "utf8"); 5 | } 6 | 7 | // Here's a TypeScript function that extracts the path from the given string: 8 | 9 | // ```ts 10 | export function extractPath(input: string): string { 11 | const pathMatch = input.match(/\/[\w\-\./]+/); 12 | const removeTailingPeriod = (path: string) => { 13 | return path[path.length - 1] === "." ? path.slice(0, -1) : path; 14 | }; 15 | 16 | return pathMatch ? removeTailingPeriod(pathMatch[0]) : ""; 17 | } 18 | 19 | // Example usage: 20 | const input = 21 | "The directory name that best matches this search is: agent which is located at: /Users/kg/Repos/code-gen-server/server/api/agent"; 22 | const extractedPath = extractPath(input); 23 | // console.log(extractedPath); // Output: /Users/kg/Repos/code-gen-server/server/api/agent 24 | // ``` 25 | 26 | // This function uses a regular expression to search for the path in the input string and returns it. If no path is found, it returns an empty string. 27 | -------------------------------------------------------------------------------- /utils/findArrayInAString.ts: -------------------------------------------------------------------------------- 1 | export function findArray(str: string): string[] | null { 2 | // Match the pattern of an array: starts with a left square bracket, contains zero or more non-square-bracket characters, and ends with a right square bracket 3 | const regex = /\[([^\[\]]*)\]/; 4 | 5 | // Use the regex to find the first match of an array in the input string 6 | const match = str.match(regex); 7 | 8 | if (match) { 9 | // Extract the captured string containing the array 10 | const arrayStr = match[1]; 11 | 12 | // Split the array string by comma and whitespace to get the individual elements 13 | const elements = arrayStr.split(/,\s*/); 14 | 15 | // Return the array as an array of strings 16 | return elements; 17 | } else { 18 | // Return null if no array was found in the input string 19 | return null; 20 | } 21 | } 22 | 23 | export function removeQuotes(str: string): string { 24 | // Use a regular expression to match the quotes in the string 25 | const regex = /"/g; 26 | 27 | const singleQuote = /'/g; 28 | 29 | // Use the replace method to remove the quotes 30 | return str.replace(regex, "") || str.replace(singleQuote, "") || str; 31 | } 32 | -------------------------------------------------------------------------------- /utils/findTextAfterComment.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function findTextAfterString(entireString: string, startString: string): string { 4 | const index = entireString.indexOf(startString); 5 | const sub = entireString.substring(index + startString.length + 1); 6 | return sub 7 | } 8 | 9 | export function lineBreakText(text: string): string { 10 | const lineBreakIndex = text.indexOf('\n'); 11 | return text.slice(lineBreakIndex + 1); 12 | }; 13 | 14 | 15 | // TODO - doesn't work in all cases. Often the test will be after a comment: // Test 16 | export function findAllTextAfterStringAndLineBreak(text: string, startString: string): string { 17 | const textAfterString = findTextAfterString(text, startString); 18 | return lineBreakText(textAfterString); 19 | } 20 | 21 | // Test 22 | const comment = 'This is a comment.test This is the text after the comment.'; 23 | const textAfterComment = findTextAfterString(comment, '.test'); 24 | console.assert(textAfterComment === 'This is the text after the comment.'); -------------------------------------------------------------------------------- /utils/funnyErrorMessage.ts: -------------------------------------------------------------------------------- 1 | const awayMessages = [ 2 | "Taking a break, will be back soon!", 3 | "Busy with work, catch you later", 4 | "Stepping out for lunch, back in 30", 5 | "In a meeting, message me later", 6 | "Taking a walk, be back soon", 7 | "Away from keyboard, I'll get back to you", 8 | "Taking a power nap, will reply when I wake up", 9 | "Running errands, back soon", 10 | "Taking a mental health break, will be back soon", 11 | "On a short vacation, will respond when I return", 12 | ]; 13 | 14 | export const getFunnyErrorMessage = () => { 15 | const randomNumberOneToTen = Math.floor(Math.random() * 10) + 1; 16 | return awayMessages[randomNumberOneToTen]; 17 | }; 18 | -------------------------------------------------------------------------------- /utils/generateCode.ts: -------------------------------------------------------------------------------- 1 | import { createTextCompletion } from "../server/api/openAi/openai.service"; 2 | import { addCodeToTheBottonOfFile } from "./appendFile"; 3 | import { createFile } from "./createfile"; 4 | import { findAllTextAfterStringAndLineBreak } from "./findTextAfterComment"; 5 | import { createTestFileName } from "./generateTestFileName"; 6 | import { findFileName } from "./getFileName"; 7 | import { removeTest } from "./removeTextAfterString"; 8 | const fs = require("fs"); 9 | 10 | const writeATestPrompt = "Write a test in the same file."; 11 | const writeAFileNamePrompt = 12 | "Add a filename in the same file with this format: filename: ."; 13 | 14 | function buildPromptWithTestAndFileName(prompt: string) { 15 | return `${prompt} ${writeATestPrompt} ${writeAFileNamePrompt}`; 16 | } 17 | 18 | export async function createCodeCompletion( 19 | prompt: string, 20 | apiKey: string, 21 | loadingMessage: string 22 | ) { 23 | return await createTextCompletion( 24 | buildPromptWithTestAndFileName(prompt), 25 | 1, 26 | loadingMessage 27 | ); 28 | } 29 | 30 | export async function createCodeCompletionAddToFiles( 31 | prompt: string, 32 | loadingMessage: string, 33 | location: string = "./" 34 | ) { 35 | const res = await createTextCompletion( 36 | buildPromptWithTestAndFileName(prompt), 37 | 1, 38 | loadingMessage 39 | ); 40 | try { 41 | let generatedCode = res; 42 | if (!generatedCode) { 43 | return; 44 | } 45 | 46 | const fileName = findFileName(generatedCode); 47 | const findTest = findAllTextAfterStringAndLineBreak(generatedCode, ".test"); 48 | if (findTest) { 49 | generatedCode = removeTest(generatedCode); 50 | createFile(createTestFileName(fileName), findTest, location); 51 | } 52 | createFile(fileName, generatedCode, location); 53 | } catch (error: any) { 54 | console.log(error); 55 | } 56 | 57 | return res; 58 | } 59 | 60 | export async function createCodeCompletionAddToNewNamedFile( 61 | prompt: string, 62 | loadingMessage: string, 63 | location: string, 64 | fileName: string 65 | ) { 66 | const res = await createTextCompletion(prompt, 1, loadingMessage, "chat"); 67 | try { 68 | let generatedCode = res; 69 | if (!generatedCode) { 70 | return; 71 | } 72 | 73 | createFile(fileName, generatedCode, location); 74 | } catch (error: any) { 75 | console.log(error); 76 | } 77 | 78 | return res; 79 | } 80 | 81 | // Takes a file name, parsed the code and uses it to prompt a new function 82 | export async function refactorFile(prompt: string, filePath: string) { 83 | const prefix = "Here is a function you can refactor:"; 84 | 85 | let response = null; 86 | 87 | await fs.readFile(filePath, "utf8", async function (err: any, data: any) { 88 | if (err) throw err; 89 | 90 | const entirePrompt = prefix + (await data) + "\n" + prompt; 91 | console.log(">>>>>>>>>>", entirePrompt); 92 | const res = await createTextCompletion(entirePrompt, 1, "Refactoring..."); 93 | if (res) { 94 | addCodeToTheBottonOfFile(filePath, res); 95 | } 96 | console.log(res); 97 | response = res; 98 | return res; 99 | }); 100 | 101 | return response; 102 | } 103 | -------------------------------------------------------------------------------- /utils/generateTestFileName.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function createTestFileName(fileName: string) { 4 | const fileNameParts = fileName.split('.'); 5 | const testFileName = `${fileNameParts[0]}.test.${fileNameParts[1]}`; 6 | return testFileName; 7 | }; 8 | 9 | // Test 10 | const testFileName = createTestFileName('myFile.txt'); 11 | console.assert(testFileName === 'myFile.test.txt', 'Test file name should be myFile.test.txt'); -------------------------------------------------------------------------------- /utils/getFileName.ts: -------------------------------------------------------------------------------- 1 | import { getTimestamp } from "./getTimestamp"; 2 | 3 | const getFilenameFromComment = (comment: any) => { 4 | const regex = /filename: (.*)/; 5 | const matches = comment.match(regex); 6 | if (matches && matches.length > 1) { 7 | return matches[1]; 8 | } 9 | return null; 10 | }; 11 | 12 | const getFileNameRegex = (str: string): string[] => { 13 | const regex = /\w+\.\w+/g; 14 | const matches = str.match(regex); 15 | return matches ? matches : []; 16 | }; 17 | 18 | export function findFileName(str: string): string { 19 | const fileNameFromComment = getFilenameFromComment(str); 20 | const fileNameRegex = getFileNameRegex(str); 21 | if (fileNameFromComment) { 22 | return fileNameFromComment; 23 | } else if (fileNameRegex.length > 0) { 24 | return fileNameRegex[0]; 25 | } else { 26 | return getTimestamp() + "-newFile.ts"; 27 | } 28 | } 29 | 30 | export function extractFileNameAndPathFromFullPath(path: string): { 31 | fileName: string; 32 | extractedPath: string; 33 | } { 34 | if (!path) return { fileName: "", extractedPath: "" }; 35 | const fileName = path.split("/"); 36 | const extractedPath = fileName.slice(0, fileName.length - 1).join("/"); 37 | return { fileName: fileName[fileName.length - 1], extractedPath }; 38 | } 39 | 40 | export function getFileSuffix(fileName: string): string { 41 | const split = fileName.split("."); 42 | return split[split.length - 1]; 43 | } 44 | 45 | export function getFilePrefix(fileName: string): string { 46 | const split = fileName.split("."); 47 | return split[0]; 48 | } 49 | 50 | export const getDirectoryNameFromPath = (path: string) => { 51 | const pathArray = path.split("/"); 52 | return pathArray[pathArray.length - 1]; 53 | }; 54 | 55 | export function convertToTestFileName(fileName: string): string { 56 | const fileExtension = fileName.split(".").pop(); 57 | const fileNameWOExtention = fileNameWithoutExtension(fileName); 58 | 59 | return `${fileNameWOExtention}.test.${fileExtension}`; 60 | } 61 | 62 | export function fileNameWithoutExtension(fileName: string): string { 63 | return fileName.substring(0, fileName.lastIndexOf(".")); 64 | } 65 | 66 | export function convertToExoSuggestionFileName(fileName: string): string { 67 | if (!fileName) return ""; 68 | const fileExtension = fileName.split(".").pop(); 69 | const indexOfLastDot = fileName.lastIndexOf("."); 70 | console.log("indexOfLastDot", indexOfLastDot); 71 | if (indexOfLastDot === -1) { 72 | return fileName + ".exo-suggestion"; 73 | } 74 | 75 | const fileNameWithoutExtension = fileName.substring( 76 | 0, 77 | fileName.lastIndexOf(".") 78 | ); 79 | 80 | return `${fileNameWithoutExtension}.exo-suggestion.${fileExtension}`; 81 | } 82 | -------------------------------------------------------------------------------- /utils/getMethodName.ts: -------------------------------------------------------------------------------- 1 | // Sure! Here's a TypeScript function that takes a block of code as a string input, and tries to extract the function or method name: 2 | 3 | // ```typescript 4 | export function extractFunctionName(code: string): string | null { 5 | const nameRegex = /\bfunction\s+([\w_$]+)|[\w_$]+\.prototype\.(?=([\w_$]+))/; 6 | const functionNameRegex = /const\s+(\w+)\s+=/; 7 | const constExportRegex = /export\s+const\s+(\w+):/; 8 | const interfaceRegex = /export\s+interface\s+(\w+)/; 9 | const defaultRegex = /export\s+default\s+(\w+);/; 10 | const typeRegex = /export\s+enum\s+(\w+)/; 11 | const enumRegex = /export\s+type\s+(\w+)/; 12 | 13 | const match = 14 | code.match(nameRegex) || 15 | code.match(functionNameRegex) || 16 | code.match(constExportRegex) || 17 | code.match(interfaceRegex) || 18 | code.match(defaultRegex) || 19 | code.match(typeRegex) || 20 | code.match(enumRegex); 21 | 22 | return match ? match[1] || match[2] : null; 23 | } 24 | 25 | // Example Usage 26 | 27 | // const codeBlock = ` 28 | // function theFunction() { 29 | // console.log("Hello, World!"); 30 | // } 31 | // `; 32 | 33 | // console.log(extractFunctionName(codeBlock)); // Output: 'theFunction' 34 | // ``` 35 | 36 | // This function uses a regular expression to search for function names in the form of `function functionName` or `object.prototype.functionName`. It returns the found function name or `null` if there's no match. Keep in mind that this might not cover all possible use-cases, but it should work for most typical JavaScript functions. 37 | 38 | export const getImportMethodNames = (code: string) => { 39 | const regexPattern = /import\s+{(.+)}\s+from\s+["']([^"']+)["']/; 40 | const match = code.match(regexPattern); 41 | if (!match) return null; 42 | const [, methodNames, importPath] = match; 43 | return { 44 | methodNames: methodNames.split(",").map((name) => name.trim()), 45 | importPath, 46 | }; 47 | }; 48 | 49 | export function stripPrefix(filePath: string): { 50 | filePath: string; 51 | removedCount: number; 52 | } { 53 | const prefix = "../"; 54 | let result = filePath; 55 | let removedCount = 0; 56 | while (result.startsWith(prefix)) { 57 | removedCount++; 58 | result = result.slice(prefix.length); 59 | } 60 | return { 61 | filePath: result, 62 | removedCount, 63 | }; 64 | } 65 | 66 | export function stripPath(filePath: string, n: number): string { 67 | const segments = filePath.split("/"); 68 | const numSegments = segments.length; 69 | 70 | if (numSegments <= n) { 71 | // If the path has fewer segments than the number to be removed, 72 | // return an empty string (or throw an error, depending on your use case) 73 | return ""; 74 | } else { 75 | // Remove the last n segments from the path and rejoin the remaining segments 76 | const newSegments = segments.slice(0, numSegments - n); 77 | return newSegments.join("/"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /utils/getSubstringFromMultilineCode.ts: -------------------------------------------------------------------------------- 1 | // Loops over an array of lines of code and returns the resulting substring match based on the start and end row and column numbers 2 | export function getSubstringFromMultilineCode( 3 | lines: Array, 4 | startRow: number, 5 | startCol: number, 6 | endRow: number, 7 | endCol: number 8 | ) { 9 | let result = ""; 10 | 11 | const startPosition = lines[startRow].substring(startCol); 12 | const endPosition = lines[endRow].substring(0, endCol); 13 | const numberOfRows = endRow - startRow; 14 | 15 | if (numberOfRows < 1) { 16 | return startPosition; 17 | } else { 18 | for (let i = startRow; i < endRow; i++) { 19 | result = result + lines[i]; 20 | } 21 | return result + endPosition; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/getTimestamp.ts: -------------------------------------------------------------------------------- 1 | export const getTimestamp = () => { 2 | return Date.now(); 3 | }; 4 | 5 | export function getTimestampFunction() { 6 | return Date.now(); 7 | }; 8 | 9 | const getTomorrow = () => { 10 | return Date.now() + 86400000; 11 | }; 12 | 13 | function getTomorrowAgain() { 14 | return Date.now() + 86400000; 15 | }; 16 | 17 | (() => { 18 | return Date.now() + 86400000; 19 | })() 20 | -------------------------------------------------------------------------------- /utils/getUniqueNumbers.ts: -------------------------------------------------------------------------------- 1 | export function extractUniqueNumbers(input: string): number[] { 2 | const regex = /\d+/g; 3 | const matches = input.match(regex); 4 | if (!matches) return []; 5 | 6 | return [...new Set(matches.map((match) => parseInt(match)))]; 7 | } 8 | -------------------------------------------------------------------------------- /utils/git.service.ts: -------------------------------------------------------------------------------- 1 | import { ParseCode, ParsedCode, Element } from "../types/parseCode.types"; 2 | import { SimpleGit, simpleGit, SimpleGitOptions } from 'simple-git'; 3 | const Parser = require("tree-sitter") 4 | const TypeScript = require('tree-sitter-typescript').typescript; 5 | 6 | const parser = new Parser(); 7 | 8 | parser.setLanguage(TypeScript); 9 | 10 | export async function getGitDiff() { 11 | console.log(process.cwd()); 12 | 13 | const options: Partial = { 14 | baseDir: process.cwd(), 15 | binary: 'git', 16 | maxConcurrentProcesses: 6, 17 | trimmed: false, 18 | }; 19 | 20 | const git: SimpleGit = simpleGit(options); 21 | 22 | const gitDiffOptions = [ 23 | "--word-diff", 24 | "--unified=0" 25 | ] 26 | 27 | const diff = await git.diff(gitDiffOptions) 28 | return diff 29 | } 30 | 31 | 32 | // To achieve this, we need to parse the git diff result, extract the file names and changed line numbers, and create an object with the extracted information. Here's a possible implementation of the function: 33 | 34 | // typescript 35 | type GitDiffOutput = { 36 | fileName: string; 37 | changedLines: number[]; 38 | }; 39 | 40 | export function parseGitDiff(gitDiffResult: string): GitDiffOutput[] { 41 | const lines = gitDiffResult.split('\n'); 42 | const result: GitDiffOutput[] = []; 43 | 44 | let currentFile: GitDiffOutput | null = null; 45 | 46 | for (const line of lines) { 47 | // Check if line is a file indicator 48 | if (line.startsWith('diff --git')) { 49 | if (currentFile) { 50 | result.push(currentFile); 51 | } 52 | 53 | const fileName = line.split(' ')[2].trim(); 54 | currentFile = { fileName, changedLines: [] }; 55 | continue; 56 | } 57 | 58 | // Check if line is a change indicator 59 | if (line.startsWith('@@')) { 60 | if (!currentFile) continue; 61 | 62 | const lineNumbersMatcher = /@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/g; 63 | const match = lineNumbersMatcher.exec(line); 64 | 65 | if (!match) continue; 66 | 67 | const startLine = parseInt(match[1], 10); 68 | const linesCount = parseInt(match[2], 10) || 1; 69 | 70 | for (let i = 0; i < linesCount; i++) { 71 | currentFile.changedLines.push(startLine + i); 72 | } 73 | } 74 | } 75 | 76 | if (currentFile) { 77 | result.push(currentFile); 78 | } 79 | 80 | return result; 81 | } 82 | 83 | export async function getDiffAndParse() { 84 | return parseGitDiff(await getGitDiff()) 85 | } 86 | -------------------------------------------------------------------------------- /utils/openAi.ts: -------------------------------------------------------------------------------- 1 | import { CompletionResponse } from "../types/openAiTypes/openAiCompletionReqRes"; 2 | import { EngineName } from "../types/openAiTypes/openAiEngine"; 3 | import { clearLoading, commandLineLoading } from "./commandLineLoadingl"; 4 | import { openAiApiKey } from "./envVariable"; 5 | import { RateLimiter } from "limiter"; 6 | 7 | const { Configuration, OpenAIApi } = require('openai'); 8 | const { encode, decode } = require("gpt-3-encoder"); 9 | 10 | // Allow 30 requests per min (the Open api limit for embeddings). Also understands 11 | // 'second', 'minute', 'day', or a number of milliseconds 12 | const limiter = new RateLimiter({ tokensPerInterval: 30, interval: "minute" }); 13 | 14 | export enum ResponseFormat { 15 | B64_JSON = "b64_json", 16 | URL = 'url' 17 | } 18 | 19 | const raisingHands = String.fromCodePoint(0x1F64C) 20 | 21 | 22 | 23 | export async function createImage(prompt: string, responseFormat: ResponseFormat = ResponseFormat.URL, n: number = 2, size: string = "1024x1024",): Promise { 24 | 25 | const configuration = new Configuration({ 26 | apiKey: openAiApiKey 27 | }); 28 | 29 | const openai = new OpenAIApi(configuration); 30 | 31 | const interval = commandLineLoading("Using OpenAI to generate an image"); 32 | 33 | try { 34 | const res = await openai.createImage({ 35 | prompt, 36 | n, 37 | size, 38 | response_format: responseFormat 39 | }); 40 | const data: CompletionResponse = res.data 41 | clearLoading(interval, `${raisingHands} Query completed ${raisingHands}`); 42 | return data 43 | } catch (error: any) { 44 | if (error.response) { 45 | clearLoading(interval, `Error status: ${error.response.status}`); 46 | console.log(error.response.data); 47 | } else { 48 | clearLoading(interval, `Error status: ${error.message}`); 49 | } 50 | return error 51 | } 52 | } 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /utils/removeTextAfterString.ts: -------------------------------------------------------------------------------- 1 | const testStringArray = ["// Test", ".test.tsx"]; 2 | 3 | export const removeTextAfterString = (str: string, substr: string): string => { 4 | const index = str.indexOf(substr); 5 | return index === -1 ? str : str.substring(0, index); 6 | }; 7 | 8 | export const removeTextAfterArrayOfStrings = ( 9 | str: string, 10 | substrs: string[] 11 | ): string => { 12 | let result = ""; 13 | for (let i = 0; i < substrs.length || !result; i++) { 14 | const found = str.indexOf(substrs[i]); 15 | if (found !== -1) { 16 | result = substrs[i]; 17 | } 18 | } 19 | return result ? removeTextAfterString(str, result) : str; 20 | }; 21 | 22 | export const removeTest = (str: string): string => { 23 | return removeTextAfterArrayOfStrings(str, testStringArray); 24 | }; 25 | -------------------------------------------------------------------------------- /utils/treeSitter.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from "tree-sitter"; 2 | import { ParsedCode } from "../types/parseCode.types"; 3 | import { getSubstringFromMultilineCode } from "./getSubstringFromMultilineCode"; 4 | 5 | //https://github.com/tree-sitter/tree-sitter-javascript/blob/7a29d06274b7cf87d643212a433d970b73969016/src/node-types.json 6 | 7 | // javascript node types ^ 8 | 9 | export enum Language { 10 | TypeScript = "typescript", 11 | Python = "python", 12 | Java = "java", 13 | } 14 | 15 | export const getProgrammingLanguage = (fileName: string): Language => { 16 | const fileExtension = fileName.split(".").pop(); 17 | 18 | // TODO - add more languages. Use LLM to determine language if file extension is not available 19 | switch (fileExtension) { 20 | case "ts": 21 | return Language.TypeScript; 22 | case "py": 23 | return Language.Python; 24 | case "java": 25 | return Language.Java; 26 | default: 27 | return Language.TypeScript; 28 | } 29 | }; 30 | 31 | export async function parseFile( 32 | fileContents: string, 33 | language: Language 34 | ): Promise { 35 | const Parser = require("tree-sitter"); 36 | const TypeScript = require("tree-sitter-typescript").typescript; 37 | const Python = require("tree-sitter-python").python; 38 | const Java = require("tree-sitter-java").java; 39 | const fs = require("fs"); 40 | 41 | const parsers = { 42 | typeScript: TypeScript, 43 | python: Python, 44 | java: Java, 45 | }; 46 | 47 | function getParser(language: Language) { 48 | switch (language) { 49 | case Language.TypeScript: 50 | return TypeScript; 51 | case Language.Python: 52 | return Python; 53 | case Language.Java: 54 | return Java; 55 | default: 56 | return TypeScript; 57 | } 58 | } 59 | const parser = new Parser(); 60 | console.log("language", language); 61 | 62 | parser.setLanguage(getParser(language)); 63 | 64 | return await parser.parse(fileContents); 65 | } 66 | 67 | export function iterateOverTree( 68 | parentTree: Tree, 69 | handleSnippet?: (text: string) => void 70 | ) { 71 | for (let element of parentTree.rootNode.children) { 72 | const { 73 | startPosition, 74 | endPosition, 75 | type, 76 | tree, 77 | isNamed, 78 | text, 79 | children, 80 | childCount, 81 | } = element; 82 | 83 | console.log("element", element); 84 | if (handleSnippet) { 85 | handleSnippet(text); 86 | } 87 | 88 | // if (children.length > 0) { 89 | // for (let child of children) { 90 | // iterateOverTree(child.tree, handleSnippet); 91 | // } 92 | // } 93 | } 94 | } 95 | 96 | export const getParsedSnippetFromCodeBlock = async ( 97 | codeWithLineBreaks: string 98 | ): Promise => { 99 | const tree = await parseFile(codeWithLineBreaks, Language.TypeScript); 100 | const lines = codeWithLineBreaks.split("\n"); 101 | 102 | const addBackNewLine = lines.map((line: any) => `${line}\n`); 103 | 104 | const element = tree.rootNode.children.entries().next().value[1]; 105 | console.log("element", element); 106 | const codeSnippet = getSubstringFromMultilineCode( 107 | addBackNewLine, 108 | element.startPosition.row, 109 | element.startPosition.column, 110 | element.endPosition.row, 111 | element.endPosition.column 112 | ); 113 | return { 114 | code: codeSnippet, 115 | metadata: { 116 | element: element, 117 | filePath: "", 118 | type: element.type, 119 | fileName: "", 120 | }, 121 | }; 122 | }; 123 | --------------------------------------------------------------------------------