├── .env.example ├── .github └── workflows │ └── frontend.yaml ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── architecture.png ├── client-poc ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app │ ├── ReltaRuntimeProvider.tsx │ ├── api │ │ ├── chat │ │ │ └── route.ts │ │ ├── feedback │ │ │ └── route.ts │ │ └── import │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── providers.tsx │ ├── repo │ │ └── [owner] │ │ │ └── [repo] │ │ │ ├── AddRepositoryToList.tsx │ │ │ ├── RepositoryInfo.client.tsx │ │ │ ├── RepositoryInfo.tsx │ │ │ ├── fallback.tsx │ │ │ └── page.tsx │ └── sso-callback │ │ └── page.tsx ├── components.json ├── components │ ├── MyAssistant.tsx │ ├── NewRepositoryDialog.tsx │ ├── flow │ │ ├── DatabaseNode.tsx │ │ ├── GitHubDataFlow.tsx │ │ ├── GitHubNode.tsx │ │ ├── QueryNode.tsx │ │ └── SemanticLayerNode.tsx │ ├── tools │ │ ├── ChartToolUI.tsx │ │ └── TextToolUI.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── table.tsx │ │ └── tabs.tsx ├── instrumentation.ts ├── lib │ ├── actions.ts │ ├── makeQueryClient.ts │ ├── reltaApi.ts │ ├── storage.ts │ └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── tailwind.config.ts └── tsconfig.json ├── graph.svg ├── launch.py ├── requirements.txt └── server-poc ├── .dockerignore ├── .env.example ├── .gitignore ├── .relta └── .DS_Store ├── Dockerfile ├── README.md ├── data_pipelines ├── .dlt │ ├── .sources │ └── config.toml ├── .gitignore ├── github │ ├── README.md │ ├── __init__.py │ ├── helpers.py │ ├── queries.py │ └── settings.py ├── github_pipeline.py └── requirements.txt ├── deploy.sh ├── infra ├── .DS_Store ├── .gitignore ├── app │ ├── .DS_Store │ ├── ecs │ │ ├── main.tf │ │ ├── output.tf │ │ └── variable.tf │ ├── main.tf │ ├── network │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variable.tf │ ├── rds │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── variable.tf └── setup │ ├── .DS_Store │ ├── main.tf │ ├── output.tf │ └── variable.tf ├── poetry.lock ├── pyproject.toml ├── semantic_layer ├── commit_activity.json ├── examples.json ├── issue_tracking.json ├── pull_request_status.json └── repository_stars.json ├── server_poc ├── __init__.py ├── models │ ├── __init__.py │ ├── githubrepoinfo.py │ └── user_prompt.py └── server.py └── tests ├── __init__.py ├── questions.csv └── test_cases.json /.env.example: -------------------------------------------------------------------------------- 1 | # Data destination - can be local path or S3 URL 2 | DATA_DESTINATION=data # or s3://your-bucket/data -------------------------------------------------------------------------------- /.github/workflows/frontend.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Frontend 2 | env: 3 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 4 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - client-poc/** 11 | - .github/workflows/** 12 | 13 | jobs: 14 | deploy: 15 | defaults: 16 | run: 17 | working-directory: client-poc 18 | 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code repository 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 1 25 | 26 | - name: Install pnpm 27 | uses: pnpm/action-setup@v4 28 | with: 29 | package_json_file: client-poc/package.json 30 | 31 | - name: Setup node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | cache-dependency-path: client-poc/pnpm-lock.yaml 35 | node-version: 20 36 | cache: "pnpm" 37 | 38 | - name: Install Vercel CLI 39 | run: npm install --global vercel@latest 40 | 41 | - name: Install dependencies 42 | run: pnpm install --frozen-lockfile 43 | 44 | - name: Pull Vercel Environment Information 45 | run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} 46 | 47 | - name: Build Project Artifacts 48 | run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} 49 | 50 | - name: Deploy Project Artifacts to Vercel 51 | run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server-poc/relta"] 2 | path = server-poc/relta 3 | url = https://github.com/reltadev/relta 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | .EXPORT_ALL_VARIABLES: 4 | APP_NAME=github-assistant 5 | 6 | TAG=latest 7 | TF_VAR_app_name=${APP_NAME} 8 | REGISTRY_NAME=${APP_NAME} 9 | TF_VAR_image=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REGISTRY_NAME}:${TAG} 10 | TF_VAR_region=${AWS_REGION} 11 | 12 | 13 | setup-ecr: 14 | cd server-poc/infra/setup && terraform init && terraform apply 15 | 16 | deploy-container: 17 | cd server-poc && sh deploy.sh 18 | 19 | deploy-service: 20 | cd server-poc/infra/app && terraform init && terraform apply -auto-approve 21 | 22 | destroy-service: 23 | cd server-poc/infra/app && terraform init && terraform destroy -auto-approve -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-assistant 2 | 3 | github-assistant is a PoC built using [Relta](http://relta.dev) and [assistant-ui](https://assistant-ui.com). You can try it out [here](https://github-assistant.com) and read more about the project in this [blog](https://medium.com/relta/github-assistant-49ae388ad758). 4 | 5 | ## Demo Video 6 | 7 |
8 | 9 | Demo video 10 | 11 |
12 | 13 | ## Architecture 14 | 15 | ![Architecture](./architecture.png) 16 | 17 | 18 | The Relta sub-module is currently not open source. You can see the semantic layer that was generated by the library and is used in the text-to-SQL pipeline in the `server-poc/semantic_layer` folder. The full library is available upon request for both commercial and non-commercial uses. Send an email to amir [at] relta.dev for access. 19 | 20 | ## Requirements to run 21 | 22 | - Python 3.9+ 23 | - npm or other Node.js package manager 24 | - Git 25 | 26 | ## Setup 27 | 28 | 1. Reach out to get access to the Relta submodule. 29 | 30 | 1. Initialize the `relta` submodule 31 | 32 | ```sh 33 | git remote add template https://github.com/reltadev/poc-template.git && git submodule update --init --recursive 34 | ``` 35 | 36 | 2. Create a virtual environment for Relta 37 | 38 | ```sh 39 | python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt 40 | ``` 41 | 42 | 3. Setup the `.env` files from `.env.example` 43 | 44 | ```sh 45 | cp client-poc/.env.example client-poc/.env && cp server-poc/.env.example server-poc/.env 46 | ``` 47 | 48 | 4. Set the following environment variables in `server-poc/.env`: 49 | - `OPENAI_API_KEY`: Your OpenAI API key 50 | - `GITHUB_DATABASE_CONNECTION_URI` the PG database where the GitHub data will be piped into 51 | 52 | 5. Launch the backend 53 | 54 | ```sh 55 | cd server-poc 56 | uvicorn server_poc.server:app --host 0.0.0.0 --port 80 --reload4 57 | ``` 58 | 59 | 5. Launch the front-end 60 | 61 | ```sh 62 | cd client-poc 63 | npm install 64 | npm run dev 65 | ``` 66 | 67 | 68 | 69 | ## Updating 70 | 71 | We will generally give instructions on how to update Relta or the POC to handle any bugs or new features. 72 | 73 | 74 | 75 | ## Contributing 76 | 77 | We would love to get new contributors! If you are interested, please reach out to amir [at] relta.dev or simon.farshid [at] outlook.com 78 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reltadev/github-assistant/2964f38b399d502d20d7e484dad4d3b232a0a8a8/architecture.png -------------------------------------------------------------------------------- /client-poc/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SERVER_DOMAIN=localhost:8000 -------------------------------------------------------------------------------- /client-poc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /client-poc/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /client-poc/README.md: -------------------------------------------------------------------------------- 1 | This is the [assistant-ui](https://github.com/Yonom/assistant-ui) starter project. 2 | 3 | ## Getting Started 4 | 5 | First, duplicate the `.env.example` file to `.env`. No changes to the default environment variables are needed. 6 | 7 | Then, run the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | # or 14 | pnpm dev 15 | # or 16 | bun dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 20 | 21 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 22 | -------------------------------------------------------------------------------- /client-poc/app/ReltaRuntimeProvider.tsx: -------------------------------------------------------------------------------- 1 | // "use client"; 2 | 3 | // import type { ReactNode } from "react"; 4 | // import { 5 | // AssistantRuntimeProvider, 6 | // useLocalRuntime, 7 | // SimpleTextAttachmentAdapter, 8 | // type ChatModelAdapter, 9 | // } from "@assistant-ui/react"; 10 | 11 | // const SERVER_DOMAIN = process.env.NEXT_PUBLIC_SERVER_DOMAIN; 12 | 13 | // const MyModelAdapter: ChatModelAdapter = { 14 | // async run({ messages, abortSignal }) { 15 | // let result; 16 | // if (messages.length === 0) { 17 | // result = await fetch(`http://${SERVER_DOMAIN}/chat`, { 18 | // method: "POST", 19 | // signal: abortSignal, 20 | // }); 21 | // } else if ( 22 | // messages[messages.length - 1].attachments && 23 | // messages[messages.length - 1].attachments.length !== 0 24 | // ) { 25 | // console.log("uploading file"); 26 | // const formData = new FormData(); 27 | // formData.append( 28 | // "file", 29 | // messages[messages.length - 1].attachments[0].file 30 | // ); 31 | // result = await fetch(`http://${SERVER_DOMAIN}/uploadfile`, { 32 | // method: "POST", 33 | // body: formData, 34 | // }); 35 | // } else { 36 | // result = await fetch(`http://${SERVER_DOMAIN}/prompt`, { 37 | // method: "POST", 38 | // headers: { 39 | // "Content-Type": "application/json", 40 | // }, 41 | // // forward the messages in the chat to the API 42 | // body: JSON.stringify({ 43 | // // chat_id:"2", 44 | // prompt: messages[messages.length - 1].content[0].text, 45 | // }), 46 | // // if the user hits the "cancel" button or escape keyboard key, cancel the request 47 | // signal: abortSignal, 48 | // }); 49 | // } 50 | // const data = await result.json(); 51 | // return { 52 | // content: [ 53 | // { 54 | // type: "text", 55 | // text: data.message.content, 56 | // }, 57 | // ], 58 | // }; 59 | // }, 60 | // }; 61 | 62 | // export function ReltaRuntimeProvider({ 63 | // children, 64 | // }: Readonly<{ 65 | // children: ReactNode; 66 | // }>) { 67 | // const runtime = useLocalRuntime(MyModelAdapter, { 68 | // adapters: { 69 | // attachments: new SimpleTextAttachmentAdapter(), 70 | // feedback: { 71 | // submit: ({ type, message }) => { 72 | // fetch(`http://${SERVER_DOMAIN}/feedback`, { 73 | // method: "POST", 74 | // headers: { 75 | // "Content-Type": "application/json", 76 | // }, 77 | // body: JSON.stringify({ type: type, message: message }), 78 | // }) 79 | // .then((response) => response.json()) 80 | // .then((data) => console.log("Feedback submitted:", data)) 81 | // .catch((error) => 82 | // console.error("Error submitting feedback:", error) 83 | // ); 84 | // console.log({ type, message }); 85 | // }, 86 | // }, 87 | // }, 88 | // }); 89 | 90 | // return ( 91 | // 92 | // {children} 93 | // 94 | // ); 95 | // } 96 | -------------------------------------------------------------------------------- /client-poc/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { openai } from "@ai-sdk/openai"; 2 | import { generateObject, streamText, CoreMessage } from "ai"; 3 | import { z } from "zod"; 4 | import { ReltaApiClient } from "../../../lib/reltaApi"; 5 | import { AISDKExporter } from "langsmith/vercel"; 6 | 7 | export const maxDuration = 30; 8 | 9 | const model = openai("gpt-4o"); 10 | 11 | const generateChartConfig = async (rows: object[]) => { 12 | const { object } = await generateObject({ 13 | model, 14 | system: 15 | "You are a helpful assistant that answers questions about a GitHub repository. Identify the correct chart type based on the provided data.", 16 | prompt: JSON.stringify({ 17 | rowCount: rows.length, 18 | rows: rows.length <= 6 ? rows : [...rows.slice(0, 3), ...rows.slice(-3)], 19 | }), 20 | schema: z.object({ 21 | type: z.enum(["bar", "line", "pie"]), 22 | title: z.string(), 23 | }), 24 | experimental_telemetry: AISDKExporter.getSettings(), 25 | }); 26 | return object; 27 | }; 28 | 29 | const getRouterSystemPrompt = ( 30 | repoName: string 31 | ) => `You are a helpful assistant that answers questions about the following GitHub repository: 32 | 33 | Selected Repository: ${repoName} 34 | 35 | You can use natural language queries to answer questions the user has about the repository. 36 | 37 | If a question is best answered by displaying a graph/chart, use the "chart" tool. 38 | If a question is about a single data point (e.g. "who made the most recent commit?"), use the "text" tool. 39 | 40 | If using the "chart" tool, specify the x-Axis type and unit on the chart (e.g. "stars per day"). 41 | 42 | When printing a chart, ONLY call the provided function call. This will print the chart to the user. Do not use images.`; 43 | 44 | export const POST = async (request: Request) => { 45 | const { 46 | owner, 47 | repo, 48 | messages: clientMessages, 49 | } = (await request.json()) as { 50 | owner: string; 51 | repo: string; 52 | messages: CoreMessage[]; 53 | }; 54 | 55 | const messages = clientMessages.map((m) => { 56 | if (m.role !== "tool") return m; 57 | return { 58 | ...m, 59 | content: 60 | typeof m.content === "string" 61 | ? m.content 62 | : m.content.map((c) => { 63 | if (c.toolName !== "chart") return c; 64 | 65 | const result = c.result as { rows: object[] }; 66 | return { 67 | ...c, 68 | result: { 69 | ...result, 70 | rowCount: result.rows.length, 71 | rows: 72 | result.rows.length <= 6 73 | ? result.rows 74 | : [...result.rows.slice(0, 3), ...result.rows.slice(-3)], 75 | }, 76 | }; 77 | }), 78 | }; 79 | }); 80 | 81 | const relta = new ReltaApiClient({ 82 | owner, 83 | repo_name: repo, 84 | }); 85 | 86 | const stream = streamText({ 87 | model, 88 | messages, 89 | system: getRouterSystemPrompt(`${owner}/${repo}`), 90 | tools: { 91 | chart: { 92 | description: 93 | "Query the GitHub metadata with the provided natural language query, and return the data as a table, which will be automatically displayed to the user in the form of a chart or table.", 94 | parameters: z.object({ 95 | query: z.string().describe("The query to provide the agent."), 96 | }), 97 | execute: async (requestData) => { 98 | const { id, rows, sql } = await relta.getDataQuery(requestData.query); 99 | const config = await generateChartConfig(rows); 100 | return { 101 | ...config, 102 | id, 103 | rows, 104 | sql, 105 | hint: 106 | rows.length > 0 107 | ? "The chart is being displayed the user. As an LLM, you only see the first 3 rows and the last 3 rows." 108 | : "No data available.", 109 | }; 110 | }, 111 | }, 112 | text: { 113 | description: 114 | "Query GitHub metadata with the provided natural language query, and return a natural language answer.", 115 | parameters: z.object({ 116 | query: z.string().describe("The query to provide the agent."), 117 | }), 118 | execute: async (requestData) => { 119 | const { text, id } = await relta.getTextQuery(requestData.query); 120 | return { text, id }; 121 | }, 122 | }, 123 | }, 124 | experimental_telemetry: AISDKExporter.getSettings(), 125 | }); 126 | 127 | return stream.toDataStreamResponse(); 128 | }; 129 | -------------------------------------------------------------------------------- /client-poc/app/api/feedback/route.ts: -------------------------------------------------------------------------------- 1 | import { ReltaApiClient } from "../../../lib/reltaApi"; 2 | 3 | export const POST = async (request: Request) => { 4 | const { owner, repo, chatId, type, message } = await request.json(); 5 | 6 | const relta = new ReltaApiClient({ 7 | owner, 8 | repo_name: repo, 9 | }); 10 | const result = await relta.submitFeedback(chatId, type, message); 11 | return Response.json(result, { status: 200 }); 12 | }; 13 | -------------------------------------------------------------------------------- /client-poc/app/api/import/route.ts: -------------------------------------------------------------------------------- 1 | import { ReltaApiClient } from "@/lib/reltaApi"; 2 | import { auth, clerkClient } from "@clerk/nextjs/server"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export const maxDuration = 60; 7 | 8 | export async function POST(request: Request) { 9 | const { owner, repo } = await request.json(); 10 | 11 | const { userId } = await auth(); 12 | if (!userId) throw new Error("User not signed in"); 13 | 14 | const client = await clerkClient(); 15 | const tokens = await client.users.getUserOauthAccessToken( 16 | userId, 17 | "oauth_github" 18 | ); 19 | const accessToken = tokens.data[0].token; 20 | 21 | const info = await new ReltaApiClient({ 22 | owner, 23 | repo_name: repo, 24 | }).loadGithubData(accessToken); 25 | 26 | return Response.json(info, { status: 200 }); 27 | } 28 | -------------------------------------------------------------------------------- /client-poc/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reltadev/github-assistant/2964f38b399d502d20d7e484dad4d3b232a0a8a8/client-poc/app/favicon.ico -------------------------------------------------------------------------------- /client-poc/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @layer base { 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 240 10% 3.9%; 8 | --card: 0 0% 100%; 9 | --card-foreground: 240 10% 3.9%; 10 | --popover: 0 0% 100%; 11 | --popover-foreground: 240 10% 3.9%; 12 | --primary: 240 5.9% 10%; 13 | --primary-foreground: 0 0% 98%; 14 | --secondary: 240 4.8% 95.9%; 15 | --secondary-foreground: 240 5.9% 10%; 16 | --muted: 240 4.8% 95.9%; 17 | --muted-foreground: 240 3.8% 46.1%; 18 | --accent: 240 4.8% 95.9%; 19 | --accent-foreground: 240 5.9% 10%; 20 | --destructive: 0 84.2% 60.2%; 21 | --destructive-foreground: 0 0% 98%; 22 | --border: 240 5.9% 90%; 23 | --input: 240 5.9% 90%; 24 | --ring: 240 10% 3.9%; 25 | --chart-1: 12 76% 61%; 26 | --chart-2: 173 58% 39%; 27 | --chart-3: 197 37% 24%; 28 | --chart-4: 43 74% 66%; 29 | --chart-5: 27 87% 67%; 30 | --radius: 0.5rem 31 | } 32 | .dark { 33 | --background: 240 10% 3.9%; 34 | --foreground: 0 0% 98%; 35 | --card: 240 10% 3.9%; 36 | --card-foreground: 0 0% 98%; 37 | --popover: 240 10% 3.9%; 38 | --popover-foreground: 0 0% 98%; 39 | --primary: 0 0% 98%; 40 | --primary-foreground: 240 5.9% 10%; 41 | --secondary: 240 3.7% 15.9%; 42 | --secondary-foreground: 0 0% 98%; 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | --accent: 240 3.7% 15.9%; 46 | --accent-foreground: 0 0% 98%; 47 | --destructive: 0 62.8% 30.6%; 48 | --destructive-foreground: 0 0% 98%; 49 | --border: 240 3.7% 15.9%; 50 | --input: 240 3.7% 15.9%; 51 | --ring: 240 4.9% 83.9%; 52 | --chart-1: 220 70% 50%; 53 | --chart-2: 160 60% 45%; 54 | --chart-3: 30 80% 55%; 55 | --chart-4: 280 65% 60%; 56 | --chart-5: 340 75% 55% 57 | } 58 | } 59 | @layer base { 60 | * { 61 | @apply border-border; 62 | } 63 | body { 64 | @apply bg-background text-foreground; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client-poc/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import Providers from "./providers"; 3 | 4 | import "./globals.css"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Create Next App", 8 | description: "Generated by create next app", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client-poc/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NewRepositoryDialog } from "@/components/NewRepositoryDialog"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Card, CardHeader } from "@/components/ui/card"; 6 | import { defaultRepos, useRepoStore } from "@/lib/storage"; 7 | import { SignedIn } from "@clerk/nextjs"; 8 | import { SignOutButton } from "@clerk/nextjs"; 9 | import { PlusIcon, TrashIcon } from "lucide-react"; 10 | import Link from "next/link"; 11 | import { Suspense } from "react"; 12 | 13 | interface RepoCardProps { 14 | name: string; 15 | id: string; 16 | href?: string; 17 | onDelete?: () => void; 18 | } 19 | 20 | function RepoCard({ name, id, href, onDelete }: RepoCardProps) { 21 | const content = ( 22 | 23 | 24 |

{name}

25 |

{id}

26 |
27 | {onDelete && ( 28 | 40 | )} 41 | 42 | 43 | ); 44 | 45 | return href ? {content} : content; 46 | } 47 | 48 | export default function RepoSelection() { 49 | const { repositories: userRepos, deleteRepository } = useRepoStore(); 50 | 51 | const renderRepoCards = ( 52 | repos: { owner: string; repo: string }[], 53 | onDelete?: (owner: string, repo: string) => void 54 | ) => { 55 | return repos.map((repo) => { 56 | const id = `${repo.owner}/${repo.repo}`; 57 | return ( 58 | onDelete(repo.owner, repo.repo) : undefined 65 | } 66 | /> 67 | ); 68 | }); 69 | }; 70 | 71 | return ( 72 |
73 |
74 |

github-assistant

75 | by assistant-ui and 76 | relta 77 |
78 | 79 | 80 | 81 |
82 |
83 |
84 |

85 | Which repository do you want to explore? 86 |

87 |

88 | GitHub assistant is a tool for exploring GitHub repositories by 89 | asking natural language questions.{" "} 90 | 94 | Learn more 95 | 96 |

97 | 98 | {/* default repos */} 99 |
100 | {renderRepoCards(defaultRepos)} 101 |
102 | 103 |
104 | 105 | {/* user added repos */} 106 |
107 | {renderRepoCards(userRepos, deleteRepository)} 108 | 109 | {/* Dotted add repo button */} 110 | 111 | 114 | 115 | 116 |

117 | Import Repository 118 |

119 |
120 | 121 | } 122 | /> 123 |
124 |
125 |
126 |
127 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /client-poc/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { makeQueryClient } from "@/lib/makeQueryClient"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { 6 | isServer, 7 | QueryClient, 8 | QueryClientProvider, 9 | } from "@tanstack/react-query"; 10 | import { PropsWithChildren } from "react"; 11 | import posthog from "posthog-js"; 12 | import { PostHogProvider } from "posthog-js/react"; 13 | 14 | let browserQueryClient: QueryClient | undefined = undefined; 15 | 16 | export function getQueryClient() { 17 | if (isServer) { 18 | // Server: always make a new query client 19 | return makeQueryClient(); 20 | } else { 21 | // Browser: make a new query client if we don't already have one 22 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 23 | return browserQueryClient; 24 | } 25 | } 26 | 27 | if (typeof window !== "undefined") { 28 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 29 | api_host: "/ingest", 30 | ui_host: "https://us.posthog.com", 31 | person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well 32 | }); 33 | } 34 | 35 | export default function Providers({ children }: PropsWithChildren) { 36 | const queryClient = getQueryClient(); 37 | 38 | return ( 39 | 40 | 41 | 42 | {children} 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /client-poc/app/repo/[owner]/[repo]/AddRepositoryToList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRepoStore } from "@/lib/storage"; 4 | import { FC, useEffect } from "react"; 5 | 6 | export const AddRepositoryToList: FC<{ 7 | owner: string; 8 | repo: string; 9 | }> = ({ owner, repo }) => { 10 | const addRepository = useRepoStore((s) => s.addRepository); 11 | useEffect(() => { 12 | addRepository({ owner, repo }); 13 | }, [addRepository, owner, repo]); 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /client-poc/app/repo/[owner]/[repo]/RepositoryInfo.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getRepoInfo } from "@/lib/actions"; 4 | import { useSuspenseQuery } from "@tanstack/react-query"; 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from "@/components/ui/popover"; 10 | import { ChevronDown, RefreshCw } from "lucide-react"; 11 | import { RepoInfo } from "@/lib/reltaApi"; 12 | import { Button } from "@/components/ui/button"; 13 | import { useState } from "react"; 14 | import { useRouter } from "next/navigation"; 15 | 16 | function getRelativeTimeString(isoString: string | null): string { 17 | if (!isoString) return "never"; 18 | 19 | const date = new Date(isoString); 20 | const now = new Date(); 21 | const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); 22 | 23 | if (diffInSeconds < 60) return "just now"; 24 | if (diffInSeconds < 3600) 25 | return `${Math.floor(diffInSeconds / 60)} minutes ago`; 26 | if (diffInSeconds < 86400) 27 | return `${Math.floor(diffInSeconds / 3600)} hours ago`; 28 | return `${Math.floor(diffInSeconds / 86400)} days ago`; 29 | } 30 | 31 | function StatusDot({ 32 | status, 33 | }: { 34 | status: "success" | "running" | "warning" | "error"; 35 | }) { 36 | const colors = { 37 | success: "bg-green-500", 38 | running: "bg-blue-500", 39 | warning: "bg-yellow-500", 40 | error: "bg-red-500", 41 | }; 42 | 43 | return ( 44 | 47 | ); 48 | } 49 | 50 | function getPipelineStatus(repoInfo: RepoInfo) { 51 | if (repoInfo.pipeline_status === "RUNNING") return "running"; 52 | if (repoInfo.pipeline_status !== "SUCCESS") return "error"; 53 | 54 | const allLoaded = 55 | repoInfo.loaded_issues && 56 | repoInfo.loaded_stars && 57 | repoInfo.loaded_pull_requests && 58 | repoInfo.loaded_commits; 59 | 60 | return allLoaded ? "success" : "warning"; 61 | } 62 | 63 | export const RepositoryInfoClient = ({ 64 | owner, 65 | repo, 66 | }: { 67 | owner: string; 68 | repo: string; 69 | }) => { 70 | const { data: repoInfo } = useSuspenseQuery({ 71 | queryKey: ["repo-info", owner, repo], 72 | staleTime: 5 * 1000, 73 | queryFn: async () => getRepoInfo(owner, repo), 74 | }); 75 | 76 | const [isReimporting, setIsReimporting] = useState(false); 77 | const router = useRouter(); 78 | 79 | const handleReimport = async () => { 80 | setIsReimporting(true); 81 | await fetch("/api/import", { 82 | method: "POST", 83 | headers: { 84 | "Content-Type": "application/json", 85 | }, 86 | body: JSON.stringify({ owner, repo }), 87 | }); 88 | router.replace(`/repo/${owner}/${repo}`); 89 | setIsReimporting(false); 90 | }; 91 | 92 | const pipelineStatus = getPipelineStatus(repoInfo); 93 | 94 | const lastImportText = 95 | repoInfo.pipeline_status === "SUCCESS" 96 | ? "Last import: " + getRelativeTimeString(repoInfo?.last_pipeline_run) 97 | : repoInfo.pipeline_status === "RUNNING" 98 | ? "Import currently in progress" 99 | : "Import failed"; 100 | 101 | return ( 102 |
103 | 104 | {lastImportText} 105 | 106 | 107 | 108 | 109 | 110 | 111 |
112 |
113 |
Import Status
114 | 123 |
124 | 125 |
126 |
127 | 130 | Pull Requests 131 |
132 | 133 | {repoInfo.loaded_pull_requests ? "Available" : "Not Available"} 134 | 135 |
136 | 137 |
138 |
139 | 142 | Commits 143 |
144 | 145 | {repoInfo.loaded_commits ? "Available" : "Not Available"} 146 | 147 |
148 | 149 |
150 |
151 | 154 | Issues 155 |
156 | 157 | {repoInfo.loaded_issues ? "Available" : "Not Available"} 158 | 159 |
160 | 161 |
162 |
163 | 166 | Stars 167 |
168 | 169 | {repoInfo.loaded_stars ? "Available" : "Not Available"} 170 | 171 |
172 | 173 |
174 | {lastImportText} 175 |
176 |
177 |
178 |
179 |
180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /client-poc/app/repo/[owner]/[repo]/RepositoryInfo.tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; 2 | import { makeQueryClient } from "@/lib/makeQueryClient"; 3 | import { RepositoryInfoClient } from "./RepositoryInfo.client"; 4 | import { getRepoInfo } from "@/lib/actions"; 5 | 6 | export async function RepositoryInfo({ 7 | owner, 8 | repo, 9 | }: { 10 | owner: string; 11 | repo: string; 12 | }) { 13 | const queryClient = makeQueryClient(); 14 | 15 | await queryClient.prefetchQuery({ 16 | queryKey: ["repo-info", owner, repo], 17 | queryFn: () => { 18 | return getRepoInfo(owner, repo); 19 | }, 20 | }); 21 | 22 | return ( 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client-poc/app/repo/[owner]/[repo]/fallback.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeft, Loader } from "lucide-react"; 2 | import Link from "next/link"; 3 | 4 | export const LoadingPage = async ({ 5 | owner, 6 | repo, 7 | }: { 8 | owner: string; 9 | repo: string; 10 | }) => { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 | 18 |

19 | {owner}/{repo} 20 |

21 |
22 |
23 |
24 | Importing repository... 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client-poc/app/repo/[owner]/[repo]/page.tsx: -------------------------------------------------------------------------------- 1 | import { MyAssistant } from "@/components/MyAssistant"; 2 | import { ReltaApiClient } from "@/lib/reltaApi"; 3 | import { ChevronLeft } from "lucide-react"; 4 | import Link from "next/link"; 5 | import { notFound, redirect } from "next/navigation"; 6 | import { LoadingPage } from "./fallback"; 7 | import { FC, Suspense } from "react"; 8 | import { AddRepositoryToList } from "./AddRepositoryToList"; 9 | import { RepositoryInfo } from "./RepositoryInfo"; 10 | 11 | export const maxDuration = 60; 12 | 13 | const EnsureRepoIsLoaded: FC<{ 14 | owner: string; 15 | repo: string; 16 | }> = async ({ owner, repo }) => { 17 | const client = new ReltaApiClient({ 18 | owner, 19 | repo_name: repo, 20 | }); 21 | try { 22 | const startTime = Date.now(); 23 | while (true) { 24 | const info = await client.getRepoInfo(); 25 | if (info.pipeline_status === "SUCCESS") { 26 | return null; 27 | } 28 | 29 | if (info.pipeline_status !== "RUNNING") 30 | throw new Error("Pipeline failed"); 31 | 32 | const age = (Date.now() - startTime) / 1000; 33 | if (age < maxDuration - 5) { 34 | await new Promise((resolve) => setTimeout(resolve, 1000)); 35 | } else { 36 | break; 37 | } 38 | } 39 | } catch (e) { 40 | console.log(e); 41 | notFound(); 42 | } 43 | 44 | redirect(`/repo/${owner}/${repo}`); 45 | }; 46 | 47 | export default async function Home({ 48 | params, 49 | }: { 50 | params: Promise<{ owner: string; repo: string }>; 51 | }) { 52 | const { owner, repo } = await params; 53 | 54 | return ( 55 | <> 56 | 57 | }> 58 | 59 |
60 |
61 |
62 | 63 | 64 | 65 |

66 | {owner}/{repo} 67 |

68 |
69 | 70 | 71 | 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /client-poc/app/sso-callback/page.tsx: -------------------------------------------------------------------------------- 1 | import { AuthenticateWithRedirectCallback } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /client-poc/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /client-poc/components/MyAssistant.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Composer, useEdgeRuntime } from "@assistant-ui/react"; 4 | import { Thread } from "@assistant-ui/react"; 5 | import { makeMarkdownText } from "@assistant-ui/react-markdown"; 6 | import { ChartToolUI } from "./tools/ChartToolUI"; 7 | import { TextToolUI } from "./tools/TextToolUI"; 8 | import { create } from "zustand"; 9 | import { GitPullRequest, LoaderCircleIcon } from "lucide-react"; 10 | 11 | const MarkdownText = makeMarkdownText(); 12 | 13 | type MyAssistantProps = { 14 | owner: string; 15 | repo: string; 16 | }; 17 | 18 | const useFeedbackState = create<{ isLoading?: boolean; prUrl?: string }>( 19 | () => ({}) 20 | ); 21 | 22 | const MyComposer = () => { 23 | const { isLoading, prUrl } = useFeedbackState((state) => state); 24 | return ( 25 | <> 26 | {!!isLoading && ( 27 |
28 | {" "} 29 |
30 |

31 | Creating a PR based on your feedback 32 |

33 |

Loading...

34 |
35 |
36 | )} 37 | {!!prUrl && ( 38 |
39 | {" "} 40 |
41 |

42 | PR #{prUrl.split("/").at(-1)} created based on your feedback 43 |

44 | 45 | {prUrl} 46 | 47 |
48 |
49 | )} 50 | 51 | 52 |

53 | powered by{" "} 54 | 55 | assistant-ui 56 | {" "} 57 | and{" "} 58 | 59 | Relta 60 | 61 |

62 | 63 | ); 64 | }; 65 | 66 | export function MyAssistant({ owner, repo }: MyAssistantProps) { 67 | const runtime = useEdgeRuntime({ 68 | api: "/api/chat", 69 | body: { owner, repo }, 70 | adapters: { 71 | feedback: { 72 | submit: async ({ type, message }) => { 73 | const chatId = 74 | message.content 75 | .map((c) => 76 | c.type === "tool-call" 77 | ? (c.result as { id?: string } | undefined)?.id 78 | : undefined 79 | ) 80 | .filter(Boolean)[0] ?? "-"; 81 | 82 | if (type === "negative") { 83 | useFeedbackState.setState({ isLoading: true }); 84 | } 85 | const { pr_url } = await fetch(`/api/feedback`, { 86 | method: "POST", 87 | headers: { "Content-Type": "application/json" }, 88 | body: JSON.stringify({ owner, repo, chatId, type, message }), 89 | }) 90 | .then((response) => response.json()) 91 | .catch((error) => 92 | console.error("Error submitting feedback:", error) 93 | ) 94 | .finally(() => useFeedbackState.setState({ isLoading: false })); 95 | 96 | useFeedbackState.setState({ prUrl: pr_url }, true); 97 | }, 98 | }, 99 | }, 100 | maxSteps: 4, 101 | unstable_AISDKInterop: true, 102 | }); 103 | 104 | return ( 105 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /client-poc/components/NewRepositoryDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog"; 12 | import { Input } from "@/components/ui/input"; 13 | import { useForm } from "react-hook-form"; 14 | import { zodResolver } from "@hookform/resolvers/zod"; 15 | import { z } from "zod"; 16 | import { 17 | Form, 18 | FormControl, 19 | FormDescription, 20 | FormField, 21 | FormItem, 22 | FormLabel, 23 | FormMessage, 24 | } from "@/components/ui/form"; 25 | import { useAuth, useSignIn } from "@clerk/nextjs"; 26 | import { MouseEvent } from "react"; 27 | import { useRouter, useSearchParams } from "next/navigation"; 28 | 29 | const GITHUB_URL_REGEX = 30 | /^https?:\/\/(?:www\.)?github\.com\/(?[^\/]+)\/(?[^\/]+)(?:\/.*)?(?:\.git)?\/?$/; 31 | 32 | const newRepoSchema = z.object({ 33 | githubUrl: z 34 | .string() 35 | .url("Please enter a valid URL.") 36 | .regex(GITHUB_URL_REGEX, "Please enter a valid GitHub repository URL."), 37 | }); 38 | 39 | type NewRepoFormData = z.infer; 40 | 41 | type NewRepositoryDialogProps = { 42 | trigger: React.ReactNode; 43 | }; 44 | 45 | export function NewRepositoryDialog({ trigger }: NewRepositoryDialogProps) { 46 | const form = useForm({ 47 | resolver: zodResolver(newRepoSchema), 48 | defaultValues: { githubUrl: "" }, 49 | }); 50 | 51 | const searchParams = useSearchParams(); 52 | 53 | const { signIn } = useSignIn(); 54 | const auth = useAuth(); 55 | const ensureLogin = async (e?: MouseEvent) => { 56 | if (!auth.isLoaded) { 57 | e?.preventDefault(); 58 | return false; 59 | } 60 | if (!auth.isSignedIn) { 61 | e?.preventDefault(); 62 | await signIn?.authenticateWithRedirect({ 63 | strategy: "oauth_github", 64 | redirectUrl: "/sso-callback", 65 | redirectUrlComplete: "/?open_new=true", 66 | }); 67 | return false; 68 | } 69 | return true; 70 | }; 71 | 72 | const router = useRouter(); 73 | const onSubmit = async (data: NewRepoFormData) => { 74 | if (!ensureLogin()) return; 75 | 76 | const { owner, repo } = 77 | data.githubUrl.match(GITHUB_URL_REGEX)?.groups ?? {}; 78 | if (!owner || !repo) return; 79 | 80 | await fetch("/api/import", { 81 | method: "POST", 82 | headers: { 83 | "Content-Type": "application/json", 84 | }, 85 | body: JSON.stringify({ owner, repo }), 86 | }); 87 | router.push(`/repo/${owner}/${repo}`); 88 | }; 89 | 90 | return ( 91 | 92 | {trigger} 93 | 94 | 95 | New Repository 96 | 97 | Add a new repository. We might ask you to sign in to GitHub to 98 | proceed. Only PUBLIC repositories are supported. 99 | 100 | 101 |
102 | 103 | ( 107 | 108 | GitHub URL 109 | 110 | 111 | 112 | 113 | The GitHub URL of the repository you want to import. 114 | 115 | 116 | 117 | )} 118 | /> 119 |
120 | 121 |
122 | 123 | 124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /client-poc/components/flow/DatabaseNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Handle, Position } from 'reactflow' 3 | import { DatabaseIcon } from 'lucide-react' 4 | 5 | export function DatabaseNode({ data }: { data: { label: string } }) { 6 | return ( 7 |
8 | 9 | 10 |
11 | 12 |
{data.label}
13 |
14 |
15 | ) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /client-poc/components/flow/GitHubDataFlow.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useMemo } from "react"; 4 | import ReactFlow, { Node, Edge, NodeTypes } from "reactflow"; 5 | import "reactflow/dist/style.css"; 6 | import { GitHubNode } from "./GitHubNode"; 7 | import { DatabaseNode } from "./DatabaseNode"; 8 | import { SemanticLayerNode } from "./SemanticLayerNode"; 9 | import { QueryNode } from "./QueryNode"; 10 | import { useParams } from "next/navigation"; 11 | 12 | const nodeTypes: NodeTypes = { 13 | github: GitHubNode, 14 | database: DatabaseNode, 15 | semanticLayer: SemanticLayerNode, 16 | query: QueryNode, 17 | }; 18 | 19 | const initialNodes = (owner: string, repo: string, table: string): Node[] => [ 20 | { 21 | id: "1", 22 | type: "github", 23 | position: { x: 0, y: 0 }, 24 | data: { label: `${owner}/${repo}` }, 25 | }, 26 | { 27 | id: "2", 28 | type: "database", 29 | position: { x: 0, y: 76 }, 30 | data: { label: "PostgreSQL" }, 31 | }, 32 | { 33 | id: "3", 34 | type: "semanticLayer", 35 | position: { x: 0, y: 150 }, 36 | data: { 37 | label: ( 38 | <> 39 | {table} 40 |
41 | DuckDB Semantic Layer 42 | 43 | ), 44 | }, 45 | }, 46 | { 47 | id: "4", 48 | type: "query", 49 | position: { x: 0, y: 240 }, 50 | data: { label: "SQL Query" }, 51 | }, 52 | ]; 53 | 54 | const initialEdges: Edge[] = [ 55 | { id: "e1-2", source: "1", target: "2", animated: true }, 56 | { id: "e2-3", source: "2", target: "3", animated: true }, 57 | { id: "e3-4", source: "3", target: "4", animated: true }, 58 | ]; 59 | 60 | export default function GitHubDataFlow({ table }: { table: string }) { 61 | const { owner, repo } = useParams() as { owner: string; repo: string }; 62 | return ( 63 |
64 | initialNodes(owner, repo, table), 67 | [owner, repo, table] 68 | )} 69 | edges={initialEdges} 70 | nodeTypes={nodeTypes} 71 | panOnDrag={false} 72 | zoomOnScroll={false} 73 | zoomOnPinch={false} 74 | zoomOnDoubleClick={false} 75 | fitView 76 | fitViewOptions={{ padding: 0.1 }} 77 | > 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /client-poc/components/flow/GitHubNode.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Handle, Position } from "reactflow"; 3 | import { GithubIcon } from "lucide-react"; 4 | 5 | export function GitHubNode({ data }: { data: { label: string } }) { 6 | return ( 7 |
8 | 9 |
10 | 11 |
{data.label}
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client-poc/components/flow/QueryNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Handle, Position } from 'reactflow' 3 | import { SearchIcon } from 'lucide-react' 4 | 5 | export function QueryNode({ data }: { data: { label: string } }) { 6 | return ( 7 |
8 | 9 |
10 | 11 |
{data.label}
12 |
13 |
14 | ) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /client-poc/components/flow/SemanticLayerNode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Handle, Position } from 'reactflow' 3 | import { LayersIcon } from 'lucide-react' 4 | 5 | export function SemanticLayerNode({ data }: { data: { label: string } }) { 6 | return ( 7 |
8 | 9 | 10 |
11 | 12 |
{data.label}
13 |
14 |
15 | ) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /client-poc/components/tools/ChartToolUI.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { 5 | Area, 6 | AreaChart, 7 | Bar, 8 | BarChart, 9 | CartesianGrid, 10 | Line, 11 | LineChart, 12 | XAxis, 13 | YAxis, 14 | } from "recharts"; 15 | import { makeAssistantToolUI } from "@assistant-ui/react"; 16 | import { 17 | ChartContainer, 18 | ChartTooltip, 19 | ChartTooltipContent, 20 | ChartLegend, 21 | ChartLegendContent, 22 | } from "@/components/ui/chart"; 23 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 24 | import { MousePointerClickIcon } from "lucide-react"; 25 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 26 | import React from "react"; 27 | import GitHubDataFlow from "../flow/GitHubDataFlow"; 28 | import { 29 | Table, 30 | TableBody, 31 | TableCell, 32 | TableHead, 33 | TableHeader, 34 | TableRow, 35 | } from "@/components/ui/table"; 36 | 37 | const getColumns = (data: object[]) => { 38 | const [xAxis, ...yAxis] = data.reduce((acc, row) => { 39 | Object.keys(row).forEach((key) => { 40 | if (!acc.includes(key)) { 41 | acc.push(key); 42 | } 43 | }); 44 | return acc; 45 | }, []); 46 | 47 | if (!xAxis || !yAxis.length) { 48 | return null; 49 | } 50 | return { 51 | xAxis, 52 | yAxis, 53 | }; 54 | }; 55 | 56 | type ChartConfig = { 57 | rows: object[]; 58 | sql: string; 59 | type: "area" | "bar" | "line"; 60 | title: string; 61 | }; 62 | 63 | const toFirstLetterUpperCase = (str: string) => { 64 | return str.charAt(0).toUpperCase() + str.slice(1); 65 | }; 66 | 67 | const getChart = (type: "area" | "bar" | "line") => { 68 | switch (type) { 69 | case "area": 70 | return AreaChart; 71 | case "bar": 72 | return BarChart; 73 | case "line": 74 | return LineChart; 75 | default: 76 | const _exhaustiveCheck: never = type; 77 | throw new Error("Invalid chart type " + _exhaustiveCheck); 78 | } 79 | }; 80 | 81 | type SeriesProps = { 82 | dataKey: string; 83 | fill: string; 84 | }; 85 | 86 | const BarSeries: FC = ({ dataKey, fill }) => { 87 | return ; 88 | }; 89 | 90 | const AreaSeries: FC = ({ dataKey, fill }) => { 91 | return ( 92 | 100 | ); 101 | }; 102 | 103 | const LineSeries: FC = ({ dataKey, fill }) => { 104 | return ( 105 | 113 | ); 114 | }; 115 | 116 | const getChartSeries = (type: "area" | "bar" | "line") => { 117 | switch (type) { 118 | case "area": 119 | return AreaSeries; 120 | case "bar": 121 | return BarSeries; 122 | case "line": 123 | return LineSeries; 124 | default: 125 | const _exhaustiveCheck: never = type; 126 | throw new Error("Invalid chart type " + _exhaustiveCheck); 127 | } 128 | }; 129 | 130 | const formatXAxis = (tick: string) => { 131 | const date = new Date(tick); 132 | if (date.toString() !== "Invalid Date") { 133 | return date.toISOString().split("T")[0]; // This will return YYYY-MM-DD 134 | } 135 | return tick; 136 | }; 137 | 138 | const isValidDate = (date: string) => { 139 | const parsedDate = new Date(date); 140 | return !isNaN(parsedDate.getTime()); 141 | }; 142 | 143 | const MyChart: FC<{ config: ChartConfig }> = ({ config }) => { 144 | const columns = getColumns(config.rows); 145 | if (!columns) return null; 146 | const { xAxis, yAxis } = columns; 147 | 148 | const chartConfig = Object.fromEntries( 149 | yAxis.map((axis) => [axis, { label: toFirstLetterUpperCase(axis) }]) 150 | ); 151 | 152 | const Chart = getChart(config.type); 153 | const getSeries = getChartSeries(config.type); 154 | 155 | const sortedRows = config.rows.sort( 156 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 157 | (a: any, b: any) => { 158 | const isDate = isValidDate(a[xAxis]); 159 | if (!isDate) return 0; 160 | 161 | return a[xAxis] > b[xAxis] ? 1 : -1; 162 | } 163 | ); 164 | 165 | return ( 166 | 167 | 168 | 169 | formatXAxis(tick)} 172 | tickMargin={10} 173 | tickLine={false} 174 | axisLine={false} 175 | /> 176 | 177 | } /> 178 | } /> 179 | {yAxis.map((axis, idx) => 180 | getSeries({ 181 | dataKey: axis, 182 | fill: `hsl(var(--chart-${idx + 1}))`, 183 | }) 184 | )} 185 | 186 | 187 | ); 188 | }; 189 | 190 | function extractTableNameFromQuery(sql: string): string | null { 191 | // This regex looks for the word "FROM" (case-insensitive), 192 | // then captures one or more non-whitespace characters (the table name). 193 | // The 'i' flag makes it case-insensitive. 194 | const fromRegex = /\bfrom\s+([^\s]+)\b/i; 195 | 196 | const match = sql.match(fromRegex); 197 | return match ? match[1] : null; 198 | } 199 | 200 | const MyTable: FC<{ config: ChartConfig }> = ({ config }) => { 201 | return ( 202 | 203 | 204 | 205 | {Object.keys(config.rows[0] ?? {}).map((col) => ( 206 | {col} 207 | ))} 208 | 209 | 210 | 211 | {config.rows.map((row, idx) => { 212 | const key = idx; 213 | return ( 214 | 215 | {Object.entries(row).map(([col, val]) => ( 216 | {val} 217 | ))} 218 | 219 | ); 220 | })} 221 | 222 |
223 | ); 224 | }; 225 | 226 | const MyChartUI: FC<{ config: ChartConfig }> = ({ config }) => { 227 | return ( 228 | 229 | 230 | Chart 231 | Data 232 | SQL 233 | Data Flow 234 | 235 | 236 | 237 | {config.title} 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | {config.title} 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | {config.title} 254 | 255 |
256 |               {config.sql}
257 |             
258 |
259 |
260 |
261 | 262 | 263 | {config.title} 264 | 265 | 268 | 269 | 270 | 271 |
272 | ); 273 | }; 274 | 275 | export const ChartToolUI = makeAssistantToolUI< 276 | Record, 277 | ChartConfig 278 | >({ 279 | toolName: "chart", 280 | render: ({ result }) => { 281 | if (!result || typeof result !== "object") 282 | return ( 283 |
284 | 285 | Querying Relta... 286 |
287 | ); 288 | 289 | if (!result.rows.length) 290 | return ( 291 |
292 | 293 | Queried Relta - No data available 294 |
295 | ); 296 | 297 | return ; 298 | }, 299 | }); 300 | -------------------------------------------------------------------------------- /client-poc/components/tools/TextToolUI.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { makeAssistantToolUI } from "@assistant-ui/react"; 4 | import { MousePointerClickIcon } from "lucide-react"; 5 | 6 | export const TextToolUI = makeAssistantToolUI< 7 | Record, 8 | Record 9 | >({ 10 | toolName: "text", 11 | render: ({ result }) => { 12 | if (!result) 13 | return ( 14 |
15 | 16 | Querying Relta... 17 |
18 | ); 19 | 20 | return ( 21 |
22 | 23 | Queried Relta 24 |
25 | ); 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /client-poc/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /client-poc/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /client-poc/components/ui/chart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as RechartsPrimitive from "recharts"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | // Format: { THEME_NAME: CSS_SELECTOR } 9 | const THEMES = { light: "", dark: ".dark" } as const; 10 | 11 | export type ChartConfig = { 12 | [k in string]: { 13 | label?: React.ReactNode; 14 | icon?: React.ComponentType; 15 | } & ( 16 | | { color?: string; theme?: never } 17 | | { color?: never; theme: Record } 18 | ); 19 | }; 20 | 21 | type ChartContextProps = { 22 | config: ChartConfig; 23 | }; 24 | 25 | const ChartContext = React.createContext(null); 26 | 27 | function useChart() { 28 | const context = React.useContext(ChartContext); 29 | 30 | if (!context) { 31 | throw new Error("useChart must be used within a "); 32 | } 33 | 34 | return context; 35 | } 36 | 37 | const ChartContainer = React.forwardRef< 38 | HTMLDivElement, 39 | React.ComponentProps<"div"> & { 40 | config: ChartConfig; 41 | children: React.ComponentProps< 42 | typeof RechartsPrimitive.ResponsiveContainer 43 | >["children"]; 44 | } 45 | >(({ id, className, children, config, ...props }, ref) => { 46 | const uniqueId = React.useId(); 47 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; 48 | 49 | return ( 50 | 51 |
60 | 61 | 62 | {children} 63 | 64 |
65 |
66 | ); 67 | }); 68 | ChartContainer.displayName = "Chart"; 69 | 70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 71 | const colorConfig = Object.entries(config).filter( 72 | ([, config]) => config.theme || config.color 73 | ); 74 | 75 | if (!colorConfig.length) { 76 | return null; 77 | } 78 | 79 | return ( 80 |