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
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from authlib.integrations.flask_client import OAuth
|
|
4
|
+
from flask import redirect, session, url_for
|
|
5
|
+
|
|
6
|
+
oauth = OAuth()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def set_oauth(app):
|
|
10
|
+
CLIENT_ID = os.getenv("CLIENT_ID") or os.getenv("sso_clientID") # noqa: SIM112
|
|
11
|
+
CLIENT_SECRET = os.getenv("CLIENT_SECRET") or os.getenv("sso_clientSecret") # noqa: SIM112
|
|
12
|
+
CERN_SSO = "https://auth.cern.ch/auth/realms/cern/.well-known/openid-configuration"
|
|
13
|
+
|
|
14
|
+
oauth.init_app(app)
|
|
15
|
+
|
|
16
|
+
oauth.register(
|
|
17
|
+
name="cern",
|
|
18
|
+
server_metadata_url=CERN_SSO,
|
|
19
|
+
client_id=CLIENT_ID,
|
|
20
|
+
client_secret=CLIENT_SECRET,
|
|
21
|
+
client_kwargs={"scope": "openid email profile"},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def login():
|
|
26
|
+
redirect_uri = url_for("auth_route", _external=True, _scheme="https")
|
|
27
|
+
return oauth.cern.authorize_redirect(redirect_uri)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def auth():
|
|
31
|
+
token = oauth.cern.authorize_access_token()
|
|
32
|
+
session["user"] = token["userinfo"]
|
|
33
|
+
return redirect("/")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def logout():
|
|
37
|
+
session.pop("user", None)
|
|
38
|
+
return redirect("/")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
from flask import jsonify
|
|
4
|
+
|
|
5
|
+
from chATLAS_Frontend.frontend.chat_request import build_chat_request_mattermost
|
|
6
|
+
from chATLAS_Frontend.frontend.html_generation import parse_answer
|
|
7
|
+
from chATLAS_Frontend.frontend.processing import call_chain
|
|
8
|
+
from chATLAS_Frontend.frontend.settings import get_env_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def process_mattermost(input_request_str: str, user_id: str | None = None):
|
|
12
|
+
"""
|
|
13
|
+
Process the Mattermost slash-command request through the ChatRequest pipeline.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
input_request_str: Raw text passed to the slash command.
|
|
17
|
+
user_id: Optional user ID from Mattermost request payload.
|
|
18
|
+
"""
|
|
19
|
+
env = get_env_settings()
|
|
20
|
+
session_like: Mapping[str, str] = {"user_id": user_id} if user_id else {}
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
chat_request = build_chat_request_mattermost(input_request_str, session=session_like, env=env)
|
|
24
|
+
answer, sources = call_chain(chat_request, log_results=True)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
return jsonify({"response_type": "in_channel", "text": f"caught_error_render_error_box\n\nError: {e!s}"})
|
|
27
|
+
|
|
28
|
+
if answer is None:
|
|
29
|
+
return jsonify({"response_type": "in_channel", "text": "No answer returned."})
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
bot_answer, bot_citations = parse_answer(answer)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return jsonify(
|
|
35
|
+
{"response_type": "in_channel", "text": f"caught_error_render_error_box\n\nError in parse_answer: {e!s}"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
md_used_links = get_links_to_used_sources(bot_citations, sources) or ["No sources used."]
|
|
40
|
+
except Exception:
|
|
41
|
+
md_used_links = ["Error extracting links."]
|
|
42
|
+
|
|
43
|
+
final_response = f"** /chatlas {input_request_str}**\n\n {bot_answer}\n\n Used Sources: \n " + "\n".join(
|
|
44
|
+
md_used_links
|
|
45
|
+
)
|
|
46
|
+
return jsonify({"response_type": "in_channel", "text": final_response})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_links_to_used_sources(bot_citations, all_sources):
|
|
50
|
+
"""
|
|
51
|
+
A helper function for mattermost endpoint which extracts the URL for each used source and puts link in md format
|
|
52
|
+
|
|
53
|
+
Returns: List[str] - List of md links to used sources
|
|
54
|
+
"""
|
|
55
|
+
# Create a mapping of name to url from all_sources
|
|
56
|
+
source_map = {source.metadata.get("name"): source.metadata.get("url") for source in all_sources}
|
|
57
|
+
|
|
58
|
+
# Generate markdown links for citations that exist in the source map
|
|
59
|
+
return [
|
|
60
|
+
f"[{citation['name']}]({source_map[citation['name']]})"
|
|
61
|
+
for citation in bot_citations
|
|
62
|
+
if citation["name"] in source_map
|
|
63
|
+
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to parse requests from both the web app and Mattermost into a common format: ChatRequest.
|
|
3
|
+
|
|
4
|
+
Once a ChatRequest is built, it can be used to call the LangChain or LangGraph chains directly using their call signatures.
|
|
5
|
+
|
|
6
|
+
This can be extended to other call signatures as needed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import MutableMapping
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from chATLAS_Frontend.frontend.settings import EnvSettings, UserSettings
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ChatRequest:
|
|
23
|
+
"""
|
|
24
|
+
Holds all options available to the user when making a chat request.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
- user_input: The user's input message.
|
|
28
|
+
- filters: A dictionary of filters to apply:
|
|
29
|
+
- type: A dictionary of source types : bool pairs
|
|
30
|
+
- category: Categories to filter on **NOTE** NOT USED
|
|
31
|
+
- date_filter: A date string in DD-MM-YYYY format
|
|
32
|
+
- k: Number of vectorstore results to retrieve
|
|
33
|
+
- k_text: Number of text search results to retrieve
|
|
34
|
+
|
|
35
|
+
used to select which DBs to search, and keyword filters to apply.
|
|
36
|
+
- assistant_mode: Whether the request is for assistant mode (True) or search mode (False).
|
|
37
|
+
- user_settings: The per-session user settings.
|
|
38
|
+
- user_id: The hashed user ID, or special strings for local/mattermost users
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
user_input: str
|
|
42
|
+
filters: dict[str, Any] | None
|
|
43
|
+
assistant_mode: bool
|
|
44
|
+
user_settings: UserSettings
|
|
45
|
+
user_id: str # hashed user id (or "Local User", "mattermost-xxx")
|
|
46
|
+
thread_id: str | None = None # for conversational graph; set when assistant_mode
|
|
47
|
+
|
|
48
|
+
def search_kwargs(self) -> dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Build a dictionary the search_kwargs 'metadata' argument expected by LangChain retrieval chains.
|
|
51
|
+
"""
|
|
52
|
+
filters = self.filters or {}
|
|
53
|
+
|
|
54
|
+
# Extract filters
|
|
55
|
+
# categories = filters.get("category")
|
|
56
|
+
|
|
57
|
+
date_filter = filters.get("date_filter")
|
|
58
|
+
|
|
59
|
+
metadata = {
|
|
60
|
+
"date_filter": date_filter,
|
|
61
|
+
"k": self.user_settings.k_vector,
|
|
62
|
+
"k_text": self.user_settings.k_text,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# if categories:
|
|
66
|
+
# args_dict["metadata"]["category"] = categories
|
|
67
|
+
|
|
68
|
+
return metadata
|
|
69
|
+
|
|
70
|
+
def build_langchain_call_signature(self) -> dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Build the dictionary matching the LangChain call signature.
|
|
73
|
+
|
|
74
|
+
Can use this to directly call chain.invoke(**kwargs)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return {"input": self.user_input, "config": {"metadata": self.search_kwargs()}}
|
|
78
|
+
|
|
79
|
+
def build_langgraph_call_signature(self) -> dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Build the dictionary matching the LangGraph call signature.
|
|
82
|
+
|
|
83
|
+
Can use this to directly call chain.invoke(**kwargs)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
return {"input": {"question": self.user_input, "search_kwargs": self.search_kwargs()}}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_chat_request_web(
|
|
90
|
+
request_json, session: MutableMapping[str, Any], env: EnvSettings, assistant_mode: bool
|
|
91
|
+
) -> ChatRequest:
|
|
92
|
+
"""
|
|
93
|
+
Build a ChatRequest from the web app payload and session.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
request_json: The JSON payload from the web request.
|
|
97
|
+
session: The Flask session mapping.
|
|
98
|
+
env: The EnvSettings.
|
|
99
|
+
assistant_mode: assistant mode (True) or search mode (False)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# need this so UserSettings can pick it up from session
|
|
103
|
+
# if it's not in the payload, UserSettings will use the default
|
|
104
|
+
if "date" in request_json:
|
|
105
|
+
session["date_filter"] = request_json["date"]
|
|
106
|
+
|
|
107
|
+
defaults = UserSettings.defaults(env)
|
|
108
|
+
settings = UserSettings.from_session(session, defaults)
|
|
109
|
+
try:
|
|
110
|
+
user_input = request_json["user_input"]
|
|
111
|
+
filters = request_json["filters"]
|
|
112
|
+
except KeyError as e:
|
|
113
|
+
raise ValueError(f"Missing required field in payload: {e!s}")
|
|
114
|
+
|
|
115
|
+
if env.local_mode:
|
|
116
|
+
user_id = "local_user"
|
|
117
|
+
else:
|
|
118
|
+
user_obj = session.get("user") or {}
|
|
119
|
+
user_id = user_obj.get("at_hash", "unknown-user")
|
|
120
|
+
|
|
121
|
+
filters["date_filter"] = _normalize_date_filter(request_json.get("date", settings.date_filter))
|
|
122
|
+
|
|
123
|
+
thread_id = None
|
|
124
|
+
if assistant_mode:
|
|
125
|
+
thread_id = session.get("thread_id")
|
|
126
|
+
if not thread_id:
|
|
127
|
+
session["thread_id"] = str(uuid.uuid4())
|
|
128
|
+
thread_id = session["thread_id"]
|
|
129
|
+
|
|
130
|
+
return ChatRequest(
|
|
131
|
+
user_input=user_input,
|
|
132
|
+
filters=filters,
|
|
133
|
+
assistant_mode=assistant_mode,
|
|
134
|
+
user_settings=settings,
|
|
135
|
+
user_id=user_id,
|
|
136
|
+
thread_id=thread_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _normalize_date_filter(raw_date: Any) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Normalize date filter string to DD-MM-YYYY for downstream SQL.
|
|
143
|
+
|
|
144
|
+
Inputs:
|
|
145
|
+
raw_date: Raw date value from payload/session in DD-MM-YYYY or YYYY-MM-DD.
|
|
146
|
+
Outputs:
|
|
147
|
+
str: Date string in DD-MM-YYYY format.
|
|
148
|
+
"""
|
|
149
|
+
if raw_date is None:
|
|
150
|
+
return "01-01-2020"
|
|
151
|
+
|
|
152
|
+
date_str = str(raw_date).strip()
|
|
153
|
+
if not date_str:
|
|
154
|
+
return "01-01-2020"
|
|
155
|
+
|
|
156
|
+
# already in DD-MM-YYYY
|
|
157
|
+
if re.fullmatch(r"\d{2}-\d{2}-\d{4}", date_str):
|
|
158
|
+
return date_str
|
|
159
|
+
|
|
160
|
+
# browser date input commonly uses YYYY-MM-DD
|
|
161
|
+
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", date_str):
|
|
162
|
+
return datetime.strptime(date_str, "%Y-%m-%d").strftime("%d-%m-%Y")
|
|
163
|
+
|
|
164
|
+
# fallback to safe default for malformed input
|
|
165
|
+
return "01-01-2020"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _parse_mattermost_input(input_str: str) -> dict[str, Any]:
|
|
169
|
+
"""
|
|
170
|
+
Parse input string from mattermost
|
|
171
|
+
|
|
172
|
+
Returns: Parsed input with keys:
|
|
173
|
+
- type (List[str]) Vectorstores to search
|
|
174
|
+
- date-filter (str) Date filter in DD-MM-YYYY format
|
|
175
|
+
- k (int) Number of vectorstore results
|
|
176
|
+
- k_text (int) Number of text search results
|
|
177
|
+
- message (str) The main user input message
|
|
178
|
+
"""
|
|
179
|
+
# Canonical source names (map legacy aliases to currently supported source values)
|
|
180
|
+
valid_sources = {
|
|
181
|
+
"twiki": "twiki",
|
|
182
|
+
"cds": "CDS",
|
|
183
|
+
"indico": "Indico",
|
|
184
|
+
"atlastalk": "AtlasTalk",
|
|
185
|
+
"atlas_talk": "AtlasTalk",
|
|
186
|
+
"gitlabmarkdown": "GitLabMarkdown",
|
|
187
|
+
"gitlab_markdown": "GitLabMarkdown",
|
|
188
|
+
"mkdocs": "GitLabMarkdown",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Defaults
|
|
192
|
+
filters = {
|
|
193
|
+
"type": [], # default: ["twiki", "MkDocs"] set below
|
|
194
|
+
"k": 4,
|
|
195
|
+
"k_text": 3,
|
|
196
|
+
"user_input": "",
|
|
197
|
+
"date-filter": "01-01-2020",
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
default_types = ["twiki", "GitLabMarkdown"]
|
|
201
|
+
|
|
202
|
+
# Remove the leading command
|
|
203
|
+
input_str = re.sub(r"^/\w+\s*", "", input_str)
|
|
204
|
+
|
|
205
|
+
# Match [content] blocks
|
|
206
|
+
bracket_pattern = re.compile(r"\[([^\[\]]+)\]")
|
|
207
|
+
matches = bracket_pattern.findall(input_str)
|
|
208
|
+
remaining = input_str
|
|
209
|
+
|
|
210
|
+
# Date pattern: DD-MM-YYYY
|
|
211
|
+
date_pattern = re.compile(r"^\d{2}-\d{2}-\d{4}$")
|
|
212
|
+
|
|
213
|
+
for match in matches:
|
|
214
|
+
remaining = remaining.replace(f"[{match}]", "", 1).strip()
|
|
215
|
+
lowered = match.lower()
|
|
216
|
+
|
|
217
|
+
# Match exact date format
|
|
218
|
+
if date_pattern.match(match.strip()):
|
|
219
|
+
filters["date-filter"] = match.strip()
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Parse k_text and k
|
|
223
|
+
if lowered.startswith("k_text="):
|
|
224
|
+
try:
|
|
225
|
+
filters["k_text"] = int(lowered.split("=")[1])
|
|
226
|
+
continue
|
|
227
|
+
except ValueError:
|
|
228
|
+
pass
|
|
229
|
+
elif lowered.startswith("k="):
|
|
230
|
+
try:
|
|
231
|
+
filters["k"] = int(lowered.split("=")[1])
|
|
232
|
+
continue
|
|
233
|
+
except ValueError:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
# Source types
|
|
237
|
+
candidates = [s.strip() for s in match.split(",")]
|
|
238
|
+
for c in candidates:
|
|
239
|
+
key = c.lower()
|
|
240
|
+
if key in valid_sources:
|
|
241
|
+
filters["type"].append(valid_sources[key])
|
|
242
|
+
|
|
243
|
+
if not filters["type"]:
|
|
244
|
+
filters["type"] = default_types
|
|
245
|
+
|
|
246
|
+
filters["user_input"] = remaining.strip()
|
|
247
|
+
return filters
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_chat_request_mattermost(
|
|
251
|
+
request_text: str, session: MutableMapping[str, Any], env: EnvSettings
|
|
252
|
+
) -> ChatRequest:
|
|
253
|
+
"""
|
|
254
|
+
Build a ChatRequest from the Mattermost request form and session.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
request_text: The text of the Mattermost request (usually get this using request.form["text"])
|
|
258
|
+
session: The Flask session mapping.
|
|
259
|
+
env: The environment settings.
|
|
260
|
+
"""
|
|
261
|
+
parsed_dict = _parse_mattermost_input(request_text)
|
|
262
|
+
defaults = UserSettings.defaults(env)
|
|
263
|
+
settings = UserSettings.from_session(session, defaults).apply_overrides(
|
|
264
|
+
{"k_vector": parsed_dict["k"], "k_text": parsed_dict["k_text"]}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
user_id = "mattermost-" + session.get("user_id", "unknown")
|
|
268
|
+
|
|
269
|
+
return ChatRequest(
|
|
270
|
+
user_input=parsed_dict["user_input"],
|
|
271
|
+
filters={
|
|
272
|
+
"sources": {source: True for source in parsed_dict["type"]},
|
|
273
|
+
"date_filter": parsed_dict["date-filter"],
|
|
274
|
+
},
|
|
275
|
+
assistant_mode=True, # TODO: add search only mode for mattermost
|
|
276
|
+
user_settings=settings,
|
|
277
|
+
user_id=user_id,
|
|
278
|
+
)
|