github-actions commited on
Commit
9826f0b
·
0 Parent(s):

Deploy to Spaces with Xet

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ static/images/* filter=lfs diff=lfs merge=lfs -text
2
+ anime.db filter=lfs diff=lfs merge=lfs -text
.github/workflows/hf_sync.yml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to HuggingFace Spaces
2
+ on:
3
+ workflow_run:
4
+ workflows: ["Run All Unit Tests", "Run Model Tests on Production Branch"]
5
+ types:
6
+ - completed
7
+ branches: [ "production" ]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ sync-to-hub:
12
+ runs-on: ubuntu-latest
13
+
14
+ # Only run if the triggering workflows succeeded or when we run it manually from GH Actions UI
15
+ if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+ with:
20
+ ref: production
21
+ fetch-depth: 0
22
+ lfs: true
23
+ - name: Install xet
24
+ run: |
25
+ curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh
26
+ echo "$HOME/.xet/bin" >> $GITHUB_PATH
27
+ - name: Set git user for xet security
28
+ run: |
29
+ git config user.name "github-actions"
30
+ git config user.email "actions@github.com"
31
+ - name: Convert binaries to xet and Push to HuggingFace
32
+ env:
33
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
34
+ run: |
35
+ # 1. Install Xet
36
+ git xet install
37
+
38
+ # 2. Create a fresh, empty branch for deployment
39
+ # This disconnects from previous history, removing the "binary in history" error
40
+ git checkout --orphan deployment-branch
41
+
42
+ # 3. Unstage everything so we can re-add them properly with Xet
43
+ git reset
44
+
45
+ # 4. Configure Xet tracking
46
+ # We do this *before* adding files so Xet handles them correctly
47
+ git xet track "anime.db" "static/images/*"
48
+
49
+ # 5. Add all files
50
+ # Since we are in a fresh branch, this adds everything currently on disk
51
+ git add .
52
+
53
+ # 6. Commit and Force Push
54
+ git config user.name "github-actions"
55
+ git config user.email "actions@github.com"
56
+ git commit -m "Deploy to Spaces with Xet"
57
+
58
+ # Force push to 'main' on Hugging Face
59
+ git push --force https://MLDevOps:$HF_TOKEN@huggingface.co/spaces/MLDevOps/CS553_CaseStudy1 HEAD:main
.github/workflows/run_model_tests.yml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run Model Tests on Production Branch
2
+
3
+ # Run on every push and pull request
4
+ on:
5
+ push:
6
+ branches: [ "production" ]
7
+ workflow_dispatch:
8
+
9
+ # Run the following Jobs (just 1)
10
+ jobs:
11
+
12
+ # Run the "run_test" job
13
+ run_test:
14
+
15
+ # It should run on latest ubuntu OS
16
+ runs-on: ubuntu-latest
17
+
18
+ # It should use the following steps
19
+ steps:
20
+
21
+ # Checkout the current repository
22
+ - uses: actions/checkout@v3
23
+ with:
24
+ ref: production
25
+ fetch-depth: 0
26
+ lfs: true
27
+
28
+ # Install Python with 3.12 version
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ # Install all required python libraries
34
+ - name: Install Required Libraries
35
+ run: |
36
+ pip install -r requirements.txt
37
+
38
+ # Run all the unit tests quietly
39
+ - name: Run Unit Test for Chat Models (Quietly)
40
+ env:
41
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
42
+ PYTHONPATH: ${{ github.workspace }}
43
+ run: |
44
+ pytest -q tests/test_chat_models.py
.github/workflows/run_tests.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run All Unit Tests
2
+
3
+ # Run on every push and pull request
4
+ on:
5
+ push:
6
+ workflow_dispatch:
7
+
8
+ # Run the following Jobs (just 1)
9
+ jobs:
10
+
11
+ # Run the "run_test" job
12
+ run_test:
13
+
14
+ # It should run on latest ubuntu OS
15
+ runs-on: ubuntu-latest
16
+
17
+ # It should use the following steps
18
+ steps:
19
+
20
+ # Checkout the current repository
21
+ - uses: actions/checkout@v3
22
+
23
+ # Install Python with 3.12 version
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ # Install all required python libraries
29
+ - name: Install Required Libraries
30
+ run: |
31
+ pip install -r requirements.txt
32
+
33
+ # Run all the unit tests quietly
34
+ - name: Run all Unit Tests (Quietly)
35
+ env:
36
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
37
+ PYTHONPATH: ${{ github.workspace }}
38
+ run: |
39
+ pytest -q --ignore=tests/test_chat_models.py
.gitignore ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore CSV files
2
+ *.csv
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[codz]
7
+ *$py.class
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py.cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ #uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ #poetry.lock
112
+ #poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ #pdm.lock
119
+ #pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ #pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ .pixi
129
+
130
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
131
+ __pypackages__/
132
+
133
+ # Celery stuff
134
+ celerybeat-schedule
135
+ celerybeat.pid
136
+
137
+ # SageMath parsed files
138
+ *.sage.py
139
+
140
+ # Environments
141
+ .env
142
+ .envrc
143
+ .venv
144
+ env/
145
+ venv/
146
+ ENV/
147
+ env.bak/
148
+ venv.bak/
149
+
150
+ # Spyder project settings
151
+ .spyderproject
152
+ .spyproject
153
+
154
+ # Rope project settings
155
+ .ropeproject
156
+
157
+ # mkdocs documentation
158
+ /site
159
+
160
+ # mypy
161
+ .mypy_cache/
162
+ .dmypy.json
163
+ dmypy.json
164
+
165
+ # Pyre type checker
166
+ .pyre/
167
+
168
+ # pytype static type analyzer
169
+ .pytype/
170
+
171
+ # Cython debug symbols
172
+ cython_debug/
173
+
174
+ # PyCharm
175
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
176
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
177
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
178
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
179
+ .idea/
180
+ .venv/
181
+
182
+ # Abstra
183
+ # Abstra is an AI-powered process automation framework.
184
+ # Ignore directories containing user credentials, local state, and settings.
185
+ # Learn more at https://abstra.io/docs
186
+ .abstra/
187
+
188
+ # Visual Studio Code
189
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
190
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
191
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
192
+ # you could uncomment the following to ignore the entire vscode folder
193
+ # .vscode/
194
+
195
+ # Ruff stuff:
196
+ .ruff_cache/
197
+
198
+ # PyPI configuration file
199
+ .pypirc
200
+
201
+ # Cursor
202
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
203
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
204
+ # refer to https://docs.cursor.com/context/ignore-files
205
+ .cursorignore
206
+ .cursorindexingignore
207
+
208
+ # Marimo
209
+ marimo/_static/
210
+ marimo/_lsp/
211
+ __marimo__/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShafathZ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: CS553 CaseStudy1
3
+ emoji: 💬
4
+ colorFrom: yellow
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 6.5.1
8
+ python_version: 3.12.3
9
+ app_file: app.py
10
+ pinned: false
11
+ hf_oauth: true
12
+ hf_oauth_scopes:
13
+ - inference-api
14
+ license: mit
15
+ ---
16
+
17
+ An Anime Recommendation chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
18
+
19
+ ## Models Used by our Chatbot
20
+ | Type of Model | Model Name (Hugging Face Path) |
21
+ |---------------|--------------------------------|
22
+ | Local Model | `Qwen/Qwen3-0.6B` |
23
+ | Inference Client Model| `openai/gpt-oss-20b` |
24
+
25
+
26
+ ## Working with UV (Ultra-Violet)
27
+ ### Install UV
28
+ Please download `uv` (Ultra-Violet) for Python Project Dependency Management: https://docs.astral.sh/uv/getting-started/installation/#installation-methods
29
+
30
+ ### Initializing a uv virtual env
31
+ Run following commands by navigating to the project directory:
32
+ ```bash
33
+ cd /path/to/your/project
34
+ uv sync
35
+ ```
36
+
37
+ ### Activating the virtual env
38
+ In the same project directory, execute the following (if virtual env is not already active):
39
+ ```bash
40
+ source .venv/bin/activate
41
+ ```
42
+
43
+ ### Adding any Libraries / Dependencies
44
+ To add any new dependencies (libraries):
45
+ ```bash
46
+ uv add <library_name>
47
+ ```
48
+
49
+ ## Working with HuggingFace Spaces Locally
50
+ ### Install Gradio with oAuth
51
+ Run the following command in your Python environment:
52
+ ```bash
53
+ uv add "gradio[oauth]"
54
+ ```
55
+
56
+ ### Set up HuggingFace Token
57
+ 1. Go to your HuggingFace profile at: https://huggingface.co/settings/tokens
58
+ 2. Generate a new token for your HuggingFace Space at `Create New Token` -> `Fine-grained`.
59
+ 3. Under `Repository permissions` section, search for the repo: "spaces/MLDevOps/CS553_CaseStudy1" and select it
60
+ 4. Check the box for "Write access to contents/settings of selected repos" and click "Create Token" at the bottom.
61
+ 5. Copy and Paste the generated token into a `.env` file in the root directory of your local copy of CS553_CaseStudy1 repo:
62
+ ```
63
+ HF_TOKEN=XXXXXXXXX
64
+ ```
65
+ 6. Login into HF:
66
+ ```bash
67
+ hf auth login
68
+ ```
69
+
70
+ ### Running Gradio App on HuggingFace Spaces Locally
71
+ Run the following command:
72
+ ```bash
73
+ python app.py
74
+ ```
75
+
76
+ It will spit out logs indicating the url to open in browser:
77
+ ```
78
+ ...
79
+ * Running on local URL: http://127.0.0.1:7860
80
+ ...
81
+ ```
82
+
83
+ ### Debugging Gradio Issue
84
+ In app.py, the line:
85
+ ```python
86
+ chatbot = gr.ChatInterface(
87
+ respond,
88
+ type="messages",
89
+ ...
90
+ )
91
+ ```
92
+ might need to be changed to remove the type line as follows due to a deprecation issue on HuggingFace Spaces:
93
+ ```python
94
+ chatbot = gr.ChatInterface(
95
+ respond,
96
+ ...
97
+ )
98
+ ```
99
+ With this, run the program and it should work locally on localhost server!
anime.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9f2c79bc2be7d84d3089640ea010361c4aea5eb4e580e17148f60e68337ced4
3
+ size 1294336
app.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from pathlib import Path
3
+ import backend
4
+ from constants import *
5
+
6
+ theme_css = Path("static/css/theme.css").read_text() if Path("static/css/theme.css").exists() else ""
7
+ main_css = Path("static/css/gradiomain.css").read_text()
8
+ CSS = theme_css + "\n\n" + main_css
9
+
10
+ # Load static directory
11
+ gr.set_static_paths(paths=[Path.cwd().absolute()/"static"])
12
+
13
+ # Adapter function between frontend and backend. Returns a generator yielding backend results.
14
+ def respond(
15
+ message,
16
+ history: list[dict[str, str]],
17
+ use_local_model,
18
+ hf_token: gr.OAuthToken,
19
+ ):
20
+ for r in backend.process_user_query(SYSTEM_PROMPT, history, message, use_local_model, MAX_TOKENS, TEMPERATURE, TOP_P, hf_token.token):
21
+ yield r
22
+
23
+ with gr.Blocks() as homepage:
24
+ gr.Markdown(
25
+ """
26
+ # Ani<span style="font-size: 2rem;">ℤ</span>enith
27
+ An AI designed to give recommendations of the best anime options based on your preferences! Has knowledge of a full database of anime!
28
+ """,
29
+ elem_classes=["page-header"]
30
+ )
31
+
32
+ with gr.Sidebar():
33
+ gr.LoginButton()
34
+
35
+ local_model = gr.Checkbox(
36
+ label="Use Local Model?",
37
+ value=False,
38
+ elem_classes=["toggle-button"]
39
+ )
40
+
41
+ # Main chatbot interface
42
+ chatbot = gr.ChatInterface(
43
+ respond,
44
+ additional_inputs=[
45
+ local_model,
46
+ ],
47
+ )
48
+ chatbot.chatbot.elem_classes = ["custom-chatbot"]
49
+
50
+ if __name__ == "__main__":
51
+ homepage.launch(css=CSS)
backend.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from huggingface_hub import InferenceClient
3
+ from transformers import pipeline
4
+ from retrieval_utils import get_recommendations
5
+
6
+ genre_list = open("genrelist.txt", "r").read().splitlines()
7
+
8
+ def process_user_query(system_message: str, history: List[dict], user_message: str, use_local_model: bool, max_tokens: int, temperature: float, top_p: float, hf_token):
9
+ # 1. Retrieve genres from the user message using naive approach
10
+ genre_list = detect_genres(user_message)
11
+
12
+ # 2. Retrieve relevant results from DB if the genre_list is not empty
13
+ recommendations_string = ""
14
+ if len(genre_list) > 0:
15
+ recommendations_string = get_recommendations(genre_list)
16
+
17
+ # 3. Query the model
18
+ for result in query_model(system_message,
19
+ history,
20
+ user_message,
21
+ recommendations_string,
22
+ use_local_model,
23
+ max_tokens,
24
+ temperature,
25
+ top_p,
26
+ hf_token):
27
+ yield result
28
+
29
+
30
+ def detect_genres(message: str) -> List[str]:
31
+ requested_genres = []
32
+ # Simple naive genre check by detecting if any of our system stored genres are within the user query
33
+ # TODO: Improve genre detection instead to use Retriever and RAG framework in the future
34
+ for genre in genre_list:
35
+ if message.__contains__(genre):
36
+ requested_genres.append(genre)
37
+ return requested_genres
38
+
39
+
40
+ def query_model(
41
+ system_message: str,
42
+ history: List[dict],
43
+ user_message: str,
44
+ recommendations_string: str,
45
+ use_local_model: bool,
46
+ max_tokens: int, # TODO: Remove this and hardcode a value in constants.py
47
+ temperature: float, # TODO: Remove this and hardcode a value in constants.py
48
+ top_p: float, # TODO: Remove this and hardcode a value in constants.py
49
+ hf_token):
50
+
51
+ # Construct messages for the language model
52
+ # Start by adding system prompt
53
+ system_prompt = system_message
54
+ if recommendations_string:
55
+ system_prompt += "\nRECOMMENDATION JSON:" + f"\n{recommendations_string}"
56
+ messages = [{"role": "system", "content": system_prompt}]
57
+
58
+ # Add the rest of the history
59
+ messages.extend(history)
60
+
61
+ # Add the current user prompt
62
+ messages.append({"role": "user", "content": user_message})
63
+
64
+ # Determine which model to use (local or external)
65
+ if use_local_model:
66
+ # Local Model -- Uses pipeline from transformers library
67
+ pipeline_local_model = pipeline(task='text-generation',
68
+ model='Qwen/Qwen3-0.6B',
69
+ max_new_tokens=max_tokens,
70
+ temperature=temperature,
71
+ do_sample=False,
72
+ top_p=top_p
73
+ )
74
+ # Get the response from the local model
75
+ response = pipeline_local_model(messages)
76
+
77
+ # Parse the output and yield it
78
+ yield response[0]['generated_text'][-1]['content'].split('</think>')[-1].strip()
79
+
80
+
81
+ elif not use_local_model:
82
+ # Non-local Model -- Use InferenceClient
83
+ client = InferenceClient(
84
+ token=hf_token,
85
+ model="openai/gpt-oss-20b",
86
+ )
87
+
88
+ response = ""
89
+ for chunk in client.chat_completion(
90
+ messages=messages,
91
+ max_tokens=max_tokens,
92
+ stream=True,
93
+ temperature=temperature,
94
+ top_p=top_p,
95
+ ):
96
+ if chunk.choices and chunk.choices[0].delta.content:
97
+ token = chunk.choices[0].delta.content
98
+ response += token
99
+ yield response
100
+
constants.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_PROMPT = f"""
2
+ You are an expert on recommending Anime shows. Please use the RECOMMENDATIONS to answer the user's question.
3
+ The RECOMMENDATIONS is a JSON String that contains information of top Anime sorted in descending order by:
4
+ 1. Number of Requested Genre Matches from the User
5
+ 2. The Score of the Anime
6
+
7
+ If the RECOMMENDATIONS JSON String is not given:
8
+ 1. Then answer the question like a Friendly Chatbot!
9
+ 2. Do not reference anything about a RECOMMENDATION JSON
10
+ 3. Ask the user to provide their favorite genre(s) for Anime Recommendations
11
+ """
12
+
13
+ MAX_TOKENS = 2048
14
+ TEMPERATURE = 0.7
15
+ TOP_P = 0.7
create_db.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import sqlite3
3
+
4
+ # Load the Anime Dataset as a Pandas DataFrame
5
+ df = pd.read_csv('data/anime-dataset-2023.csv')
6
+
7
+ # Separate the DataFrame into separate tables
8
+ # Genre Table
9
+ # Create a separate DataFrame using Genres column
10
+ genres_df = df['Genres'].str.split(', ').explode().str.strip().drop_duplicates().reset_index(drop=True)
11
+ genres_df = pd.DataFrame({'genre_id': range(1, len(genres_df) + 1), 'genre_name': genres_df})
12
+
13
+ # Exclude some Genres
14
+ excluded_genres = ['Hentai', 'UNKNOWN', 'Erotica', 'Ecchi']
15
+ genres_df = genres_df[~genres_df['genre_name'].isin(excluded_genres)]
16
+
17
+
18
+ # Anime Table
19
+ # Filter out animes that contain excluded genres
20
+ pattern = '|'.join(excluded_genres)
21
+ anime_df = df[~df['Genres'].str.contains(pattern, na=True)]
22
+
23
+ # Filter out animes without English Names
24
+ anime_df = anime_df[anime_df['English name'] != 'UNKNOWN']
25
+
26
+ # Filter out animes without Scores
27
+ anime_df = anime_df[anime_df['Score'] != 'UNKNOWN']
28
+
29
+ # Filter out animes without Synopsis
30
+ anime_df = anime_df[anime_df['Synopsis'] != 'No description available for this anime.']
31
+
32
+ # Sort by Score and Keep only Top 1000 Animes
33
+ anime_df = anime_df.sort_values(by='Score', ascending=False)
34
+ anime_df = anime_df.head(1000)
35
+
36
+ # Rename Columns
37
+ anime_df = anime_df.rename(columns={'English name': 'name', 'Score': 'score', 'Synopsis': 'synopsis'})
38
+
39
+
40
+
41
+
42
+ # AnimeGenre Table
43
+ # Create a mapping of genre_name to genre_id
44
+ genre_mapping = genres_df.set_index('genre_name')['genre_id'].to_dict()
45
+
46
+ # Explode genres for filtered animes and map to genre_ids
47
+ anime_genre_df = anime_df[['anime_id', 'Genres']].copy()
48
+ anime_genre_df = anime_genre_df.assign(genre_name=anime_genre_df['Genres'].str.split(', ')).explode('genre_name')
49
+ anime_genre_df['genre_name'] = anime_genre_df['genre_name'].str.strip()
50
+ anime_genre_df['genre_id'] = anime_genre_df['genre_name'].map(genre_mapping)
51
+ anime_genre_df = anime_genre_df[['anime_id', 'genre_id']].dropna()
52
+ anime_genre_df['genre_id'] = anime_genre_df['genre_id'].astype(int)
53
+
54
+
55
+
56
+ # Final Clean Up
57
+ # Keep only anime_id, Name, Score and Synopsis Columns
58
+ anime_df = anime_df[['anime_id', 'name', 'score', 'synopsis']]
59
+ anime_df = anime_df.rename(columns={'anime_id': 'id'})
60
+ genres_df = genres_df.rename(columns={'genre_id': 'id'})
61
+
62
+
63
+ SCHEMA_SQL = '''
64
+ PRAGMA foreign_keys = ON;
65
+
66
+ CREATE TABLE IF NOT EXISTS Anime (
67
+ id INTEGER PRIMARY KEY,
68
+ name VARCHAR(50),
69
+ score FLOAT,
70
+ synopsis TEXT
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS Genre (
74
+ id INTEGER PRIMARY KEY,
75
+ genre_name VARCHAR(20)
76
+ );
77
+
78
+ CREATE TABLE IF NOT EXISTS AnimeGenre (
79
+ anime_id INTEGER NOT NULL,
80
+ genre_id INTEGER NOT NULL,
81
+ PRIMARY KEY (anime_id, genre_id),
82
+ FOREIGN KEY (anime_id) REFERENCES Anime(id) ON DELETE CASCADE ON UPDATE CASCADE,
83
+ FOREIGN KEY (genre_id) REFERENCES Genre(id) ON DELETE CASCADE ON UPDATE CASCADE
84
+ );
85
+
86
+ CREATE INDEX IF NOT EXISTS idx_anime_id ON AnimeGenre(anime_id);
87
+ CREATE INDEX IF NOT EXISTS idx_genre_id ON AnimeGenre(genre_id);
88
+ '''
89
+
90
+ with sqlite3.connect('anime.db') as conn:
91
+ conn.executescript(SCHEMA_SQL)
92
+ anime_df.to_sql('Anime', conn, if_exists='delete_rows', index=False, method='multi')
93
+ genres_df.to_sql('Genre', conn, if_exists='delete_rows', index=False, method='multi')
94
+ anime_genre_df.to_sql('AnimeGenre', conn, if_exists='delete_rows', index=False, method='multi')
data/.gitkeep ADDED
File without changes
genrelist.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Action
2
+ Award Winning
3
+ Sci-Fi
4
+ Adventure
5
+ Drama
6
+ Mystery
7
+ Supernatural
8
+ Fantasy
9
+ Sports
10
+ Comedy
11
+ Romance
12
+ Slice of Life
13
+ Suspense
14
+ Gourmet
15
+ Avant Garde
16
+ Horror
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from cs553-casestudy1!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "cs553-casestudy1"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "gradio[oauth]>=6.5.1",
9
+ "huggingface-hub>=1.3.5",
10
+ "pytest>=9.0.2",
11
+ "itsdangerous>=2.2.0",
12
+ "torch>=2.10.0",
13
+ "transformers>=5.0.0",
14
+ ]
requirements.txt ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==24.1.0
2
+ annotated-doc==0.0.4
3
+ annotated-types==0.7.0
4
+ anyio==4.12.1
5
+ authlib==1.6.6
6
+ brotli==1.2.0
7
+ certifi==2026.1.4
8
+ cffi==2.0.0
9
+ click==8.3.1
10
+ cryptography==46.0.4
11
+ cuda-bindings==12.9.4
12
+ cuda-pathfinder==1.3.3
13
+ fastapi==0.128.0
14
+ ffmpy==1.0.0
15
+ filelock==3.20.3
16
+ fsspec==2026.1.0
17
+ gradio==6.5.1
18
+ gradio-client==2.0.3
19
+ groovy==0.1.2
20
+ h11==0.16.0
21
+ hf-xet==1.2.0
22
+ httpcore==1.0.9
23
+ httpx==0.28.1
24
+ huggingface-hub==1.3.7
25
+ idna==3.11
26
+ iniconfig==2.3.0
27
+ itsdangerous==2.2.0
28
+ jinja2==3.1.6
29
+ markdown-it-py==4.0.0
30
+ markupsafe==3.0.3
31
+ mdurl==0.1.2
32
+ mpmath==1.3.0
33
+ networkx==3.6.1
34
+ numpy==2.4.2
35
+ nvidia-cublas-cu12==12.8.4.1
36
+ nvidia-cuda-cupti-cu12==12.8.90
37
+ nvidia-cuda-nvrtc-cu12==12.8.93
38
+ nvidia-cuda-runtime-cu12==12.8.90
39
+ nvidia-cudnn-cu12==9.10.2.21
40
+ nvidia-cufft-cu12==11.3.3.83
41
+ nvidia-cufile-cu12==1.13.1.3
42
+ nvidia-curand-cu12==10.3.9.90
43
+ nvidia-cusolver-cu12==11.7.3.90
44
+ nvidia-cusparse-cu12==12.5.8.93
45
+ nvidia-cusparselt-cu12==0.7.1
46
+ nvidia-nccl-cu12==2.27.5
47
+ nvidia-nvjitlink-cu12==12.8.93
48
+ nvidia-nvshmem-cu12==3.4.5
49
+ nvidia-nvtx-cu12==12.8.90
50
+ orjson==3.11.7
51
+ packaging==26.0
52
+ pandas==3.0.0
53
+ pillow==12.1.0
54
+ pluggy==1.6.0
55
+ pycparser==3.0
56
+ pydantic==2.12.5
57
+ pydantic-core==2.41.5
58
+ pydub==0.25.1
59
+ pygments==2.19.2
60
+ pytest==9.0.2
61
+ python-dateutil==2.9.0.post0
62
+ python-multipart==0.0.22
63
+ pytz==2025.2
64
+ pyyaml==6.0.3
65
+ regex==2026.1.15
66
+ rich==14.3.2
67
+ safehttpx==0.1.7
68
+ safetensors==0.7.0
69
+ semantic-version==2.10.0
70
+ setuptools==80.10.2
71
+ shellingham==1.5.4
72
+ six==1.17.0
73
+ starlette==0.50.0
74
+ sympy==1.14.0
75
+ tokenizers==0.22.2
76
+ tomlkit==0.13.3
77
+ torch==2.10.0
78
+ tqdm==4.67.3
79
+ transformers==5.0.0
80
+ triton==3.6.0
81
+ typer==0.21.1
82
+ typer-slim==0.21.1
83
+ typing-extensions==4.15.0
84
+ typing-inspection==0.4.2
85
+ uvicorn==0.40.0
retrieval_utils.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from typing import List, Tuple
3
+ import json
4
+
5
+ # Constants
6
+ DB_PATH = "anime.db"
7
+
8
+ def get_recommendations(requested_genres: List[str], limit: int = 5) -> str:
9
+ # Establish Connection and Cursor
10
+ connection = sqlite3.connect(DB_PATH)
11
+ cursor = connection.cursor()
12
+
13
+ # Prepare placeholders for the SQL Query
14
+ placeholders = ', '.join(['?'] * len(requested_genres))
15
+
16
+ # Define SQL Query for the Weighted Ranking Logic
17
+ query = f"""
18
+ -- Project only Anime's name, score, and synopsis
19
+ SELECT
20
+ A.name,
21
+ A.score,
22
+ A.synopsis
23
+
24
+ -- From Anime Table
25
+ FROM Anime A
26
+
27
+ -- Join Anime Table with AnimeGenre on ids
28
+ JOIN AnimeGenre AG ON A.id = AG.anime_id
29
+
30
+ -- Join Genre Table with AnimeGenre on ids
31
+ JOIN Genre G ON AG.genre_id = G.id
32
+
33
+ -- Filter by only genre_name which belong in the list of requested genres
34
+ WHERE G.genre_name IN ({placeholders})
35
+
36
+ -- Group by Anime id
37
+ GROUP BY A.id
38
+
39
+ -- Primary Sort by Count of Matches in the requested_genres list
40
+ -- Secondary Sort by Anime's score
41
+ ORDER BY COUNT(G.id) DESC, A.score DESC
42
+
43
+ -- Return only the top 5 matches
44
+ LIMIT ?
45
+ """
46
+
47
+ # Execute the Query with the requested genres and the limit
48
+ cursor.execute(query, requested_genres + [limit])
49
+
50
+ # Gather the results
51
+ results = cursor.fetchall()
52
+
53
+ # Close the Connection
54
+ connection.close()
55
+
56
+ # Compose a JSON String from the results and return it
57
+ return jsonify_recommendations(results)
58
+
59
+
60
+ def jsonify_recommendations(recommendations: List[Tuple[str, float, str]]) -> str:
61
+ # Process each anime recommendations into a list of dicts (for easy JSON conversion)
62
+ list_of_dicts = []
63
+ for anime in recommendations:
64
+ list_of_dicts.append({
65
+ 'name': anime[0],
66
+ 'score': anime[1],
67
+ 'description': anime[2]
68
+ })
69
+
70
+ # JSONify and return the list of dicts
71
+ return json.dumps(list_of_dicts, indent=4)
72
+
73
+
74
+ # Driver Code
75
+ if __name__ == '__main__':
76
+ requested_genres = ["Action", "Drama"]
77
+ recommendations = get_recommendations(requested_genres)
78
+ print(recommendations)
static/css/gradiomain.css ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ====== MAIN APP CONTAINER AND HEADER ====== */
2
+ .page-header {
3
+ text-align: center;
4
+ border-radius: 22px;
5
+ margin-bottom: 1rem;
6
+ margin-top: 0.25rem;
7
+ }
8
+
9
+ .page-header h1 {
10
+ margin-bottom: 1.2rem;
11
+ margin-top: 2rem;
12
+ }
13
+
14
+ .gradio-container {
15
+ background-image:
16
+ linear-gradient(rgba(12, 13, 35, 0.6), rgba(12, 13, 35, 0.8)),
17
+ url('/gradio_api/file=static/images/background.png');
18
+ background-size: cover;
19
+ background-position: center;
20
+ background-repeat: no-repeat;
21
+ background-attachment: fixed;
22
+ background-blend-mode: hard-light;
23
+
24
+ color: var(--text-main);
25
+ font-family: 'Inter', sans-serif;
26
+ min-height: 100vh;
27
+ padding: 20px;
28
+ }
29
+
30
+ /* ====== CHATBOT CONTAINER ====== */
31
+ .custom-chatbot,
32
+ [role="log"] {
33
+ background: var(--bg-glass-strong) !important;
34
+ border-radius: 22px !important;
35
+ }
36
+
37
+ /* Why is Gradio like this )): */
38
+ .block,
39
+ .form,
40
+ .gr-group {
41
+ background: var(--bg-glass-strong);
42
+ border-radius: 22px !important;
43
+ }
44
+
45
+ /* ====== CHAT MESSAGES ====== */
46
+ .bot,
47
+ .user {
48
+ padding: 16px 20px;
49
+ border-radius: 18px;
50
+ margin: 12px 0;
51
+
52
+ max-width: 70%;
53
+ min-width: fit-content;
54
+ display: inline-flex;
55
+
56
+ border: 1px solid var(--border-soft);
57
+ box-shadow: 0 4px 20px rgba(122, 162, 255, 0.2);
58
+
59
+ white-space: normal;
60
+ overflow-wrap: break-word;
61
+ word-break: normal;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ .bot {
66
+ background: var(--bot-message);
67
+ color: var(--text-main);
68
+ margin-right: auto;
69
+ margin-left: 0;
70
+ }
71
+
72
+ .user {
73
+ background: var(--user-message);
74
+ color: white;
75
+ margin-left: auto;
76
+ margin-right: 0;
77
+ }
78
+
79
+ /* ====== CHAT INPUT AREA ====== */
80
+ .gr-text-input,
81
+ [data-testid="textbox"] {
82
+ background: rgba(40, 24, 65, 0.6) !important;
83
+ border-radius: 18px !important;
84
+ border: 1px solid var(--border-soft) !important;
85
+ min-height: 60px !important;
86
+ height: auto !important;
87
+ margin-right: 12px !important;
88
+ box-shadow: 0 0 10px rgba(255, 124, 229, 0.1);
89
+ }
90
+
91
+ .gr-text-input:hover,
92
+ textarea:hover {
93
+ box-shadow: 0 0 15px var(--accent-glow) !important;
94
+ transition: box-shadow 0.3s ease !important;
95
+ }
96
+
97
+ .gr-text-input:focus,
98
+ textarea:focus {
99
+ box-shadow: 0 0 20px var(--accent-glow) !important;
100
+ outline: none !important;
101
+ transition: box-shadow 0.3s ease !important;
102
+ }
103
+
104
+ textarea {
105
+ background: transparent !important;
106
+ color: var(--text-main) !important;
107
+ border: none !important;
108
+ border-radius: 16px !important;
109
+ padding: 16px 20px !important;
110
+ font-size: 16px !important;
111
+ line-height: 1.5 !important;
112
+ min-height: 60px !important;
113
+ height: auto !important;
114
+ resize: vertical !important;
115
+ overflow-y: auto !important;
116
+ max-height: 200px !important;
117
+ white-space: pre-wrap !important;
118
+ word-wrap: break-word !important;
119
+ }
120
+
121
+ textarea::placeholder {
122
+ color: var(--text-muted) !important;
123
+ opacity: 0.75 !important;
124
+ }
125
+
126
+ /* ====== BUTTON STUFF ====== */
127
+ button[data-testid="login-button"],
128
+ .submit-button {
129
+ background: var(--btn-bg);
130
+ border: none !important;
131
+ border-radius: 16px !important;
132
+ color: white !important;
133
+ font-weight: 600 !important;
134
+ padding: 16px 28px !important;
135
+ min-height: 60px !important;
136
+ box-shadow: none;
137
+ cursor: pointer !important;
138
+ }
139
+
140
+ .submit-button:hover,
141
+ .submit-button:focus-visible {
142
+ box-shadow: 0 0 18px var(--accent-glow);
143
+ transform: translateY(-1px);
144
+ filter: brightness(1.05);
145
+ }
146
+
147
+ /* Sidebar buttons */
148
+ .sidebar button {
149
+ background: linear-gradient(135deg, #ff7ce5, #7ac5ff) !important;
150
+ }
151
+
152
+ .sidebar button:hover {
153
+ background: linear-gradient(135deg, #ff9de0, #7ac5ff) !important;
154
+ }
155
+
156
+ /* Toggle Buttons */
157
+ .block.toggle-button {
158
+ margin-left: auto !important;
159
+ margin-right: auto !important;
160
+ width: fit-content;
161
+ }
162
+
163
+ .toggle-button input[type="checkbox"] {
164
+ display: none;
165
+ }
166
+
167
+ .toggle-button label {
168
+ display: inline-flex;
169
+ align-items: center;
170
+ gap: 0.4rem;
171
+
172
+ padding: 0.65rem 1.2rem;
173
+ border-radius: 999px;
174
+ cursor: pointer;
175
+
176
+ font-weight: 600;
177
+ letter-spacing: 0.02em;
178
+
179
+ background: var(--btn-bg);
180
+ color: var(--btn-text);
181
+ border: 1px solid var(--btn-border);
182
+
183
+ transition:
184
+ background 0.25s ease,
185
+ box-shadow 0.25s ease,
186
+ transform 0.25s ease;
187
+ }
188
+
189
+ .toggle-button label:hover {
190
+ background: var(--btn-bg-hover);
191
+ box-shadow: 0 0 22px rgba(122, 197, 255, 0.7);
192
+ }
193
+
194
+ .toggle-button label:has(input:checked) {
195
+ background: var(--btn-bg-active);
196
+ box-shadow: 0 0 28px rgba(122, 197, 255, 0.9);
197
+ }
198
+
199
+ /* Add the word ON in green text if enabled local model */
200
+ .toggle-button label:has(input:checked)::after {
201
+ content: " ON";
202
+ font-weight: 700;
203
+ color: #4cffc3;
204
+ margin-left: 0.3rem;
205
+ text-shadow: 0 0 6px rgba(76, 255, 195, 0.8);
206
+ }
207
+
208
+ .toggle-button label:active {
209
+ transform: scale(0.96);
210
+ }
211
+
212
+ /* ====== SLIDERS ====== */
213
+ .custom-slider input[type="range"]::-webkit-slider-thumb {
214
+ background: var(--accent-secondary) !important;
215
+ width: 22px !important;
216
+ height: 22px !important;
217
+ border-radius: 50% !important;
218
+ border: 2px solid white !important;
219
+ box-shadow: 0 0 12px var(--accent-glow);
220
+ }
221
+
222
+ .custom-slider input[type="range"]::-webkit-slider-runnable-track {
223
+ height: 8px;
224
+ border-radius: 8px;
225
+ background: linear-gradient(120deg, var(--accent-primary), var(--accent-secondary));
226
+ }
227
+
228
+ /* ====== SIDEBAR ====== */
229
+ .sidebar {
230
+ background-image:
231
+ linear-gradient(rgba(20, 24, 55, 0.4), rgba(20, 24, 55, 0.4)),
232
+ url('/gradio_api/file=static/images/sidebar.jpg');
233
+ background-size: auto, cover;
234
+ background-position: top left, center;
235
+ background-repeat: no-repeat, no-repeat;
236
+
237
+ border-right: 1px solid var(--border-soft) !important;
238
+ backdrop-filter: blur(15px);
239
+ padding: 20px !important;
240
+ }
static/css/theme.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* This page contains base theme colors for the app */
2
+ :root {
3
+ /* Base backgrounds */
4
+ --bg-main: #0c0d23;
5
+ --bg-glass: rgba(28, 20, 55, 0.55);
6
+ --bg-glass-strong: rgba(28, 20, 55, 0.85);
7
+ --bg-gradient: linear-gradient(135deg, #1b1b40, #3a1f5f);
8
+
9
+ /* Accents */
10
+ --accent-primary: #ff7ce5;
11
+ --accent-secondary: #7ac5ff;
12
+ --accent-glow: rgba(255, 124, 229, 0.5);
13
+
14
+ /* Borders and shadows */
15
+ --border-soft: rgba(255, 255, 255, 0.08);
16
+ --shadow-soft: 0 15px 35px rgba(255, 124, 229, 0.3);
17
+
18
+ /* Text */
19
+ --text-main: #f0f0ff;
20
+ --text-muted: #b9b6ff;
21
+
22
+ /* Chat messages */
23
+ --user-message: linear-gradient(135deg, #ff9de0, #7ac5ff);
24
+ --bot-message: linear-gradient(135deg, #ff9de0, #7ac5ff);
25
+
26
+ /* Buttons */
27
+ --btn-bg: linear-gradient(135deg, #7ac5ff, #9aa4ff);
28
+ --btn-bg-hover: linear-gradient(135deg, #8fd0ff, #aab3ff);
29
+ --btn-bg-active: linear-gradient(135deg, #6bb6f5, #8f98ff);
30
+
31
+ --btn-text: #0c0d23;
32
+ --btn-border: rgba(122, 197, 255, 0.45);
33
+
34
+ --btn-glow: 0 0 18px rgba(122, 197, 255, 0.55);
35
+ }
static/images/background.png ADDED

Git LFS Details

  • SHA256: f0bcd0de1788bb1f4d95780afc11429cd8009c8d9825bb8fd29af1f54e09073c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.43 MB
static/images/sidebar.jpg ADDED

Git LFS Details

  • SHA256: 0ea120fe1e7e72cd01e57f3b9d141262239b0daa9fb5089e250332b1fe207a29
  • Pointer size: 131 Bytes
  • Size of remote file: 525 kB
test_detect_genre.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from backend import detect_genres
2
+
3
+ def test_one_genre():
4
+ detect_genres("I like Action"),["Action"]
5
+
6
+ def test_multiple_genres():
7
+ detect_genres("I like Action and Mystery"),["Action","Mystery"]
8
+
9
+ def test_no_genres():
10
+ detect_genres("I like Interesting shows"),[]
11
+
tests/test_chat_models.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import pytest
4
+
5
+ from backend import process_user_query
6
+
7
+ TEST_SYSTEM_MESSAGE = "You are a friendly chatbot."
8
+ TEST_USER_MESSAGE = "Hello!"
9
+ HF_TOKEN = os.getenv("HF_TOKEN")
10
+
11
+ def test_HF_token_exists():
12
+ token = os.getenv("HF_TOKEN")
13
+ assert token is not None
14
+ assert len(token) > 1
15
+
16
+ def test_local_model_runs():
17
+ use_local_model = True
18
+ collected_result = ""
19
+ for result in process_user_query(system_message=TEST_SYSTEM_MESSAGE,
20
+ history=[],
21
+ user_message=TEST_USER_MESSAGE,
22
+ use_local_model=use_local_model,
23
+ max_tokens=100,
24
+ temperature=0.7,
25
+ top_p=0.7,
26
+ hf_token=HF_TOKEN):
27
+ collected_result = result
28
+
29
+ assert len(collected_result) > 0
30
+
31
+ def test_external_model_runs():
32
+ use_local_model = False
33
+ collected_result = ""
34
+ for result in process_user_query(system_message=TEST_SYSTEM_MESSAGE,
35
+ history=[],
36
+ user_message=TEST_USER_MESSAGE,
37
+ use_local_model=use_local_model,
38
+ max_tokens=100,
39
+ temperature=0.7,
40
+ top_p=0.7,
41
+ hf_token=HF_TOKEN):
42
+ collected_result = result
43
+ assert len(collected_result) > 0
44
+
45
+ if __name__ == "__main__":
46
+ pytest.main()
47
+
48
+
tests/test_retrieval_utils.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sqlite3
3
+ from pathlib import Path
4
+ import pytest
5
+ import retrieval_utils
6
+ from retrieval_utils import get_recommendations
7
+
8
+ # Setup Test DB
9
+ def _setup_test_db(db_path: str):
10
+ # Setup the DB Connection
11
+ connection = sqlite3.connect(db_path)
12
+ cursor = connection.cursor()
13
+
14
+ # Create Test Anime Table
15
+ cursor.execute(
16
+ """
17
+ CREATE TABLE Anime (
18
+ id INTEGER PRIMARY KEY,
19
+ name TEXT NOT NULL,
20
+ score REAL NOT NULL,
21
+ synopsis TEXT
22
+ );
23
+ """
24
+ )
25
+
26
+ # Create Test Genre Table
27
+ cursor.execute(
28
+ """
29
+ CREATE TABLE Genre (
30
+ id INTEGER PRIMARY KEY,
31
+ genre_name TEXT NOT NULL
32
+ );
33
+ """
34
+ )
35
+
36
+ # Create Test AnimeGenre Table
37
+ cursor.execute(
38
+ """
39
+ CREATE TABLE AnimeGenre (
40
+ anime_id INTEGER NOT NULL,
41
+ genre_id INTEGER NOT NULL
42
+ );
43
+ """
44
+ )
45
+
46
+ # Define new values to be inserted in the Anime Table
47
+ anime_rows = [
48
+ (1, "Alpha", 9.1, "Alpha synopsis"),
49
+ (2, "Beta", 8.7, "Beta synopsis"),
50
+ (3, "Gamma", 8.9, "Gamma synopsis"),
51
+ (4, "Delta", 7.5, "Delta synopsis"),
52
+ ]
53
+
54
+ # Define new values to be inserted in the Genre Table
55
+ genre_rows = [
56
+ (1, "Action"),
57
+ (2, "Drama"),
58
+ (3, "Comedy"),
59
+ ]
60
+
61
+ # Define new values to be inserted in the AnimeGenre Table
62
+ anime_genre_rows = [
63
+ (1, 1), (1, 2), # Alpha: Action, Drama (2 matches)
64
+ (2, 1), # Beta: Action (1 match)
65
+ (3, 2), # Gamma: Drama (1 match)
66
+ (4, 3), # Delta: Comedy (0 matches for Action/Drama)
67
+ ]
68
+
69
+ # Insert into all Tables the defined new values above
70
+ cursor.executemany("INSERT INTO Anime VALUES (?, ?, ?, ?);", anime_rows)
71
+ cursor.executemany("INSERT INTO Genre VALUES (?, ?);", genre_rows)
72
+ cursor.executemany("INSERT INTO AnimeGenre VALUES (?, ?);", anime_genre_rows)
73
+
74
+ # Commit all the writes to the DB file
75
+ connection.commit()
76
+
77
+ # Close the cursor and the connection
78
+ cursor.close()
79
+ connection.close()
80
+
81
+
82
+ def test_get_recommendations_orders_by_match_count_then_score(tmp_path: Path,
83
+ monkeypatch: pytest.MonkeyPatch):
84
+ # Setup Test Data and Mocks
85
+ # Construct a temporary path for the Test DB
86
+ db_path = tmp_path / "test.db"
87
+
88
+ # Setup the test db
89
+ _setup_test_db(str(db_path))
90
+
91
+ # Monkeypatch the DB_PATH variable of retrieval_utils file
92
+ monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
93
+
94
+ # Execute the Method under Test
95
+ result_json = get_recommendations(["Action", "Drama"], limit=3)
96
+ result = json.loads(result_json)
97
+
98
+ # Assert on the results
99
+ assert [item["name"] for item in result] == ["Alpha", "Gamma", "Beta"]
100
+ assert result[0]["score"] == 9.1
101
+ assert "description" in result[0]
102
+
103
+
104
+ def test_get_recommendations_respects_limit(tmp_path: Path,
105
+ monkeypatch: pytest.MonkeyPatch):
106
+ # Setup Test Data and Mocks
107
+ # Construct a temporary path for the Test DB
108
+ db_path = tmp_path / "test.db"
109
+
110
+ # Setup the test db
111
+ _setup_test_db(str(db_path))
112
+
113
+ # Monkeypatch the DB_PATH variable of retrieval_utils file
114
+ monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
115
+
116
+ # Execute the Method under Test
117
+ result_json = get_recommendations(["Action", "Drama"], limit=1)
118
+ result = json.loads(result_json)
119
+
120
+ # Assert on the results
121
+ assert len(result) == 1
122
+ assert result[0]["name"] == "Alpha"
123
+
124
+
125
+ def test_get_recommendations_single_genre(tmp_path: Path,
126
+ monkeypatch: pytest.MonkeyPatch):
127
+ # Setup Test Data and Mocks
128
+ # Construct a temporary path for the Test DB
129
+ db_path = tmp_path / "test.db"
130
+
131
+ # Setup the test db
132
+ _setup_test_db(db_path)
133
+
134
+ # Monkeypatch the DB_PATH variable of retrieval_utils file
135
+ monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
136
+
137
+ # Execute the Method under Test
138
+ result_json = get_recommendations(["Drama"], limit=5)
139
+ result = json.loads(result_json)
140
+
141
+ # Assert on the results
142
+ assert [item["name"] for item in result] == ["Alpha", "Gamma"]
143
+ assert all("description" in item for item in result)
144
+
145
+
146
+ def test_get_recommendations_no_genre(tmp_path: Path,
147
+ monkeypatch: pytest.MonkeyPatch):
148
+ # Setup Test Data and Mocks
149
+ # Construct a temporary path for the Test DB
150
+ db_path = tmp_path / "test.db"
151
+
152
+ # Setup the test db
153
+ _setup_test_db(db_path)
154
+
155
+ # Monkeypatch the DB_PATH variable of retrieval_utils file
156
+ monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
157
+
158
+ # Execute the Method under Test
159
+ result_json = get_recommendations([], limit=5)
160
+ result = json.loads(result_json)
161
+
162
+ # Assert on the results
163
+ assert len(result) == 0
uv.lock ADDED
The diff for this file is too large to render. See raw diff