chATLAS_Frontend 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chATLAS_Frontend/__init__.py +0 -0
- chATLAS_Frontend/frontend/__init__.py +0 -0
- chATLAS_Frontend/frontend/auth.py +38 -0
- chATLAS_Frontend/frontend/chat_processing.py +63 -0
- chATLAS_Frontend/frontend/chat_request.py +278 -0
- chATLAS_Frontend/frontend/html_generation.py +361 -0
- chATLAS_Frontend/frontend/logging.py +387 -0
- chATLAS_Frontend/frontend/processing.py +254 -0
- chATLAS_Frontend/frontend/resources.py +126 -0
- chATLAS_Frontend/frontend/routes.py +250 -0
- chATLAS_Frontend/frontend/settings.py +177 -0
- chATLAS_Frontend/frontend/utils.py +11 -0
- chATLAS_Frontend/launch.py +73 -0
- chATLAS_Frontend/static/css/markdown_styling.css +147 -0
- chATLAS_Frontend/static/css/min.extra.css +3899 -0
- chATLAS_Frontend/static/css/styles.css +956 -0
- chATLAS_Frontend/static/css/styles_backup.css +289 -0
- chATLAS_Frontend/static/images/favicon.ico +0 -0
- chATLAS_Frontend/static/js/app/api.js +109 -0
- chATLAS_Frontend/static/js/app/bootstrap.js +162 -0
- chATLAS_Frontend/static/js/app/state.js +29 -0
- chATLAS_Frontend/static/js/components/chatThread.js +377 -0
- chATLAS_Frontend/static/js/components/composer.js +43 -0
- chATLAS_Frontend/static/js/components/feedback.js +31 -0
- chATLAS_Frontend/static/js/components/filtersPanel.js +95 -0
- chATLAS_Frontend/static/js/components/modeToggle.js +21 -0
- chATLAS_Frontend/static/js/components/settingsModal.js +48 -0
- chATLAS_Frontend/static/js/components/sourcePanel.js +141 -0
- chATLAS_Frontend/static/js/main.js +7 -0
- chATLAS_Frontend/static/js/vendor/marked.min.js +6 -0
- chATLAS_Frontend/templates/page/base.html +32 -0
- chATLAS_Frontend/templates/page/categories/atlas_talk.html +21 -0
- chATLAS_Frontend/templates/page/categories/cds.html +3 -0
- chATLAS_Frontend/templates/page/categories/gitlab_markdown.html +43 -0
- chATLAS_Frontend/templates/page/categories/indico.html +8 -0
- chATLAS_Frontend/templates/page/categories/twiki.html +54 -0
- chATLAS_Frontend/templates/page/filters.html +65 -0
- chATLAS_Frontend/templates/page/footer.html +13 -0
- chATLAS_Frontend/templates/page/header.html +93 -0
- chATLAS_Frontend/templates/page/index.html +40 -0
- chATLAS_Frontend/templates/page/searchbar.html +22 -0
- chATLAS_Frontend/templates/response/all_sources.html +10 -0
- chATLAS_Frontend/templates/response/bot_response.html +7 -0
- chATLAS_Frontend/templates/response/citations_block.html +4 -0
- chATLAS_Frontend/templates/response/error_box.html +11 -0
- chATLAS_Frontend/templates/response/feedback_buttons.html +10 -0
- chATLAS_Frontend/templates/response/macros.html +9 -0
- chATLAS_Frontend/templates/response/source_display_button.html +22 -0
- chATLAS_Frontend/templates/response/source_similarity.html +6 -0
- chATLAS_Frontend/templates/response/used_docs.html +8 -0
- chATLAS_Frontend/templates/response/user_query.html +17 -0
- chATLAS_Frontend/usage/__init__.py +0 -0
- chATLAS_Frontend/usage/db_utils.py +85 -0
- chATLAS_Frontend/usage/execute_query.py +123 -0
- chATLAS_Frontend/usage/plots.py +45 -0
- chATLAS_Frontend/usage/queries.py +46 -0
- chatlas_frontend-1.0.0.dist-info/METADATA +117 -0
- chatlas_frontend-1.0.0.dist-info/RECORD +61 -0
- chatlas_frontend-1.0.0.dist-info/WHEEL +5 -0
- chatlas_frontend-1.0.0.dist-info/licenses/LICENSE +201 -0
- chatlas_frontend-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import markdown
|
|
8
|
+
from flask import render_template
|
|
9
|
+
|
|
10
|
+
from chATLAS_Frontend.frontend.resources import get_db_logger
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_file_name(file_name, CAPTION_SWITCH):
|
|
16
|
+
if ":" in file_name:
|
|
17
|
+
return file_name, None
|
|
18
|
+
|
|
19
|
+
name_out = os.path.basename(file_name).rsplit(".", 1)[0]
|
|
20
|
+
caption_out = None
|
|
21
|
+
|
|
22
|
+
return name_out, caption_out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_full_response(user_input, sources, answer, sources_used_by_model, assistant_mode=True):
|
|
26
|
+
"""
|
|
27
|
+
Render all html for the response. Some pieces are not rendered when assistant_mode is False.
|
|
28
|
+
|
|
29
|
+
In assistant mode, citations (used docs + all sources) are placed in a hidden block
|
|
30
|
+
and a "Citations" button opens them in the right sidebar. In search mode, sources
|
|
31
|
+
are rendered inline.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict with keys:
|
|
35
|
+
- 'bot_html': the rendered bot reply fragment
|
|
36
|
+
- 'citations_html': the citations block HTML (assistant mode) or sources HTML (search mode)
|
|
37
|
+
"""
|
|
38
|
+
_user_display, bot_display = generate_chat(answer, user_input, len(sources), assistant_mode=assistant_mode)
|
|
39
|
+
|
|
40
|
+
# Raw sources list (no show/hide button wrapper)
|
|
41
|
+
sources_display_raw = generate_sources_display(sources, assistant_mode=False)
|
|
42
|
+
|
|
43
|
+
if assistant_mode:
|
|
44
|
+
used_sources_html = generate_used_docs_display(sources, sources_used_by_model)
|
|
45
|
+
citations_content = used_sources_html + sources_display_raw
|
|
46
|
+
citations_block_html = render_template("response/citations_block.html", citations_content=citations_content)
|
|
47
|
+
# citations_block goes inside the bot reply so the "Citations" button
|
|
48
|
+
# lives in the chat row (sourcePanel.js reads it on click)
|
|
49
|
+
full_bot_html = bot_display + citations_block_html
|
|
50
|
+
else:
|
|
51
|
+
citations_content = sources_display_raw
|
|
52
|
+
full_bot_html = bot_display
|
|
53
|
+
|
|
54
|
+
return {"bot_html": full_bot_html, "citations_html": citations_content}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _used_doc_utility_for_source(doc_utility_map: dict, source_metadata_name: str) -> str | None:
|
|
58
|
+
"""
|
|
59
|
+
Return the utility for a retrieved source if it is listed in used_docs (exact or suffix match).
|
|
60
|
+
|
|
61
|
+
Inputs:
|
|
62
|
+
doc_utility_map: Map from document name (as returned by LLM) to utility string.
|
|
63
|
+
source_metadata_name: The 'name' from the retrieved source's metadata.
|
|
64
|
+
Outputs:
|
|
65
|
+
Utility string if the source is in used_docs, else None.
|
|
66
|
+
"""
|
|
67
|
+
stripped = (source_metadata_name or "").strip('"').strip("'")
|
|
68
|
+
if not stripped:
|
|
69
|
+
return None
|
|
70
|
+
if stripped in doc_utility_map:
|
|
71
|
+
return doc_utility_map[stripped]
|
|
72
|
+
for key, utility in doc_utility_map.items():
|
|
73
|
+
if key == stripped:
|
|
74
|
+
return utility
|
|
75
|
+
if key.endswith("/" + stripped) or (key and key.rstrip("/").endswith("/" + stripped)):
|
|
76
|
+
return utility
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generate_used_docs_display(sources, used_docs):
|
|
81
|
+
"""
|
|
82
|
+
Generates html to display the sources used by the model in the response.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
----------
|
|
86
|
+
used_docs : list of dict
|
|
87
|
+
List of dictionaries containing document information used by the model.
|
|
88
|
+
Each dictionary has 'name' and 'utility' keys.
|
|
89
|
+
|
|
90
|
+
sources: list Document objects
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
--------
|
|
94
|
+
str
|
|
95
|
+
HTML representation of the used documents
|
|
96
|
+
"""
|
|
97
|
+
# Enforce that used_docs must be a list
|
|
98
|
+
if not isinstance(used_docs, list):
|
|
99
|
+
raise TypeError(f"used_docs must be a list of dictionaries, not {type(used_docs)}")
|
|
100
|
+
|
|
101
|
+
if not used_docs:
|
|
102
|
+
# If there are no used docs, render a template with empty data
|
|
103
|
+
return render_template("response/used_docs.html", has_sources=False)
|
|
104
|
+
|
|
105
|
+
# Get source ids for feedback buttons and sources data
|
|
106
|
+
db_logger = get_db_logger()
|
|
107
|
+
source_ids = db_logger.get_most_recent_source_ids()
|
|
108
|
+
|
|
109
|
+
# Create a mapping of document names to their utility values (exact keys from LLM)
|
|
110
|
+
doc_utility_map = {}
|
|
111
|
+
for doc in used_docs:
|
|
112
|
+
if isinstance(doc, dict) and "name" in doc and "utility" in doc:
|
|
113
|
+
name = doc["name"]
|
|
114
|
+
if isinstance(name, str):
|
|
115
|
+
doc_utility_map[name.strip('"').strip("'")] = doc["utility"]
|
|
116
|
+
else:
|
|
117
|
+
doc_utility_map[str(name).strip('"').strip("'")] = doc["utility"]
|
|
118
|
+
|
|
119
|
+
# Prepare the source data: include a source if it matches used_docs (exact or suffix match)
|
|
120
|
+
source_entries = []
|
|
121
|
+
for source in sources:
|
|
122
|
+
source_name = source.metadata.get("name", "Unknown")
|
|
123
|
+
utility = _used_doc_utility_for_source(doc_utility_map, source_name)
|
|
124
|
+
if utility is None:
|
|
125
|
+
continue
|
|
126
|
+
score_metric = "rrf_score" if "rrf_score" in source.metadata else "similarity"
|
|
127
|
+
source_data = extract_source_data(source, source_ids, score_metric=score_metric)
|
|
128
|
+
source_data["utility"] = utility
|
|
129
|
+
source_entries.append(source_data)
|
|
130
|
+
|
|
131
|
+
# Render the template with the prepared data
|
|
132
|
+
return render_template("response/used_docs.html", has_sources=True, sources=source_entries)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_source_data(source, source_ids, score_metric="similarity"):
|
|
136
|
+
"""Format the source data to a dictionary for html generation"""
|
|
137
|
+
|
|
138
|
+
importance_thresholds = {"similarity": 0.5, "rrf_score": 0.015}
|
|
139
|
+
|
|
140
|
+
if score_metric not in importance_thresholds:
|
|
141
|
+
raise ValueError(f"Unknown score metric: {score_metric}")
|
|
142
|
+
|
|
143
|
+
source_name = source.metadata.get("name", "Unknown")
|
|
144
|
+
source_name_key = source.metadata.get("name")
|
|
145
|
+
source_id = source_ids.get(source_name_key)
|
|
146
|
+
score = round(source.metadata.get(score_metric, 0), 3)
|
|
147
|
+
url = source.metadata.get("url", "#")
|
|
148
|
+
is_important = score > importance_thresholds[score_metric]
|
|
149
|
+
NAME_OF_SOURCE, summary = get_file_name(source_name, True)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"url": url,
|
|
153
|
+
"display_name": NAME_OF_SOURCE,
|
|
154
|
+
"summary": summary,
|
|
155
|
+
"similarity_score": score,
|
|
156
|
+
"similarity_formatted": f"{score:.3f}",
|
|
157
|
+
"is_important": is_important,
|
|
158
|
+
# Add variables needed by feedback_buttons.html
|
|
159
|
+
"button_type": "source",
|
|
160
|
+
"feedback_id": source_id,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def generate_sources_display(sources, assistant_mode=True):
|
|
165
|
+
"""
|
|
166
|
+
Generates html for the used sources, grouped by Type and SearchType
|
|
167
|
+
with scrollable container.
|
|
168
|
+
|
|
169
|
+
In assistant mode, the sources are wrapped in a button to show/hide them.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
if not sources:
|
|
173
|
+
all_sources_html = render_template("response/all_sources.html", all_sources_data=[])
|
|
174
|
+
return (
|
|
175
|
+
render_template("response/source_display_button.html", sources_display=all_sources_html)
|
|
176
|
+
if assistant_mode
|
|
177
|
+
else all_sources_html
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Get the ids of the sources
|
|
181
|
+
db_logger = get_db_logger()
|
|
182
|
+
source_ids = db_logger.get_most_recent_source_ids()
|
|
183
|
+
|
|
184
|
+
if not source_ids:
|
|
185
|
+
source_ids = {}
|
|
186
|
+
|
|
187
|
+
# Group sources by type (twiki, indico, CDS ...) and search type (vector, text)
|
|
188
|
+
grouped_by_source_and_search_type = {}
|
|
189
|
+
|
|
190
|
+
if "rrf_score" in sources[0].metadata:
|
|
191
|
+
score_metric = "rrf_score"
|
|
192
|
+
for source in sources:
|
|
193
|
+
source.metadata["search_type"] = "RRF"
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
score_metric = "similarity"
|
|
197
|
+
|
|
198
|
+
for source in sources:
|
|
199
|
+
source_type = source.metadata.get("source", "Unknown Type")
|
|
200
|
+
search_type = source.metadata.get("search_type", "Unknown Search")
|
|
201
|
+
grouped_by_source_and_search_type.setdefault(source_type, {}).setdefault(search_type, []).append(source)
|
|
202
|
+
|
|
203
|
+
# Prepare data structure for template context
|
|
204
|
+
all_sources_data = []
|
|
205
|
+
|
|
206
|
+
# For each source type (twiki, indico, CDS ...)
|
|
207
|
+
for source_type, sources_by_search_type in grouped_by_source_and_search_type.items():
|
|
208
|
+
search_types_data = []
|
|
209
|
+
|
|
210
|
+
# For each search type (vector, text)
|
|
211
|
+
for search_type, sources_list in sources_by_search_type.items():
|
|
212
|
+
source_entries = []
|
|
213
|
+
|
|
214
|
+
# For each individual source
|
|
215
|
+
for source in sources_list:
|
|
216
|
+
source_data = extract_source_data(source, source_ids, score_metric=score_metric)
|
|
217
|
+
|
|
218
|
+
source_entries.append(source_data)
|
|
219
|
+
|
|
220
|
+
# Add this search type's data
|
|
221
|
+
search_types_data.append({"search_type": search_type, "sources": source_entries})
|
|
222
|
+
|
|
223
|
+
# Add this source type's data
|
|
224
|
+
all_sources_data.append({"source_type": source_type, "search_types": search_types_data})
|
|
225
|
+
|
|
226
|
+
# Render everything at once
|
|
227
|
+
all_sources_html = render_template("response/all_sources.html", all_sources_data=all_sources_data)
|
|
228
|
+
|
|
229
|
+
# wrap source display behind a "Show Full Sources" button
|
|
230
|
+
if assistant_mode:
|
|
231
|
+
all_sources_html = render_template("response/source_display_button.html", sources_display=all_sources_html)
|
|
232
|
+
|
|
233
|
+
return all_sources_html
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def fix_markdown_links(text):
|
|
237
|
+
"""
|
|
238
|
+
Converts links from [[url][label]] format to standard [label](url) Markdown,
|
|
239
|
+
but only if the url looks like a valid web address.
|
|
240
|
+
"""
|
|
241
|
+
pattern = r"\[\[(https?://[^\[\]]+|www\.[^\[\]]+)\]\[([^\[\]]+)\]\]"
|
|
242
|
+
return re.sub(pattern, r"[\2](\1)", text)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def parse_answer(chatbot_answer):
|
|
246
|
+
"""
|
|
247
|
+
Parse the LLM output to extract the actual answer and JSON.
|
|
248
|
+
"""
|
|
249
|
+
# Clean up the input from markdown/code block markers
|
|
250
|
+
cleaned_answer = chatbot_answer.strip()
|
|
251
|
+
cleaned_answer = cleaned_answer.removeprefix("```json")
|
|
252
|
+
cleaned_answer = cleaned_answer.removeprefix("```")
|
|
253
|
+
cleaned_answer = cleaned_answer.removesuffix("```")
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
# Handle cases where JSON is escaped
|
|
257
|
+
if cleaned_answer.startswith("{") and '"' in cleaned_answer:
|
|
258
|
+
cleaned_answer = json.loads(cleaned_answer)
|
|
259
|
+
# If cleaned_answer is still a string, try parsing as JSON
|
|
260
|
+
parsed_ans = json.loads(cleaned_answer) if isinstance(cleaned_answer, str) else cleaned_answer
|
|
261
|
+
|
|
262
|
+
answer = parsed_ans.get("text", "")
|
|
263
|
+
# Fix malformed markdown links if there are any
|
|
264
|
+
answer = fix_markdown_links(answer)
|
|
265
|
+
|
|
266
|
+
used_docs = parsed_ans.get("sources", [])
|
|
267
|
+
return answer, used_docs
|
|
268
|
+
except json.JSONDecodeError:
|
|
269
|
+
try:
|
|
270
|
+
# Fallback: attempt parsing using ast.literal_eval
|
|
271
|
+
parsed_ans = ast.literal_eval(cleaned_answer)
|
|
272
|
+
if isinstance(parsed_ans, dict):
|
|
273
|
+
return parsed_ans.get("text", ""), parsed_ans.get("sources", [])
|
|
274
|
+
except (ValueError, SyntaxError):
|
|
275
|
+
pass
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Parsing error: {e}")
|
|
278
|
+
|
|
279
|
+
# Fallback: return raw text if parsing fails
|
|
280
|
+
logger.warning(
|
|
281
|
+
"parse_answer fallback: returning raw text. chatbot_answer=%r",
|
|
282
|
+
chatbot_answer[:500] if chatbot_answer else chatbot_answer,
|
|
283
|
+
)
|
|
284
|
+
return str(chatbot_answer), []
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def post_process_markdown(html_content):
|
|
288
|
+
"""
|
|
289
|
+
Clean up markdown-generated HTML to fix:
|
|
290
|
+
- Code blocks with language identifiers
|
|
291
|
+
- Code blocks that don't have <pre> tags
|
|
292
|
+
"""
|
|
293
|
+
# Fix code blocks with language identifiers
|
|
294
|
+
html_content = re.sub(r"<code>(\w+)\n", r"<code>", html_content)
|
|
295
|
+
|
|
296
|
+
# Fix code blocks that don't have <pre> tags
|
|
297
|
+
html_content = re.sub(r"<br />\s*<code>", r"<pre><code>", html_content)
|
|
298
|
+
html_content = re.sub(r"</code>\s*<br />", r"</code></pre>", html_content)
|
|
299
|
+
|
|
300
|
+
return html_content
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def generate_chat(answer, user_input, num_sources, assistant_mode=True):
|
|
304
|
+
"""
|
|
305
|
+
Renders the HTML templates for the bot response and feedback button.
|
|
306
|
+
Returns (user_output, bot_display) — user_output is kept for compatibility
|
|
307
|
+
but is no longer included in the JSON response blob (the client renders the
|
|
308
|
+
user message itself before sending).
|
|
309
|
+
"""
|
|
310
|
+
user_output = render_template("response/user_query.html", user_input=user_input, result_count=num_sources)
|
|
311
|
+
|
|
312
|
+
if assistant_mode:
|
|
313
|
+
if answer:
|
|
314
|
+
# --- Preprocess for code blocks ---
|
|
315
|
+
answer = re.sub(r"```(\w*)", r"```\1", answer)
|
|
316
|
+
|
|
317
|
+
code_blocks = re.findall(r"```(?:\w*)\n(.*?)```", answer, re.DOTALL)
|
|
318
|
+
for block in code_blocks:
|
|
319
|
+
lines = block.split("\n")
|
|
320
|
+
non_empty_lines = [line for line in lines if line.strip()]
|
|
321
|
+
if non_empty_lines:
|
|
322
|
+
indentation = min(len(line) - len(line.lstrip()) for line in non_empty_lines if line.strip())
|
|
323
|
+
normalized_block = "\n".join(line[indentation:] if line.strip() else line for line in lines)
|
|
324
|
+
answer = answer.replace(block, normalized_block)
|
|
325
|
+
|
|
326
|
+
# Ensure a blank line before any table
|
|
327
|
+
answer = re.sub(r"(?m)(?<!\|)\n(?=\|)", r"\n\n", answer)
|
|
328
|
+
|
|
329
|
+
# --- Markdown conversion ---
|
|
330
|
+
# Note: nl2br is intentionally excluded — it converts bare newlines to <br>
|
|
331
|
+
# before the list/heading parsers run, which breaks bullet list rendering.
|
|
332
|
+
extensions = [
|
|
333
|
+
"markdown.extensions.fenced_code",
|
|
334
|
+
"markdown.extensions.codehilite",
|
|
335
|
+
"markdown.extensions.tables",
|
|
336
|
+
"markdown.extensions.extra",
|
|
337
|
+
]
|
|
338
|
+
extension_configs = {
|
|
339
|
+
"markdown.extensions.codehilite": {
|
|
340
|
+
"css_class": "codehilite",
|
|
341
|
+
"linenums": False,
|
|
342
|
+
"guess_lang": False,
|
|
343
|
+
"use_pygments": False,
|
|
344
|
+
},
|
|
345
|
+
"markdown.extensions.fenced_code": {"lang_prefix": ""},
|
|
346
|
+
}
|
|
347
|
+
processed_answer = markdown.markdown(answer, extensions=extensions, extension_configs=extension_configs)
|
|
348
|
+
processed_answer = post_process_markdown(processed_answer)
|
|
349
|
+
else:
|
|
350
|
+
processed_answer = ""
|
|
351
|
+
|
|
352
|
+
bot_display = render_template(
|
|
353
|
+
"response/bot_response.html",
|
|
354
|
+
bot_response=processed_answer,
|
|
355
|
+
button_type="response",
|
|
356
|
+
feedback_id=get_db_logger().get_most_recent_query_id(),
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
bot_display = ""
|
|
360
|
+
|
|
361
|
+
return user_output, bot_display
|