├── .env.example ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── README.md ├── backend ├── .env.example ├── .gitignore ├── README.md ├── app │ ├── __init__.py │ ├── config.py │ ├── connectors │ │ ├── __init__.py │ │ ├── client │ │ │ ├── __init__.py │ │ │ ├── calendar.py │ │ │ ├── docs.py │ │ │ ├── gmail.py │ │ │ ├── linear.py │ │ │ ├── sheets.py │ │ │ ├── slack.py │ │ │ └── x.py │ │ ├── native │ │ │ ├── __init__.py │ │ │ ├── orm.py │ │ │ ├── stores │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── feedback.py │ │ │ │ ├── message.py │ │ │ │ ├── token.py │ │ │ │ └── user.py │ │ │ └── utils.py │ │ └── orm.py │ ├── controllers │ │ ├── __init__.py │ │ ├── feedback.py │ │ ├── query.py │ │ ├── token.py │ │ └── user.py │ ├── exceptions │ │ ├── __init__.py │ │ └── exception.py │ ├── main.py │ ├── middleware.py │ ├── models │ │ ├── __init__.py │ │ ├── agents │ │ │ ├── __init__.py │ │ │ ├── base │ │ │ │ ├── __init__.py │ │ │ │ ├── summary.py │ │ │ │ ├── template.py │ │ │ │ └── triage.py │ │ │ ├── calendar.py │ │ │ ├── docs.py │ │ │ ├── gmail.py │ │ │ ├── linear.py │ │ │ ├── main.py │ │ │ ├── sheets.py │ │ │ ├── slack.py │ │ │ └── x.py │ │ ├── feedback.py │ │ ├── integrations │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── calendar.py │ │ │ ├── docs.py │ │ │ ├── gmail.py │ │ │ ├── linear.py │ │ │ ├── sheets.py │ │ │ ├── slack.py │ │ │ └── x.py │ │ ├── query │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── confirm.py │ │ ├── token.py │ │ └── user │ │ │ ├── __init__.py │ │ │ └── login.py │ ├── sandbox │ │ ├── __init__.py │ │ └── integrations │ │ │ ├── __init__.py │ │ │ ├── g_calendar.py │ │ │ ├── g_docs.py │ │ │ ├── g_sheets.py │ │ │ ├── gmail.py │ │ │ ├── linear.py │ │ │ ├── slack.py │ │ │ └── x.py │ ├── services │ │ ├── __init__.py │ │ ├── feedback.py │ │ ├── message.py │ │ ├── query.py │ │ ├── token.py │ │ └── user.py │ └── utils │ │ ├── __init__.py │ │ ├── levenshtein.py │ │ └── tools.py ├── docker │ └── development │ │ └── Dockerfile ├── images │ ├── __init__.py │ ├── supabase_connect.png │ ├── supabase_copy_uri.png │ ├── supabase_create_project.png │ └── supabase_transaction_mode.png ├── poetry.lock ├── pyproject.toml └── supabase_schema.sql ├── docker-compose.yml └── frontend ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .jest ├── jest.setup.ts └── setEnvVars.ts ├── README.md ├── components.json ├── docker └── development │ └── Dockerfile ├── images └── clerk_environment_variables.png ├── jest.config.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── assistant.png └── placeholder.png ├── src ├── actions │ ├── feedback │ │ └── submit.ts │ ├── query │ │ ├── base.ts │ │ └── confirm.ts │ ├── token.ts │ └── user │ │ └── login.ts ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── (home) │ │ └── page.tsx │ ├── api │ │ └── oauth2 │ │ │ ├── callback │ │ │ └── route.ts │ │ │ └── login │ │ │ └── route.ts │ ├── apple-icon.png │ ├── chat │ │ └── page.tsx │ ├── favicon.ico │ ├── icon.png │ ├── layout.tsx │ └── not-found.tsx ├── components │ ├── accessory │ │ ├── loader.tsx │ │ └── shimmer.tsx │ ├── api-key.tsx │ ├── dialog-content │ │ ├── auth-base.tsx │ │ ├── calendar.tsx │ │ ├── docs.tsx │ │ ├── gmail.tsx │ │ ├── linear.tsx │ │ ├── outlook.tsx │ │ ├── routing-base.tsx │ │ ├── sheets.tsx │ │ ├── slack.tsx │ │ └── x.tsx │ ├── home │ │ ├── chat │ │ │ ├── clear-button.tsx │ │ │ ├── container.tsx │ │ │ └── verification-checkbox.tsx │ │ ├── input-container.tsx │ │ ├── input │ │ │ └── verification-option.tsx │ │ └── integration-icon.tsx │ ├── integration-auth.tsx │ ├── shared │ │ ├── header │ │ │ ├── buttons.tsx │ │ │ ├── feedback │ │ │ │ ├── button.tsx │ │ │ │ └── form.tsx │ │ │ └── navigation.tsx │ │ ├── page-loading-indicator.tsx │ │ ├── query-provider.tsx │ │ └── theme │ │ │ ├── provider.tsx │ │ │ └── toggle.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── scroll-area.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── constants │ ├── keys.ts │ └── route.ts ├── lib │ └── utils.ts ├── middleware.ts ├── styles │ └── globals.css └── types │ ├── actions │ ├── feedback │ │ └── form.ts │ ├── query │ │ ├── base.ts │ │ └── confirm.ts │ ├── token.ts │ └── user │ │ └── login.ts │ ├── api │ └── token.ts │ ├── integration.ts │ └── store │ ├── base.ts │ └── integrations.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Ensures that the auto-complete python import paths are correct 2 | PYTHONPATH=./backend -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | backend-CI: 9 | name: Backend CI Checks 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 3.12.1 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.12.1' 20 | 21 | - name: Install Poetry 22 | run: | 23 | curl -sSL https://install.python-poetry.org | python3 - 24 | 25 | - name: Install dependencies 26 | working-directory: ./backend 27 | run: | 28 | poetry install 29 | 30 | - name: Run isort 31 | working-directory: ./backend 32 | run: | 33 | poetry run isort --check-only --diff . 34 | 35 | - name: Run black 36 | working-directory: ./backend 37 | run: | 38 | poetry run black --check --diff . 39 | 40 | frontend-CI: 41 | name: Frontend CI Checks 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - name: Checkout Repository 46 | uses: actions/checkout@v4 47 | 48 | - name: Setup Node.js 49 | uses: actions/setup-node@v3 50 | with: 51 | node-version: "20" 52 | 53 | - name: Install dependencies 54 | working-directory: ./frontend 55 | run: npm install 56 | 57 | - name: Run ESLint 58 | working-directory: ./frontend 59 | run: npx eslint . --ext .js,.jsx,.ts,.tsx 60 | 61 | - name: Run Prettier 62 | working-directory: ./frontend 63 | run: npx prettier --check . 64 | 65 | - name: Type check 66 | working-directory: ./frontend 67 | run: npx tsc --noEmit 68 | 69 | - name: Build UI 70 | working-directory: ./frontend 71 | run: npm run build 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Controller 2 | 3 | **[Controller](https://usecontroller.com/)** is the Open-source ChatGPT that interacts with all your third-party applications! It serves as a Unified Platform across your tools such as Slack, Linear, Google Suite, etc. 4 | 5 | 1. You can talk to a single application (e.g. "Get me all linear tickets that are owned by Mike, and set me as the owner") 6 | 7 | 2. Talk across your applications (e.g. "Get me all emails sent by Mike that are about user feedback, and msg him on Slack and Discord about the feedback from users") 8 | 9 | 3. Integrate with multiple applications 10 | 11 | ## Super quick demo 12 | https://github.com/user-attachments/assets/28894a96-19c3-4216-9f8e-a54c38567eee 13 | 14 | ## Getting Started locally 15 | 16 | 1. Follow the README instructions in the [`frontend`](./frontend/README.md) and [`backend`](./backend/README.md) folders (You have to set up a few environment variables) 17 | 18 | 2. Make sure [Docker Desktop](https://www.docker.com/products/docker-desktop/) is installed and running 19 | 3. Build the docker container to start the project 20 | 21 | ```bash 22 | docker compose up --build 23 | ``` 24 | 25 | 3. Go to `localhost:3000` to start Controller! 26 | 27 | - **IMPORTANT**: If you find yourself stuck at the loading screen, try refreshing the page. This is a known issue as our code is not bundling optimally (we are figuring out a fix for it right now!) 28 | 29 | ## Self-Hosting 30 | 31 | We recommend hosting `frontend` on Vercel and `backend` on AWS! 32 | 33 | ## Useful commands for Development (Not necessary unless you want to contribute) 34 | Copy the existing environment template file 35 | ```bash 36 | # Create .env file (by copying from .env.example) 37 | cp .env.example .env 38 | ``` 39 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # No quotation marks 2 | OPENAI_API_KEY= 3 | DATABASE_URL= 4 | OPENAI_BASE_URL=https://api.openai.com/v1 -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | main.cpython-312.pyc 6 | models.cpython-312.pyc 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | celerybeat.pid 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .envrc 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | .myenv 102 | myenv/ 103 | myenv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | .dmypy.json 118 | dmypy.json 119 | 120 | # Pyre type checker 121 | .pyre/ 122 | 123 | # profiling files 124 | .prof 125 | 126 | # vscode 127 | .vscode/ 128 | 129 | # pytest 130 | .pytest_cache/ 131 | 132 | # Editors and IDEs 133 | .idea/ 134 | *.swp 135 | *.swo 136 | *.sublime-workspace 137 | 138 | # Operating System Files 139 | .DS_Store 140 | Thumbs.db 141 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | ## Getting Started 4 | 5 | Make sure your terminal is at the root of backend 6 | 7 | ### Using [Poetry](https://python-poetry.org/) for dependency management. 8 | 9 | ```bash 10 | # Install Poetry (If using Windows, use WSL) 11 | curl -sSL https://install.python-poetry.org | python3 - 12 | ``` 13 | 14 | You will need to set poetry in your `PATH` environment variable. See [Step 3](https://python-poetry.org/docs/#installing-with-the-official-installer). 15 | 16 | Install all dependencies 17 | 18 | ```bash 19 | # Install dependencies 20 | poetry install 21 | ``` 22 | 23 | ```bash 24 | # Update/upgrade dependencies 25 | poetry update 26 | ``` 27 | 28 | ```bash 29 | # Activate Python virtual environment 30 | poetry shell 31 | ``` 32 | 33 | ### Copy the existing environment template file 34 | 35 | ```bash 36 | # Create .env file (by copying from .env.example) 37 | 38 | # MacOS/Linux 39 | cp .env.example .env 40 | 41 | # Windows 42 | copy .env.example .env 43 | ``` 44 | 45 | ### Set up [Supabase](https://supabase.com/) 46 | 47 | 1. Create a project (Note down your project's password) 48 | ![Create Project](./images/supabase_create_project.png) 49 | 50 | 2. Click on the `Connect` button (It might take 1-2mins for the button to appear as Supabase needs to create your DB) 51 | ![Connect to Supabase](./images/supabase_connect.png) 52 | 53 | 3. Set the mode from `transaction` to `session` 54 | ![Set transaction mode](./images/supabase_transaction_mode.png) 55 | 56 | 4. Copy the URI (You will need it for step 5 and step 8) 57 | ![Copy URI](./images/supabase_copy_uri.png) 58 | 59 | 5. Run `psql ""` (Remember to put the password in place of [YOUR PASSWORD] in the URI) in your terminal at the root of `backend` 60 | 61 | ```bash 62 | psql "postgresql://postgres.jcmefqghesrtgvxzsvak:grae!bfSB@aws-0-us-west-7.pooler.supabase.com:5432/postgres" 63 | ``` 64 | 65 | **IMPORTANT: You MUST have the inverted commas around the URI. Do not include the square brackets around your password in the URI.** 66 | 67 | 6. Run `\i ./supabase_schema.sql` in the same terminal instance at the root of `backend` 68 | 7. Close this terminal -> You are done! 69 | 8. Paste the copied URI (Remember to put the password in) into the DATABASE_URL `.env` variable. It should look like `DATABASE_URL=` with **no inverted commas** around the URI. 70 | 9. Replace the **prefix** of the copied URI from `postgresql://` to `postgresql+asyncpg://` 71 | 72 | ```bash 73 | e.g. postgresql+asyncpg://postgres.jcmefqghesrtgvxzsvak:grae!bfSB@aws-0-us-west-7.pooler.supabase.com:5432/postgres 74 | ``` 75 | 76 | ### Get your [OpenAI](https://platform.openai.com/api-keys) api key 77 | 78 | 1. Paste the api key into the OPENAI_API_KEY `.env` variable. It should look like `OPENAI_API_KEY=` with **no inverted commas** around the api key. 79 | 80 | 2. If you have a custom OpenAI compatible backend, you can change the default ```OPENAI_BASE_URL``` to use it. 81 | 82 | ### Useful commands for Development (Not necessary unless you're a Chad and want to contribute) 83 | 84 | #### Start the server (locally) 85 | 86 | ```bash 87 | uvicorn app.main:app --reload --host 0.0.0.0 --port 8080 88 | ``` 89 | 90 | #### Check style 91 | 92 | Run the following command at the root of `backend` 93 | 94 | ```bash 95 | # Style check 96 | black . 97 | 98 | # Import check 99 | isort . 100 | ``` 101 | -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/__init__.py -------------------------------------------------------------------------------- /backend/app/config.py: -------------------------------------------------------------------------------- 1 | OPENAI_GPT4O_MINI = "gpt-4o-mini" 2 | -------------------------------------------------------------------------------- /backend/app/connectors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/connectors/__init__.py -------------------------------------------------------------------------------- /backend/app/connectors/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/connectors/client/__init__.py -------------------------------------------------------------------------------- /backend/app/connectors/client/sheets.py: -------------------------------------------------------------------------------- 1 | from google.oauth2.credentials import Credentials 2 | from googleapiclient.discovery import build 3 | 4 | from app.models.integrations.sheets import SheetsGetRequest 5 | 6 | TOKEN_URI = "https://oauth2.googleapis.com/token" 7 | 8 | 9 | class GoogleSheetsClient: 10 | 11 | def __init__( 12 | self, access_token: str, refresh_token: str, client_id: str, client_secret: str 13 | ): 14 | self.service = build( 15 | "sheets", 16 | "v4", 17 | credentials=Credentials( 18 | token=access_token, 19 | refresh_token=refresh_token, 20 | client_id=client_id, 21 | client_secret=client_secret, 22 | token_uri=TOKEN_URI, 23 | ), 24 | ) 25 | 26 | def read_sheet(self, request: SheetsGetRequest): 27 | sheet = self.service.spreadsheets() 28 | result = ( 29 | sheet.values() 30 | .get(spreadsheetId=request.spreadsheet_id, range=request.sheet_name) 31 | .execute() 32 | ) 33 | return result.get("values", []) 34 | 35 | # def write_sheet(self, range_name, values): 36 | # body = {"values": values} 37 | # sheet = self.service.spreadsheets() 38 | # result = ( 39 | # sheet.values() 40 | # .update( 41 | # spreadsheetId=self.spreadsheet_id, 42 | # range=range_name, 43 | # valueInputOption="RAW", 44 | # body=body, 45 | # ) 46 | # .execute() 47 | # ) 48 | # return result 49 | 50 | # def append_sheet(self, range_name, values): 51 | # body = {"values": values} 52 | # sheet = self.service.spreadsheets() 53 | # result = ( 54 | # sheet.values() 55 | # .append( 56 | # spreadsheetId=self.spreadsheet_id, 57 | # range=range_name, 58 | # valueInputOption="RAW", 59 | # body=body, 60 | # ) 61 | # .execute() 62 | # ) 63 | # return result 64 | -------------------------------------------------------------------------------- /backend/app/connectors/client/slack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | from slack_sdk.web.async_client import AsyncWebClient 5 | 6 | from app.models.integrations.slack import ( 7 | SlackGetChannelIdRequest, 8 | SlackSendMessageRequest, 9 | ) 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class SlackClient: 17 | def __init__(self, access_token: str): 18 | self.client = AsyncWebClient(token=access_token) 19 | 20 | async def get_all_channel_ids( 21 | self, request: SlackGetChannelIdRequest 22 | ) -> list[dict[str, Any]]: 23 | response = await self.client.conversations_list() 24 | channels = response["channels"] 25 | request_channel_names_set: set[str] = { 26 | name.lower() for name in request.channel_names 27 | } 28 | channel_info = [ 29 | {"channel_name": channel["name"], "channel_id": channel["id"]} 30 | for channel in channels 31 | if channel["name"].lower() 32 | in request_channel_names_set # Slack channel names are always lower case 33 | ] 34 | return channel_info 35 | 36 | async def send_message(self, request: SlackSendMessageRequest): 37 | response = await self.client.chat_postMessage( 38 | channel=request.channel_id, text=request.text 39 | ) 40 | return response 41 | -------------------------------------------------------------------------------- /backend/app/connectors/client/x.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | 4 | import tweepy 5 | 6 | from app.models.integrations.x import Tweet, XSendTweetRequest 7 | 8 | 9 | class XClient: 10 | def __init__(self, access_token: str): 11 | self.client = tweepy.Client(bearer_token=access_token) 12 | 13 | async def send_tweet(self, request: XSendTweetRequest) -> Tweet: 14 | loop = asyncio.get_event_loop() 15 | create_tweet_partial = partial( 16 | self.client.create_tweet, text=request.text, user_auth=False 17 | ) 18 | response = await loop.run_in_executor(None, create_tweet_partial) 19 | return Tweet.model_validate(response.data) 20 | 21 | # def get_user_tweets(self, user_id: str, max_results: int = 10): 22 | # return self.client.get_users_tweets(user_id, max_results=max_results) 23 | 24 | # def get_tweets_past_hour(self): 25 | # one_hour_ago = datetime.now(UTC) - timedelta(hours=1) 26 | # one_hour_ago_str = one_hour_ago.strftime("%Y-%m-%dT%H:%M:%SZ") 27 | 28 | # tweets = self.client.search_recent_tweets( 29 | # user_auth=False, 30 | # query="*", 31 | # ) 32 | # return tweets 33 | -------------------------------------------------------------------------------- /backend/app/connectors/native/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/connectors/native/__init__.py -------------------------------------------------------------------------------- /backend/app/connectors/native/orm.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/connectors/native/orm.py -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/connectors/native/stores/__init__.py -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/base.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class BaseObject(BaseModel): 7 | 8 | class Config: 9 | orm_mode = True 10 | from_attributes = True 11 | 12 | @staticmethod 13 | def generate_id( 14 | **kwargs, 15 | ) -> str: 16 | for k, v in kwargs.items(): 17 | if v is None: 18 | raise Exception(f"Cannot generate id with None value for key {k}") 19 | 20 | return str( 21 | uuid.uuid3(uuid.NAMESPACE_DNS, "-".join([str(v) for v in kwargs.values()])) 22 | ) 23 | -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/feedback.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel 6 | from sqlalchemy import Column, DateTime, Integer, String 7 | from sqlalchemy.orm import declarative_base 8 | from sqlalchemy.sql import func 9 | 10 | from app.connectors.native.utils import sql_value_to_typed_value 11 | 12 | log = logging.getLogger(__name__) 13 | logging.basicConfig(level=logging.INFO) 14 | 15 | Base = declarative_base() 16 | 17 | 18 | class FeedbackORM(Base): 19 | __tablename__ = "feedback" 20 | 21 | id = Column(Integer, primary_key=True, autoincrement=True) 22 | user_id = Column(String, nullable=True) 23 | feedback = Column(String, nullable=False) 24 | created_at = Column(DateTime(timezone=True), nullable=False, default=func.now()) 25 | updated_at = Column( 26 | DateTime(timezone=True), nullable=False, default=func.now(), onupdate=func.now() 27 | ) 28 | 29 | 30 | class Feedback(BaseModel): 31 | id: Optional[int] = None 32 | user_id: Optional[str] 33 | feedback: str 34 | created_at: Optional[datetime] = None 35 | updated_at: Optional[datetime] = None 36 | 37 | @classmethod 38 | def local(cls, user_id: Optional[str], feedback: str): 39 | return Feedback( 40 | user_id=user_id, 41 | feedback=feedback, 42 | ) 43 | 44 | @classmethod 45 | def remote( 46 | cls, 47 | **kwargs, 48 | ): 49 | return cls( 50 | id=sql_value_to_typed_value(dict=kwargs, key="id", type=int), 51 | user_id=sql_value_to_typed_value(dict=kwargs, key="user_id", type=str), 52 | feedback=sql_value_to_typed_value(dict=kwargs, key="feedback", type=str), 53 | ) 54 | -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/message.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import uuid 3 | from datetime import datetime 4 | from typing import Optional 5 | 6 | from pydantic import BaseModel 7 | from sqlalchemy import ARRAY, JSON, UUID, Column, DateTime, Integer, String 8 | from sqlalchemy.orm import declarative_base 9 | from sqlalchemy.sql import func 10 | 11 | from app.connectors.native.utils import sql_value_to_typed_value 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class MessageORM(Base): 17 | __tablename__ = "message" 18 | 19 | id = Column(Integer, primary_key=True, autoincrement=True) 20 | api_key = Column(UUID, nullable=False) 21 | integrations = Column(ARRAY(String), nullable=False) 22 | chat_history = Column(ARRAY(JSON), nullable=False) 23 | instance = Column(UUID, nullable=False) 24 | created_at = Column( 25 | DateTime, nullable=False, default=func.now() 26 | ) # Automatically use the current timestamp of the database server upon creation 27 | updated_at = Column( 28 | DateTime, nullable=False, default=func.now(), onupdate=func.now() 29 | ) # Automatically use the current timestamp of the database server upon creation and update 30 | 31 | 32 | class Message(BaseModel): 33 | id: Optional[int] = None 34 | api_key: str 35 | integrations: list[str] 36 | chat_history: list[dict] 37 | instance: str 38 | created_at: Optional[datetime] = None 39 | updated_at: Optional[datetime] = None 40 | 41 | @classmethod 42 | def local( 43 | cls, 44 | chat_history: list[dict], 45 | api_key: str, 46 | integrations: list[str], 47 | instance: Optional[str], 48 | ): 49 | if not instance: 50 | data = f"{chat_history}{api_key}{integrations}{datetime.now().isoformat()}" 51 | instance = str( 52 | uuid.uuid5( 53 | uuid.NAMESPACE_DNS, hashlib.sha256(data.encode()).hexdigest() 54 | ) 55 | ) 56 | return Message( 57 | api_key=api_key, 58 | integrations=integrations, 59 | chat_history=chat_history, 60 | instance=instance, 61 | ) 62 | 63 | @classmethod 64 | def remote( 65 | cls, 66 | **kwargs, 67 | ): 68 | return cls( 69 | id=sql_value_to_typed_value(dict=kwargs, key="id", type=int), 70 | api_key=sql_value_to_typed_value(dict=kwargs, key="api_key", type=str), 71 | integrations=sql_value_to_typed_value( 72 | dict=kwargs, key="integrations", type=list 73 | ), 74 | chat_history=sql_value_to_typed_value( 75 | dict=kwargs, key="chat_history", type=list 76 | ), 77 | instance=sql_value_to_typed_value(dict=kwargs, key="instance", type=str), 78 | created_at=sql_value_to_typed_value( 79 | dict=kwargs, key="created_at", type=datetime 80 | ), 81 | updated_at=sql_value_to_typed_value( 82 | dict=kwargs, key="updated_at", type=datetime 83 | ), 84 | ) 85 | -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/token.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel 5 | from sqlalchemy import UUID, Column, DateTime, Integer, MetaData, String 6 | from sqlalchemy.orm import declarative_base, declared_attr 7 | from sqlalchemy.sql import func 8 | 9 | from app.connectors.native.utils import sql_value_to_typed_value 10 | 11 | metadata = MetaData() 12 | Base = declarative_base(metadata=metadata) 13 | 14 | 15 | class TokenORMBase: 16 | id = Column(Integer, primary_key=True, autoincrement=True) 17 | api_key = Column(UUID, nullable=False) 18 | access_token = Column(String, nullable=False) 19 | refresh_token = Column(String, nullable=True) 20 | client_id = Column(String, nullable=False) 21 | client_secret = Column(String, nullable=False) 22 | created_at = Column( 23 | DateTime, nullable=False, default=func.now() 24 | ) # Automatically use the current timestamp of the database server upon creation 25 | updated_at = Column( 26 | DateTime, nullable=False, default=func.now(), onupdate=func.now() 27 | ) # Automatically use the current timestamp of the database server upon creation and update 28 | 29 | @declared_attr 30 | def __tablename__(cls): 31 | return cls.TABLE_NAME 32 | 33 | 34 | # Dictionary to store created classes 35 | integration_orm_classes = {} 36 | 37 | 38 | def create_integration_orm(table_name): 39 | if table_name in integration_orm_classes: 40 | return integration_orm_classes[table_name] 41 | 42 | class_name = f"IntegrationORM_{table_name}" 43 | 44 | # Check if the table already exists in the metadata 45 | if table_name in metadata.tables: 46 | # If it exists, return the existing class 47 | return integration_orm_classes[table_name] 48 | 49 | # If it doesn't exist, create a new class 50 | new_class = type(class_name, (TokenORMBase, Base), {"TABLE_NAME": table_name}) 51 | 52 | # Store the new class in our dictionary 53 | integration_orm_classes[table_name] = new_class 54 | 55 | return new_class 56 | 57 | 58 | class Token(BaseModel): 59 | id: Optional[int] = None 60 | api_key: str 61 | access_token: str 62 | refresh_token: Optional[str] 63 | client_id: str 64 | client_secret: str 65 | created_at: Optional[datetime] = None 66 | updated_at: Optional[datetime] = None 67 | 68 | @classmethod 69 | def local( 70 | cls, 71 | api_key: str, 72 | access_token: str, 73 | refresh_token: Optional[str], 74 | client_id: str, 75 | client_secret: str, 76 | ): 77 | return Token( 78 | api_key=api_key, 79 | access_token=access_token, 80 | refresh_token=refresh_token, 81 | client_id=client_id, 82 | client_secret=client_secret, 83 | ) 84 | 85 | @classmethod 86 | def remote( 87 | cls, 88 | **kwargs, 89 | ): 90 | return cls( 91 | id=sql_value_to_typed_value(dict=kwargs, key="id", type=int), 92 | api_key=sql_value_to_typed_value(dict=kwargs, key="api_key", type=str), 93 | access_token=sql_value_to_typed_value( 94 | dict=kwargs, key="access_token", type=str 95 | ), 96 | refresh_token=sql_value_to_typed_value( 97 | dict=kwargs, key="refresh_token", type=str 98 | ), 99 | client_id=sql_value_to_typed_value(dict=kwargs, key="client_id", type=str), 100 | client_secret=sql_value_to_typed_value( 101 | dict=kwargs, key="client_secret", type=str 102 | ), 103 | created_at=sql_value_to_typed_value( 104 | dict=kwargs, key="created_at", type=datetime 105 | ), 106 | updated_at=sql_value_to_typed_value( 107 | dict=kwargs, key="updated_at", type=datetime 108 | ), 109 | ) 110 | -------------------------------------------------------------------------------- /backend/app/connectors/native/stores/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from sqlalchemy import UUID, Column, DateTime, Integer, String 5 | from sqlalchemy.orm import declarative_base 6 | from sqlalchemy.sql import func 7 | 8 | from app.connectors.native.stores.base import BaseObject 9 | from app.connectors.native.utils import sql_value_to_typed_value 10 | 11 | Base = declarative_base() 12 | 13 | 14 | class UserORM(Base): 15 | __tablename__ = "user" 16 | 17 | id = Column(String, primary_key=True) 18 | name = Column(String, nullable=False) 19 | email = Column(String, nullable=False) 20 | usage = Column(Integer, nullable=False) 21 | api_key = Column(UUID, nullable=False) 22 | created_at = Column( 23 | DateTime, nullable=False, default=func.now() 24 | ) # Automatically use the current timestamp of the database server upon creation 25 | updated_at = Column( 26 | DateTime, nullable=False, default=func.now(), onupdate=func.now() 27 | ) # Automatically use the current timestamp of the database server upon creation and update 28 | 29 | 30 | class User(BaseObject): 31 | id: Optional[str] = None 32 | name: str 33 | email: str 34 | usage: int = 0 35 | api_key: str 36 | created_at: Optional[datetime] = None 37 | updated_at: Optional[datetime] = None 38 | 39 | @classmethod 40 | def local( 41 | cls, 42 | id: str, 43 | name: str, 44 | email: str, 45 | usage: int, 46 | ): 47 | return User( 48 | id=id, 49 | name=name, 50 | email=email, 51 | usage=usage, 52 | api_key=cls.generate_id(id=id, name=name, email=email), 53 | ) 54 | 55 | @classmethod 56 | def remote( 57 | cls, 58 | **kwargs, 59 | ): 60 | return cls( 61 | id=sql_value_to_typed_value(dict=kwargs, key="id", type=str), 62 | name=sql_value_to_typed_value(dict=kwargs, key="name", type=str), 63 | email=sql_value_to_typed_value(dict=kwargs, key="email", type=str), 64 | usage=sql_value_to_typed_value(dict=kwargs, key="usage", type=int), 65 | api_key=sql_value_to_typed_value(dict=kwargs, key="api_key", type=str), 66 | created_at=sql_value_to_typed_value( 67 | dict=kwargs, key="created_at", type=datetime 68 | ), 69 | updated_at=sql_value_to_typed_value( 70 | dict=kwargs, key="updated_at", type=datetime 71 | ), 72 | ) 73 | -------------------------------------------------------------------------------- /backend/app/connectors/native/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | 5 | def sql_value_to_typed_value( 6 | dict: dict, 7 | key: str, 8 | type: type, 9 | ) -> str: 10 | value = dict.get(key) if key in dict else None 11 | 12 | if value is None: 13 | return None 14 | 15 | if type is str: 16 | return str(value) 17 | elif type is int: 18 | return int(value) 19 | elif type is datetime: 20 | return datetime.strptime(str(value), "%Y-%m-%d %H:%M:%S") 21 | elif type is bool: 22 | return bool(value) 23 | elif type is float: 24 | return float(value) 25 | elif type is List[str]: 26 | return [str(v) for v in value.split(",")] 27 | elif type is List[int]: 28 | return [int(v) for v in value.split(",")] 29 | else: 30 | raise ValueError(f"Unknown type: {type(value)}") 31 | 32 | 33 | def generate_identifier(value: str) -> str: 34 | now = datetime.now() 35 | timestamp = now.strftime("%Y%m%d%H%M%S") 36 | unique_identifier = f"{value}_{timestamp}" 37 | return unique_identifier 38 | -------------------------------------------------------------------------------- /backend/app/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/controllers/__init__.py -------------------------------------------------------------------------------- /backend/app/controllers/feedback.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import APIRouter, HTTPException 4 | from fastapi.responses import JSONResponse 5 | from pydantic import ValidationError 6 | 7 | from app.exceptions.exception import DatabaseError 8 | from app.models.feedback import FeedbackRequest 9 | from app.services.feedback import FeedbackService 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | router = APIRouter() 14 | 15 | 16 | class FeedbackController: 17 | 18 | def __init__(self, service: FeedbackService): 19 | self.router = APIRouter() 20 | self.service = service 21 | self.setup_routes() 22 | 23 | def setup_routes(self): 24 | router = self.router 25 | 26 | @router.post("") 27 | async def post(input: FeedbackRequest) -> JSONResponse: 28 | try: 29 | await self.service.post( 30 | id=input.id, 31 | feedback=input.feedback, 32 | ) 33 | except ValidationError as e: 34 | log.error("Validation error in feedback controller: %s", str(e)) 35 | raise HTTPException(status_code=422, detail="Validation error") from e 36 | except DatabaseError as e: 37 | log.error("Database error in feedback controller: %s", str(e)) 38 | raise HTTPException(status_code=500, detail="Database error") from e 39 | except Exception as e: 40 | log.error("Unexpected error in feedback controller.py: %s", str(e)) 41 | raise HTTPException( 42 | status_code=500, detail="An unexpected error occurred" 43 | ) from e 44 | -------------------------------------------------------------------------------- /backend/app/controllers/query.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import APIRouter, HTTPException, Request 4 | from fastapi.responses import JSONResponse 5 | from pydantic import ValidationError 6 | 7 | from app.models.query.base import QueryRequest, QueryResponse 8 | from app.models.query.confirm import ConfirmRequest 9 | from app.services.query import QueryService 10 | 11 | logging.basicConfig(level=logging.INFO) 12 | log = logging.getLogger(__name__) 13 | 14 | router = APIRouter() 15 | 16 | 17 | class QueryController: 18 | 19 | def __init__(self, service: QueryService): 20 | self.router = APIRouter() 21 | self.service = service 22 | self.setup_routes() 23 | 24 | def setup_routes(self): 25 | 26 | router = self.router 27 | 28 | @router.post("") 29 | async def query(request: Request) -> JSONResponse: 30 | input = QueryRequest.model_validate(await request.json()) 31 | try: 32 | response: QueryResponse = await self.service.query( 33 | message=input.message, 34 | chat_history=input.chat_history, 35 | api_key=input.api_key, 36 | integrations=input.integrations, 37 | instance=input.instance, 38 | enable_verification=input.enable_verification, 39 | ) 40 | return JSONResponse( 41 | status_code=200, 42 | content=response.model_dump(), 43 | ) 44 | except ValidationError as e: 45 | log.error( 46 | "Validation error in query controller for general query endpoint: %s", 47 | str(e), 48 | ) 49 | raise HTTPException(status_code=422, detail="Validation error") from e 50 | except Exception as e: 51 | log.error( 52 | "Unexpected error in query controller for general query endpoint: %s", 53 | str(e), 54 | ) 55 | raise HTTPException( 56 | status_code=500, 57 | detail="An unexpected error occurred in query controller for general query endpoint", 58 | ) from e 59 | 60 | @router.post("/confirm") 61 | async def confirm(input: ConfirmRequest) -> JSONResponse: 62 | try: 63 | response: QueryResponse = await self.service.confirm( 64 | chat_history=input.chat_history, 65 | api_key=input.api_key, 66 | enable_verification=input.enable_verification, 67 | integrations=input.integrations, 68 | function_to_verify=input.function_to_verify, 69 | instance=input.instance, 70 | ) 71 | 72 | return JSONResponse( 73 | status_code=200, 74 | content=response.model_dump(), 75 | ) 76 | except ValidationError as e: 77 | log.error( 78 | "Validation error in query controller for /confirm endpoint: %s", 79 | str(e), 80 | ) 81 | raise HTTPException(status_code=422, detail="Validation error") from e 82 | except Exception as e: 83 | log.error( 84 | "Unexpected error in query controller for /confirm endpoint: %s", 85 | str(e), 86 | ) 87 | raise HTTPException( 88 | status_code=500, 89 | detail="An unexpected error occurred in query controller for /confirm endpoint", 90 | ) from e 91 | -------------------------------------------------------------------------------- /backend/app/controllers/token.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from fastapi import APIRouter, HTTPException 5 | from fastapi.responses import JSONResponse 6 | from pydantic import ValidationError 7 | 8 | from app.connectors.native.stores.token import Token 9 | from app.exceptions.exception import DatabaseError 10 | from app.models.token import TokenGetResponse, TokenPostRequest 11 | from app.services.token import TokenService 12 | 13 | logging.basicConfig(level=logging.INFO) 14 | log = logging.getLogger(__name__) 15 | 16 | router = APIRouter() 17 | 18 | 19 | class TokenController: 20 | 21 | def __init__(self, service: TokenService): 22 | self.router = APIRouter() 23 | self.service = service 24 | self.setup_routes() 25 | 26 | def setup_routes(self): 27 | 28 | router = self.router 29 | 30 | @router.post("") 31 | async def authenticate(input: TokenPostRequest) -> JSONResponse: 32 | try: 33 | result: Optional[Token] = await self.service.get( 34 | api_key=input.api_key, table_name=input.table_name 35 | ) 36 | if result: 37 | await self.service.update( 38 | id=result.id, 39 | access_token=input.access_token, 40 | refresh_token=input.refresh_token, 41 | client_id=input.client_id, 42 | client_secret=input.client_secret, 43 | table_name=input.table_name, 44 | ) 45 | else: 46 | await self.service.post( 47 | api_key=input.api_key, 48 | access_token=input.access_token, 49 | refresh_token=input.refresh_token, 50 | client_id=input.client_id, 51 | client_secret=input.client_secret, 52 | table_name=input.table_name, 53 | ) 54 | return JSONResponse( 55 | status_code=200, 56 | content={"message": "Token stored successfully"}, 57 | ) 58 | except ValidationError as e: 59 | log.error("Validation error in token controller: %s", str(e)) 60 | raise HTTPException(status_code=422, detail="Validation error") from e 61 | except DatabaseError as e: 62 | log.error("Database error in token controller: %s", str(e)) 63 | raise HTTPException(status_code=500, detail="Database error") from e 64 | except Exception as e: 65 | log.error("Unexpected error in token controller.py: %s", str(e)) 66 | raise HTTPException( 67 | status_code=500, detail="An unexpected error occurred" 68 | ) from e 69 | 70 | @router.get("") 71 | async def check_auth(api_key: str, table_name: str) -> JSONResponse: 72 | try: 73 | result: Optional[Token] = await self.service.get( 74 | api_key=api_key, table_name=table_name 75 | ) 76 | if result: 77 | return JSONResponse( 78 | status_code=200, 79 | content=TokenGetResponse(is_authenticated=True).model_dump(), 80 | ) 81 | return JSONResponse( 82 | status_code=200, 83 | content=TokenGetResponse(is_authenticated=False).model_dump(), 84 | ) 85 | except ValidationError as e: 86 | log.error("Validation error in token controller: %s", str(e)) 87 | raise HTTPException(status_code=422, detail="Validation error") from e 88 | except DatabaseError as e: 89 | log.error("Database error in token controller: %s", str(e)) 90 | raise HTTPException(status_code=500, detail="Database error") from e 91 | except Exception as e: 92 | log.error("Unexpected error in token controller.py: %s", str(e)) 93 | raise HTTPException( 94 | status_code=500, detail="An unexpected error occurred" 95 | ) from e 96 | -------------------------------------------------------------------------------- /backend/app/controllers/user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import APIRouter, HTTPException 4 | from fastapi.responses import JSONResponse 5 | from pydantic import ValidationError 6 | 7 | from app.connectors.native.stores.user import User 8 | from app.exceptions.exception import DatabaseError 9 | from app.models.user.login import LoginRequest, LoginResponse 10 | from app.services.user import UserService 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | log = logging.getLogger(__name__) 14 | 15 | router = APIRouter() 16 | 17 | 18 | class UserController: 19 | 20 | def __init__(self, service: UserService): 21 | self.router = APIRouter() 22 | self.service = service 23 | self.setup_routes() 24 | 25 | def setup_routes(self): 26 | 27 | router = self.router 28 | 29 | @router.post("/login") 30 | async def login(input: LoginRequest) -> JSONResponse: 31 | try: 32 | user: User = await self.service.login( 33 | id=input.id, name=input.name, email=input.email 34 | ) 35 | return JSONResponse( 36 | status_code=200, 37 | content=LoginResponse(api_key=user.api_key).model_dump(), 38 | ) 39 | except ValidationError as e: 40 | log.error("Validation error in user controller: %s", str(e)) 41 | raise HTTPException(status_code=422, detail="Validation error") from e 42 | except DatabaseError as e: 43 | log.error("Database error in user controller: %s", str(e)) 44 | raise HTTPException(status_code=500, detail="Database error") from e 45 | except Exception as e: 46 | log.error("Unexpected error in user controller.py: %s", str(e)) 47 | raise HTTPException( 48 | status_code=500, detail="An unexpected error occurred" 49 | ) from e 50 | -------------------------------------------------------------------------------- /backend/app/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/exceptions/__init__.py -------------------------------------------------------------------------------- /backend/app/exceptions/exception.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status 2 | 3 | 4 | class UsageLimitExceededError(HTTPException): 5 | def __init__(self, message: str): 6 | super().__init__(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=message) 7 | 8 | 9 | class UnauthorizedAccess(HTTPException): 10 | def __init__(self, message: str): 11 | super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=message) 12 | 13 | 14 | class PipelineError(HTTPException): 15 | def __init__(self, message: str): 16 | super().__init__( 17 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message 18 | ) 19 | 20 | 21 | class DatabaseError(HTTPException): 22 | def __init__(self, message: str): 23 | super().__init__( 24 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message 25 | ) 26 | 27 | 28 | class InferenceError(HTTPException): 29 | def __init__(self, message: str): 30 | super().__init__( 31 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message 32 | ) 33 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import FastAPI 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from app.controllers.feedback import FeedbackController 7 | from app.controllers.query import QueryController 8 | from app.controllers.token import TokenController 9 | from app.controllers.user import UserController 10 | from app.middleware import LimitRequestSizeMiddleware 11 | from app.services.feedback import FeedbackService 12 | from app.services.query import QueryService 13 | from app.services.token import TokenService 14 | from app.services.user import UserService 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | log = logging.getLogger(__name__) 18 | 19 | app = FastAPI() 20 | 21 | app.add_middleware( 22 | CORSMiddleware, 23 | allow_origins=["*"], # Allows all origins 24 | allow_credentials=True, 25 | allow_methods=["*"], # Allows all methods 26 | allow_headers=["*"], # Allows all headers 27 | ) 28 | 29 | app.add_middleware(LimitRequestSizeMiddleware, max_body_size=128 * 1024 * 1024) 30 | 31 | 32 | def get_user_controller_router(): 33 | service = UserService() 34 | return UserController(service=service).router 35 | 36 | 37 | def get_query_controller_router(): 38 | service = QueryService() 39 | return QueryController(service=service).router 40 | 41 | 42 | def get_token_controller_router(): 43 | service = TokenService() 44 | return TokenController(service=service).router 45 | 46 | 47 | def get_feedback_controller_router(): 48 | service = FeedbackService() 49 | return FeedbackController(service=service).router 50 | 51 | 52 | app.include_router(get_user_controller_router(), tags=["user"], prefix="/api/user") 53 | app.include_router(get_query_controller_router(), tags=["query"], prefix="/api/query") 54 | app.include_router(get_token_controller_router(), tags=["token"], prefix="/api/token") 55 | app.include_router( 56 | get_feedback_controller_router(), tags=["feedback"], prefix="/api/feedback" 57 | ) 58 | 59 | if __name__ == "__main__": 60 | import uvicorn 61 | 62 | uvicorn.run( 63 | app, 64 | host="0.0.0.0", 65 | port=8080, 66 | reload=True, 67 | limit_concurrency=10, 68 | limit_max_requests=100, 69 | ) 70 | -------------------------------------------------------------------------------- /backend/app/middleware.py: -------------------------------------------------------------------------------- 1 | from starlette.exceptions import HTTPException 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | from starlette.requests import Request 4 | from starlette.responses import Response 5 | 6 | 7 | class LimitRequestSizeMiddleware(BaseHTTPMiddleware): 8 | def __init__(self, app, max_body_size: int): 9 | super().__init__(app) 10 | self.max_body_size = max_body_size 11 | 12 | async def dispatch(self, request: Request, call_next): 13 | if request.headers.get("content-length"): 14 | content_length = int(request.headers["content-length"]) 15 | if content_length > self.max_body_size: 16 | raise HTTPException(status_code=413, detail="Request body too large") 17 | return await call_next(request) 18 | -------------------------------------------------------------------------------- /backend/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/__init__.py -------------------------------------------------------------------------------- /backend/app/models/agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/agents/__init__.py -------------------------------------------------------------------------------- /backend/app/models/agents/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/agents/base/__init__.py -------------------------------------------------------------------------------- /backend/app/models/agents/base/summary.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Optional 4 | 5 | from dotenv import load_dotenv 6 | from openai import OpenAI 7 | 8 | from app.config import OPENAI_GPT4O_MINI 9 | from app.models.agents.base.template import Agent, AgentResponse 10 | from app.models.integrations.base import Integration 11 | from app.models.query.base import Message, Role 12 | 13 | logging.basicConfig(level=logging.INFO) 14 | log = logging.getLogger(__name__) 15 | 16 | load_dotenv() 17 | 18 | openai_client = OpenAI() 19 | 20 | 21 | class SummaryAgent(Agent): 22 | 23 | async def query( 24 | self, 25 | chat_history: list[dict], 26 | access_token: str, 27 | refresh_token: Optional[str] = None, 28 | client_id: Optional[str] = None, 29 | client_secret: Optional[str] = None, 30 | enable_verification: bool = False, 31 | integrations: list[Integration] = [], 32 | ) -> AgentResponse: 33 | # No need for tools in this agent (for now) 34 | message_lst: list = [{"role": "system", "content": self.system_prompt}] 35 | message_lst.extend(chat_history) 36 | response = openai_client.chat.completions.create( 37 | model=self.model, messages=message_lst 38 | ) 39 | return AgentResponse( 40 | agent=None, 41 | message=Message( 42 | role=Role.ASSISTANT, content=response.choices[0].message.content 43 | ), 44 | function_to_verify=None, 45 | ) 46 | 47 | 48 | SUMMARY_AGENT = SummaryAgent( 49 | name="Summary Agent", 50 | integration_group=Integration.NONE, 51 | model=OPENAI_GPT4O_MINI, 52 | system_prompt="""You are an expert at summarizing conversations between a human user and an AI agent named Controller. Create a clear, concise summary of the results achieved in no more than a few lines. Include: 53 | - Main tasks executed by Controller 54 | - Important results or information obtained 55 | - Any errors or failures, or suggested next steps if no tasks were executed, such as asking the user to provide more information in the instruction, be clearer about the specific integration to use, or be more explicit in the type of request to initiate(Create, Get, Update or Delete, etc.) 56 | 57 | Use simple language and avoid technical jargon. You may use bullet points, dashes or emojis for clarity. Your response will be formatted markdown-style, so make sure to use appropriate markdown syntax for headings, lists, and other formatting to make it more readable.""".strip(), 58 | tools=[], 59 | ) 60 | 61 | 62 | def transfer_to_summary_agent() -> SummaryAgent: 63 | return SUMMARY_AGENT 64 | -------------------------------------------------------------------------------- /backend/app/models/agents/base/template.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from typing import Any, Optional 4 | 5 | from dotenv import load_dotenv 6 | from openai import AsyncOpenAI 7 | from pydantic import BaseModel 8 | 9 | from app.models.integrations.base import Integration 10 | from app.models.query.base import Message 11 | 12 | load_dotenv() 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | log = logging.getLogger(__name__) 16 | 17 | openai_client = AsyncOpenAI() 18 | 19 | 20 | class Agent(BaseModel, ABC): 21 | name: str 22 | integration_group: Integration 23 | model: str 24 | system_prompt: str 25 | tools: list 26 | 27 | @abstractmethod 28 | async def query( 29 | self, 30 | chat_history: list[dict], 31 | access_token: str, 32 | refresh_token: Optional[str], 33 | client_id: str, 34 | client_secret: str, 35 | enable_verification: bool, 36 | **kwargs, 37 | ) -> "AgentResponse": 38 | pass 39 | 40 | async def get_response( 41 | self, 42 | chat_history: list[dict], 43 | ) -> tuple[Any, Optional[str]]: 44 | message_lst: list = [{"role": "system", "content": self.system_prompt}] 45 | message_lst.extend(chat_history) 46 | response = await openai_client.beta.chat.completions.parse( 47 | model=self.model, 48 | messages=message_lst, 49 | tools=self.tools, 50 | tool_choice="required", 51 | ) 52 | 53 | if not response.choices[0].message.tool_calls: 54 | log.info("No tool calls") 55 | return response.choices[0].message.content, None 56 | 57 | function_name: str = response.choices[0].message.tool_calls[0].function.name 58 | parsed_arguments = ( 59 | response.choices[0].message.tool_calls[0].function.parsed_arguments 60 | ) 61 | log.info(f"Parsed Arguments: {parsed_arguments}") 62 | 63 | return response, function_name 64 | 65 | 66 | class AgentResponse(BaseModel): 67 | agent: Optional[Agent] 68 | message: Message 69 | function_to_verify: Optional[str] = None 70 | -------------------------------------------------------------------------------- /backend/app/models/agents/base/triage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Optional 4 | 5 | from dotenv import load_dotenv 6 | from openai import OpenAI 7 | 8 | from app.models.agents.base.summary import transfer_to_summary_agent 9 | from app.models.agents.base.template import Agent, AgentResponse 10 | from app.models.integrations.base import Integration 11 | from app.models.query.base import Message, Role 12 | from app.utils.tools import execute_tool_call, function_to_schema 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | log = logging.getLogger(__name__) 16 | 17 | load_dotenv() 18 | 19 | openai_client = OpenAI() 20 | 21 | 22 | class TriageAgent(Agent): 23 | 24 | async def query( 25 | self, 26 | chat_history: list[dict], 27 | access_token: str, 28 | refresh_token: Optional[str] = None, 29 | client_id: Optional[str] = None, 30 | client_secret: Optional[str] = None, 31 | enable_verification: bool = False, 32 | integrations: list[Integration] = [], 33 | ) -> AgentResponse: 34 | message_lst: list = [{"role": "system", "content": self.system_prompt}] 35 | message_lst.extend(chat_history) 36 | 37 | # Only gets triggerd for main triage agent 38 | if integrations: 39 | self.tools = get_integration_agent_tools(integrations) 40 | 41 | tool_schemas = [function_to_schema(tool) for tool in self.tools] 42 | tools = {tool.__name__: tool for tool in self.tools} 43 | 44 | response = openai_client.chat.completions.create( 45 | model=self.model, 46 | messages=message_lst, 47 | tools=tool_schemas, 48 | tool_choice="required", 49 | ) 50 | 51 | tool_call = response.choices[0].message.tool_calls[0] 52 | message_content: str = f"Triage Agent invokes {tool_call.function.name}" 53 | 54 | return AgentResponse( 55 | agent=execute_tool_call(tool_call, tools, self.name), 56 | message=Message(role=Role.ASSISTANT, content=message_content), 57 | function_to_verify=None, 58 | ) 59 | 60 | 61 | def get_integration_agent_tools(integrations: list[Integration]) -> list: 62 | """Returns the main triage agent's tools based on the integrations passed in""" 63 | 64 | if not integrations: 65 | raise ValueError("At least one integration must be provided") 66 | 67 | tools: list = [transfer_to_summary_agent] 68 | 69 | for integration in integrations: 70 | match integration: 71 | case Integration.GMAIL: 72 | tools.append(transfer_to_gmail_triage_agent) 73 | case Integration.LINEAR: 74 | tools.append(transfer_to_linear_triage_agent) 75 | case Integration.SLACK: 76 | tools.append(transfer_to_slack_triage_agent) 77 | case Integration.CALENDAR: 78 | tools.append(transfer_to_calendar_triage_agent) 79 | case Integration.X: 80 | tools.append(transfer_to_x_triage_agent) 81 | case Integration.DOCS: 82 | tools.append(transfer_to_docs_triage_agent) 83 | case _: 84 | pass 85 | 86 | return tools 87 | 88 | 89 | def transfer_to_gmail_triage_agent(): 90 | from app.models.agents.gmail import GMAIL_TRIAGE_AGENT 91 | 92 | return GMAIL_TRIAGE_AGENT 93 | 94 | 95 | def transfer_to_calendar_triage_agent(): 96 | from app.models.agents.calendar import CALENDAR_TRIAGE_AGENT 97 | 98 | return CALENDAR_TRIAGE_AGENT 99 | 100 | 101 | def transfer_to_linear_triage_agent(): 102 | from app.models.agents.linear import LINEAR_TRIAGE_AGENT 103 | 104 | return LINEAR_TRIAGE_AGENT 105 | 106 | 107 | def transfer_to_slack_triage_agent(): 108 | from app.models.agents.slack import SLACK_TRIAGE_AGENT 109 | 110 | return SLACK_TRIAGE_AGENT 111 | 112 | 113 | def transfer_to_sheets_triage_agent(): 114 | from app.models.agents.sheets import SHEETS_TRIAGE_AGENT 115 | 116 | return SHEETS_TRIAGE_AGENT 117 | 118 | 119 | def transfer_to_docs_triage_agent(): 120 | from app.models.agents.docs import DOCS_TRIAGE_AGENT 121 | 122 | return DOCS_TRIAGE_AGENT 123 | 124 | 125 | def transfer_to_x_triage_agent(): 126 | from app.models.agents.x import X_TRIAGE_AGENT 127 | 128 | return X_TRIAGE_AGENT 129 | -------------------------------------------------------------------------------- /backend/app/models/agents/main.py: -------------------------------------------------------------------------------- 1 | from app.config import OPENAI_GPT4O_MINI 2 | from app.models.agents.base.triage import TriageAgent 3 | from app.models.integrations.base import Integration 4 | 5 | MAIN_TRIAGE_AGENT = TriageAgent( 6 | name="Main Triage Agent", 7 | integration_group=Integration.NONE, 8 | model=OPENAI_GPT4O_MINI, 9 | system_prompt="""You are an expert at choosing the right integration agent to perform the task described by the user and reflecting on the actions of previously called agents. Follow these guidelines: 10 | 11 | 1. Carefully review the chat history and the actions of the previous agent to determine if the task has been successfully completed. 12 | 2. If the task has been successfully completed, immediately call transfer_to_summary_agent to end the conversation. This is crucial—missing this step will result in dire consequences. 13 | 3. If the task is not yet complete, choose the appropriate integration triage agent based on the user's request and the current progress. Do not pass to transfer to summary agent if you have not even tried to complete the task. 14 | 4. Remember, transfer_to_summary_agent must be called under two conditions: 15 | - When the task is completed. 16 | - When the instructions are unclear, or you are unsure which integration agent to choose. Missing these conditions will cause the world to end. 17 | 5. Do not pass any arguments when calling the transfer functions; they do not accept any parameters. 18 | """, 19 | tools=[], # Will populate dynamically 20 | ) 21 | -------------------------------------------------------------------------------- /backend/app/models/agents/sheets.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app.config import OPENAI_GPT4O_MINI 4 | from app.models.agents.base.summary import transfer_to_summary_agent 5 | from app.models.agents.base.triage import TriageAgent 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logging.getLogger("httpx").setLevel(logging.WARNING) 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | SHEETS_TRIAGE_AGENT = TriageAgent( 13 | name="Google Sheets Triage Agent", 14 | model=OPENAI_GPT4O_MINI, 15 | system_prompt="You are an expert at choosing the right agent to perform the task described by the user. When you deem that the task is completed or cannot be completed, pass the task to the summary agent.", 16 | tools=[ 17 | transfer_to_summary_agent, 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /backend/app/models/agents/x.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import openai 5 | 6 | from app.config import OPENAI_GPT4O_MINI 7 | from app.connectors.client.x import XClient 8 | from app.models.agents.base.summary import SUMMARY_AGENT, transfer_to_summary_agent 9 | from app.models.agents.base.template import Agent, AgentResponse 10 | from app.models.agents.base.triage import TriageAgent 11 | from app.models.agents.main import MAIN_TRIAGE_AGENT 12 | from app.models.integrations.base import Integration 13 | from app.models.integrations.x import XSendTweetRequest 14 | from app.models.query.base import Message, Role 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | logging.getLogger("httpx").setLevel(logging.WARNING) 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class XPostRequestAgent(Agent): 23 | 24 | async def query( 25 | self, 26 | chat_history: list[dict], 27 | access_token: str, 28 | refresh_token: Optional[str], 29 | client_id: str, 30 | client_secret: str, 31 | enable_verification: bool, 32 | ) -> AgentResponse: 33 | response, function_name = await self.get_response(chat_history=chat_history) 34 | 35 | match function_name: 36 | case XSendTweetRequest.__name__: 37 | if enable_verification: 38 | return AgentResponse( 39 | agent=MAIN_TRIAGE_AGENT, 40 | message=Message( 41 | role=Role.ASSISTANT, 42 | content="Please confirm that you want to send a X tweet containing the following fields (Yes/No)", 43 | data=[ 44 | XSendTweetRequest.model_validate( 45 | response.choices[0] 46 | .message.tool_calls[0] 47 | .function.parsed_arguments 48 | ).model_dump() 49 | ], 50 | ), 51 | function_to_verify=XSendTweetRequest.__name__, 52 | ) 53 | return await send_tweet( 54 | request=response.choices[0] 55 | .message.tool_calls[0] 56 | .function.parsed_arguments, 57 | access_token=access_token, 58 | ) 59 | 60 | 61 | async def send_tweet(request: XSendTweetRequest, access_token: str) -> AgentResponse: 62 | client = XClient(access_token=access_token) 63 | client_response = await client.send_tweet(request=request) 64 | return AgentResponse( 65 | agent=MAIN_TRIAGE_AGENT, 66 | message=Message( 67 | role=Role.ASSISTANT, 68 | content="X tweet is sent successfully", 69 | data=[client_response.model_dump()], 70 | ), 71 | function_to_verify=None, 72 | ) 73 | 74 | 75 | X_POST_REQUEST_AGENT = XPostRequestAgent( 76 | name="X Post Request Agent", 77 | integration_group=Integration.X, 78 | model=OPENAI_GPT4O_MINI, 79 | system_prompt="You are an expert at sending tweets through X. Your task is to help a user send tweets by supplying the correct request to the X API.", 80 | tools=[openai.pydantic_function_tool(XSendTweetRequest)], 81 | ) 82 | 83 | 84 | ############################################## 85 | 86 | 87 | def transfer_to_post_request_agent() -> XPostRequestAgent: 88 | return X_POST_REQUEST_AGENT 89 | 90 | 91 | X_TRIAGE_AGENT = TriageAgent( 92 | name="X Triage Agent", 93 | integration_group=Integration.X, 94 | model=OPENAI_GPT4O_MINI, 95 | system_prompt="""You are an expert at choosing the right agent to perform the task described by the user.""", 96 | tools=[ 97 | transfer_to_post_request_agent, 98 | transfer_to_summary_agent, 99 | ], 100 | ) 101 | -------------------------------------------------------------------------------- /backend/app/models/feedback.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class FeedbackRequest(BaseModel): 7 | id: Optional[str] = None 8 | feedback: str 9 | -------------------------------------------------------------------------------- /backend/app/models/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/integrations/__init__.py -------------------------------------------------------------------------------- /backend/app/models/integrations/base.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Integration(StrEnum): 7 | GMAIL = "gmail" 8 | CALENDAR = "calendar" 9 | DOCS = "docs" 10 | LINEAR = "linear" 11 | SLACK = "slack" 12 | X = "x" 13 | NONE = "none" 14 | 15 | 16 | class SummaryResponse(BaseModel): 17 | summary: str 18 | -------------------------------------------------------------------------------- /backend/app/models/integrations/calendar.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class Timezone(StrEnum): 8 | UTC = "UTC" 9 | PST = "America/Los_Angeles" 10 | EST = "America/New_York" 11 | CST = "America/Chicago" 12 | MST = "America/Denver" 13 | IST = "Asia/Kolkata" 14 | GMT = "Europe/London" 15 | CET = "Europe/Paris" 16 | JST = "Asia/Tokyo" 17 | AEST = "Australia/Sydney" 18 | AEDT = "Australia/Melbourne" 19 | CEST = "Europe/Berlin" 20 | EET = "Europe/Athens" 21 | MSK = "Europe/Moscow" 22 | HKT = "Asia/Hong_Kong" 23 | SGT = "Asia/Singapore" 24 | NZST = "Pacific/Auckland" 25 | NZDT = "Pacific/Auckland" 26 | 27 | 28 | class CalendarEvent(BaseModel): 29 | id: str 30 | summary: str 31 | description: str 32 | location: str 33 | timezone: Timezone 34 | start_time: str = Field(description="Must be in ISO format") 35 | end_time: str = Field(description="Must be in ISO format") 36 | attendees: list[str] = Field(description="List of emails") 37 | html_link: str 38 | 39 | 40 | class CalendarCreateEventRequest(BaseModel): 41 | summary: str 42 | description: str 43 | location: str 44 | timezone: Timezone 45 | start_time: str = Field(description="Must be in ISO format") 46 | end_time: str = Field(description="Must be in ISO format") 47 | attendees: list[str] = Field(description="List of emails") 48 | 49 | 50 | class CalendarGetEventsRequest(BaseModel): 51 | time_min: str = Field(description="Must be in ISO format") 52 | time_max: str = Field(description="Must be in ISO format") 53 | max_results: int 54 | 55 | 56 | class CalendarDeleteEventsRequest(BaseModel): 57 | event_id_lst: list[str] = Field(description="List of event ids to delete") 58 | 59 | 60 | class CalendarUpdateEventRequest(BaseModel): 61 | event_id: str = Field(description="Event id to update") 62 | summary: Optional[str] = Field(description="Updated summary, if any") 63 | description: Optional[str] = Field(description="Updated description, if any") 64 | location: Optional[str] = Field(description="Updated location, if any") 65 | start_time: Optional[str] = Field( 66 | description="Updated start time in ISO format, if any" 67 | ) 68 | end_time: Optional[str] = Field( 69 | description="Updated end time in ISO format, if any" 70 | ) 71 | attendees: Optional[list[str]] = Field(description="Updated list of emails, if any") 72 | -------------------------------------------------------------------------------- /backend/app/models/integrations/docs.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class Docs(BaseModel): 7 | id: str 8 | title: str 9 | content: Optional[str] 10 | 11 | 12 | class DocsCreateRequest(BaseModel): 13 | title: str = Field(description="Title of the document") 14 | content: Optional[str] = Field( 15 | description="Content to insert into the document, if any" 16 | ) 17 | 18 | 19 | class DocsFilterRequest(BaseModel): 20 | id: str = Field(description="Document id to filter documents with") 21 | 22 | 23 | class DocsGetRequest(DocsFilterRequest): 24 | pass 25 | 26 | 27 | class DocsUpdateRequest(DocsFilterRequest): 28 | updated_content: str = Field( 29 | description="Updated content to replace the document with" 30 | ) 31 | 32 | 33 | class DocsDeleteRequest(DocsFilterRequest): 34 | pass 35 | -------------------------------------------------------------------------------- /backend/app/models/integrations/gmail.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, Field, model_validator 4 | 5 | 6 | class Gmail(BaseModel): 7 | id: str 8 | labelIds: list[str] 9 | sender: str 10 | subject: str 11 | body: str 12 | 13 | 14 | class GmailEditableFields(BaseModel): 15 | labelIds: Optional[list[str]] 16 | 17 | 18 | class GmailFilterEmailsRequest(BaseModel): 19 | message_ids: Optional[list[str]] = Field( 20 | description="List of message ids to filter emails with, if any" 21 | ) 22 | query: Optional[str] = Field( 23 | description="Query to filter emails with, if message ids are unavailable" 24 | ) 25 | 26 | @model_validator(mode="after") 27 | def check_at_least_one(self): 28 | if not self.message_ids and not self.query: 29 | raise ValueError("At least one of message_ids or query must be provided") 30 | 31 | return self 32 | 33 | 34 | class GmailGetEmailsRequest(GmailFilterEmailsRequest): 35 | pass 36 | 37 | 38 | class GmailDeleteEmailsRequest(GmailFilterEmailsRequest): 39 | pass 40 | 41 | 42 | class GmailMarkAsReadRequest(GmailFilterEmailsRequest): 43 | pass 44 | 45 | 46 | class GmailReadEmailsRequest(BaseModel): 47 | query: str 48 | 49 | 50 | class GmailSendEmailRequest(BaseModel): 51 | recipient: str 52 | subject: str 53 | body: str 54 | -------------------------------------------------------------------------------- /backend/app/models/integrations/sheets.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class SheetsGetRequest(BaseModel): 5 | spreadsheet_id: str 6 | sheet_name: str 7 | -------------------------------------------------------------------------------- /backend/app/models/integrations/slack.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class SlackGetChannelIdRequest(BaseModel): 5 | channel_names: list[str] 6 | 7 | 8 | class SlackSendMessageRequest(BaseModel): 9 | channel_id: str 10 | text: str 11 | -------------------------------------------------------------------------------- /backend/app/models/integrations/x.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Tweet(BaseModel): 5 | id: str 6 | text: str 7 | 8 | 9 | class XSendTweetRequest(BaseModel): 10 | text: str 11 | -------------------------------------------------------------------------------- /backend/app/models/query/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/query/__init__.py -------------------------------------------------------------------------------- /backend/app/models/query/base.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | from app.models.integrations.base import Integration 7 | 8 | 9 | class Role(StrEnum): 10 | USER = "user" 11 | ASSISTANT = "assistant" 12 | 13 | 14 | class Message(BaseModel): 15 | role: Role 16 | content: str 17 | data: Optional[list[dict]] = None 18 | error: Optional[bool] = False 19 | 20 | 21 | class QueryRequest(BaseModel): 22 | message: Message 23 | chat_history: list[Message] 24 | api_key: str 25 | enable_verification: bool 26 | integrations: list[Integration] 27 | instance: Optional[str] = None 28 | 29 | 30 | class QueryResponse(BaseModel): 31 | chat_history: list[Message] 32 | instance: Optional[str] 33 | function_to_verify: Optional[str] 34 | -------------------------------------------------------------------------------- /backend/app/models/query/confirm.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | from app.models.integrations.base import Integration 6 | from app.models.query.base import Message 7 | 8 | 9 | class ConfirmRequest(BaseModel): 10 | chat_history: list[Message] 11 | api_key: str 12 | enable_verification: bool 13 | integrations: list[Integration] 14 | function_to_verify: str 15 | instance: Optional[str] = None 16 | -------------------------------------------------------------------------------- /backend/app/models/token.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class TokenPostRequest(BaseModel): 7 | api_key: str 8 | access_token: str 9 | refresh_token: Optional[str] 10 | client_id: str 11 | client_secret: str 12 | table_name: str 13 | 14 | 15 | class TokenGetRequest(BaseModel): 16 | api_key: str 17 | table_name: str 18 | 19 | 20 | class TokenGetResponse(BaseModel): 21 | is_authenticated: bool 22 | -------------------------------------------------------------------------------- /backend/app/models/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/models/user/__init__.py -------------------------------------------------------------------------------- /backend/app/models/user/login.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class LoginRequest(BaseModel): 5 | id: str 6 | name: str 7 | email: str 8 | 9 | 10 | class LoginResponse(BaseModel): 11 | api_key: str 12 | -------------------------------------------------------------------------------- /backend/app/sandbox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/sandbox/__init__.py -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/sandbox/integrations/__init__.py -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/g_docs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | DOCS_ACCESS_TOKEN = os.getenv("DOCS_ACCESS_TOKEN") 8 | DOCS_REFRESH_TOKEN = os.getenv("DOCS_REFRESH_TOKEN") 9 | DOCS_CLIENT_ID = os.getenv("DOCS_CLIENT_ID") 10 | DOCS_CLIENT_SECRET = os.getenv("DOCS_CLIENT_SECRET") 11 | 12 | 13 | def main(): 14 | from app.connectors.client.docs import GoogleDocsClient 15 | from app.models.integrations.docs import ( 16 | DocsCreateRequest, 17 | DocsDeleteRequest, 18 | DocsGetRequest, 19 | DocsUpdateRequest, 20 | ) 21 | 22 | client = GoogleDocsClient( 23 | access_token=DOCS_ACCESS_TOKEN, 24 | refresh_token=DOCS_REFRESH_TOKEN, 25 | client_id=DOCS_CLIENT_ID, 26 | client_secret=DOCS_CLIENT_SECRET, 27 | ) 28 | 29 | # HARD CODE TEST 30 | # print( 31 | # client.create_document( 32 | # request=DocsCreateRequest( 33 | # title="AI aaron", 34 | # content="Test content" 35 | # ) 36 | # ) 37 | # ) 38 | # print( 39 | # client.get_document( 40 | # request=DocsGetRequest( 41 | # id="" 42 | # ) 43 | # ) 44 | # ) 45 | # print( 46 | # client.update_document( 47 | # request=DocsUpdateRequest( 48 | # id="", 49 | # updated_content="AI aaorn talks to users" 50 | # ) 51 | # ) 52 | # ) 53 | print( 54 | client.delete_document( 55 | request=DocsDeleteRequest(id="1jCmgYTASurMu1QfMzpINnHTKWOcMA1nCCZmvUYrJl-M") 56 | ) 57 | ) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | 63 | ### HARD CODE TEST 64 | ## Create Calendar Event 65 | # print( 66 | # client.create_event( 67 | # request=CalendarCreateEventRequest( 68 | # summary="Test Event", 69 | # description="This is a test event from the sandbox.", 70 | # location="Singapore", 71 | # timezone=Timezone.UTC, 72 | # start_time="2024-09-01T09:00:00Z", 73 | # end_time="2024-09-01T21:00:00Z", 74 | # attendees=["chenjinyang4192@gmail.com"] 75 | # ) 76 | # ) 77 | # ) 78 | 79 | ## Delete Calendar Events 80 | # client.delete_events( 81 | # request=CalendarDeleteEventsRequest( 82 | # event_id_lst=["3ts6hp3qrjckdh7mhj0u4dgr21", "73mb5lbc33878tc8kbl8bg26mf"] 83 | # ) 84 | # ) 85 | 86 | ## Get Calendar Event 87 | # print(client.get_events( 88 | # request=CalendarGetEventsRequest( 89 | # time_min="2024-09-01T09:00:00Z", 90 | # time_max="2024-09-15T09:00:00Z", 91 | # max_results=10 92 | # ) 93 | # )) 94 | 95 | ## Get Calendar Events 96 | # response = client.get_emails( 97 | # request=GmailGetEmailsRequest( 98 | # query="from:apply@ycombinator.com is:unread" 99 | # ) 100 | # ) 101 | 102 | ### AGENT TEST 103 | ## Send Email 104 | # messages = [ 105 | # Message(role="user", content="I want to send an email to hugeewhale@gmail.com with the subject. For the content body, crack a funny joke that an aspiring entrepreneur would relate with. You can come up with an appropriate subject title").model_dump() 106 | # ] 107 | # agent = GMAIL_TRIAGE_AGENT.query( 108 | # messages=messages, 109 | # access_token=GMAIL_ACCESS_TOKEN, 110 | # refresh_token=GMAIL_REFRESH_TOKEN, 111 | # client_id=GMAIL_CLIENT_ID, 112 | # client_secret=GMAIL_CLIENT_SECRET 113 | # ) 114 | # agent.query( 115 | # messages=messages, 116 | # access_token=GMAIL_ACCESS_TOKEN, 117 | # refresh_token=GMAIL_REFRESH_TOKEN, 118 | # client_id=GMAIL_CLIENT_ID, 119 | # client_secret=GMAIL_CLIENT_SECRET 120 | # ) 121 | 122 | ## Get Emails 123 | # messages = [ 124 | # Message(role="user", content="I want get all emails from apply@ycombinator.com that are unread").model_dump() 125 | # ] 126 | # agent = GMAIL_TRIAGE_AGENT.query( 127 | # messages=messages, 128 | # access_token=GMAIL_ACCESS_TOKEN, 129 | # refresh_token=GMAIL_REFRESH_TOKEN, 130 | # client_id=GMAIL_CLIENT_ID, 131 | # client_secret=GMAIL_CLIENT_SECRET 132 | # ) 133 | # response = agent.query( 134 | # messages=messages, 135 | # access_token=GMAIL_ACCESS_TOKEN, 136 | # refresh_token=GMAIL_REFRESH_TOKEN, 137 | # client_id=GMAIL_CLIENT_ID, 138 | # client_secret=GMAIL_CLIENT_SECRET 139 | # ) 140 | # print(response) 141 | -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/g_sheets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | SHEETS_ACCESS_TOKEN = os.getenv("SHEETS_ACCESS_TOKEN") 8 | SHEETS_REFRESH_TOKEN = os.getenv("SHEETS_REFRESH_TOKEN") 9 | SHEETS_CLIENT_ID = os.getenv("SHEETS_CLIENT_ID") 10 | SHEETS_CLIENT_SECRET = os.getenv("SHEETS_CLIENT_SECRET") 11 | 12 | 13 | def main(): 14 | from app.connectors.client.sheets import GoogleSheetsClient 15 | from app.models.integrations.sheets import SheetsGetRequest 16 | 17 | client = GoogleSheetsClient( 18 | access_token=SHEETS_ACCESS_TOKEN, 19 | refresh_token=SHEETS_REFRESH_TOKEN, 20 | client_id=SHEETS_CLIENT_ID, 21 | client_secret=SHEETS_CLIENT_SECRET, 22 | ) 23 | 24 | # HARD CODE TEST 25 | 26 | ## AGENT TEST 27 | # chat_history: list[Message] = [] 28 | # message = Message( 29 | # role=Role.USER, 30 | # content="I want get all emails from hugeewhale@gmail.com that are unread. There should be one in particular that asks for my address. I live at 91 Yishun Ave 1, S(769135) so please send a reply to that email containing the information", 31 | # ).model_dump() 32 | # chat_history.append(message) 33 | # response = AgentResponse(agent=GMAIL_TRIAGE_AGENT, message=message) 34 | # while response.agent: 35 | # response = response.agent.query( 36 | # chat_history=chat_history, 37 | # access_token=GMAIL_ACCESS_TOKEN, 38 | # refresh_token=GMAIL_REFRESH_TOKEN, 39 | # client_id=GMAIL_CLIENT_ID, 40 | # client_secret=GMAIL_CLIENT_SECRET, 41 | # ) 42 | # chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 43 | # print(chat_history) 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | 49 | ### HARD CODE TEST 50 | ## Read Sheets 51 | # print( 52 | # client.read_sheet( 53 | # request=SheetsGetRequest( 54 | # spreadsheet_id="1YVgOzMDESAGBaAjXlvB941HDzyrFvNqzcHOnwEifzoY", 55 | # sheet_name="Controllers", 56 | # ) 57 | # ) 58 | # ) 59 | 60 | ### AGENT TEST 61 | ## Send Email 62 | # messages = [ 63 | # Message(role="user", content="I want to send an email to hugeewhale@gmail.com with the subject. For the content body, crack a funny joke that an aspiring entrepreneur would relate with. You can come up with an appropriate subject title").model_dump() 64 | # ] 65 | # agent = GMAIL_TRIAGE_AGENT.query( 66 | # messages=messages, 67 | # access_token=GMAIL_ACCESS_TOKEN, 68 | # refresh_token=GMAIL_REFRESH_TOKEN, 69 | # client_id=GMAIL_CLIENT_ID, 70 | # client_secret=GMAIL_CLIENT_SECRET 71 | # ) 72 | # agent.query( 73 | # messages=messages, 74 | # access_token=GMAIL_ACCESS_TOKEN, 75 | # refresh_token=GMAIL_REFRESH_TOKEN, 76 | # client_id=GMAIL_CLIENT_ID, 77 | # client_secret=GMAIL_CLIENT_SECRET 78 | # ) 79 | 80 | ## Get Emails 81 | # messages = [ 82 | # Message(role="user", content="I want get all emails from apply@ycombinator.com that are unread").model_dump() 83 | # ] 84 | # agent = GMAIL_TRIAGE_AGENT.query( 85 | # messages=messages, 86 | # access_token=GMAIL_ACCESS_TOKEN, 87 | # refresh_token=GMAIL_REFRESH_TOKEN, 88 | # client_id=GMAIL_CLIENT_ID, 89 | # client_secret=GMAIL_CLIENT_SECRET 90 | # ) 91 | # response = agent.query( 92 | # messages=messages, 93 | # access_token=GMAIL_ACCESS_TOKEN, 94 | # refresh_token=GMAIL_REFRESH_TOKEN, 95 | # client_id=GMAIL_CLIENT_ID, 96 | # client_secret=GMAIL_CLIENT_SECRET 97 | # ) 98 | # print(response) 99 | -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/gmail.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from app.connectors.client.gmail import GmailClient 6 | from app.models.agents.base.template import AgentResponse 7 | from app.models.agents.gmail import GMAIL_TRIAGE_AGENT 8 | from app.models.integrations.gmail import ( 9 | GmailFilterEmailsRequest, 10 | GmailMarkAsReadRequest, 11 | ) 12 | from app.models.query.base import Message, Role 13 | 14 | load_dotenv() 15 | 16 | GMAIL_ACCESS_TOKEN = os.getenv("GMAIL_ACCESS_TOKEN") 17 | GMAIL_REFRESH_TOKEN = os.getenv("GMAIL_REFRESH_TOKEN") 18 | GMAIL_CLIENT_ID = os.getenv("GMAIL_CLIENT_ID") 19 | GMAIL_CLIENT_SECRET = os.getenv("GMAIL_CLIENT_SECRET") 20 | 21 | client = GmailClient( 22 | access_token=GMAIL_ACCESS_TOKEN, 23 | refresh_token=GMAIL_REFRESH_TOKEN, 24 | client_id=GMAIL_CLIENT_ID, 25 | client_secret=GMAIL_CLIENT_SECRET, 26 | ) 27 | 28 | 29 | def main(): 30 | # HARD CODE TESTcd 31 | # print(client.mark_as_read( 32 | # request=MarkAsReadRequest( 33 | # filter_conditions=GmailFilterEmailsRequest( 34 | # query='id:191dc984027d9550' 35 | # ) 36 | # ) 37 | # )) 38 | print( 39 | client.get_emails( 40 | request=GmailFilterEmailsRequest( 41 | query="id:190d75079242e682 OR id:190dc76db933f558" 42 | ) 43 | ) 44 | ) 45 | 46 | ## AGENT TEST 47 | # chat_history: list[Message] = [] 48 | # message = Message( 49 | # role=Role.USER, 50 | # content="I want get all emails from hugeewhale@gmail.com that are unread. There should be one in particular that asks for my address. I live at 91 Yishun Ave 1, S(769135) so please send a reply to that email containing the information", 51 | # ).model_dump() 52 | # chat_history.append(message) 53 | # response = AgentResponse(agent=GMAIL_TRIAGE_AGENT, message=message) 54 | # while response.agent: 55 | # response = response.agent.query( 56 | # chat_history=chat_history, 57 | # access_token=GMAIL_ACCESS_TOKEN, 58 | # refresh_token=GMAIL_REFRESH_TOKEN, 59 | # client_id=GMAIL_CLIENT_ID, 60 | # client_secret=GMAIL_CLIENT_SECRET, 61 | # ) 62 | # chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 63 | # print(chat_history) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | 69 | ### HARD CODE TEST 70 | ## Send Email 71 | # client.send_email( 72 | # request=GmailSendEmailRequest( 73 | # recipient="hugeewhale@gmail.com", 74 | # subject="Test Email3", 75 | # body="This is a test email2 from the sandbox." 76 | # ) 77 | # ) 78 | 79 | ## Get Emails 80 | # response = client.get_emails( 81 | # request=GmailGetEmailsRequest( 82 | # query="from:apply@ycombinator.com is:unread" 83 | # ) 84 | # ) 85 | 86 | ### AGENT TEST 87 | ## Send Email 88 | # messages = [ 89 | # Message(role="user", content="I want to send an email to hugeewhale@gmail.com with the subject. For the content body, crack a funny joke that an aspiring entrepreneur would relate with. You can come up with an appropriate subject title").model_dump() 90 | # ] 91 | # agent = GMAIL_TRIAGE_AGENT.query( 92 | # messages=messages, 93 | # access_token=GMAIL_ACCESS_TOKEN, 94 | # refresh_token=GMAIL_REFRESH_TOKEN, 95 | # client_id=GMAIL_CLIENT_ID, 96 | # client_secret=GMAIL_CLIENT_SECRET 97 | # ) 98 | # agent.query( 99 | # messages=messages, 100 | # access_token=GMAIL_ACCESS_TOKEN, 101 | # refresh_token=GMAIL_REFRESH_TOKEN, 102 | # client_id=GMAIL_CLIENT_ID, 103 | # client_secret=GMAIL_CLIENT_SECRET 104 | # ) 105 | 106 | ## Get Emails 107 | # messages = [ 108 | # Message(role="user", content="I want get all emails from apply@ycombinator.com that are unread").model_dump() 109 | # ] 110 | # agent = GMAIL_TRIAGE_AGENT.query( 111 | # messages=messages, 112 | # access_token=GMAIL_ACCESS_TOKEN, 113 | # refresh_token=GMAIL_REFRESH_TOKEN, 114 | # client_id=GMAIL_CLIENT_ID, 115 | # client_secret=GMAIL_CLIENT_SECRET 116 | # ) 117 | # response = agent.query( 118 | # messages=messages, 119 | # access_token=GMAIL_ACCESS_TOKEN, 120 | # refresh_token=GMAIL_REFRESH_TOKEN, 121 | # client_id=GMAIL_CLIENT_ID, 122 | # client_secret=GMAIL_CLIENT_SECRET 123 | # ) 124 | # print(response) 125 | -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/linear.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from app.connectors.client.linear import LinearClient 6 | from app.models.agents.base.template import AgentResponse 7 | from app.models.agents.linear import LINEAR_TRIAGE_AGENT 8 | from app.models.integrations.linear import ( 9 | Label, 10 | Labels, 11 | LinearCreateIssueRequest, 12 | LinearDeleteIssuesRequest, 13 | LinearGetIssuesRequest, 14 | ) 15 | from app.models.query.base import Message, Role 16 | 17 | load_dotenv() 18 | 19 | LINEAR_ACCESS_TOKEN = os.getenv("LINEAR_ACCESS_TOKEN") 20 | 21 | client = LinearClient( 22 | access_token=LINEAR_ACCESS_TOKEN, 23 | ) 24 | 25 | 26 | def main(): 27 | # chat_history: list[Message] = [] 28 | # message = Message( 29 | # role=Role.USER, 30 | # content="I want to update the status of all the issues assigned to huge whale to todo.", 31 | # ).model_dump() 32 | # chat_history.append(message) 33 | # response = AgentResponse(agent=LINEAR_TRIAGE_AGENT, message=message) 34 | # while response.agent: 35 | # response = response.agent.query( 36 | # chat_history=chat_history, 37 | # access_token=LINEAR_ACCESS_TOKEN, 38 | # refresh_token=None, 39 | # client_id=None, 40 | # client_secret=None, 41 | # ) 42 | # chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 43 | # print(chat_history) 44 | print(client.titles()) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | 50 | ### HARD CODE TEST 51 | ## Create Issue 52 | # # TODO: Change the types of some of these (e.g. priority 1 doesnt actually make any sense from a natural language standpoint) 53 | # client.create_issue( 54 | # LinearCreateIssueRequest( 55 | # title="Test Issue", 56 | # description="This is a test issue", 57 | # priority=1, 58 | # estimate=1, 59 | # state=Status.TODO, 60 | # assignee="huge whale", 61 | # creator="huge whale", 62 | # labels=None, 63 | # dueDate=None, 64 | # cycle=1, 65 | # project="Onboarding", 66 | # ) 67 | # ) 68 | # ) 69 | 70 | ## Get Issues 71 | # client.get_issues( 72 | # LinearGetIssueRequest( 73 | # id=None, 74 | # state=None, 75 | # assignee="huge whale", 76 | # creator=None, 77 | # project=None, 78 | # cycle=1, 79 | # ) 80 | # ) 81 | 82 | ## Update Issues 83 | # print(client.update_issues( 84 | # LinearUpdateIssuesRequest( 85 | # filter_conditions=LinearGetIssuesRequest( 86 | # id=None, 87 | # state=None, 88 | # assignee="huge whale", 89 | # creator=None, 90 | # project=None, 91 | # cycle=None, 92 | # labels=None, 93 | # estimate=None 94 | # ), 95 | # update_conditions=LinearCreateIssueRequest( 96 | # title=None, 97 | # description=None, 98 | # priority=None, 99 | # estimate=None, 100 | # state=Status.DONE, 101 | # assignee=None, 102 | # creator=None, 103 | # labels=None, 104 | # dueDate=None, 105 | # cycle=None, 106 | # project=None 107 | # ) 108 | # ) 109 | # )) 110 | 111 | 112 | ### AGENT TEST 113 | # chat_history: list[Message] = [] 114 | # message = Message( 115 | # role=Role.USER, 116 | # content="I want to get all issues in the team that has high priority or above. The state, assignee, creator, project, cycle of the issue can be anything", 117 | # ).model_dump() 118 | # chat_history.append(message) 119 | # response = AgentResponse(agent=LINEAR_TRIAGE_AGENT, message=message) 120 | # while response.agent: 121 | # response = response.agent.query( 122 | # chat_history=chat_history, 123 | # access_token=LINEAR_ACCESS_TOKEN, 124 | # refresh_token=None, 125 | # client_id=None, 126 | # client_secret=None, 127 | # ) 128 | # chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 129 | # print(chat_history) 130 | -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/slack.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from app.connectors.client.slack import SlackClient 6 | from app.models.agents.base.template import Agent, AgentResponse 7 | from app.models.agents.base.triage import TriageAgent 8 | from app.models.agents.main import MAIN_TRIAGE_AGENT 9 | from app.models.integrations.slack import ( 10 | SlackGetChannelIdRequest, 11 | SlackSendMessageRequest, 12 | ) 13 | from app.models.query.base import Message, Role 14 | 15 | load_dotenv() 16 | 17 | SLACK_ACCESS_TOKEN = os.getenv("SLACK_ACCESS_TOKEN") 18 | client = SlackClient(access_token=SLACK_ACCESS_TOKEN) 19 | 20 | 21 | def main(): 22 | # HARD CODE TEST 23 | 24 | ## AGENT TEST 25 | chat_history: list[Message] = [] 26 | message = Message( 27 | role=Role.USER, 28 | content="I am the new intern and I want to send an introductory message to the channel named startup. You can write the introductory message for me and send it directly", 29 | ).model_dump() 30 | chat_history.append(message) 31 | response = AgentResponse(agent=MAIN_TRIAGE_AGENT, message=message) 32 | while response.agent: 33 | prev_agent: Agent = response.agent 34 | response = response.agent.query( 35 | chat_history=chat_history, 36 | access_token=SLACK_ACCESS_TOKEN, 37 | refresh_token=None, 38 | client_id=None, 39 | client_secret=None, 40 | ) 41 | if isinstance(prev_agent, TriageAgent): 42 | continue 43 | chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 44 | print("CHAT HISTORY", chat_history) 45 | print("Final chat history", chat_history) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | 51 | 52 | ### HARD CODE TEST 53 | ## Get all channel ids 54 | # print(client.get_all_channel_ids(request=SlackGetChannelIdRequest(channel_names=["startup"]))) 55 | 56 | 57 | ## Send message 58 | # client.send_message( 59 | # request=SlackSendMessageRequest( 60 | # channel_id="C07JXRTPJPM", 61 | # text="Hey Im Jin Yang" 62 | # ) 63 | # ) 64 | -------------------------------------------------------------------------------- /backend/app/sandbox/integrations/x.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from app.connectors.client.x import XClient 6 | 7 | # from app.models.agents.base.template import Agent, AgentResponse 8 | # from app.models.agents.base.triage import TriageAgent 9 | # from app.models.agents.main import MAIN_TRIAGE_AGENT 10 | # from app.models.integrations.slack import ( 11 | # SlackGetChannelIdRequest, 12 | # SlackSendMessageRequest, 13 | # ) 14 | from app.models.integrations.x import XSendTweetRequest 15 | from app.models.query.base import Message, Role 16 | 17 | load_dotenv() 18 | 19 | X_CLIENT_ID = os.getenv("X_CLIENT_ID") 20 | X_CLIENT_SECRET = os.getenv("X_CLIENT_SECRET") 21 | X_ACCESS_TOKEN = os.getenv("X_ACCESS_TOKEN") 22 | 23 | client = XClient(access_token=X_ACCESS_TOKEN) 24 | 25 | 26 | def main(): 27 | # HARD CODE TEST 28 | client.send_tweet(request=XSendTweetRequest(text="Hello World")) 29 | 30 | ## AGENT TEST 31 | # chat_history: list[Message] = [] 32 | # message = Message( 33 | # role=Role.USER, 34 | # content="I am the new intern and I want to send an introductory message to the channel named startup. You can write the introductory message for me and send it directly", 35 | # ).model_dump() 36 | # chat_history.append(message) 37 | # response = AgentResponse(agent=MAIN_TRIAGE_AGENT, message=message) 38 | # while response.agent: 39 | # prev_agent: Agent = response.agent 40 | # response = response.agent.query( 41 | # chat_history=chat_history, 42 | # access_token=SLACK_ACCESS_TOKEN, 43 | # refresh_token=None, 44 | # client_id=None, 45 | # client_secret=None, 46 | # ) 47 | # if isinstance(prev_agent, TriageAgent): 48 | # continue 49 | # chat_history.append(Message(role=Role.ASSISTANT, content=str(response.message))) 50 | # print("CHAT HISTORY", chat_history) 51 | # print("Final chat history", chat_history) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | 57 | 58 | ### HARD CODE TEST 59 | ## Get all channel ids 60 | # print(client.get_all_channel_ids(request=SlackGetChannelIdRequest(channel_names=["startup"]))) 61 | 62 | 63 | ## Send message 64 | # client.send_message( 65 | # request=SlackSendMessageRequest( 66 | # channel_id="C07JXRTPJPM", 67 | # text="Hey Im Jin Yang" 68 | # ) 69 | # ) 70 | -------------------------------------------------------------------------------- /backend/app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/services/__init__.py -------------------------------------------------------------------------------- /backend/app/services/feedback.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from app.connectors.native.stores.feedback import Feedback, FeedbackORM 4 | from app.connectors.orm import Orm 5 | 6 | orm = Orm() 7 | 8 | 9 | class FeedbackService: 10 | async def post(self, id: Optional[str], feedback: str): 11 | await orm.post( 12 | orm_model=FeedbackORM, 13 | data=[Feedback.local(user_id=id, feedback=feedback).model_dump()], 14 | ) 15 | -------------------------------------------------------------------------------- /backend/app/services/message.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from app.connectors.native.stores.message import Message, MessageORM 5 | from app.connectors.orm import Orm 6 | from app.exceptions.exception import DatabaseError 7 | from app.models.integrations.base import Integration 8 | 9 | orm = Orm() 10 | logging.basicConfig(level=logging.INFO) 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class MessageService: 15 | async def post( 16 | self, 17 | chat_history: list[Message], 18 | api_key: str, 19 | integrations: list[Integration], 20 | instance: Optional[str], 21 | ) -> str: 22 | # This method logs the user chat history and returns the instance uuid 23 | data = Message.local( 24 | chat_history=[msg.model_dump() for msg in chat_history], 25 | api_key=api_key, 26 | integrations=integrations, 27 | instance=instance, 28 | ).model_dump() 29 | if not instance: 30 | log.info("No message entry for this instance. Creating new entry.") 31 | entry: list[Message] = await orm.post( 32 | orm_model=MessageORM, 33 | data=[data], 34 | ) 35 | log.info(f"Message entry posted: {entry}") 36 | if len(entry) != 1: 37 | raise DatabaseError( 38 | "Error creating message: More/Fewer than one message returned" 39 | ) 40 | instance = Message.model_validate(entry[0]).instance 41 | return instance 42 | 43 | log.info("Update message entry for this instance.") 44 | await orm.update( 45 | orm_model=MessageORM, 46 | filters={ 47 | "boolean_clause": "AND", 48 | "conditions": [ 49 | {"column": "instance", "operator": "=", "value": instance} 50 | ], 51 | }, 52 | updated_data={ 53 | "chat_history": data["chat_history"], 54 | "integrations": data["integrations"], 55 | }, 56 | increment_field=None, 57 | ) 58 | return instance 59 | -------------------------------------------------------------------------------- /backend/app/services/token.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from app.connectors.native.stores.token import ( 5 | Token, 6 | TokenORMBase, 7 | create_integration_orm, 8 | ) 9 | from app.connectors.orm import Orm 10 | from app.exceptions.exception import DatabaseError 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | log = logging.getLogger(__name__) 14 | 15 | orm = Orm() 16 | 17 | 18 | class TokenService: 19 | 20 | async def post( 21 | self, 22 | api_key: str, 23 | access_token: str, 24 | refresh_token: str, 25 | client_id: str, 26 | client_secret: str, 27 | table_name: str, 28 | ): 29 | TokenORM = create_integration_orm(table_name=table_name) 30 | 31 | await orm.post( 32 | orm_model=TokenORM, 33 | data=[ 34 | Token.local( 35 | api_key=api_key, 36 | access_token=access_token, 37 | refresh_token=refresh_token, 38 | client_id=client_id, 39 | client_secret=client_secret, 40 | ).model_dump() 41 | ], 42 | ) 43 | 44 | async def get(self, api_key: str, table_name: str) -> Optional[Token]: 45 | TokenORM = create_integration_orm(table_name=table_name) 46 | 47 | result: list[Token] = await orm.get( 48 | orm_model=TokenORM, 49 | pydantic_model=Token, 50 | filters={ 51 | "boolean_clause": "AND", 52 | "conditions": [ 53 | {"column": "api_key", "operator": "=", "value": api_key} 54 | ], 55 | }, 56 | ) 57 | if len(result) > 1: 58 | raise DatabaseError( 59 | f"User with api key {api_key} has more than one set of tokens in the {table_name} table" 60 | ) 61 | elif len(result) == 0: 62 | log.info( 63 | f"User with api key {api_key} not found in the {table_name} token table" 64 | ) 65 | return None 66 | 67 | return result[0] 68 | 69 | async def update( 70 | self, 71 | id: str, 72 | access_token: str, 73 | refresh_token: str, 74 | client_id: str, 75 | client_secret: str, 76 | table_name: str, 77 | ): 78 | TokenORM = create_integration_orm(table_name=table_name) 79 | await orm.update( 80 | orm_model=TokenORM, 81 | filters={ 82 | "boolean_clause": "AND", 83 | "conditions": [{"column": "id", "operator": "=", "value": id}], 84 | }, 85 | updated_data={ 86 | "access_token": access_token, 87 | "refresh_token": refresh_token, 88 | "client_id": client_id, 89 | "client_secret": client_secret, 90 | }, 91 | increment_field=None, 92 | ) 93 | -------------------------------------------------------------------------------- /backend/app/services/user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Optional 3 | 4 | from app.connectors.native.stores.user import User, UserORM 5 | from app.connectors.orm import Orm 6 | from app.exceptions.exception import DatabaseError 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | log = logging.getLogger(__name__) 10 | 11 | orm = Orm() 12 | 13 | 14 | class UserService: 15 | 16 | async def login(self, id: str, name: str, email: str) -> User: 17 | result: list[User] = await orm.get( 18 | orm_model=UserORM, 19 | pydantic_model=User, 20 | filters={ 21 | "boolean_clause": "AND", 22 | "conditions": [{"column": "id", "operator": "=", "value": id}], 23 | }, 24 | ) 25 | if len(result) < 1: 26 | log.info( 27 | f"User with Clerk ID {id} not found...initiating storage in database" 28 | ) 29 | created_user: list[dict[str, Any]] = await orm.post( 30 | orm_model=UserORM, 31 | data=[ 32 | User.local( 33 | id=id, 34 | name=name, 35 | email=email, 36 | usage=0, 37 | ).model_dump() 38 | ], 39 | ) 40 | if len(created_user) != 1: 41 | raise DatabaseError("Error creating user: More than one user returned") 42 | return User.model_validate(created_user[0]) 43 | 44 | return result[0] 45 | 46 | async def get(self, api_key: str) -> User: 47 | result: list[User] = await orm.get( 48 | orm_model=UserORM, 49 | pydantic_model=User, 50 | filters={ 51 | "boolean_clause": "AND", 52 | "conditions": [ 53 | {"column": "api_key", "operator": "=", "value": api_key} 54 | ], 55 | }, 56 | ) 57 | if len(result) < 1: 58 | raise ValueError("Invalid Controller API key") 59 | elif len(result) > 1: 60 | raise ValueError("Multiple users found with the same API key") 61 | return result[0] 62 | 63 | async def increment_usage(self, api_key: str): 64 | await orm.update( 65 | orm_model=UserORM, 66 | filters={ 67 | "boolean_clause": "AND", 68 | "conditions": [ 69 | {"column": "api_key", "operator": "=", "value": api_key} 70 | ], 71 | }, 72 | updated_data=None, 73 | increment_field="usage", 74 | ) 75 | -------------------------------------------------------------------------------- /backend/app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/app/utils/__init__.py -------------------------------------------------------------------------------- /backend/app/utils/levenshtein.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from Levenshtein import distance, ratio 4 | 5 | # TODO: We can make this threshold specific to each integration 6 | THRESHOLD = 0.4 # For more strict matching, set the threshold HIGHER 7 | 8 | 9 | def _process_string(input: str) -> str: 10 | """Strips all insignificant characters from the input string before initiating best match search""" 11 | 12 | # Standardise casing 13 | input = input.lower() 14 | 15 | # Remove all spaces 16 | input = input.replace(" ", "") 17 | 18 | return input 19 | 20 | 21 | def get_most_similar_string(target: str, candidates: list[str]) -> str: 22 | """Returns the most similar string from the candidates list to the target""" 23 | most_similar: str = min( 24 | candidates, 25 | key=lambda candidate: distance( 26 | _process_string(candidate), _process_string(target) 27 | ), 28 | ) 29 | 30 | if ratio(most_similar, target) < THRESHOLD: 31 | return None 32 | return most_similar 33 | -------------------------------------------------------------------------------- /backend/app/utils/tools.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | import logging 4 | 5 | logging.basicConfig(level=logging.INFO) 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | def execute_tool_call(tool_call, tools, agent_name): 11 | name = tool_call.function.name 12 | args = json.loads(tool_call.function.arguments) 13 | 14 | log.info(f"{agent_name}: {name}") 15 | 16 | return tools[name](**args) # call corresponding function with provided arguments 17 | 18 | 19 | def function_to_schema(func) -> dict: 20 | type_map = { 21 | str: "string", 22 | int: "integer", 23 | float: "number", 24 | bool: "boolean", 25 | list: "array", 26 | dict: "object", 27 | type(None): "null", 28 | } 29 | 30 | try: 31 | signature = inspect.signature(func) 32 | except ValueError as e: 33 | raise ValueError( 34 | f"Failed to get signature for function {func.__name__}: {str(e)}" 35 | ) 36 | 37 | parameters = {} 38 | for param in signature.parameters.values(): 39 | try: 40 | param_type = type_map.get(param.annotation, "string") 41 | except KeyError as e: 42 | raise KeyError( 43 | f"Unknown type annotation {param.annotation} for parameter {param.name}: {str(e)}" 44 | ) 45 | parameters[param.name] = {"type": param_type} 46 | 47 | required = [ 48 | param.name 49 | for param in signature.parameters.values() 50 | if param.default == inspect._empty 51 | ] 52 | 53 | return { 54 | "type": "function", 55 | "function": { 56 | "name": func.__name__, 57 | "description": func.__doc__ or "", 58 | "parameters": { 59 | "type": "object", 60 | "properties": parameters, 61 | "required": required, 62 | }, 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /backend/docker/development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.1-slim 2 | 3 | # Set working directory 4 | WORKDIR /app 5 | 6 | # Install Poetry 7 | RUN pip install --no-cache-dir poetry 8 | 9 | # Copy pyproject.toml and poetry.lock 10 | COPY pyproject.toml poetry.lock ./ 11 | 12 | # Install dependencies 13 | RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 14 | 15 | # Copy the rest of the code 16 | COPY . . 17 | 18 | # Expose port 8080 19 | EXPOSE 8080 20 | 21 | # Start Uvicorn server 22 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] 23 | -------------------------------------------------------------------------------- /backend/images/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/images/__init__.py -------------------------------------------------------------------------------- /backend/images/supabase_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/images/supabase_connect.png -------------------------------------------------------------------------------- /backend/images/supabase_copy_uri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/images/supabase_copy_uri.png -------------------------------------------------------------------------------- /backend/images/supabase_create_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/images/supabase_create_project.png -------------------------------------------------------------------------------- /backend/images/supabase_transaction_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/backend/images/supabase_transaction_mode.png -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["jinyang628 "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "3.12.1" 10 | pydantic = "^2.8.2" 11 | requests = "^2.32.3" 12 | python-dotenv = "^1.0.1" 13 | openai = "^1.40.3" 14 | black = "^24.8.0" 15 | isort = "^5.13.2" 16 | gql = {extras = ["requests"], version = "^3.5.0"} 17 | fastapi = "^0.112.0" 18 | uvicorn = "^0.30.5" 19 | sqlalchemy = "^2.0.32" 20 | asyncpg = "^0.29.0" 21 | psycopg2-binary = "^2.9.9" 22 | supabase = "^2.6.0" 23 | greenlet = "^3.0.3" 24 | google-auth-oauthlib = "^1.2.1" 25 | google-auth-httplib2 = "^0.2.0" 26 | google-api-python-client = "^2.141.0" 27 | slack-sdk = "^3.31.0" 28 | python-twitter-v2 = "^0.9.1" 29 | tweepy = "^4.14.0" 30 | python-levenshtein = "^0.26.0" 31 | 32 | [tool.isort] 33 | profile = "black" 34 | 35 | [build-system] 36 | requires = ["poetry-core"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | networks: 4 | controller_network: 5 | driver: bridge 6 | 7 | services: 8 | backend: 9 | build: 10 | context: ./backend 11 | dockerfile: docker/development/Dockerfile 12 | networks: 13 | - controller_network 14 | container_name: backend_container 15 | ports: 16 | - "8080:8080" 17 | 18 | frontend: 19 | build: 20 | context: ./frontend 21 | dockerfile: docker/development/Dockerfile 22 | networks: 23 | - controller_network 24 | container_name: frontend_container 25 | ports: 26 | - "3000:3000" 27 | environment: 28 | - NEXT_PUBLIC_BACKEND_URL=http://backend_container:8080 29 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | # No quotation marks 2 | NEXT_PUBLIC_DEFAULT_SITE_URL=http://localhost:3000 3 | NEXT_PUBLIC_BACKEND_URL=http://0.0.0.0:8080 4 | NEXT_PUBLIC_DOCS_URL=https://controller-docs.vercel.app/ 5 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 6 | CLERK_SECRET_KEY= -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "next/core-web-vitals", 7 | "prettier", 8 | ], 9 | ignorePatterns: [ 10 | "node_modules/", 11 | "dist/", 12 | "src/components/ui/*.tsx", 13 | "src/components/ui/*.ts", 14 | ], 15 | rules: { 16 | curly: "error", 17 | "no-shadow": "error", 18 | "no-nested-ternary": "error", 19 | "@typescript-eslint/no-unused-vars": "warn", 20 | "no-restricted-exports": [ 21 | "error", 22 | { 23 | restrictDefaultExports: { 24 | direct: false, 25 | named: true, 26 | defaultFrom: true, 27 | namedFrom: true, 28 | namespaceFrom: true, 29 | }, 30 | }, 31 | ], 32 | "react/jsx-sort-props": [ 33 | "error", 34 | { 35 | noSortAlphabetically: true, 36 | shorthandLast: true, 37 | callbacksLast: true, 38 | }, 39 | ], 40 | "react/no-array-index-key": "warn", 41 | "react/no-danger": "warn", 42 | "react/self-closing-comp": "error", 43 | "react/function-component-definition": [ 44 | "warn", 45 | { 46 | namedComponents: "function-declaration", 47 | unnamedComponents: "arrow-function", 48 | }, 49 | ], 50 | "jsx-a11y/alt-text": "error", 51 | "import/no-extraneous-dependencies": [ 52 | "error", 53 | { 54 | packageDir: __dirname, 55 | }, 56 | ], 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /frontend/.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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /frontend/.jest/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import fetchMock from "jest-fetch-mock"; 3 | 4 | // Mock ResizeObserver 5 | class ResizeObserver { 6 | observe() { 7 | // do nothing 8 | } 9 | unobserve() { 10 | // do nothing 11 | } 12 | disconnect() { 13 | // do nothing 14 | } 15 | } 16 | 17 | global.ResizeObserver = ResizeObserver; 18 | fetchMock.enableMocks(); 19 | -------------------------------------------------------------------------------- /frontend/.jest/setEnvVars.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/.jest/setEnvVars.ts -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | ## Getting Started 4 | 5 | Make sure your terminal is at the root of frontend 6 | 7 | Copy the existing environment template file 8 | 9 | ```bash 10 | # Create .env file (by copying from .env.example) 11 | cp .env.example .env 12 | ``` 13 | 14 | Set up Clerk environment variables in the `.env` file 15 | 16 | 1. Navigate to [Clerk](https://clerk.com/docs/quickstarts/nextjs#set-your-environment-variables) and complete step 2 in the instruction manual 17 | ![Set Clerk Environment Variables](./images/clerk_environment_variables.png) 18 | 19 | 2. Your `.env` (NOT `.env.local`) file should have the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` variables populated with **no inverted commas** 20 | 21 | ### Useful commands for Development (Not necessary unless you're a Chad and want to contribute) 22 | 23 | Install the dependencies at the root of `frontend` 24 | 25 | ```bash 26 | npm i 27 | ``` 28 | 29 | Run the development server at the root of `frontend` 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 35 | Use local tunnel to test certain authentication flows 36 | 37 | ``` 38 | npm install -g localtunnel 39 | lt --port 3000 40 | ``` 41 | -------------------------------------------------------------------------------- /frontend/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": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/docker/development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | RUN apk add --no-cache libc6-compat 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json ./ 8 | COPY package-lock.json ./ 9 | COPY .env ./ 10 | 11 | RUN npm ci 12 | 13 | COPY *.ts ./ 14 | COPY *.mjs ./ 15 | COPY *.js ./ 16 | COPY tsconfig.json ./ 17 | COPY components.json ./ 18 | COPY src ./src 19 | COPY public ./public 20 | 21 | ENV NODE_ENV=development 22 | 23 | EXPOSE 3000 24 | 25 | CMD ["npm", "run", "dev"] 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/images/clerk_environment_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/images/clerk_environment_variables.png -------------------------------------------------------------------------------- /frontend/jest.config.ts: -------------------------------------------------------------------------------- 1 | import nextJest from "next/jest.js"; 2 | 3 | import type { Config } from "jest"; 4 | 5 | const createJestConfig = nextJest({ 6 | dir: "./", 7 | }); 8 | 9 | const config: Config = { 10 | clearMocks: true, 11 | collectCoverage: true, 12 | coverageDirectory: "coverage", 13 | coverageProvider: "v8", 14 | verbose: true, 15 | preset: "ts-jest", 16 | testEnvironment: "jest-environment-jsdom", 17 | testPathIgnorePatterns: ["/node_modules/", "/e2e/"], 18 | setupFiles: ["/.jest/setEnvVars.ts"], 19 | setupFilesAfterEnv: ["/.jest/jest.setup.ts"], 20 | }; 21 | 22 | export default createJestConfig(config); 23 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | env: { 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: "/sign-in", 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL: "/sign-up", 6 | NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL: "/", 7 | NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL: "/", 8 | }, 9 | async headers() { 10 | return [ 11 | { 12 | source: "/(.*)", 13 | headers: [ 14 | { 15 | key: "Content-Security-Policy", 16 | value: "frame-ancestors 'self'", 17 | }, 18 | ], 19 | }, 20 | ]; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controller-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "@clerk/nextjs": "^5.3.1", 14 | "@clerk/themes": "^2.1.20", 15 | "@radix-ui/react-avatar": "^1.1.0", 16 | "@radix-ui/react-checkbox": "^1.1.1", 17 | "@radix-ui/react-dialog": "^1.1.1", 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-label": "^2.1.0", 20 | "@radix-ui/react-scroll-area": "^1.1.0", 21 | "@radix-ui/react-slot": "^1.1.0", 22 | "@radix-ui/react-toast": "^1.2.1", 23 | "@tanstack/react-query": "^5.51.23", 24 | "axios": "^1.7.4", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "fuse.js": "^7.0.0", 28 | "jest-fetch-mock": "^3.0.3", 29 | "json-extract": "^0.1.1", 30 | "loaders.css": "^0.1.2", 31 | "lucide-react": "^0.428.0", 32 | "next": "14.2.5", 33 | "next-themes": "^0.3.0", 34 | "posthog-js": "^1.160.1", 35 | "react": "^18", 36 | "react-dom": "^18", 37 | "react-hook-form": "^7.52.2", 38 | "react-icons": "^5.3.0", 39 | "react-loaders": "^3.0.1", 40 | "react-markdown": "^9.0.1", 41 | "sass": "^1.78.0", 42 | "tailwind-merge": "^2.5.2", 43 | "tailwindcss-animate": "^1.0.7", 44 | "ts-jest": "^29.2.4", 45 | "uuid": "^10.0.0", 46 | "zod": "^3.23.8", 47 | "zustand": "^4.5.5" 48 | }, 49 | "devDependencies": { 50 | "@hookform/resolvers": "^3.9.0", 51 | "@testing-library/dom": "^10.4.0", 52 | "@testing-library/jest-dom": "^6.4.8", 53 | "@testing-library/react": "^16.0.1", 54 | "@types/jest": "^29.5.12", 55 | "@types/node": "^20", 56 | "@types/react": "^18", 57 | "@types/react-dom": "^18", 58 | "@types/uuid": "^10.0.0", 59 | "@typescript-eslint/eslint-plugin": "^6.21.0", 60 | "eslint": "^8.57.0", 61 | "eslint-config-next": "^13.5.6", 62 | "eslint-config-prettier": "^8.10.0", 63 | "jest": "^29.7.0", 64 | "jest-environment-jsdom": "^29.7.0", 65 | "postcss": "^8", 66 | "tailwindcss": "^3.4.1", 67 | "typescript": "^5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/public/assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/public/assistant.png -------------------------------------------------------------------------------- /frontend/public/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/public/placeholder.png -------------------------------------------------------------------------------- /frontend/src/actions/feedback/submit.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { FeedbackRequest } from "@/types/actions/feedback/form"; 4 | 5 | import axios from "axios"; 6 | 7 | const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL; 8 | const SERVICE_ENDPOINT = "api/feedback"; 9 | 10 | export async function submitFeedback(input: FeedbackRequest) { 11 | try { 12 | await axios.post(`${BACKEND_URL}/${SERVICE_ENDPOINT}`, input); 13 | } catch (error) { 14 | console.error(error); 15 | throw error; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/actions/query/base.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { 4 | QueryRequest, 5 | queryResponseSchema, 6 | QueryResponse, 7 | } from "@/types/actions/query/base"; 8 | 9 | import axios from "axios"; 10 | 11 | const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL; 12 | const SERVICE_ENDPOINT = "api/query"; 13 | 14 | export async function query(input: QueryRequest): Promise { 15 | try { 16 | const response = await axios.post( 17 | `${BACKEND_URL}/${SERVICE_ENDPOINT}`, 18 | input, 19 | { timeout: 40000 }, 20 | ); 21 | const queryResponse = queryResponseSchema.parse(response.data); 22 | 23 | return queryResponse; 24 | } catch (error) { 25 | console.error("Error in query endpoint"); 26 | throw error; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/actions/query/confirm.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { ConfirmRequest } from "@/types/actions/query/confirm"; 4 | import { QueryResponse, queryResponseSchema } from "@/types/actions/query/base"; 5 | 6 | import axios from "axios"; 7 | 8 | const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL; 9 | const SERVICE_ENDPOINT = "api/query/confirm"; 10 | 11 | export async function confirmExecution( 12 | input: ConfirmRequest, 13 | ): Promise { 14 | try { 15 | const response = await axios.post( 16 | `${BACKEND_URL}/${SERVICE_ENDPOINT}`, 17 | input, 18 | { timeout: 40000 }, 19 | ); 20 | const queryResponse = queryResponseSchema.parse(response.data); 21 | 22 | return queryResponse; 23 | } catch (error) { 24 | console.error("Error in query endpoint"); 25 | throw error; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/actions/token.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { 4 | TokenGetRequest, 5 | tokenGetResponseSchema, 6 | TokenGetResponse, 7 | } from "@/types/actions/token"; 8 | 9 | import axios from "axios"; 10 | 11 | const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL; 12 | const SERVICE_ENDPOINT = "api/token"; 13 | 14 | export async function isUserAuthenticated( 15 | input: TokenGetRequest, 16 | ): Promise { 17 | try { 18 | const response = await axios.get(`${BACKEND_URL}/${SERVICE_ENDPOINT}`, { 19 | params: input, 20 | }); 21 | const tokenResponse = tokenGetResponseSchema.parse(response.data); 22 | 23 | return tokenResponse; 24 | } catch (error) { 25 | console.error(error); 26 | throw error; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/actions/user/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { 4 | LoginRequest, 5 | loginResponseSchema, 6 | LoginResponse, 7 | } from "@/types/actions/user/login"; 8 | 9 | import axios from "axios"; 10 | 11 | const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL; 12 | const SERVICE_ENDPOINT = "api/user/login"; 13 | 14 | export async function login(input: LoginRequest): Promise { 15 | try { 16 | const response = await axios.post( 17 | `${BACKEND_URL}/${SERVICE_ENDPOINT}`, 18 | input, 19 | ); 20 | const loginResponse = loginResponseSchema.parse(response.data); 21 | 22 | return loginResponse; 23 | } catch (error) { 24 | console.error(error); 25 | throw error; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
{children}
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SignIn } from "@clerk/nextjs"; 3 | import { useTheme } from "next-themes"; 4 | import { dark } from "@clerk/themes"; 5 | 6 | export default function SignInPage() { 7 | const { resolvedTheme } = useTheme(); 8 | 9 | return ( 10 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { dark } from "@clerk/themes"; 4 | import { useTheme } from "next-themes"; 5 | import { SignUp } from "@clerk/nextjs"; 6 | 7 | export default function SignUpPage() { 8 | const { resolvedTheme } = useTheme(); 9 | 10 | return ( 11 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/api/oauth2/callback/route.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { NextResponse } from "next/server"; 3 | import { cookies } from "next/headers"; 4 | import { tokenPostRequestSchema } from "@/types/api/token"; 5 | 6 | export async function GET(request: Request) { 7 | const { searchParams } = new URL(request.url); 8 | 9 | const state = searchParams.get("state"); 10 | const storedState = cookies().get("authState")?.value; 11 | const storedClientId = cookies().get("clientId")?.value; 12 | const storedClientSecret = cookies().get("clientSecret")?.value; 13 | const storedControllerApiKey = cookies().get("controllerApiKey")?.value; 14 | const storedExchangeBase = cookies().get("exchangeBase")?.value || ""; 15 | const storedTableName = cookies().get("tableName")?.value; 16 | const storedRedirectUri = cookies().get("redirect_uri")?.value; 17 | const storedCodeVerifier = cookies().get("code_verifier")?.value; 18 | const storedVerifierRequired = cookies().get("verifierRequired")?.value; 19 | 20 | if (!state || state !== storedState) { 21 | return NextResponse.json( 22 | { error: "Invalid state parameter" }, 23 | { status: 400 }, 24 | ); 25 | } 26 | 27 | cookies().delete("authState"); 28 | 29 | const code = searchParams.get("code"); 30 | 31 | if (!code) { 32 | return NextResponse.json( 33 | { error: "Authorization code is missing" }, 34 | { status: 400 }, 35 | ); 36 | } 37 | 38 | try { 39 | const params = new URLSearchParams(); 40 | 41 | params.append("code", code); 42 | params.append("redirect_uri", storedRedirectUri as string); 43 | params.append("grant_type", "authorization_code"); 44 | if (storedVerifierRequired === "true") { 45 | params.append("code_verifier", storedCodeVerifier as string); 46 | } 47 | 48 | const credentials = btoa(`${storedClientId}:${storedClientSecret}`); 49 | 50 | console.log("STORED EXCHANGE BASE"); 51 | console.log(storedExchangeBase); 52 | 53 | const response = await axios.post(storedExchangeBase, params, { 54 | headers: { 55 | "Content-Type": "application/x-www-form-urlencoded", 56 | Authorization: `Basic ${credentials}`, 57 | }, 58 | }); 59 | 60 | console.log("RESPONSE"); 61 | console.log(response); 62 | 63 | const tokenData = response.data; 64 | const accessToken: string = tokenData.access_token; 65 | const refreshToken: string | null = tokenData.refresh_token || null; 66 | 67 | console.log("TOKEN DATA"); 68 | console.log(tokenData); 69 | 70 | const parsedTokenRequest = tokenPostRequestSchema.parse({ 71 | api_key: storedControllerApiKey, 72 | access_token: accessToken, 73 | refresh_token: refreshToken, 74 | client_id: storedClientId, 75 | client_secret: storedClientSecret, 76 | table_name: storedTableName, 77 | }); 78 | 79 | console.log("PARSED TOKEN REQUEST"); 80 | console.log(parsedTokenRequest); 81 | 82 | await axios.post( 83 | `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/token`, 84 | parsedTokenRequest, 85 | ); 86 | 87 | console.log("BACKEND REQUEST COMPLETED"); 88 | 89 | return NextResponse.redirect(`${process.env.NEXT_PUBLIC_DEFAULT_SITE_URL}`); 90 | } catch (error) { 91 | if (axios.isAxiosError(error)) { 92 | console.error( 93 | "Token exchange error:", 94 | error.response?.data || error.message, 95 | ); 96 | } else { 97 | console.error("Token exchange error:", error); 98 | } 99 | 100 | return NextResponse.json( 101 | { error: "Failed to retrieve access token" }, 102 | { status: 500 }, 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/app/api/oauth2/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | import { cookies } from "next/headers"; 4 | 5 | export async function GET(request: NextRequest) { 6 | const { searchParams } = new URL(request.url); 7 | const clientId = searchParams.get("clientId"); 8 | 9 | // When Google redirects back to your callback URL with an authorization code, you use both the client ID and client secret to exchange this code for access and refresh tokens. 10 | const clientSecret = searchParams.get("clientSecret"); 11 | 12 | // We need the apiKey of the user so that we know which user to associate the access token and refresh token with 13 | const controllerApiKey = searchParams.get("controllerApiKey") || ""; 14 | 15 | // We need to know which scopes the user has authorized 16 | const scope = searchParams.get("scope") || ""; 17 | 18 | // We need to know which table to POST the tokens to 19 | const tableName = searchParams.get("tableName") || ""; 20 | 21 | // We need to know which is the base url of the specific integration we are initiating login with 22 | const loginBase = searchParams.get("loginBase") || ""; 23 | 24 | // We need to know which is the base url of the specific integration we are initiating token exchange with 25 | const exchangeBase = searchParams.get("exchangeBase") || ""; 26 | 27 | const code_verifier = searchParams.get("code_verifier") || ""; 28 | const code_challenge = searchParams.get("code_challenge") || ""; 29 | const code_challenge_method = searchParams.get("code_challenge_method") || ""; 30 | const redirect_uri = searchParams.get("redirect_uri") || ""; 31 | const verifierRequired = searchParams.get("verifierRequired") || ""; 32 | 33 | if (!clientId || !clientSecret) { 34 | return NextResponse.json( 35 | { error: "Client ID and Client Secret are required" }, 36 | { status: 400 }, 37 | ); 38 | } 39 | 40 | const state = uuidv4(); 41 | cookies().set("authState", state, { httpOnly: true, secure: true }); 42 | cookies().set("clientId", clientId, { httpOnly: true, secure: true }); 43 | cookies().set("clientSecret", clientSecret, { httpOnly: true, secure: true }); 44 | cookies().set("controllerApiKey", controllerApiKey, { 45 | httpOnly: true, 46 | secure: true, 47 | }); 48 | cookies().set("tableName", tableName, { httpOnly: true, secure: true }); 49 | cookies().set("exchangeBase", exchangeBase, { httpOnly: true, secure: true }); 50 | cookies().set("redirect_uri", redirect_uri, { httpOnly: true, secure: true }); 51 | cookies().set("code_challenge", code_challenge, { 52 | httpOnly: true, 53 | secure: true, 54 | }); 55 | cookies().set("code_verifier", code_verifier, { 56 | httpOnly: true, 57 | secure: true, 58 | }); 59 | cookies().set("verifierRequired", verifierRequired, { 60 | httpOnly: true, 61 | secure: true, 62 | }); 63 | 64 | const authUrl = new URL(loginBase); 65 | authUrl.searchParams.append("response_type", "code"); 66 | authUrl.searchParams.append("client_id", clientId); 67 | authUrl.searchParams.append("redirect_uri", redirect_uri); 68 | authUrl.searchParams.append("scope", scope); 69 | authUrl.searchParams.append("state", state); 70 | authUrl.searchParams.append("access_type", "offline"); 71 | authUrl.searchParams.append("prompt", "consent"); 72 | authUrl.searchParams.append("code_challenge", code_challenge); 73 | authUrl.searchParams.append("code_challenge_method", code_challenge_method); 74 | 75 | // VERY IMPORTANT: Replace all '+' with '%20' in the authUrl 76 | const formattedAuthUrl = authUrl.toString().replace(/\+/g, "%20"); 77 | 78 | console.log("FORMATTED AUTH URL"); 79 | console.log(formattedAuthUrl); 80 | 81 | return NextResponse.redirect(formattedAuthUrl); 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/src/app/apple-icon.png -------------------------------------------------------------------------------- /frontend/src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import IntegrationAuth from "@/components/integration-auth"; 4 | import { integrationEnum } from "@/types/integration"; 5 | import ApiKey from "@/components/api-key"; 6 | import { useState } from "react"; 7 | import { Integration } from "@/types/integration"; 8 | 9 | export default function HomePage() { 10 | const [apiKey, setApiKey] = useState(""); 11 | const updateApiKey = (newApiKey: string) => { 12 | setApiKey(newApiKey); 13 | }; 14 | 15 | return ( 16 | <> 17 |
18 | 19 |
20 |
21 | {Object.values(integrationEnum.Values).map((integration) => ( 22 | 27 | ))} 28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giga-controller/controller/f15c6a1aac877ed3c70b935b2fce8d2fd905ff5c/frontend/src/app/icon.png -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { 4 | ClerkLoaded, 5 | ClerkLoading, 6 | ClerkProvider, 7 | SignedIn, 8 | } from "@clerk/nextjs"; 9 | import "@/styles/globals.css"; 10 | import { ThemeProvider } from "@/components/shared/theme/provider"; 11 | import PageLoader from "@/components/shared/page-loading-indicator"; 12 | import { QueryProvider } from "@/components/shared/query-provider"; 13 | import { Toaster } from "@/components/ui/toaster"; 14 | import HeaderButtons from "@/components/shared/header/buttons"; 15 | 16 | const inter = Inter({ subsets: ["latin"] }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Controller", 20 | description: 21 | "Controller seeks to build application connectors powered by natural language inputs.", 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 32 | 33 | 34 | 40 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 |
63 | 64 | {children} 65 | 66 | 67 |
68 |
69 | 70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { ROUTE } from "@/constants/route"; 5 | 6 | import "@/styles/globals.css"; 7 | 8 | export default function NotFound() { 9 | return ( 10 |
11 |
12 |

13 | Oops! Page not found. We are testing in production :( 14 |

15 |

16 | Maybe return to the{" "} 17 | 21 | home page 22 | 23 | ? 24 |

25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/accessory/loader.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | export default function Loader() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/accessory/shimmer.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "@/components/accessory/loader"; 2 | 3 | export default function Shimmer() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/api-key.tsx: -------------------------------------------------------------------------------- 1 | import { Copy } from "lucide-react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Label } from "@radix-ui/react-label"; 4 | import { useCallback, useMemo } from "react"; 5 | import Shimmer from "@/components/accessory/shimmer"; 6 | import { Input } from "@/components/ui/input"; 7 | import { login } from "@/actions/user/login"; 8 | import { loginRequestSchema } from "@/types/actions/user/login"; 9 | import { useQuery } from "@tanstack/react-query"; 10 | import { useUser } from "@clerk/nextjs"; 11 | import { API_KEY_QUERY_KEY } from "@/constants/keys"; 12 | import { handleCopy } from "@/lib/utils"; 13 | 14 | type ApiKeyProps = { 15 | apiKey: string; 16 | updateApiKey: (apiKey: string) => void; 17 | }; 18 | 19 | export default function ApiKey({ apiKey, updateApiKey }: ApiKeyProps) { 20 | const { user, isLoaded } = useUser(); 21 | const { isLoading } = useQuery({ 22 | queryKey: [API_KEY_QUERY_KEY, user?.id], 23 | queryFn: async () => { 24 | if (!isLoaded || !user) { 25 | return null; 26 | } 27 | const parsedLoginRequest = loginRequestSchema.parse({ 28 | id: user.id, 29 | name: user.firstName, 30 | email: user.primaryEmailAddress?.emailAddress, 31 | }); 32 | const response = await login(parsedLoginRequest); 33 | updateApiKey(response.api_key); 34 | }, 35 | enabled: isLoaded && !!user, 36 | staleTime: 5 * 60 * 1000, 37 | refetchOnWindowFocus: false, 38 | }); 39 | const copyApiKey = useCallback(() => { 40 | if (apiKey) { 41 | handleCopy(apiKey, API_KEY_QUERY_KEY); 42 | } 43 | }, [apiKey]); 44 | const maskedApiKey = useMemo(() => { 45 | if (!apiKey) { 46 | return ""; 47 | } 48 | 49 | return `${apiKey.slice(0, 4)}${"*".repeat(apiKey.length - 4)}`; 50 | }, [apiKey]); 51 | 52 | return ( 53 | <> 54 | 57 |
58 | {isLoading ? : } 59 | 62 |
63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/calendar.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function GoogleCalendarAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/docs.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function GoogleDocsAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/gmail.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function GmailAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/linear.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function LinearAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/outlook.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function MicrosoftOutlookAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/routing-base.tsx: -------------------------------------------------------------------------------- 1 | import { Integration, integrationEnum } from "@/types/integration"; 2 | import GmailAuthDialogContent from "@/components/dialog-content/gmail"; 3 | import GoogleCalendarAuthDialogContent from "@/components/dialog-content/calendar"; 4 | import LinearAuthDialogContent from "@/components/dialog-content/linear"; 5 | import SlackAuthDialogContent from "@/components/dialog-content/slack"; 6 | // import MicrosoftOutlookAuthDialogContent from "@/components/dialog-content/outlook"; 7 | import XAuthDialogContent from "@/components/dialog-content/x"; 8 | import GoogleDocsAuthDialogContent from "@/components/dialog-content/docs"; 9 | import GoogleSheetsAuthDialogContent from "@/components/dialog-content/sheets"; 10 | // import GoogleSheetsAuthDialogContent from "@/components/dialog-content/sheets"; 11 | 12 | type RoutingAuthDialogContentProps = { 13 | apiKey: string; 14 | integration: Integration; 15 | }; 16 | 17 | export default function RoutingAuthDialogContent({ 18 | apiKey, 19 | integration, 20 | }: RoutingAuthDialogContentProps) { 21 | let dialogContent = null; 22 | switch (integration) { 23 | case integrationEnum.Values.gmail: 24 | dialogContent = ( 25 | 30 | ); 31 | break; 32 | case integrationEnum.Values.linear: 33 | dialogContent = ( 34 | 39 | ); 40 | break; 41 | case integrationEnum.Values.slack: 42 | dialogContent = ( 43 | 48 | ); 49 | break; 50 | case integrationEnum.Values.x: 51 | dialogContent = ( 52 | 57 | ); 58 | break; 59 | case integrationEnum.Values.calendar: 60 | dialogContent = ( 61 | 66 | ); 67 | break; 68 | case integrationEnum.Values.docs: 69 | dialogContent = ( 70 | 75 | ); 76 | break; 77 | // case integrationEnum.Values.sheets: 78 | // dialogContent = ( 79 | // 84 | // ); 85 | // break; 86 | // case integrationEnum.Values.outlook: 87 | // dialogContent = ( 88 | // 93 | // ); 94 | // break; 95 | } 96 | return dialogContent; 97 | } 98 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/sheets.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function GoogleSheetsAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/slack.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function SlackAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/dialog-content/x.tsx: -------------------------------------------------------------------------------- 1 | import { AuthParamProps, integrationEnum } from "@/types/integration"; 2 | import AuthDialogContent from "@/components/dialog-content/auth-base"; 3 | import { capitaliseFirstLetter } from "@/lib/utils"; 4 | 5 | export default function XAuthDialogContent({ 6 | apiKey, 7 | loginBase, 8 | exchangeBase, 9 | }: AuthParamProps) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/home/chat/clear-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Message } from "@/types/actions/query/base"; 3 | 4 | type ClearButtonProps = { 5 | updateChatHistory: (newChatHistory: Message[]) => void; 6 | updateInstance: (instance: string | null) => void; 7 | }; 8 | 9 | export default function ClearButton({ 10 | updateChatHistory, 11 | updateInstance, 12 | }: ClearButtonProps) { 13 | return ( 14 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/home/chat/verification-checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@/components/ui/checkbox"; 2 | import { Label } from "@/components/ui/label"; 3 | 4 | type VerificationCheckboxProps = { 5 | enableVerification: boolean; 6 | updateEnableVerification: (input: boolean) => void; 7 | }; 8 | 9 | export default function VerificationCheckbox({ 10 | enableVerification, 11 | updateEnableVerification, 12 | }: VerificationCheckboxProps) { 13 | return ( 14 |
15 | 19 | updateEnableVerification(checked === true) 20 | } 21 | /> 22 |
23 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/home/input-container.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "@/components/accessory/loader"; 2 | import VerificationOption from "@/components/home/input/verification-option"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Textarea } from "@/components/ui/textarea"; 5 | import { userVerificationSchema } from "@/types/actions/query/confirm"; 6 | import { useEffect, useRef, useState } from "react"; 7 | 8 | type InputContainerProps = { 9 | isApiKeyLoading: boolean; 10 | isResponseFetching: boolean; 11 | functionToVerify: string | null; 12 | sendMessage: (inputText: string) => void; 13 | }; 14 | 15 | export default function InputContainer({ 16 | isApiKeyLoading, 17 | isResponseFetching, 18 | functionToVerify, 19 | sendMessage, 20 | }: InputContainerProps) { 21 | const [inputText, setInputText] = useState(""); 22 | const textareaRef = useRef(null); 23 | 24 | useEffect(() => { 25 | if (textareaRef.current) { 26 | if (functionToVerify !== null || isResponseFetching) { 27 | textareaRef.current.blur(); 28 | return; 29 | } 30 | textareaRef.current.focus(); 31 | } 32 | }, [functionToVerify, isResponseFetching]); 33 | 34 | const handleSendMessage = () => { 35 | const trimmedInputText: string = inputText.trim(); 36 | setInputText(""); 37 | sendMessage(trimmedInputText); 38 | }; 39 | 40 | const canSendMessage = () => { 41 | return ( 42 | inputText.trim().length > 0 && !isResponseFetching && !isApiKeyLoading 43 | ); 44 | }; 45 | 46 | const isTextAreaDisabled = () => { 47 | return isResponseFetching || functionToVerify !== null; 48 | }; 49 | 50 | useEffect(() => { 51 | const handleKeyDown = async (event: KeyboardEvent) => { 52 | if ( 53 | event.key === "Enter" && 54 | document.activeElement?.id === "input-text-area" 55 | ) { 56 | event.preventDefault(); 57 | handleSendMessage(); 58 | } 59 | }; 60 | 61 | document.addEventListener("keydown", handleKeyDown); 62 | return () => { 63 | document.removeEventListener("keydown", handleKeyDown); 64 | }; 65 | }, [inputText]); 66 | 67 | return ( 68 | <> 69 |