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.
Files changed (61) hide show
  1. chATLAS_Frontend/__init__.py +0 -0
  2. chATLAS_Frontend/frontend/__init__.py +0 -0
  3. chATLAS_Frontend/frontend/auth.py +38 -0
  4. chATLAS_Frontend/frontend/chat_processing.py +63 -0
  5. chATLAS_Frontend/frontend/chat_request.py +278 -0
  6. chATLAS_Frontend/frontend/html_generation.py +361 -0
  7. chATLAS_Frontend/frontend/logging.py +387 -0
  8. chATLAS_Frontend/frontend/processing.py +254 -0
  9. chATLAS_Frontend/frontend/resources.py +126 -0
  10. chATLAS_Frontend/frontend/routes.py +250 -0
  11. chATLAS_Frontend/frontend/settings.py +177 -0
  12. chATLAS_Frontend/frontend/utils.py +11 -0
  13. chATLAS_Frontend/launch.py +73 -0
  14. chATLAS_Frontend/static/css/markdown_styling.css +147 -0
  15. chATLAS_Frontend/static/css/min.extra.css +3899 -0
  16. chATLAS_Frontend/static/css/styles.css +956 -0
  17. chATLAS_Frontend/static/css/styles_backup.css +289 -0
  18. chATLAS_Frontend/static/images/favicon.ico +0 -0
  19. chATLAS_Frontend/static/js/app/api.js +109 -0
  20. chATLAS_Frontend/static/js/app/bootstrap.js +162 -0
  21. chATLAS_Frontend/static/js/app/state.js +29 -0
  22. chATLAS_Frontend/static/js/components/chatThread.js +377 -0
  23. chATLAS_Frontend/static/js/components/composer.js +43 -0
  24. chATLAS_Frontend/static/js/components/feedback.js +31 -0
  25. chATLAS_Frontend/static/js/components/filtersPanel.js +95 -0
  26. chATLAS_Frontend/static/js/components/modeToggle.js +21 -0
  27. chATLAS_Frontend/static/js/components/settingsModal.js +48 -0
  28. chATLAS_Frontend/static/js/components/sourcePanel.js +141 -0
  29. chATLAS_Frontend/static/js/main.js +7 -0
  30. chATLAS_Frontend/static/js/vendor/marked.min.js +6 -0
  31. chATLAS_Frontend/templates/page/base.html +32 -0
  32. chATLAS_Frontend/templates/page/categories/atlas_talk.html +21 -0
  33. chATLAS_Frontend/templates/page/categories/cds.html +3 -0
  34. chATLAS_Frontend/templates/page/categories/gitlab_markdown.html +43 -0
  35. chATLAS_Frontend/templates/page/categories/indico.html +8 -0
  36. chATLAS_Frontend/templates/page/categories/twiki.html +54 -0
  37. chATLAS_Frontend/templates/page/filters.html +65 -0
  38. chATLAS_Frontend/templates/page/footer.html +13 -0
  39. chATLAS_Frontend/templates/page/header.html +93 -0
  40. chATLAS_Frontend/templates/page/index.html +40 -0
  41. chATLAS_Frontend/templates/page/searchbar.html +22 -0
  42. chATLAS_Frontend/templates/response/all_sources.html +10 -0
  43. chATLAS_Frontend/templates/response/bot_response.html +7 -0
  44. chATLAS_Frontend/templates/response/citations_block.html +4 -0
  45. chATLAS_Frontend/templates/response/error_box.html +11 -0
  46. chATLAS_Frontend/templates/response/feedback_buttons.html +10 -0
  47. chATLAS_Frontend/templates/response/macros.html +9 -0
  48. chATLAS_Frontend/templates/response/source_display_button.html +22 -0
  49. chATLAS_Frontend/templates/response/source_similarity.html +6 -0
  50. chATLAS_Frontend/templates/response/used_docs.html +8 -0
  51. chATLAS_Frontend/templates/response/user_query.html +17 -0
  52. chATLAS_Frontend/usage/__init__.py +0 -0
  53. chATLAS_Frontend/usage/db_utils.py +85 -0
  54. chATLAS_Frontend/usage/execute_query.py +123 -0
  55. chATLAS_Frontend/usage/plots.py +45 -0
  56. chATLAS_Frontend/usage/queries.py +46 -0
  57. chatlas_frontend-1.0.0.dist-info/METADATA +117 -0
  58. chatlas_frontend-1.0.0.dist-info/RECORD +61 -0
  59. chatlas_frontend-1.0.0.dist-info/WHEEL +5 -0
  60. chatlas_frontend-1.0.0.dist-info/licenses/LICENSE +201 -0
  61. 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
+ )