ThuanLuong commited on
Commit
14aed39
·
1 Parent(s): c6335af
Files changed (5) hide show
  1. .gitignore +223 -0
  2. app.css +299 -0
  3. app.py +325 -0
  4. requirements.txt +6 -0
  5. router.py +109 -0
.gitignore ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+ .agents/
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
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+ output/
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ cmd.txt
221
+ chroma_db/
222
+ local_docs/
223
+ test.py
app.css ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body,
2
+ .gradio-container {
3
+ background: #000000 !important;
4
+ color: #edf7ff !important;
5
+ }
6
+
7
+ .gradio-container {
8
+ min-height: 100vh !important;
9
+ }
10
+
11
+ #app-title {
12
+ padding: 22px 16px 12px !important;
13
+ text-align: center !important;
14
+ }
15
+
16
+ #app-title h1 {
17
+ color: #f3fbff !important;
18
+ font-size: 30px !important;
19
+ font-weight: 700 !important;
20
+ letter-spacing: 0.02em !important;
21
+ margin: 0 0 8px !important;
22
+ }
23
+
24
+ #app-title p {
25
+ color: #92aeca !important;
26
+ margin: 0 !important;
27
+ }
28
+
29
+ #title-divider-wrap {
30
+ max-width: 860px !important;
31
+ margin: -3px auto 12px !important;
32
+ padding: 0 16px !important;
33
+ }
34
+
35
+ #title-divider {
36
+ width: 100% !important;
37
+ height: 1px !important;
38
+ background: linear-gradient(
39
+ 90deg,
40
+ rgba(255, 255, 255, 0) 0%,
41
+ rgba(255, 255, 255, 0.18) 22%,
42
+ rgba(255, 255, 255, 0.18) 78%,
43
+ rgba(255, 255, 255, 0) 100%
44
+ ) !important;
45
+ }
46
+
47
+
48
+ #chatbot,
49
+ #chatbot > div,
50
+ #chatbot .wrap,
51
+ #chatbot .chatbot,
52
+ #chatbot .bubble-wrap {
53
+ background: #000000 !important;
54
+ border: 0 !important;
55
+ box-shadow: none !important;
56
+ }
57
+
58
+ #chatbot {
59
+ max-width: 860px !important;
60
+ margin: 0 auto !important;
61
+ padding: 0 16px !important;
62
+ }
63
+
64
+ #chatbot .wrap,
65
+ #chatbot .message,
66
+ #chatbot .message-wrap,
67
+ #chatbot .chatbot,
68
+ #chatbot .prose,
69
+ #chatbot .prose * {
70
+ /* Keep text bubble contents transparent to let bubble background show */
71
+ }
72
+
73
+ #chatbot .message,
74
+ #chatbot .message-wrap {
75
+ border-radius: 22px !important;
76
+ }
77
+
78
+ #chatbot .message-wrap {
79
+ padding: 12px 0 !important;
80
+ }
81
+
82
+ /*
83
+ #chatbot .message.user,
84
+ #chatbot .message.assistant,
85
+ [data-testid="chatbot"] .message.user,
86
+ [data-testid="chatbot"] .message.assistant {
87
+ width: fit-content !important;
88
+ max-width: 85% !important;
89
+ flex-shrink: 0 !important;
90
+ }
91
+
92
+ #chatbot .message.user > div,
93
+ #chatbot .message.user > p,
94
+ #chatbot .message.user > span,
95
+ #chatbot .message.user .prose,
96
+ #chatbot .message.user .prose p {
97
+ width: max-content !important;
98
+ max-width: 100% !important;
99
+ white-space: pre-wrap !important;
100
+ word-break: break-word !important;
101
+ } */
102
+
103
+ /* #chatbot .message.assistant .prose,
104
+ #chatbot .message.assistant .prose p {
105
+ width: auto !important;
106
+ white-space: pre-wrap !important;
107
+ word-break: break-word !important;
108
+ margin: 0 !important;
109
+ } */
110
+
111
+
112
+
113
+ #chatbot .message.bot,
114
+ #chatbot .message.assistant {
115
+ color: #d6d6d6 !important;
116
+ padding: 14px 18px !important;
117
+ }
118
+
119
+ #chatbot textarea,
120
+ #chatbot input {
121
+ background: rgba(8, 18, 29, 0.72) !important;
122
+ color: #f5fbff !important;
123
+ border: 1px solid rgba(120, 183, 224, 0.18) !important;
124
+ }
125
+
126
+ .chatbot button.copy-text,
127
+ #chatbot button {
128
+ display: none !important;
129
+ }
130
+
131
+ #composer-row {
132
+ align-items: end !important;
133
+ background: rgba(7, 17, 28, 0.72) !important;
134
+ border: 1px solid rgba(120, 183, 224, 0.16) !important;
135
+ border-radius: 28px !important;
136
+ box-shadow: 0 16px 38px rgba(0, 0, 0, 0.28) !important;
137
+ gap: 8px !important;
138
+ margin: 12px auto 8px !important;
139
+ max-width: 860px !important;
140
+ padding: 10px 10px 10px 16px !important;
141
+ backdrop-filter: blur(18px);
142
+ }
143
+
144
+ #composer-row textarea {
145
+ background: transparent !important;
146
+ border: 0 !important;
147
+ box-shadow: none !important;
148
+ color: #f4fbff !important;
149
+ min-height: 44px !important;
150
+ padding: 10px 2px !important;
151
+ }
152
+
153
+ #composer-row textarea::placeholder {
154
+ color: #89a6c0 !important;
155
+ }
156
+
157
+ #composer-row label,
158
+ #composer-row .wrap {
159
+ background: transparent !important;
160
+ border: 0 !important;
161
+ box-shadow: none !important;
162
+ }
163
+
164
+ #status-box {
165
+ color: #9db7cd !important;
166
+ font-size: 13px !important;
167
+ max-width: 860px !important;
168
+ margin: 0 auto !important;
169
+ min-height: 24px !important;
170
+ padding: 0 20px !important;
171
+ }
172
+
173
+ #send-arrow,
174
+ #composer-row #send-arrow {
175
+ min-width: 42px !important;
176
+ max-width: 42px !important;
177
+ width: 42px !important;
178
+ height: 42px !important;
179
+ flex: 0 0 42px !important;
180
+ border-radius: 50% !important;
181
+ padding: 0 !important;
182
+ margin: 0 0 2px !important;
183
+ background: linear-gradient(135deg, #2dd4bf 0%, #0d9488 100%) !important;
184
+ color: #ffffff !important;
185
+ border: 0 !important;
186
+ box-shadow: 0 8px 20px rgba(13, 148, 136, 0.35) !important;
187
+ display: flex !important;
188
+ align-items: center !important;
189
+ justify-content: center !important;
190
+ cursor: pointer !important;
191
+ transition: all 0.2s ease !important;
192
+ }
193
+
194
+
195
+ #send-arrow:hover,
196
+ #composer-row #send-arrow:hover {
197
+ background: linear-gradient(135deg, #34d399 0%, #059669 100%) !important;
198
+ transform: translateY(-1px) scale(1.04) !important;
199
+ }
200
+
201
+ #send-arrow *,
202
+ #composer-row #send-arrow * {
203
+ color: #ffffff !important;
204
+ fill: #ffffff !important;
205
+ }
206
+
207
+ #settings-row {
208
+ gap: 10px !important;
209
+ margin: 12px auto 0 !important;
210
+ max-width: 860px !important;
211
+ padding: 0 16px !important;
212
+ }
213
+
214
+ #settings-row button {
215
+ background: rgba(7, 17, 28, 0.66) !important;
216
+ border: 1px solid rgba(120, 183, 224, 0.16) !important;
217
+ border-radius: 14px !important;
218
+ color: #d8f0ff !important;
219
+ backdrop-filter: blur(14px);
220
+ }
221
+
222
+ #model-panel,
223
+ #upload-panel {
224
+ background: rgba(7, 17, 28, 0.72) !important;
225
+ border: 1px solid rgba(120, 183, 224, 0.16) !important;
226
+ border-radius: 18px !important;
227
+ margin: 10px auto 0 !important;
228
+ max-width: 828px !important;
229
+ padding: 14px !important;
230
+ backdrop-filter: blur(18px);
231
+ }
232
+
233
+ #model-panel label,
234
+ #upload-panel label,
235
+ #model-panel .wrap,
236
+ #upload-panel .wrap {
237
+ background: transparent !important;
238
+ border-color: rgba(120, 183, 224, 0.14) !important;
239
+ }
240
+
241
+ #model-panel select,
242
+ #model-panel input,
243
+ #upload-panel input,
244
+ #upload-panel textarea {
245
+ background: rgba(9, 20, 33, 0.78) !important;
246
+ color: #eff8ff !important;
247
+ border-color: rgba(120, 183, 224, 0.16) !important;
248
+ }
249
+
250
+ #upload-panel button {
251
+ background: rgba(10, 26, 41, 0.72) !important;
252
+ border: 1px solid rgba(120, 183, 224, 0.16) !important;
253
+ color: #d8f0ff !important;
254
+ }
255
+
256
+ #upload-panel button:hover,
257
+ #settings-row button:hover {
258
+ border-color: rgba(92, 199, 255, 0.36) !important;
259
+ }
260
+
261
+ #composer-row .wrap,
262
+ #settings-row .wrap {
263
+ box-shadow: none !important;
264
+ }
265
+
266
+
267
+ #chatbot .message.user,
268
+ [data-testid="chatbot"] .message.user {
269
+ display: block !important;
270
+ width: fit-content !important;
271
+ max-width: 85% !important;
272
+ padding: 8px 14px !important;
273
+ margin-left: auto !important;
274
+ border-radius: 18px !important;
275
+ box-sizing: border-box !important;
276
+ }
277
+
278
+ #chatbot .message.user .message-wrap,
279
+ #chatbot .message.user .prose {
280
+ display: block !important;
281
+ width: auto !important;
282
+ max-width: 100% !important;
283
+ padding: 0 !important;
284
+ margin: 0 !important;
285
+ background: transparent !important;
286
+ }
287
+ #chatbot .message.user p,
288
+ #chatbot .message.user .prose p,
289
+ #chatbot .message.user > div {
290
+ display: inline-block !important;
291
+ max-width: 100% !important;
292
+ white-space: pre-wrap !important;
293
+ word-break: normal !important;
294
+ overflow-wrap: break-word !important;
295
+ color: #edf7ff !important;
296
+ margin: 0 !important;
297
+ padding: 0 !important;
298
+ line-height: 1.4 !important;
299
+ }
app.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import inspect
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import gradio as gr
9
+ from dotenv import load_dotenv
10
+ from pypdf import PdfReader
11
+
12
+ BASE_DIR = Path(__file__).resolve().parent
13
+ PROJECT_ROOT = BASE_DIR.parent
14
+ if str(PROJECT_ROOT) not in sys.path:
15
+ sys.path.insert(0, str(PROJECT_ROOT))
16
+
17
+ from .router import build_model_route
18
+
19
+
20
+ load_dotenv()
21
+
22
+ PROXY_URL = os.environ.get("HTTP_PROXY") or None
23
+ DEFAULT_PROVIDER = os.environ.get("DEFAULT_PROVIDER", "glm/4.7-flash")
24
+ MAX_UPLOAD_CHARS = int(os.environ.get("MAX_UPLOAD_CHARS", "40000"))
25
+
26
+ PROVIDER_CHOICES = [
27
+ ("GitHub Models · GPT-4.1 Mini", "github/gpt-4.1-mini"),
28
+ ("Gemini · 3.5 Flash", "gemini/3.5-flash"),
29
+ ("GLM · 4.7 Flash", "glm/4.7-flash"),
30
+ ]
31
+ PROVIDER_OPTIONS = [value for _, value in PROVIDER_CHOICES]
32
+
33
+
34
+
35
+ def _supports_kwarg(callable_obj: object, kwarg_name: str) -> bool:
36
+ try:
37
+ return kwarg_name in inspect.signature(callable_obj).parameters
38
+ except (TypeError, ValueError):
39
+ return False
40
+ CHATBOT_USES_MESSAGES = _supports_kwarg(gr.Chatbot, "type")
41
+
42
+ def _normalize_uploads(files: object | None) -> list[str]:
43
+ if not files:
44
+ return []
45
+
46
+ if isinstance(files, (str, Path)):
47
+ return [str(files)]
48
+
49
+ normalized: list[str] = []
50
+ for file_item in files if isinstance(files, list) else [files]:
51
+ if not file_item:
52
+ continue
53
+ if isinstance(file_item, (str, Path)):
54
+ normalized.append(str(file_item))
55
+ continue
56
+ file_path = getattr(file_item, "path", None) or getattr(file_item, "name", None)
57
+ if file_path:
58
+ normalized.append(str(file_path))
59
+ return normalized
60
+
61
+
62
+ def _read_text_file(file_path: Path) -> str:
63
+ return file_path.read_text(encoding="utf-8", errors="ignore")
64
+
65
+
66
+ def _read_pdf_file(file_path: Path) -> str:
67
+ reader = PdfReader(str(file_path))
68
+ pages: list[str] = []
69
+ for page in reader.pages:
70
+ text = page.extract_text() or ""
71
+ if text.strip():
72
+ pages.append(text)
73
+ return "\n\n".join(pages)
74
+
75
+
76
+ def read_uploaded_context(uploaded_files: object | None) -> tuple[str, str]:
77
+ file_paths = _normalize_uploads(uploaded_files)
78
+ if not file_paths:
79
+ return "", "No uploads attached."
80
+
81
+ parts: list[str] = []
82
+ summary_lines = ["Uploaded context:"]
83
+
84
+ for file_path_str in file_paths:
85
+ file_path = Path(file_path_str)
86
+ summary_lines.append(f"- {file_path.name}")
87
+
88
+ try:
89
+ if file_path.suffix.lower() == ".pdf":
90
+ extracted = _read_pdf_file(file_path)
91
+ else:
92
+ extracted = _read_text_file(file_path)
93
+ except Exception as exc:
94
+ parts.append(f"[Could not read {file_path.name}: {exc}]")
95
+ continue
96
+
97
+ extracted = extracted.strip()
98
+ if not extracted:
99
+ parts.append(f"[No readable text found in {file_path.name}]")
100
+ continue
101
+
102
+ if len(extracted) > MAX_UPLOAD_CHARS:
103
+ extracted = extracted[:MAX_UPLOAD_CHARS] + "\n\n[Truncated for context limit.]"
104
+
105
+ parts.append(f"## {file_path.name}\n\n{extracted}")
106
+
107
+ return "\n\n".join(parts), "\n".join(summary_lines)
108
+
109
+
110
+ def build_system_prompt(provider: str, model: str, upload_summary: str, uploaded_context: str) -> str:
111
+ base_prompt = (
112
+ "You are CodeAgent, a helpful assistant in a deployed web UI. "
113
+ "Local workspace tools, local file read/write, and RAG knowledge base tools are disabled in this deployment. "
114
+ "You can still help by reading user-uploaded text or PDF files, reasoning over that uploaded text, and generating markdown responses the user can download."
115
+ )
116
+
117
+ capability_note = (
118
+ f"\n\nCurrent model route: {provider}/{model}. "
119
+ "If the user asks for unavailable local tools, explain the limitation briefly and continue with the available uploaded context."
120
+ )
121
+
122
+ upload_note = f"\n\n{upload_summary}"
123
+ if uploaded_context.strip():
124
+ upload_note += f"\n\nUploaded content:\n{uploaded_context}"
125
+
126
+ return base_prompt + capability_note + upload_note
127
+
128
+
129
+
130
+
131
+ def _format_chatbot_history(history: list[dict[str, str]]) -> object:
132
+ """Return chat history in the shape expected by the active Gradio Chatbot."""
133
+
134
+ return history
135
+
136
+
137
+ def run_turn(
138
+ user_message: str,
139
+ history: list[dict[str, str]],
140
+ provider: str,
141
+ uploaded_files: object | None,
142
+ user_already_in_history: bool = False,
143
+ ) -> tuple[list[dict[str, str]], str, str]:
144
+ provider = (provider or DEFAULT_PROVIDER).strip().lower()
145
+ if provider not in PROVIDER_OPTIONS:
146
+ raise gr.Error(f"Unknown provider '{provider}'. Choose one of: {', '.join(PROVIDER_OPTIONS)}")
147
+ provider_name = provider.split("/")[0]
148
+ try:
149
+ route = build_model_route(provider_name=provider_name, proxy_url=PROXY_URL)
150
+ except Exception as exc:
151
+ raise gr.Error(str(exc)) from exc
152
+
153
+ uploaded_context, upload_summary = read_uploaded_context(uploaded_files)
154
+
155
+ system_prompt = build_system_prompt(provider, route.model, upload_summary, uploaded_context)
156
+ messages: list[dict[str, str]] = [{"role": "system", "content": system_prompt}]
157
+ messages.extend(history)
158
+ if not user_already_in_history:
159
+ messages.append({"role": "user", "content": user_message})
160
+
161
+ status = "Model is generating a markdown response. Local tools are disabled in this deployment."
162
+ if uploaded_context.strip():
163
+ status = "Model is generating a markdown response using uploaded text/PDF context. Local tools are disabled in this deployment."
164
+ response = route.client.chat.completions.create(
165
+ model=route.model,
166
+ messages=messages,
167
+ )
168
+ assistant_text = response.choices[0].message.content or ""
169
+ assistant_text = assistant_text.strip() or "The model returned an empty response."
170
+
171
+ updated_history = history + ([{"role": "user", "content": user_message}] if not user_already_in_history else []) + [
172
+ {"role": "assistant", "content": assistant_text},
173
+ ]
174
+
175
+ return updated_history, assistant_text, status
176
+
177
+
178
+ def add_upload_summary(uploaded_files: object | None) -> str:
179
+ _, upload_summary = read_uploaded_context(uploaded_files)
180
+ return f"**Upload status**\n\n{upload_summary}"
181
+
182
+
183
+ # Load CSS from file before creating Blocks
184
+ APP_CSS = ""
185
+ try:
186
+ with open(os.path.join(BASE_DIR, "app.css"), "r", encoding="utf-8") as file_handle:
187
+ APP_CSS = file_handle.read()
188
+ except Exception:
189
+ pass
190
+
191
+ blocks_kwargs = {"title": "CodeAgent Web UI"}
192
+ launch_extra_kwargs = {}
193
+ theme = gr.themes.Ocean()
194
+ if _supports_kwarg(gr.Blocks.launch, "css"):
195
+ launch_extra_kwargs["css"] = APP_CSS
196
+ else:
197
+ blocks_kwargs["css"] = APP_CSS
198
+ if _supports_kwarg(gr.Blocks.launch, "theme"):
199
+ launch_extra_kwargs["theme"] = theme
200
+ elif _supports_kwarg(gr.Blocks, "theme"):
201
+ blocks_kwargs["theme"] = theme
202
+
203
+ with gr.Blocks(**blocks_kwargs) as demo:
204
+ gr.Markdown(
205
+ "<div id='app-title'>"
206
+ "<h1>CodeAgent</h1>"
207
+ "<p>AI assistant for research and analysis</p>"
208
+ "</div>",
209
+ elem_id="app-title",
210
+ )
211
+ gr.Markdown("<div id='title-divider'></div>", elem_id="title-divider-wrap")
212
+
213
+ chat_state = gr.State([])
214
+ model_panel_visible = gr.State(False)
215
+ upload_panel_visible = gr.State(False)
216
+ pending_user_message = gr.State("")
217
+
218
+ chatbot_kwargs = {"label": "Conversation", "height": 560, "show_label": False}
219
+ if CHATBOT_USES_MESSAGES:
220
+ chatbot_kwargs["type"] = "messages"
221
+ chatbot = gr.Chatbot(**chatbot_kwargs, elem_id="chatbot")
222
+
223
+ status_box = gr.Markdown("Ready. Send a message to start.", elem_id="status-box")
224
+
225
+ with gr.Row(scale=1, elem_id="composer-row"):
226
+ textbox_kwargs = {
227
+ "label": "Message",
228
+ "placeholder": "Message CodeAgent",
229
+ "lines": 1,
230
+ "scale": 8,
231
+ "show_label": False,
232
+ "container": False,
233
+ }
234
+ if _supports_kwarg(gr.Textbox, "submit_on_enter"):
235
+ textbox_kwargs["submit_on_enter"] = True
236
+ user_input = gr.Textbox(**textbox_kwargs)
237
+ send_btn = gr.Button("↑", elem_id="send-arrow")
238
+
239
+ with gr.Row(scale=1, elem_id="settings-row"):
240
+ toggle_model_btn = gr.Button("Model settings", scale=1)
241
+ toggle_upload_btn = gr.Button("Upload files", scale=1)
242
+
243
+ with gr.Column(visible=False, elem_id="model-panel") as model_panel:
244
+ provider = gr.Dropdown(
245
+ choices=PROVIDER_CHOICES,
246
+ value=DEFAULT_PROVIDER if DEFAULT_PROVIDER in PROVIDER_OPTIONS else PROVIDER_OPTIONS[0],
247
+ label="Model provider",
248
+ info="Choose where the request is sent",
249
+ )
250
+
251
+ with gr.Column(visible=False, elem_id="upload-panel") as upload_panel:
252
+ upload_box = gr.File(
253
+ label="Upload text or PDF",
254
+ file_count="multiple",
255
+ type="filepath",
256
+ )
257
+ upload_summary = gr.Markdown("**Upload status**\n\nNo uploads attached.")
258
+
259
+ upload_box.change(
260
+ fn=add_upload_summary,
261
+ inputs=[upload_box],
262
+ outputs=[upload_summary],
263
+ )
264
+
265
+ def toggle_model_panel(is_visible: bool):
266
+ new_visible = not is_visible
267
+ return new_visible, gr.update(visible=new_visible)
268
+
269
+ def toggle_upload_panel(is_visible: bool):
270
+ new_visible = not is_visible
271
+ return new_visible, gr.update(visible=new_visible)
272
+
273
+ toggle_model_btn.click(
274
+ fn=toggle_model_panel,
275
+ inputs=[model_panel_visible],
276
+ outputs=[model_panel_visible, model_panel],
277
+ )
278
+
279
+ toggle_upload_btn.click(
280
+ fn=toggle_upload_panel,
281
+ inputs=[upload_panel_visible],
282
+ outputs=[upload_panel_visible, upload_panel],
283
+ )
284
+ def handle_message(user_text: str, history: list[dict[str, str]], selected_provider: str, files: object | None):
285
+ if not user_text or not user_text.strip():
286
+ raise gr.Error("Please enter a message.")
287
+
288
+ current_history = history or []
289
+ stripped_text = user_text.strip()
290
+
291
+ updated_history = current_history + [{"role": "user", "content": stripped_text}]
292
+ yield updated_history, _format_chatbot_history(updated_history), "Model is generating a response...", ""
293
+
294
+ final_history, assistant_text, status = run_turn(
295
+ stripped_text,
296
+ current_history,
297
+ selected_provider,
298
+ files,
299
+ user_already_in_history=False,
300
+ )
301
+
302
+ yield final_history, _format_chatbot_history(final_history), status, ""
303
+
304
+ send_btn.click(
305
+ fn=handle_message,
306
+ inputs=[user_input, chat_state, provider, upload_box],
307
+ outputs=[chat_state, chatbot, status_box, user_input],
308
+ show_progress="hidden"
309
+ )
310
+
311
+ user_input.submit(
312
+ fn=handle_message,
313
+ inputs=[user_input, chat_state, provider, upload_box],
314
+ outputs=[chat_state, chatbot, status_box, user_input],
315
+ show_progress="hidden"
316
+ )
317
+
318
+ if __name__ == "__main__":
319
+ launch_kwargs = {
320
+ "server_name": "0.0.0.0",
321
+ "server_port": int(os.environ.get("PORT", "7862")),
322
+ }
323
+ launch_kwargs.update(launch_extra_kwargs)
324
+
325
+ demo.queue().launch(**launch_kwargs)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openai
2
+ httpx
3
+ requests
4
+ python-dotenv
5
+ pypdf
6
+ gradio
router.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from dotenv import load_dotenv
6
+ import httpx
7
+ from openai import OpenAI
8
+
9
+
10
+ load_dotenv()
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ModelRoute:
15
+ """OpenAI-compatible client plus the concrete model name to call."""
16
+
17
+ client: OpenAI
18
+ model: str
19
+ provider: str
20
+ base_url: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ProviderConfig:
25
+ name: str
26
+ base_url: str
27
+ api_key_env: str
28
+ default_model: str
29
+
30
+
31
+ PROVIDERS: dict[str, ProviderConfig] = {
32
+ "github": ProviderConfig(
33
+ name="github",
34
+ base_url="https://models.inference.ai.azure.com",
35
+ api_key_env="GITHUB_API_KEY",
36
+ default_model="gpt-4.1-mini",
37
+ ),
38
+ "openai": ProviderConfig(
39
+ name="openai",
40
+ base_url="https://api.openai.com/v1",
41
+ api_key_env="OPENAI_API_KEY",
42
+ default_model="gpt-4.1-mini",
43
+ ),
44
+ "gemini": ProviderConfig(
45
+ name="gemini",
46
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
47
+ api_key_env="GEMINI_API_KEY",
48
+ default_model="gemini-3.5-flash",
49
+ ),
50
+ "glm": ProviderConfig(
51
+ name="glm",
52
+ base_url="https://api.z.ai/api/paas/v4/",
53
+ api_key_env="GLM_API_KEY",
54
+ default_model="glm-4.7-flash",
55
+ ),
56
+ }
57
+
58
+
59
+ def _normalize(value: str | None) -> str:
60
+ return (value or "").strip().lower()
61
+
62
+
63
+ def resolve_model(provider_name: str | None = None) -> tuple[ProviderConfig, str]:
64
+ """Resolve a friendly model/provider name into a provider config and model id."""
65
+
66
+ normalized_provider = _normalize(provider_name)
67
+
68
+ if normalized_provider:
69
+ if normalized_provider not in PROVIDERS:
70
+ known = ", ".join(sorted(PROVIDERS))
71
+ raise ValueError(f"Unknown provider '{provider_name}'. Known providers: {known}")
72
+ provider = PROVIDERS[normalized_provider]
73
+ return provider, provider.default_model
74
+
75
+
76
+ provider = PROVIDERS["github"]
77
+ return provider, provider.default_model
78
+
79
+
80
+ def build_model_route(
81
+ provider_name: str | None = None,
82
+ proxy_url: str | None = None,
83
+ ) -> ModelRoute:
84
+ """Create the chat-completions client and model id for inference."""
85
+
86
+ provider, model = resolve_model(provider_name)
87
+ selected_base_url = provider.base_url.strip()
88
+ selected_api_key = os.environ.get(provider.api_key_env)
89
+
90
+
91
+ if not selected_api_key:
92
+ raise ValueError(
93
+ f"Missing API key for provider '{provider.name}'. Set {provider.api_key_env}, "
94
+ "MODEL_API_KEY, API_TOKEN, or pass --api-key."
95
+ )
96
+
97
+ http_client = httpx.Client(proxy=proxy_url) if proxy_url else None
98
+ client = OpenAI(
99
+ base_url=selected_base_url,
100
+ api_key=selected_api_key,
101
+ http_client=http_client,
102
+ )
103
+
104
+ return ModelRoute(
105
+ client=client,
106
+ model=model,
107
+ provider=provider.name,
108
+ base_url=selected_base_url,
109
+ )