├── .gitignore
├── README.md
├── backend
├── .env
├── .gitignore
├── chat
│ ├── __init__.py
│ ├── admin.py
│ ├── agents
│ │ ├── __init__.py
│ │ ├── agent_factory.py
│ │ └── callbacks.py
│ ├── api_urls.py
│ ├── apps.py
│ ├── consumers.py
│ ├── messages
│ │ ├── __init__.py
│ │ └── chat_message_repository.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_chatmessage_chat_id.py
│ │ ├── 0003_chat_remove_chatmessage_chat_id_and_more.py
│ │ ├── 0004_agent.py
│ │ ├── 0005_alter_agent_token.py
│ │ └── __init__.py
│ ├── models.py
│ ├── routing.py
│ ├── serializers.py
│ ├── tests.py
│ ├── views.py
│ └── websocket_urls.py
├── dump.rdb
├── manage.py
├── project
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── requirements.txt
└── frontend
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── components
│ └── chat
│ │ ├── ChatBox.tsx
│ │ ├── ChatInput.tsx
│ │ ├── ChatMessage.tsx
│ │ ├── LoadingIndicator.tsx
│ │ ├── SettingsModal.tsx
│ │ ├── Sidebar.tsx
│ │ ├── TypingIndicator.tsx
│ │ └── debug
│ │ ├── ChatMenu.tsx
│ │ └── DebugDrawer.tsx
├── css
│ └── chat
│ │ └── TypingIndicator.css
├── data
│ └── Message.ts
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
├── types
│ └── chat.ts
└── utils
│ └── DateFormatter.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
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 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 | myenv/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | .DS_Store
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | .idea/
164 | .idea
165 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
166 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
167 |
168 | # User-specific stuff
169 | .idea/**/workspace.xml
170 | .idea/**/tasks.xml
171 | .idea/**/usage.statistics.xml
172 | .idea/**/dictionaries
173 | .idea/**/shelf
174 |
175 | # AWS User-specific
176 | .idea/**/aws.xml
177 |
178 | # Generated files
179 | .idea/**/contentModel.xml
180 |
181 | # Sensitive or high-churn files
182 | .idea/**/dataSources/
183 | .idea/**/dataSources.ids
184 | .idea/**/dataSources.local.xml
185 | .idea/**/sqlDataSources.xml
186 | .idea/**/dynamic.xml
187 | .idea/**/uiDesigner.xml
188 | .idea/**/dbnavigator.xml
189 |
190 | # Gradle
191 | .idea/**/gradle.xml
192 | .idea/**/libraries
193 |
194 | # Gradle and Maven with auto-import
195 | # When using Gradle or Maven with auto-import, you should exclude module files,
196 | # since they will be recreated, and may cause churn. Uncomment if using
197 | # auto-import.
198 | # .idea/artifacts
199 | # .idea/compiler.xml
200 | # .idea/jarRepositories.xml
201 | # .idea/modules.xml
202 | # .idea/*.iml
203 | # .idea/modules
204 | # *.iml
205 | # *.ipr
206 |
207 | # CMake
208 | cmake-build-*/
209 |
210 | # Mongo Explorer plugin
211 | .idea/**/mongoSettings.xml
212 |
213 | # File-based project format
214 | *.iws
215 |
216 | # IntelliJ
217 | out/
218 |
219 | # mpeltonen/sbt-idea plugin
220 | .idea_modules/
221 |
222 | # JIRA plugin
223 | atlassian-ide-plugin.xml
224 |
225 | # Cursive Clojure plugin
226 | .idea/replstate.xml
227 |
228 | # SonarLint plugin
229 | .idea/sonarlint/
230 |
231 | # Crashlytics plugin (for Android Studio and IntelliJ)
232 | com_crashlytics_export_strings.xml
233 | crashlytics.properties
234 | crashlytics-build.properties
235 | fabric.properties
236 |
237 | # Editor-based Rest Client
238 | .idea/httpRequests
239 |
240 | # Android studio 3.1+ serialized cache file
241 | .idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LLM-Powered Chat Application
2 | This repository contains all of the starter code needed to run an LLM-powered chat app on your local machine:
3 | 1. Django backend
4 | 2. React TypeScript frontend
5 | 3. LangChain Agents and LLMs
6 |
7 | ## Getting Started 🚀
8 | To run the chat app, you need to:
9 |
10 | 1. Clone this GitHub repo
11 | 2. Run the backend server
12 | 3. Run the frontend app
13 |
14 | ### 1. Clone this GitHub repo 📁
15 | To clone this GitHub repo, open up your Terminal (MacOS) or Bash terminal (Windows) and navigate to wherever you want to save this repo on your local machine. Then, run:
16 |
17 | ```
18 | git clone https://github.com/virattt/chat_app.git
19 | ```
20 |
21 | Make sure that you have git installed ([instructions](https://github.com/git-guides/install-git)).
22 |
23 | ### 2. Run the backend server 🖥️
24 | Once you have this `chat_app` project cloned locally, navigate to the `backend` directory:
25 |
26 | ```
27 | cd ~/path_to/chat_app/backend
28 | ```
29 |
30 | Create and activate a virtual environment:
31 |
32 | ```
33 | python3 -m venv myenv
34 | ```
35 |
36 | For MacOS/Linux:
37 | ```
38 | source myenv/bin/activate
39 | ```
40 |
41 | For Windows:
42 | ```
43 | myenv\Scripts\activate
44 | ```
45 |
46 | Install the necessary libraries:
47 | ```
48 | pip install -r requirements.txt
49 | ```
50 |
51 | Make sure that you have Redis installed. You can find instructions [here](https://redis.io/docs/getting-started/installation/).
52 | Once installed, run redis:
53 | ```
54 | redis-server
55 | ```
56 |
57 | Run the backend server:
58 | ```
59 | daphne project.asgi:application
60 | ```
61 |
62 | If your backend server is running correctly, you should see something like this:
63 | ```
64 | "WSCONNECTING /ws/chat/" - -
65 | "WSCONNECT /ws/chat/" - -
66 | ```
67 |
68 | **Important**: In order to run the LLM, set your Open AI API key [here](https://github.com/virattt/chat_app/blob/main/backend/.env#L1).
69 |
70 | ### 3. Run the frontend app 💻
71 | In a new Terminal window (or tab), navigate to the `frontend` directory:
72 | ```
73 | cd ~/path_to/chat_app/frontend
74 | ```
75 |
76 | Make sure that you have Node and npm installed (MacOS [instructions](https://nodejs.org/en/download/package-manager#macos) and Windows [instructions](https://nodejs.org/en/download/package-manager#windows-1))
77 |
78 | Install the necessary packages:
79 | ```
80 | npm install
81 | ```
82 |
83 | Run the frontend app:
84 | ```
85 | npm start
86 | ```
87 |
88 | If successful, your browser should open and navigate to http://localhost:3000/. The chat app should load automatically.
89 |
90 | ## The Chat App UX 🤖
91 | _As of May 17, 2023_
92 |
93 |
94 | ## Troubleshooting ⚠️
95 | If you encounter any issues, send me a message on [Twitter](https://twitter.com/virat)!
96 |
--------------------------------------------------------------------------------
/backend/.env:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=YOUR_OPENAI_API_KEY
2 | SECRET_KEY=YOUR_DJANGO_SECRET_KEY # you can make this value up; primarily used for deploying
3 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
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 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 | myenv/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | .DS_Store
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | .idea/
164 | .idea
165 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
166 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
167 |
168 | # User-specific stuff
169 | .idea/**/workspace.xml
170 | .idea/**/tasks.xml
171 | .idea/**/usage.statistics.xml
172 | .idea/**/dictionaries
173 | .idea/**/shelf
174 |
175 | # AWS User-specific
176 | .idea/**/aws.xml
177 |
178 | # Generated files
179 | .idea/**/contentModel.xml
180 |
181 | # Sensitive or high-churn files
182 | .idea/**/dataSources/
183 | .idea/**/dataSources.ids
184 | .idea/**/dataSources.local.xml
185 | .idea/**/sqlDataSources.xml
186 | .idea/**/dynamic.xml
187 | .idea/**/uiDesigner.xml
188 | .idea/**/dbnavigator.xml
189 |
190 | # Gradle
191 | .idea/**/gradle.xml
192 | .idea/**/libraries
193 |
194 | # Gradle and Maven with auto-import
195 | # When using Gradle or Maven with auto-import, you should exclude module files,
196 | # since they will be recreated, and may cause churn. Uncomment if using
197 | # auto-import.
198 | # .idea/artifacts
199 | # .idea/compiler.xml
200 | # .idea/jarRepositories.xml
201 | # .idea/modules.xml
202 | # .idea/*.iml
203 | # .idea/modules
204 | # *.iml
205 | # *.ipr
206 |
207 | # CMake
208 | cmake-build-*/
209 |
210 | # Mongo Explorer plugin
211 | .idea/**/mongoSettings.xml
212 |
213 | # File-based project format
214 | *.iws
215 |
216 | # IntelliJ
217 | out/
218 |
219 | # mpeltonen/sbt-idea plugin
220 | .idea_modules/
221 |
222 | # JIRA plugin
223 | atlassian-ide-plugin.xml
224 |
225 | # Cursive Clojure plugin
226 | .idea/replstate.xml
227 |
228 | # SonarLint plugin
229 | .idea/sonarlint/
230 |
231 | # Crashlytics plugin (for Android Studio and IntelliJ)
232 | com_crashlytics_export_strings.xml
233 | crashlytics.properties
234 | crashlytics-build.properties
235 | fabric.properties
236 |
237 | # Editor-based Rest Client
238 | .idea/httpRequests
239 |
240 | # Android studio 3.1+ serialized cache file
241 | .idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/backend/chat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/__init__.py
--------------------------------------------------------------------------------
/backend/chat/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/backend/chat/agents/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/agents/__init__.py
--------------------------------------------------------------------------------
/backend/chat/agents/agent_factory.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from langchain.agents import initialize_agent, load_tools, AgentType, AgentExecutor
4 | from langchain.callbacks.base import BaseCallbackHandler
5 | from langchain.chat_models import ChatOpenAI
6 | from langchain.memory import ConversationBufferMemory
7 |
8 | from chat.messages.chat_message_repository import ChatMessageRepository
9 | from chat.models import MessageSender, ChatMessage
10 | from project import settings
11 |
12 |
13 | class AgentFactory:
14 |
15 | def __init__(self):
16 | self.chat_message_repository = ChatMessageRepository()
17 |
18 | async def create_agent(
19 | self,
20 | tool_names: List[str],
21 | chat_id: str = None,
22 | streaming=False,
23 | callback_handlers: List[BaseCallbackHandler] = None,
24 | ) -> AgentExecutor:
25 | # Instantiate the OpenAI LLM
26 | llm = ChatOpenAI(
27 | temperature=0,
28 | openai_api_key=settings.openai_api_key,
29 | streaming=streaming,
30 | callbacks=callback_handlers,
31 | )
32 |
33 | # Load the Tools that the Agent will use
34 | tools = load_tools(tool_names, llm=llm)
35 |
36 | # Load the memory and populate it with any previous messages
37 | memory = await self._load_agent_memory(chat_id)
38 |
39 | # Initialize and return the agent
40 | return initialize_agent(
41 | tools=tools,
42 | llm=llm,
43 | agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
44 | verbose=True,
45 | memory=memory
46 | )
47 |
48 | async def _load_agent_memory(
49 | self,
50 | chat_id: str = None,
51 | ) -> ConversationBufferMemory:
52 | if not chat_id:
53 | return ConversationBufferMemory(memory_key="chat_history", return_messages=True)
54 |
55 | # Create the conversational memory for the agent
56 | memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
57 |
58 | # Load the messages for the chat_id from the DB
59 | chat_messages: List[ChatMessage] = await self.chat_message_repository.get_chat_messages(chat_id)
60 |
61 | # Add the messages to the memory
62 | for message in chat_messages:
63 | if message.sender == MessageSender.USER.value:
64 | # Add user message to the memory
65 | memory.chat_memory.add_user_message(message.content)
66 | elif message.sender == MessageSender.AI.value:
67 | # Add AI message to the memory
68 | memory.chat_memory.add_ai_message(message.content)
69 |
70 | return memory
71 |
--------------------------------------------------------------------------------
/backend/chat/agents/callbacks.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Optional, Any, Dict, List
3 | from uuid import UUID
4 |
5 | from channels.generic.websocket import AsyncWebsocketConsumer
6 | from langchain.callbacks.base import AsyncCallbackHandler
7 | from langchain.schema import LLMResult, BaseMessage
8 |
9 |
10 | class AsyncStreamingCallbackHandler(AsyncCallbackHandler):
11 |
12 | def __init__(self, consumer: AsyncWebsocketConsumer):
13 | self.consumer = consumer
14 |
15 | async def on_llm_new_token(
16 | self,
17 | token: str,
18 | *,
19 | run_id: UUID,
20 | parent_run_id: Optional[UUID] = None,
21 | **kwargs: Any,
22 | ) -> None:
23 | # Send the token to any consumers (e.g. frontend client)
24 | await self.consumer.send(text_data=json.dumps({'message': token, 'type': 'debug'}))
25 |
26 | async def on_llm_end(
27 | self,
28 | response: LLMResult,
29 | *,
30 | run_id: UUID,
31 | parent_run_id: Optional[UUID] = None,
32 | **kwargs: Any,
33 | ) -> None:
34 | # When the LLM ends, add a new line so that debug messages are spaced with new lines.
35 | await self.consumer.send(text_data=json.dumps({'message': '\n\n', 'type': 'debug'}))
36 |
37 | async def on_chat_model_start(
38 | self, serialized: Dict[str, Any],
39 | messages: List[List[BaseMessage]],
40 | *,
41 | run_id: UUID,
42 | parent_run_id: Optional[UUID] = None,
43 | **kwargs: Any
44 | ) -> Any:
45 | # Do nothing
46 | pass
47 |
--------------------------------------------------------------------------------
/backend/chat/api_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from rest_framework.routers import DefaultRouter
3 | from .views import ChatViewSet, AgentViewSet
4 |
5 | router = DefaultRouter()
6 | router.register(r'chats', ChatViewSet)
7 | router.register(r'agents', AgentViewSet)
8 |
9 | urlpatterns = [
10 | path('', include(router.urls)),
11 | ]
--------------------------------------------------------------------------------
/backend/chat/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ChatConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'chat'
7 |
--------------------------------------------------------------------------------
/backend/chat/consumers.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | import django
5 |
6 | from chat.agents.agent_factory import AgentFactory
7 | from chat.agents.callbacks import AsyncStreamingCallbackHandler
8 | from chat.messages.chat_message_repository import ChatMessageRepository
9 |
10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
11 | django.setup()
12 |
13 | from channels.generic.websocket import AsyncWebsocketConsumer
14 | from langchain.agents import AgentExecutor
15 |
16 | from chat.models import MessageSender
17 |
18 |
19 | class ChatConsumer(AsyncWebsocketConsumer):
20 | # The LLM agent for this chat application
21 | agent: AgentExecutor
22 |
23 | def __init__(self, *args, **kwargs):
24 | super().__init__(*args, **kwargs)
25 | self.agent_factory = AgentFactory()
26 | self.chat_message_repository = ChatMessageRepository()
27 |
28 | async def connect(self):
29 | # Get the chat_id from the client
30 | chat_id = self.scope['url_route']['kwargs'].get('chat_id')
31 |
32 | # Create the agent when the websocket connection with the client is established
33 | self.agent = await self.agent_factory.create_agent(
34 | tool_names=["llm-math"],
35 | chat_id=chat_id,
36 | streaming=True,
37 | callback_handlers=[AsyncStreamingCallbackHandler(self)],
38 | )
39 |
40 | await self.accept()
41 |
42 | async def disconnect(self, close_code):
43 | pass
44 |
45 | async def receive(self, text_data):
46 | text_data_json = json.loads(text_data)
47 | message = text_data_json['message']
48 | chat_id = text_data_json['chat_id']
49 |
50 | # Forward the message to LangChain
51 | response = await self.message_agent(message, chat_id)
52 |
53 | # Send the response from the OpenAI Chat API to the frontend client
54 | await self.send(text_data=json.dumps({'message': response, 'type': 'answer'}))
55 |
56 | async def message_agent(self, message: str, chat_id: str):
57 | # Save the user message to the database
58 | await self.chat_message_repository.save_message(message=message, sender=MessageSender.USER.value, chat_id=chat_id)
59 |
60 | # Call the agent
61 | response = await self.agent.arun(message)
62 |
63 | # Save the AI message to the database
64 | await self.chat_message_repository.save_message(message=response, sender=MessageSender.AI.value, chat_id=chat_id)
65 |
66 | return response
67 |
68 | def my_callback(self, message):
69 | print("Callback received:", message)
70 |
--------------------------------------------------------------------------------
/backend/chat/messages/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/messages/__init__.py
--------------------------------------------------------------------------------
/backend/chat/messages/chat_message_repository.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import List
3 |
4 | import django
5 | from channels.db import database_sync_to_async
6 |
7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
8 | django.setup()
9 |
10 | from chat.models import ChatMessage
11 |
12 |
13 | class ChatMessageRepository:
14 |
15 | @database_sync_to_async
16 | def get_chat_messages(self, chat_id: str, order_by='timestamp') -> List[ChatMessage]:
17 | # Retrieve the chat history for `chat_id` from the database
18 | return list(ChatMessage.objects.filter(chat_id=chat_id).order_by(order_by))
19 |
20 | @database_sync_to_async
21 | def save_message(self, message: str, sender: str, chat_id: str):
22 | # Save the message to the database
23 | ChatMessage.objects.create(sender=sender, content=message, chat_id=chat_id)
24 |
--------------------------------------------------------------------------------
/backend/chat/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-05-13 19:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ChatMessage',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('content', models.TextField()),
19 | ('sender', models.CharField(choices=[('USER', 'USER'), ('SYSTEM', 'SYSTEM')], max_length=10)),
20 | ('timestamp', models.DateTimeField(auto_now_add=True)),
21 | ],
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/backend/chat/migrations/0002_chatmessage_chat_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-05-15 13:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('chat', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='chatmessage',
15 | name='chat_id',
16 | field=models.CharField(db_index=True, max_length=255),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/backend/chat/migrations/0003_chat_remove_chatmessage_chat_id_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-05-19 20:06
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('chat', '0002_chatmessage_chat_id'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Chat',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(max_length=255)),
19 | ('created_at', models.DateTimeField(auto_now_add=True)),
20 | ('updated_at', models.DateTimeField(auto_now=True)),
21 | ],
22 | ),
23 | migrations.RemoveField(
24 | model_name='chatmessage',
25 | name='chat_id',
26 | ),
27 | migrations.AlterField(
28 | model_name='chatmessage',
29 | name='sender',
30 | field=models.CharField(choices=[('USER', 'USER'), ('AI', 'AI')], max_length=10),
31 | ),
32 | migrations.AddField(
33 | model_name='chatmessage',
34 | name='chat',
35 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.chat'),
36 | preserve_default=False,
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/backend/chat/migrations/0004_agent.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-05-24 22:48
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('chat', '0003_chat_remove_chatmessage_chat_id_and_more'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Agent',
15 | fields=[
16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('name', models.CharField(max_length=255)),
18 | ('agent_type', models.CharField(max_length=255)),
19 | ('token', models.CharField(default='a_6e744ad2-4677-482f-8d52-02c34624a1d3', max_length=255, unique=True)),
20 | ('created_at', models.DateTimeField(auto_now_add=True)),
21 | ('updated_at', models.DateTimeField(auto_now=True)),
22 | ('is_active', models.BooleanField(default=True)),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/backend/chat/migrations/0005_alter_agent_token.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-05-24 22:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('chat', '0004_agent'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='agent',
15 | name='token',
16 | field=models.CharField(db_index=True, default='a_d1e13ad0-7e9f-437f-9d66-5e18c45367d4', max_length=255, unique=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/backend/chat/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/migrations/__init__.py
--------------------------------------------------------------------------------
/backend/chat/models.py:
--------------------------------------------------------------------------------
1 | # models.py
2 | from enum import Enum
3 | import uuid
4 |
5 | from django.db import models
6 |
7 |
8 | class MessageSender(Enum):
9 | USER = 'USER'
10 | AI = 'AI'
11 |
12 |
13 | class Chat(models.Model):
14 | name = models.CharField(max_length=255)
15 | created_at = models.DateTimeField(auto_now_add=True)
16 | updated_at = models.DateTimeField(auto_now=True)
17 |
18 |
19 | class ChatMessage(models.Model):
20 | content = models.TextField()
21 | chat = models.ForeignKey(Chat, related_name="messages", on_delete=models.CASCADE)
22 | sender = models.CharField(max_length=10, choices=[(tag.value, tag.name) for tag in MessageSender])
23 | timestamp = models.DateTimeField(auto_now_add=True)
24 |
25 |
26 |
27 | class Agent(models.Model):
28 | name = models.CharField(max_length=255)
29 | agent_type = models.CharField(max_length=255)
30 | token = models.CharField(max_length=255, default='a_' + str(uuid.uuid4()), unique=True, db_index=True)
31 | created_at = models.DateTimeField(auto_now_add=True)
32 | updated_at = models.DateTimeField(auto_now=True)
33 | is_active = models.BooleanField(default=True)
34 |
35 | def __str__(self):
36 | return self.name
37 |
--------------------------------------------------------------------------------
/backend/chat/routing.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import consumers
3 |
4 | websocket_urlpatterns = [
5 | path('ws/chat/', consumers.ChatConsumer.as_asgi()),
6 | ]
7 |
--------------------------------------------------------------------------------
/backend/chat/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from .models import Agent
4 | from .models import Chat, ChatMessage
5 |
6 |
7 | class ChatSerializer(serializers.ModelSerializer):
8 | class Meta:
9 | model = Chat
10 | fields = ['id', 'name', 'created_at', 'updated_at']
11 |
12 |
13 | class ChatMessageSerializer(serializers.ModelSerializer):
14 | class Meta:
15 | model = ChatMessage
16 | fields = ['id', 'content', 'chat', 'sender', 'timestamp']
17 |
18 |
19 | class AgentSerializer(serializers.ModelSerializer):
20 | class Meta:
21 | model = Agent
22 | fields = ['name', 'agent_type', 'token', 'created_at', 'updated_at', 'is_active']
23 |
--------------------------------------------------------------------------------
/backend/chat/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/chat/views.py:
--------------------------------------------------------------------------------
1 | from django.http import JsonResponse
2 | from rest_framework import viewsets
3 | from rest_framework.decorators import action
4 | from rest_framework.response import Response
5 |
6 | from .models import Agent
7 | from .models import Chat, ChatMessage
8 | from .serializers import AgentSerializer
9 | from .serializers import ChatSerializer, ChatMessageSerializer
10 |
11 |
12 | class ChatViewSet(viewsets.ModelViewSet):
13 | queryset = Chat.objects.all()
14 | serializer_class = ChatSerializer
15 |
16 | def create(self, request, *args, **kwargs):
17 | serializer = self.get_serializer(data=request.data)
18 | serializer.is_valid(raise_exception=True)
19 | self.perform_create(serializer)
20 | return Response(serializer.data)
21 |
22 | def perform_destroy(self, instance):
23 | # Also delete related chat messages
24 | ChatMessage.objects.filter(chat_id=instance.id).delete()
25 | instance.delete()
26 |
27 | def list(self, request, *args, **kwargs):
28 | queryset = self.filter_queryset(self.get_queryset())
29 | serializer = self.get_serializer(queryset, many=True)
30 | return JsonResponse({"chats": serializer.data})
31 |
32 | @action(detail=True, methods=['get'])
33 | def messages(self, request, pk=None):
34 | chat = self.get_object()
35 | messages = ChatMessage.objects.filter(chat_id=chat.id).order_by('timestamp')
36 | serializer = ChatMessageSerializer(messages, many=True)
37 | return Response(serializer.data)
38 |
39 |
40 | class AgentViewSet(viewsets.ModelViewSet):
41 | queryset = Agent.objects.all()
42 | serializer_class = AgentSerializer
43 | lookup_field = 'token'
44 |
45 | def destroy(self, request, *args, **kwargs):
46 | agent = self.get_object()
47 | agent.is_active = False
48 | agent.save()
49 | return Response(status=204)
50 |
51 | def update(self, request, *args, **kwargs):
52 | agent = self.get_object()
53 | agent.agent_type = request.data.get('agent_type')
54 | agent.save()
55 | return Response(AgentSerializer(agent).data)
56 |
--------------------------------------------------------------------------------
/backend/chat/websocket_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.urls import re_path
3 | from . import consumers
4 |
5 | websocket_urlpatterns = [
6 | path('ws/chat/', consumers.ChatConsumer.as_asgi()),
7 |
8 | # Websocket URL for a given chat_id
9 | re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()),
10 | ]
11 |
--------------------------------------------------------------------------------
/backend/dump.rdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/dump.rdb
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/backend/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/project/__init__.py
--------------------------------------------------------------------------------
/backend/project/asgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.asgi import get_asgi_application
3 | from channels.routing import ProtocolTypeRouter, URLRouter
4 | from channels.auth import AuthMiddlewareStack
5 | import chat.websocket_urls
6 |
7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
8 |
9 | application = ProtocolTypeRouter({
10 | 'http': get_asgi_application(),
11 | 'websocket': AuthMiddlewareStack(
12 | URLRouter(
13 | chat.websocket_urls.websocket_urlpatterns
14 | )
15 | ),
16 | })
17 |
--------------------------------------------------------------------------------
/backend/project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for backend project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.2/ref/settings/
11 | """
12 | import os
13 | from pathlib import Path
14 |
15 | from dotenv import load_dotenv
16 |
17 | # Load the environment variables from .env file
18 | load_dotenv()
19 |
20 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
21 | BASE_DIR = Path(__file__).resolve().parent.parent
22 |
23 | # Quick-start development settings - unsuitable for production
24 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
25 |
26 | # SECURITY WARNING: keep the secret key used in production secret!
27 | SECRET_KEY = os.environ.get('SECRET_KEY')
28 |
29 | # SECURITY WARNING: don't run with debug turned on in production!
30 | DEBUG = True
31 |
32 | ALLOWED_HOSTS = []
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | 'django.contrib.admin',
38 | 'django.contrib.auth',
39 | 'django.contrib.contenttypes',
40 | 'django.contrib.sessions',
41 | 'django.contrib.messages',
42 | 'django.contrib.staticfiles',
43 | 'channels',
44 | 'chat',
45 | 'corsheaders',
46 | ]
47 |
48 | # Django Channels
49 | CHANNEL_LAYERS = {
50 | 'default': {
51 | 'BACKEND': 'channels_redis.core.RedisChannelLayer',
52 | 'CONFIG': {
53 | 'hosts': [('127.0.0.1', 6379)],
54 | },
55 | },
56 | }
57 |
58 | # Use channels layer as default backend
59 | ASGI_APPLICATION = 'project.asgi.application'
60 |
61 | MIDDLEWARE = [
62 | 'django.middleware.security.SecurityMiddleware',
63 | 'django.contrib.sessions.middleware.SessionMiddleware',
64 | 'django.middleware.common.CommonMiddleware',
65 | 'django.middleware.csrf.CsrfViewMiddleware',
66 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
67 | 'django.contrib.messages.middleware.MessageMiddleware',
68 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
69 | 'corsheaders.middleware.CorsMiddleware',
70 | ]
71 |
72 | CORS_ORIGIN_WHITELIST = [
73 | "http://localhost:3000",
74 | ]
75 |
76 | ROOT_URLCONF = 'project.urls'
77 |
78 | TEMPLATES = [
79 | {
80 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
81 | 'DIRS': [],
82 | 'APP_DIRS': True,
83 | 'OPTIONS': {
84 | 'context_processors': [
85 | 'django.template.context_processors.debug',
86 | 'django.template.context_processors.request',
87 | 'django.contrib.auth.context_processors.auth',
88 | 'django.contrib.messages.context_processors.messages',
89 | ],
90 | },
91 | },
92 | ]
93 |
94 | WSGI_APPLICATION = 'project.wsgi.application'
95 |
96 | # Database
97 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
98 |
99 | DATABASES = {
100 | 'default': {
101 | 'ENGINE': 'django.db.backends.sqlite3',
102 | 'NAME': BASE_DIR / 'db.sqlite3',
103 | }
104 | }
105 |
106 | # Password validation
107 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
108 |
109 | AUTH_PASSWORD_VALIDATORS = [
110 | {
111 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
112 | },
113 | {
114 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
115 | },
116 | {
117 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
118 | },
119 | {
120 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
121 | },
122 | ]
123 |
124 | # Internationalization
125 | # https://docs.djangoproject.com/en/4.2/topics/i18n/
126 |
127 | LANGUAGE_CODE = 'en-us'
128 |
129 | TIME_ZONE = 'UTC'
130 |
131 | USE_I18N = True
132 |
133 | USE_TZ = True
134 |
135 | # Static files (CSS, JavaScript, Images)
136 | # https://docs.djangoproject.com/en/4.2/howto/static-files/
137 |
138 | STATIC_URL = 'static/'
139 |
140 | # Default primary key field type
141 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
142 |
143 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
144 |
145 | # Access the OPENAI_API_KEY environment variable
146 | openai_api_key = os.environ.get('OPENAI_API_KEY')
147 |
--------------------------------------------------------------------------------
/backend/project/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | URL configuration for backend project.
3 |
4 | The `urlpatterns` list routes URLs to views. For more information please see:
5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/
6 | Examples:
7 | Function views
8 | 1. Add an import: from my_app import views
9 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
10 | Class-based views
11 | 1. Add an import: from other_app.views import Home
12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13 | Including another URLconf
14 | 1. Import the include() function: from django.urls import include, path
15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16 | """
17 | from django.contrib import admin
18 | from django.urls import include, path
19 |
20 | urlpatterns = [
21 | path('admin/', admin.site.urls),
22 | path('api/', include('chat.api_urls')),
23 | ]
24 |
--------------------------------------------------------------------------------
/backend/project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for backend project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.8.4
2 | aiosignal==1.3.1
3 | asgiref==3.6.0
4 | async-timeout==4.0.2
5 | attrs==23.1.0
6 | autobahn==23.1.2
7 | Automat==22.10.0
8 | beautifulsoup4==4.12.2
9 | certifi==2022.12.7
10 | cffi==1.15.1
11 | channels==4.0.0
12 | channels-redis==4.1.0
13 | charset-normalizer==3.1.0
14 | constantly==15.1.0
15 | cryptography==40.0.2
16 | daphne==4.0.0
17 | dataclasses-json==0.5.7
18 | Django==4.2
19 | django-cors-headers==3.14.0
20 | djangorestframework==3.14.0
21 | frozenlist==1.3.3
22 | greenlet==2.0.2
23 | hyperlink==21.0.0
24 | idna==3.4
25 | incremental==22.10.0
26 | langchain==0.0.176
27 | lxml==4.9.2
28 | marshmallow==3.19.0
29 | marshmallow-enum==1.5.1
30 | msgpack==1.0.5
31 | multidict==6.0.4
32 | mypy-extensions==1.0.0
33 | numexpr==2.8.4
34 | numpy==1.24.3
35 | openai==0.27.6
36 | openapi-schema-pydantic==1.2.4
37 | packaging==23.1
38 | pandas==2.0.1
39 | pdfkit==1.0.0
40 | pyasn1==0.5.0
41 | pyasn1-modules==0.3.0
42 | pycparser==2.21
43 | pydantic==1.10.7
44 | pyOpenSSL==23.1.1
45 | python-dateutil==2.8.2
46 | python-dotenv==1.0.0
47 | pytz==2023.3
48 | PyYAML==6.0
49 | redis==4.5.4
50 | requests==2.29.0
51 | service-identity==21.1.0
52 | six==1.16.0
53 | soupsieve==2.4.1
54 | SQLAlchemy==2.0.13
55 | sqlparse==0.4.4
56 | tenacity==8.2.2
57 | tqdm==4.65.0
58 | Twisted==22.10.0
59 | txaio==23.1.1
60 | typing-inspect==0.8.0
61 | typing_extensions==4.5.0
62 | tzdata==2023.3
63 | urllib3==1.26.15
64 | yarl==1.9.2
65 | zope.interface==6.0
66 |
--------------------------------------------------------------------------------
/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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat_frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.0",
7 | "@emotion/styled": "^11.11.0",
8 | "@fortawesome/fontawesome-svg-core": "^6.4.0",
9 | "@fortawesome/free-solid-svg-icons": "^6.4.0",
10 | "@fortawesome/react-fontawesome": "^0.2.0",
11 | "@mui/material": "^5.12.3",
12 | "@testing-library/jest-dom": "^5.16.5",
13 | "@testing-library/react": "^13.4.0",
14 | "@testing-library/user-event": "^13.5.0",
15 | "@types/jest": "^27.5.2",
16 | "@types/node": "^16.18.25",
17 | "@types/react": "^18.2.6",
18 | "@types/react-dom": "^18.2.4",
19 | "@types/styled-components": "^5.1.26",
20 | "@types/ws": "^8.5.4",
21 | "axios": "^1.4.0",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-scripts": "5.0.1",
25 | "react-syntax-highlighter": "^15.5.0",
26 | "react-textarea-autosize": "^8.4.1",
27 | "react-use-websocket": "^4.3.1",
28 | "react-websocket": "^2.1.0",
29 | "reconnecting-websocket": "^4.4.0",
30 | "styled-components": "^6.0.0-rc.1",
31 | "typescript": "^4.9.5",
32 | "web-vitals": "^2.1.4",
33 | "ws": "^8.13.0"
34 | },
35 | "scripts": {
36 | "start": "react-scripts start",
37 | "build": "react-scripts build",
38 | "test": "react-scripts test",
39 | "eject": "react-scripts eject"
40 | },
41 | "proxy": "http://localhost:8000",
42 | "eslintConfig": {
43 | "extends": [
44 | "react-app",
45 | "react-app/jest"
46 | ]
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | },
60 | "devDependencies": {
61 | "@types/react-syntax-highlighter": "^15.5.6"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
18 |
19 |
28 | React App
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | // App.tsx
2 |
3 | import React, { useEffect, useRef, useState } from 'react';
4 | import { Sidebar } from './components/chat/Sidebar';
5 | import { ChatBox } from './components/chat/ChatBox';
6 | import { ChatInput } from "./components/chat/ChatInput";
7 | import styled from 'styled-components';
8 | import ReconnectingWebSocket from "reconnecting-websocket";
9 | import { Message } from "./data/Message";
10 | import { ChatMenu } from "./components/chat/debug/ChatMenu";
11 | import { DebugDrawer } from "./components/chat/debug/DebugDrawer";
12 |
13 | export const App = () => {
14 | const [currentChatId, setCurrentChatId] = useState(null);
15 | const [messages, setMessages] = useState([]);
16 | const webSocket = useRef(null);
17 | const [loading, setLoading] = useState(false);
18 | const [debugMessage, setDebugMessage] = useState("");
19 | const [debugMode, setDebugMode] = useState(false);
20 |
21 | // Set up websocket connection when currentChatId changes
22 | useEffect(() => {
23 | if (currentChatId) {
24 | webSocket.current = new ReconnectingWebSocket(`ws://localhost:8000/ws/chat/${currentChatId}/`);
25 | webSocket.current.onmessage = (event) => {
26 | const data = JSON.parse(event.data);
27 | if (data.type === "debug") {
28 | // Debug message received. Replace newline characters with
tags
29 | const formattedToken = data.message.replace(/\n/g, '
');
30 | setDebugMessage(prevMessage => prevMessage + formattedToken);
31 | } else {
32 | // Entire message received
33 | setLoading(false)
34 | const newMessage = {sender: 'AI', content: data['message']};
35 | setMessages(prevMessages => [...prevMessages, newMessage]);
36 | }
37 | };
38 |
39 | webSocket.current.onclose = () => {
40 | console.error('Chat socket closed unexpectedly');
41 | };
42 | // Fetch chat messages for currentChatId
43 | fetchMessages(currentChatId)
44 | }
45 | return () => {
46 | webSocket.current?.close();
47 | };
48 | }, [currentChatId]);
49 |
50 | const onChatSelected = (chatId: string | null) => {
51 | if (currentChatId === chatId) return; // Prevent unnecessary re-renders.
52 | if (chatId == null) {
53 | // Clear messages if no chat is selected
54 | setMessages([])
55 | }
56 | setCurrentChatId(chatId);
57 | };
58 |
59 | const onNewUserMessage = (chatId: string, message: Message) => {
60 | webSocket.current?.send(
61 | JSON.stringify({
62 | message: message.content,
63 | chat_id: chatId,
64 | })
65 | );
66 | setMessages(prevMessages => [...prevMessages, message]);
67 | setLoading(true); // Set loading to true when sending a message
68 | };
69 |
70 | const onNewChatCreated = (chatId: string) => {
71 | onChatSelected(chatId)
72 | };
73 |
74 | const fetchMessages = (currentChatId: string | null) => {
75 | fetch(`http://localhost:8000/api/chats/${currentChatId}/messages/`)
76 | .then(response => response.json())
77 | .then(data => {
78 | setMessages(data)
79 | });
80 | }
81 |
82 | return (
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {debugMode && }
91 |
92 | );
93 | };
94 |
95 | const AppContainer = styled.div`
96 | display: flex;
97 | height: 100vh;
98 | `;
99 |
100 | const ChatContainer = styled.div<{ debugMode: boolean }>`
101 | display: flex;
102 | flex-direction: column;
103 | width: ${({debugMode}) => debugMode ? '70%' : '100%'};
104 | transition: all 0.2s; // Smooth transition
105 | `;
106 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/ChatBox.tsx:
--------------------------------------------------------------------------------
1 | // ChatBox.tsx
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 | import TypingIndicator from "./TypingIndicator";
6 | import { ChatMessage } from "./ChatMessage";
7 |
8 | type Message = {
9 | sender: string;
10 | content: string;
11 | };
12 |
13 | type ChatBoxProps = {
14 | messages: Message[];
15 | isLoading: boolean;
16 | };
17 |
18 | export const ChatBox: React.FC = ({messages, isLoading}) => {
19 | return (
20 |
21 | {messages.map((message, index) => (
22 |
23 | ))}
24 |
25 |
26 | );
27 | };
28 |
29 | const MessageList = styled.div`
30 | flex-grow: 1;
31 | overflow-y: auto;
32 | `;
33 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/ChatInput.tsx:
--------------------------------------------------------------------------------
1 | // ChatInput.tsx
2 |
3 | import React, { useState } from 'react';
4 | import TextareaAutosize from 'react-textarea-autosize';
5 | import styled from 'styled-components';
6 | import { Message } from "../../data/Message";
7 |
8 | type ChatInputProps = {
9 | onNewUserMessage: (chatId: string, message: Message) => void;
10 | onNewChatCreated: (chatId: string) => void;
11 | chatId: string | null;
12 | };
13 |
14 | export const ChatInput: React.FC = ({onNewUserMessage, onNewChatCreated, chatId}) => {
15 | const [message, setMessage] = useState('');
16 |
17 | const handleSubmit = (event: React.FormEvent) => {
18 | event.preventDefault();
19 |
20 | if (message.trim() === '') return;
21 |
22 | if (chatId) {
23 | // If there is a chatId, just send the message.
24 | const newMessage = {sender: 'USER', content: message};
25 | onNewUserMessage(chatId, newMessage)
26 | } else {
27 | // If there is no chatId, create a new chat.
28 | createChat()
29 | }
30 | setMessage(''); // Clear the input message
31 | }
32 |
33 | const createChat = () => {
34 | fetch('http://localhost:8000/api/chats/', {
35 | method: 'POST',
36 | headers: {'Content-Type': 'application/json'},
37 | body: JSON.stringify({name: 'New Chat'}) // Adjust this as necessary.
38 | })
39 | .then((response) => response.json())
40 | .then((newChat) => {
41 | // Update listeners that a new chat was created.
42 | onNewChatCreated(newChat.id)
43 |
44 | // Send the message after a timeout to ensure that the Chat has been created
45 | setTimeout(function () {
46 | // This block of code will be executed after 0.5 seconds
47 | onNewUserMessage(newChat.id, {sender: 'USER', content: message})
48 | }, 500);
49 | });
50 | };
51 |
52 | return (
53 |
68 | );
69 | }
70 | ;
71 |
72 | const Form = styled.form`
73 | display: flex;
74 | align-items: flex-start;
75 | padding: 10px;
76 | border-top: 1px solid #eee;
77 | `;
78 |
79 | const StyledTextareaAutosize = styled(TextareaAutosize)`
80 | flex-grow: 1;
81 | border: 1px solid #eee;
82 | border-radius: 3px;
83 | padding: 10px;
84 | margin-right: 10px;
85 | resize: none;
86 | overflow: auto;
87 | font-family: inherit;
88 | font-size: 16px;
89 | min-height: 14px; // Initial height
90 | max-height: 500px; // Max height
91 | &:focus,
92 | &:active {
93 | border-color: #1C1C1C;
94 | outline: none;
95 | }
96 | `;
97 |
98 | const Button = styled.button`
99 | height: 40px;
100 | padding: 10px 20px;
101 | border: none;
102 | background-color: #1C1C1C;
103 | color: white;
104 | cursor: pointer;
105 | border-radius: 3px;
106 | font-size: 1em;
107 | align-self: flex-end;
108 | &:hover {
109 | background-color: #333333; /* Change this to the desired lighter shade */
110 | }
111 | `;
112 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/ChatMessage.tsx:
--------------------------------------------------------------------------------
1 | // Message.tsx
2 |
3 | import React from 'react';
4 | import styled from 'styled-components';
5 |
6 | type MessageProps = {
7 | sender: string;
8 | content: string;
9 | isUser: boolean;
10 | };
11 |
12 | const Container = styled.div<{ isUser: boolean }>`
13 | background-color: ${({isUser}) => (isUser ? 'white' : '#F5F5F5')};
14 | padding-top: 10px;
15 | padding-bottom: 10px;
16 | display: flex;
17 | justify-content: center;
18 | width: 100%; // Make the container full width
19 | border-top: 0.5px solid #c4c4c4; // Add a thin gray line to the bottom
20 | `;
21 |
22 | const Bubble = styled.div<{ isUser: boolean }>`
23 | margin: 10px;
24 | padding: 10px;
25 | border-radius: 10px;
26 | width: 50%; // Set a fixed width for the bubble
27 | display: flex; // Make it a flex container
28 | align-items: baseline; // Align items to the text baseline
29 | font-family: 'Inter', sans-serif; // Set the font to Inter
30 | font-size: 16px;
31 | `;
32 |
33 | const Content = styled.div`
34 | margin-left: 14px;
35 | line-height: 1.5;
36 | font-size: 16px;
37 | `;
38 |
39 | const Sender = styled.div`
40 | font-weight: 700; // Make the sender name bold
41 | font-family: 'Inter', sans-serif; // Set the font to Inter
42 | font-size: 16px;
43 | min-width: 50px;
44 | `;
45 |
46 | export const ChatMessage: React.FC = ({sender, content, isUser}) => (
47 |
48 |
49 | {sender}
50 |
51 | {content.toString().split('\n').map((line, index) => (
52 | line === '' ?
: {line}
53 | ))}
54 |
55 |
56 |
57 | );
58 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/LoadingIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | interface LoadingIndicatorProps {
5 | isLoading: boolean;
6 | }
7 |
8 | // Define the pulsating animation
9 | const pulse = keyframes`
10 | 0% {
11 | background-color: rgba(0, 0, 0, 1);
12 | }
13 | 50% {
14 | background-color: rgba(0, 0, 0, 0.5);
15 | }
16 | 100% {
17 | background-color: rgba(0, 0, 0, 1);
18 | }
19 | `;
20 |
21 | const Container = styled.div`
22 | display: flex;
23 | justify-content: center;
24 | width: 100%;
25 | `;
26 |
27 | const InnerContainer = styled.div`
28 | display: flex;
29 | justify-content: flex-start;
30 | width: 40%;
31 | `;
32 |
33 | const Rectangle = styled.div`
34 | width: 8px;
35 | height: 16px;
36 | background-color: black;
37 | animation: ${pulse} 1s infinite ease-in-out;
38 | `;
39 |
40 | const LoadingIndicator: React.FC = ({isLoading}) => {
41 | if (!isLoading) return
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default LoadingIndicator;
53 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/SettingsModal.tsx:
--------------------------------------------------------------------------------
1 | // SettingsModal.tsx
2 |
3 | import React, { useRef, useState } from 'react';
4 | import styled from 'styled-components';
5 |
6 | type SettingsModalProps = {
7 | setShowSettingsModal: (showSettingsModal: boolean) => void;
8 | };
9 |
10 | const SettingsModal: React.FC = ({setShowSettingsModal}) => {
11 | const [apiKey, setApiKey] = useState('');
12 | const [isApiKeyValid, setIsApiKeyValid] = useState(false);
13 | const modalContentRef = useRef(null);
14 |
15 | const handleApiKeyChange = (e: React.ChangeEvent) => {
16 | const value = e.target.value;
17 | setApiKey(value);
18 | setIsApiKeyValid(value.trim() !== '');
19 | };
20 |
21 | const handleSaveApiKey = () => {
22 | localStorage.setItem('OPENAI_API_KEY', apiKey);
23 | setShowSettingsModal(false);
24 | };
25 |
26 | const handleOutsideClick = (e: React.MouseEvent) => {
27 | if (!modalContentRef.current?.contains(e.target as Node)) {
28 | setShowSettingsModal(false);
29 | }
30 | };
31 |
32 | return (
33 |
34 |
35 | Enter your OpenAI API Key
36 |
37 |
43 |
44 |
45 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | const ModalContainer = styled.div`
55 | position: fixed;
56 | top: 0;
57 | left: 0;
58 | right: 0;
59 | bottom: 0;
60 | display: flex;
61 | align-items: center;
62 | justify-content: center;
63 | background-color: rgba(0, 0, 0, 0.5);
64 | `;
65 |
66 | const ModalContent = styled.div`
67 | width: 400px;
68 | background-color: #fff;
69 | border-radius: 5px;
70 | padding: 20px;
71 | `;
72 |
73 | const ModalHeader = styled.h2`
74 | font-size: 1.2em;
75 | margin-bottom: 10px;
76 | `;
77 |
78 | const ModalBody = styled.div`
79 | margin-bottom: 20px;
80 | `;
81 |
82 | const ApiKeyInput = styled.input`
83 | width: 100%;
84 | padding: 10px;
85 | font-size: 1em;
86 | border: 1px solid #ccc;
87 | border-radius: 3px;
88 | box-sizing: border-box; /* Add this line */
89 | `;
90 | const ModalFooter = styled.div`
91 | display: flex;
92 | justify-content: flex-end;
93 | `;
94 |
95 | const Button = styled.button`
96 | padding: 10px 20px;
97 | font-size: 1em;
98 | border: none;
99 | border-radius: 3px;
100 | background-color: #1c1c1c;
101 | color: white;
102 | cursor: pointer;
103 | &:hover {
104 | background-color: #3f3f3f;
105 | }
106 | `;
107 |
108 | export default SettingsModal;
109 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import { faTrash } from '@fortawesome/free-solid-svg-icons';
5 | import SettingsModal from "./SettingsModal";
6 | import { formatDate } from "../../utils/DateFormatter";
7 | import { Chat } from "../../types/chat";
8 |
9 | type SidebarProps = {
10 | onChatSelected: (chatId: string | null) => void;
11 | selectedChatId: string | null;
12 | };
13 |
14 | export const Sidebar: React.FC = ({onChatSelected, selectedChatId}) => {
15 | const [chats, setChats] = useState([]);
16 | const [showSettingsModal, setShowSettingsModal] = useState(false);
17 |
18 | // Fetch chats when the selectedChatId changes
19 | useEffect(() => {
20 | fetchChats();
21 | }, [selectedChatId]);
22 |
23 | const fetchChats = () => {
24 | fetch('http://localhost:8000/api/chats/')
25 | .then((response) => response.json())
26 | .then((data) => {
27 | const sortedChats = sortChats(data.chats)
28 | setChats(sortedChats);
29 | });
30 | };
31 |
32 | const sortChats = (chats: Chat[]) => {
33 | return chats.sort((a, b) => {
34 | const dateA = new Date(a.created_at);
35 | const dateB = new Date(b.created_at);
36 |
37 | // sort in descending order
38 | return dateB.getTime() - dateA.getTime();
39 | })
40 | }
41 |
42 | const createChat = () => {
43 | fetch('http://localhost:8000/api/chats/', {
44 | method: 'POST',
45 | headers: {'Content-Type': 'application/json'},
46 | body: JSON.stringify({name: 'New Chat'}) // Adjust this as necessary.
47 | })
48 | .then((response) => response.json())
49 | .then((newChat) => {
50 | setChats((prevChats) => [...prevChats, newChat]);
51 | onChatSelected(newChat.id); // Select the new chat automatically
52 | });
53 | };
54 |
55 | const onDeleteChat = (chatId: string) => {
56 | fetch(`http://localhost:8000/api/chats/${chatId}/`, {
57 | method: 'DELETE'
58 | })
59 | .then(() => {
60 | // Update the state to remove the deleted chat
61 | setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));
62 | // If the deleted chat was the currently selected one, nullify the selection
63 | if (chatId === selectedChatId) {
64 | onChatSelected(null);
65 | }
66 | })
67 | .catch((error) => {
68 | console.error('Error:', error);
69 | });
70 | };
71 |
72 | const handleSettingsClick = () => {
73 | setShowSettingsModal(true);
74 | };
75 |
76 | return (
77 |
78 |
79 |
80 | {chats.map((chat) => (
81 | onChatSelected(chat.id)}
84 | isSelected={chat.id === selectedChatId}
85 | >
86 | {formatDate(chat.created_at)}
87 | {
90 | e.stopPropagation(); // Prevent the chat row's onClick event from firing.
91 | onDeleteChat(chat.id);
92 | }}
93 | />
94 |
95 | ))}
96 |
97 | Settings
98 | {showSettingsModal && (
99 |
100 | )}
101 |
102 | );
103 | };
104 |
105 | const SidebarContainer = styled.div`
106 | width: 250px;
107 | background-color: #1c1c1c;
108 | display: flex;
109 | flex-direction: column;
110 | justify-content: space-between;
111 | height: 100vh; // Adjust the height to fit your layout
112 | `;
113 |
114 | const ChatListContainer = styled.div`
115 | overflow-y: auto;
116 | `;
117 |
118 | const ChatRow = styled.div<{ isSelected?: boolean }>`
119 | padding: 10px;
120 | cursor: pointer;
121 | background-color: ${(props) => (props.isSelected ? '#4c4c4c' : 'transparent')};
122 | &:hover {
123 | background-color: #3f3f3f;
124 | }
125 | color: white;
126 | font-size: 14px;
127 | display: flex;
128 | justify-content: space-between;
129 | align-items: center; // ensure text and icon are aligned
130 | overflow: hidden; // add overflow handling
131 |
132 | & > span { // add a span tag around the text inside ChatRow
133 | white-space: nowrap;
134 | overflow: hidden;
135 | text-overflow: ellipsis;
136 | margin-right: 10px;
137 | }
138 | `;
139 |
140 | const Button = styled.button`
141 | padding: 20px;
142 | border: none;
143 | background-color: #1c1c1c;
144 | width: 100%;
145 | color: white;
146 | cursor: pointer;
147 | border-radius: 3px;
148 | border-color: #fff;
149 | font-size: 14px;
150 | &:hover {
151 | background-color: #3f3f3f;
152 | }
153 | `;
154 |
155 | const SettingsRow = styled.div`
156 | padding: 10px;
157 | margin: 10px;
158 | cursor: pointer;
159 | background-color: transparent;
160 | &:hover {
161 | background-color: #3f3f3f;
162 | }
163 | border-top: 1px solid #3f3f3f;
164 | color: white;
165 | font-size: 14px;
166 | display: flex;
167 | justify-content: center;
168 | align-items: center;
169 | `;
170 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/TypingIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../../css/chat/TypingIndicator.css'
3 | import styled from "styled-components";
4 |
5 | interface TypingIndicatorProps {
6 | isTyping: boolean;
7 | }
8 |
9 | const Container = styled.div`
10 | display: flex;
11 | justify-content: center;
12 | width: 100%; // Make the container full width
13 | `;
14 |
15 | const InnerContainer = styled.div`
16 | display: flex;
17 | justify-content: flex-start;
18 | width: 40%; // Set a fixed width for the inner container
19 | `;
20 |
21 | const TypingIndicator: React.FC = ({isTyping}) => {
22 | if (!isTyping) return
23 |
24 | return (
25 |
26 |
27 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default TypingIndicator;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/debug/ChatMenu.tsx:
--------------------------------------------------------------------------------
1 | // ChatMenu.tsx
2 | import React from 'react';
3 | import styled from 'styled-components';
4 |
5 | interface ChatMenuProps {
6 | debugMode: boolean;
7 | setDebugMode: (value: boolean) => void;
8 | }
9 |
10 | export const ChatMenu: React.FC = ({ debugMode, setDebugMode }) => {
11 | return (
12 |
22 | );
23 | };
24 |
25 | const Menu = styled.div`
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | margin: 20px;
30 | `;
31 |
--------------------------------------------------------------------------------
/frontend/src/components/chat/debug/DebugDrawer.tsx:
--------------------------------------------------------------------------------
1 | // DebugDrawer.tsx
2 | import React from 'react';
3 | import styled from 'styled-components';
4 | import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
5 | import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
6 | import python from 'react-syntax-highlighter/dist/esm/languages/hljs/python';
7 |
8 | SyntaxHighlighter.registerLanguage('json', json);
9 | SyntaxHighlighter.registerLanguage('python', python);
10 |
11 | interface DebugDrawerProps {
12 | message: string;
13 | debugMode: boolean;
14 | }
15 |
16 | export const DebugDrawer: React.FC = ({message, debugMode}) => {
17 | return (
18 |
19 | Agent Thoughts
20 |
21 |
22 | );
23 | };
24 |
25 | const Drawer = styled.div<{ debugMode: boolean }>`
26 | width: ${({debugMode}) => debugMode ? '30%' : '0'};
27 | height: 100vh;
28 | background: white;
29 | border-left: 1px solid gray;
30 | overflow: auto;
31 | padding: 20px;
32 | box-sizing: border-box;
33 | transition: all 0.2s; // Smooth transition
34 | `;
35 |
36 | const CenteredHeading = styled.h3`
37 | text-align: center;
38 | `;
39 |
--------------------------------------------------------------------------------
/frontend/src/css/chat/TypingIndicator.css:
--------------------------------------------------------------------------------
1 |
2 | .typing-indicator-container {
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | height: 16px;
7 | margin-top: 8px;
8 | margin-bottom: 8px;
9 | padding: 0.3rem;
10 | border-radius: 1rem;
11 | background-color: #d1d1d1;
12 | opacity: 0;
13 | transition: opacity 0.3s;
14 | width: 40px;
15 | text-align: left;
16 | }
17 |
18 | .typing-indicator-container.visible {
19 | opacity: 1;
20 | }
21 |
22 | .typing-indicator-container.hidden {
23 | opacity: 0;
24 | }
25 |
26 | .typing-indicator-dot {
27 | width: 6px;
28 | height: 6px;
29 | border-radius: 50%;
30 | background-color: #ffffff;
31 | margin: 0 1px;
32 | animation: typing-indicator-bounce 1.2s infinite ease-in-out;
33 | }
34 |
35 | .typing-indicator-dot:nth-child(2) {
36 | animation-delay: 0.2s;
37 | }
38 |
39 | .typing-indicator-dot:nth-child(3) {
40 | animation-delay: 0.4s;
41 | }
42 |
43 | @keyframes typing-indicator-bounce {
44 | 0%, 80%, 100% {
45 | transform: translateY(0);
46 | }
47 |
48 | 40% {
49 | transform: translateY(-5px);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/data/Message.ts:
--------------------------------------------------------------------------------
1 | export type Message = {
2 | sender: string;
3 | content: string;
4 | };
5 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import reportWebVitals from './reportWebVitals';
5 | import { App } from "./App";
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module 'react-syntax-highlighter/dist/esm/languages/hljs/json';
3 | declare module 'react-syntax-highlighter/dist/esm/languages/hljs/python';
4 | declare module 'react-syntax-highlighter/dist/esm/styles/hljs/docco';
5 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/src/types/chat.ts:
--------------------------------------------------------------------------------
1 | export type Chat = {
2 | id: string;
3 | name: string;
4 | created_at: string,
5 | };
--------------------------------------------------------------------------------
/frontend/src/utils/DateFormatter.ts:
--------------------------------------------------------------------------------
1 | export const formatDate = (datetime: string) => {
2 | const options: Intl.DateTimeFormatOptions = {
3 | hour: 'numeric',
4 | minute: 'numeric',
5 | second: 'numeric',
6 | hour12: true,
7 | };
8 |
9 | const date = new Date(datetime);
10 |
11 | // Format the time using toLocaleTimeString
12 | const time = date.toLocaleTimeString('en-US', options);
13 |
14 | // Format the date manually
15 | const year = date.getFullYear();
16 | const month = date.toLocaleString('en-US', { month: 'long' });
17 | const day = date.getDate();
18 |
19 | return `${time} on ${month} ${day}, ${year}`;
20 | };
21 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------