syntaxmatrix 2.6.4.3__py3-none-any.whl → 3.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.
- syntaxmatrix/__init__.py +6 -4
- syntaxmatrix/agentic/agents.py +195 -15
- syntaxmatrix/agentic/agents_orchestrer.py +16 -10
- syntaxmatrix/client_docs.py +237 -0
- syntaxmatrix/commentary.py +96 -25
- syntaxmatrix/core.py +156 -54
- syntaxmatrix/dataset_preprocessing.py +2 -2
- syntaxmatrix/db.py +60 -0
- syntaxmatrix/db_backends/__init__.py +1 -0
- syntaxmatrix/db_backends/postgres_backend.py +14 -0
- syntaxmatrix/db_backends/sqlite_backend.py +258 -0
- syntaxmatrix/db_contract.py +71 -0
- syntaxmatrix/kernel_manager.py +174 -150
- syntaxmatrix/page_builder_generation.py +654 -50
- syntaxmatrix/page_layout_contract.py +25 -3
- syntaxmatrix/page_patch_publish.py +368 -15
- syntaxmatrix/plugins/__init__.py +0 -0
- syntaxmatrix/plugins/plugin_manager.py +114 -0
- syntaxmatrix/premium/__init__.py +18 -0
- syntaxmatrix/premium/catalogue/__init__.py +121 -0
- syntaxmatrix/premium/gate.py +119 -0
- syntaxmatrix/premium/state.py +507 -0
- syntaxmatrix/premium/verify.py +222 -0
- syntaxmatrix/profiles.py +1 -1
- syntaxmatrix/routes.py +9782 -8004
- syntaxmatrix/settings/model_map.py +50 -65
- syntaxmatrix/settings/prompts.py +1435 -380
- syntaxmatrix/settings/string_navbar.py +4 -4
- syntaxmatrix/static/icons/bot_icon.png +0 -0
- syntaxmatrix/static/icons/bot_icon2.png +0 -0
- syntaxmatrix/templates/admin_billing.html +408 -0
- syntaxmatrix/templates/admin_branding.html +65 -2
- syntaxmatrix/templates/admin_features.html +54 -0
- syntaxmatrix/templates/dashboard.html +285 -8
- syntaxmatrix/templates/edit_page.html +199 -18
- syntaxmatrix/themes.py +17 -17
- syntaxmatrix/workspace_db.py +0 -23
- syntaxmatrix-3.0.0.dist-info/METADATA +219 -0
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/RECORD +42 -30
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/WHEEL +1 -1
- syntaxmatrix/settings/default.yaml +0 -13
- syntaxmatrix-2.6.4.3.dist-info/METADATA +0 -539
- syntaxmatrix-2.6.4.3.dist-info/licenses/LICENSE.txt +0 -21
- /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/top_level.txt +0 -0
syntaxmatrix/commentary.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import re, json
|
|
4
|
+
import html as _html
|
|
5
|
+
import re as _re
|
|
4
6
|
from typing import Any, Dict, List, Optional
|
|
5
7
|
|
|
6
8
|
from syntaxmatrix import profiles as _prof
|
|
@@ -114,11 +116,49 @@ def sniff_tables_from_html(html: str) -> List[Dict[str, Any]]:
|
|
|
114
116
|
return tables
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
def sniff_pre_text_from_html(html: str, *, max_lines: int = 18, max_chars: int = 900) -> List[str]:
|
|
120
|
+
"""Extract short, useful plain-text snippets from <pre> blocks (metrics, tests, notes)."""
|
|
121
|
+
if not html:
|
|
122
|
+
return []
|
|
123
|
+
pres = _re.findall(r"<pre[^>]*>(.*?)</pre>", html, flags=_re.DOTALL | _re.IGNORECASE)
|
|
124
|
+
out: List[str] = []
|
|
125
|
+
for p in pres:
|
|
126
|
+
t = _strip_tags(p)
|
|
127
|
+
t = _html.unescape(t)
|
|
128
|
+
t = t.replace("\r\n", "\n").replace("\r", "\n")
|
|
129
|
+
|
|
130
|
+
for ln in t.split("\n"):
|
|
131
|
+
ln = (ln or "").strip()
|
|
132
|
+
if not ln:
|
|
133
|
+
continue
|
|
134
|
+
# keep only “result-ish” lines; drop giant dumps if any sneak in
|
|
135
|
+
if len(ln) > 260:
|
|
136
|
+
ln = ln[:260].rstrip() + "…"
|
|
137
|
+
out.append(ln)
|
|
138
|
+
|
|
139
|
+
# de-dup, preserve order
|
|
140
|
+
seen = set()
|
|
141
|
+
cleaned = []
|
|
142
|
+
for ln in out:
|
|
143
|
+
k = ln.lower()
|
|
144
|
+
if k in seen:
|
|
145
|
+
continue
|
|
146
|
+
seen.add(k)
|
|
147
|
+
cleaned.append(ln)
|
|
148
|
+
|
|
149
|
+
# cap total length
|
|
150
|
+
joined = "\n".join(cleaned)
|
|
151
|
+
joined = joined[:max_chars]
|
|
152
|
+
cleaned = joined.split("\n")[:max_lines]
|
|
153
|
+
return [c for c in (x.strip() for x in cleaned) if c]
|
|
154
|
+
|
|
155
|
+
|
|
117
156
|
def build_display_summary(question: str,
|
|
118
157
|
mpl_axes: List[Dict[str, Any]],
|
|
119
158
|
html_blocks: List[str]) -> Dict[str, Any]:
|
|
120
159
|
html_joined = "\n".join(str(b) for b in html_blocks)
|
|
121
160
|
tables = sniff_tables_from_html(html_joined)
|
|
161
|
+
pre_snips = sniff_pre_text_from_html(html_joined)
|
|
122
162
|
|
|
123
163
|
axes_clean=[]
|
|
124
164
|
for ax in mpl_axes:
|
|
@@ -132,11 +172,14 @@ def build_display_summary(question: str,
|
|
|
132
172
|
return {
|
|
133
173
|
"question": (question or "").strip(),
|
|
134
174
|
"axes": axes_clean,
|
|
135
|
-
"tables": tables
|
|
175
|
+
"tables": tables,
|
|
176
|
+
"text_snippets": pre_snips,
|
|
136
177
|
}
|
|
137
178
|
|
|
138
179
|
def _context_strings(context: Dict[str, Any]) -> List[str]:
|
|
139
180
|
s = [context.get("question","")]
|
|
181
|
+
s += (context.get("text_snippets", []) or [])
|
|
182
|
+
|
|
140
183
|
for ax in context.get("axes", []) or []:
|
|
141
184
|
s += [ax.get("title",""), ax.get("x_label",""), ax.get("y_label","")]
|
|
142
185
|
s += (ax.get("legend", []) or [])
|
|
@@ -159,35 +202,63 @@ def phrase_commentary_vision(context: Dict[str, Any], images_b64: List[str]) ->
|
|
|
159
202
|
send figures + text; otherwise fall back to a text-only prompt grounded by labels.
|
|
160
203
|
"""
|
|
161
204
|
|
|
162
|
-
_SYSTEM_VISION =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
_SYSTEM_VISION = """
|
|
206
|
+
You are an applied data analyst writing an answer to the user's question.
|
|
207
|
+
|
|
208
|
+
Your priority:
|
|
209
|
+
1) Answer the question directly (clear verdict first).
|
|
210
|
+
2) Justify the verdict using evidence from the figures and the provided context.
|
|
211
|
+
3) Keep it readable for a non-technical stakeholder.
|
|
212
|
+
|
|
213
|
+
Rules:
|
|
214
|
+
- Do NOT write a preamble.
|
|
215
|
+
- Do NOT narrate what a chart “looks like”; interpret it in relation to the question.
|
|
216
|
+
- Only use numbers if they are visible in the figures or included in the text snippets/context.
|
|
217
|
+
- Output must be safe HTML only: <b>, <p>, <ul>, <li>, <br>. No <style>, no <script>, no images.
|
|
218
|
+
""".strip()
|
|
167
219
|
|
|
168
220
|
_USER_TMPL_VISION = """
|
|
169
|
-
|
|
170
|
-
{q}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
221
|
+
<b>Question</b>
|
|
222
|
+
<p>{q}</p>
|
|
223
|
+
|
|
224
|
+
<b>Available evidence</b>
|
|
225
|
+
<p><b>Text snippets:</b><br>{snips}</p>
|
|
226
|
+
<p><b>Plot/table context strings (titles, axes, legends, headers):</b><br>{ctx}</p>
|
|
227
|
+
|
|
228
|
+
Write the response in this exact structure:
|
|
229
|
+
|
|
230
|
+
<b>Answer</b>
|
|
231
|
+
<p>
|
|
232
|
+
Give a direct answer to the question in 2-4 sentences.
|
|
233
|
+
If the correct output is a decision (e.g., association vs none, higher vs lower, best model, significant vs not),
|
|
234
|
+
state it explicitly.
|
|
235
|
+
</p>
|
|
236
|
+
|
|
237
|
+
<b>Key evidence</b>
|
|
238
|
+
<ul>
|
|
239
|
+
<li>5–8 bullets. Each bullet must link evidence → conclusion.</li>
|
|
240
|
+
<li>Reference plots/tables by their titles/axes/headers when possible.</li>
|
|
241
|
+
<li>Use numbers only if present in snippets or clearly visible.</li>
|
|
242
|
+
</ul>
|
|
243
|
+
|
|
244
|
+
<b>What this means</b>
|
|
245
|
+
<ul>
|
|
246
|
+
<li>2-4 bullets translating the finding into a practical takeaway.</li>
|
|
247
|
+
</ul>
|
|
248
|
+
|
|
249
|
+
<b>Limitations</b>
|
|
250
|
+
<ul><li>1-2 bullets (short).</li></ul>
|
|
251
|
+
|
|
252
|
+
<b>Next steps</b>
|
|
253
|
+
<ul><li>2 bullets (actionable).</li></ul>
|
|
254
|
+
""".strip()
|
|
186
255
|
|
|
187
256
|
visible = _context_strings(context)
|
|
257
|
+
snips = "\n".join(context.get("text_snippets", []) or [])
|
|
188
258
|
user = _USER_TMPL_VISION.format(
|
|
189
259
|
q=context.get("question",""),
|
|
190
|
-
|
|
260
|
+
snips=_html.escape(snips).replace("\n", "<br>"),
|
|
261
|
+
ctx=_html.escape(json.dumps(visible, ensure_ascii=False, indent=2)).replace("\n", "<br>")
|
|
191
262
|
)
|
|
192
263
|
|
|
193
264
|
commentary_profile = _prof.get_profile("imagetexter") or _prof.get_profile("admin")
|
syntaxmatrix/core.py
CHANGED
|
@@ -16,7 +16,7 @@ from .file_processor import process_admin_pdf_files
|
|
|
16
16
|
from google.genai import types
|
|
17
17
|
from .vector_db import query_embeddings
|
|
18
18
|
from .vectorizer import embed_text
|
|
19
|
-
from typing import List, Generator
|
|
19
|
+
from typing import List, Generator, Optional
|
|
20
20
|
from .auth import init_auth_db
|
|
21
21
|
from . import profiles as _prof
|
|
22
22
|
from syntaxmatrix.smiv import SMIV
|
|
@@ -27,13 +27,16 @@ from html import unescape
|
|
|
27
27
|
from .plottings import render_plotly, pyplot, describe_plotly, describe_matplotlib
|
|
28
28
|
from threading import RLock
|
|
29
29
|
from syntaxmatrix.settings.model_map import GPT_MODELS_LATEST
|
|
30
|
+
from syntaxmatrix.settings.client_items import read_client_file, getenv_api_key
|
|
31
|
+
from syntaxmatrix.plugins.plugin_manager import PluginManager
|
|
32
|
+
from .premium import FeatureGate, ensure_premium_state
|
|
33
|
+
from pathlib import Path
|
|
30
34
|
from syntaxmatrix.settings.prompts import(
|
|
31
35
|
SMXAI_CHAT_IDENTITY,
|
|
32
36
|
SMXAI_CHAT_INSTRUCTIONS,
|
|
33
37
|
SMXAI_WEBSITE_DESCRIPTION,
|
|
34
38
|
)
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
|
|
37
40
|
# ──────── framework‐local storage paths ────────
|
|
38
41
|
# this ensures the key & data always live under the package dir,
|
|
39
42
|
_CLIENT_DIR = detect_project_root()
|
|
@@ -55,7 +58,7 @@ EDA_OUTPUT = {} # global buffer for EDA output by session
|
|
|
55
58
|
|
|
56
59
|
class SyntaxMUI:
|
|
57
60
|
def __init__(self,
|
|
58
|
-
|
|
61
|
+
host="127.0.0.1",
|
|
59
62
|
port="5080",
|
|
60
63
|
user_icon="👩🏿🦲",
|
|
61
64
|
bot_icon="<img src='/static/icons/bot_icon.png' width=20' alt='bot'/>",
|
|
@@ -63,10 +66,12 @@ class SyntaxMUI:
|
|
|
63
66
|
site_logo="<img src='/static/icons/logo.png' width='45' alt='logo'/>",
|
|
64
67
|
site_title="SyntaxMatrix",
|
|
65
68
|
project_name="smxAI",
|
|
66
|
-
theme_name="
|
|
69
|
+
theme_name="chark",
|
|
67
70
|
ui_mode = "default"
|
|
68
71
|
):
|
|
69
|
-
self.app = Flask(__name__)
|
|
72
|
+
self.app = Flask(__name__)
|
|
73
|
+
self._client_dir = Path(_CLIENT_DIR).resolve()
|
|
74
|
+
self._client_root = self._client_dir.parent
|
|
70
75
|
self.host = host
|
|
71
76
|
self.port = port
|
|
72
77
|
|
|
@@ -78,7 +83,6 @@ class SyntaxMUI:
|
|
|
78
83
|
self.bot_icon = bot_icon
|
|
79
84
|
self.site_title = site_title
|
|
80
85
|
self.project_name = project_name
|
|
81
|
-
|
|
82
86
|
self._default_site_logo = self.site_logo
|
|
83
87
|
self._default_favicon = self.favicon
|
|
84
88
|
self._default_bot_icon = self.bot_icon
|
|
@@ -89,25 +93,45 @@ class SyntaxMUI:
|
|
|
89
93
|
self.theme_toggle_enabled = False
|
|
90
94
|
self.user_files_enabled = False
|
|
91
95
|
self.registration_enabled = False
|
|
96
|
+
self.site_documentation_enabled = False
|
|
97
|
+
self.ml_lab_enabled = False
|
|
98
|
+
|
|
92
99
|
self.smxai_identity = SMXAI_CHAT_IDENTITY
|
|
93
100
|
self.smxai_instructions = SMXAI_CHAT_INSTRUCTIONS
|
|
94
101
|
self.website_description = SMXAI_WEBSITE_DESCRIPTION
|
|
102
|
+
# Preserve framework defaults so client branding can safely override and reset.
|
|
103
|
+
self._default_smxai_identity = self.smxai_identity
|
|
104
|
+
self._default_smxai_instructions = self.smxai_instructions
|
|
105
|
+
self._default_website_description = self.website_description
|
|
106
|
+
|
|
95
107
|
self._eda_output = {} # {chat_id: html}
|
|
96
108
|
self._eda_lock = RLock()
|
|
97
109
|
|
|
98
110
|
db.init_db()
|
|
111
|
+
self.db = db
|
|
99
112
|
self.page = ""
|
|
100
113
|
self.pages = db.get_pages()
|
|
114
|
+
|
|
101
115
|
init_auth_db()
|
|
116
|
+
try:
|
|
117
|
+
ensure_premium_state(db=db, client_dir=str(_CLIENT_DIR), trial_days=7)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
102
120
|
|
|
103
121
|
self.widgets = OrderedDict()
|
|
104
|
-
self.theme = DEFAULT_THEMES.get(theme_name, DEFAULT_THEMES["
|
|
105
|
-
self.system_output_buffer = "" # Ephemeral buffer
|
|
122
|
+
self.theme = DEFAULT_THEMES.get(theme_name, DEFAULT_THEMES["chark"])
|
|
123
|
+
self.system_output_buffer = "" # Ephemeral buffer initialised
|
|
106
124
|
self.app_token = str(uuid.uuid4()) # NEW: Unique token for each app launch.
|
|
107
125
|
self.admin_pdf_chunks = {} # In-memory store for admin PDF chunks
|
|
108
|
-
self.user_file_chunks = {} # In-memory store of user‑uploaded chunks, scoped per chat
|
|
109
|
-
|
|
126
|
+
self.user_file_chunks = {} # In-memory store of user‑uploaded chunks, scoped per chat
|
|
110
127
|
self._last_llm_usage = None
|
|
128
|
+
|
|
129
|
+
# Apply persisted feature flags + theme (fail-soft)
|
|
130
|
+
try:
|
|
131
|
+
self._apply_feature_flags_from_db()
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
111
135
|
routes.setup_routes(self)
|
|
112
136
|
|
|
113
137
|
# Apply client branding overrides if present on disk
|
|
@@ -127,6 +151,18 @@ class SyntaxMUI:
|
|
|
127
151
|
self.is_streaming = False
|
|
128
152
|
self.stream_args = {}
|
|
129
153
|
self._apply_feature_flags_from_db()
|
|
154
|
+
# Premium (entitlements + plugins). Safe no-op unless configured.
|
|
155
|
+
try:
|
|
156
|
+
self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR, db=db)
|
|
157
|
+
except Exception:
|
|
158
|
+
self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
self.plugins = PluginManager(self, gate=self.feature_gate, db=db)
|
|
162
|
+
self.plugins.load_all()
|
|
163
|
+
except Exception:
|
|
164
|
+
# Never break app boot because of premium plumbing
|
|
165
|
+
self.plugins = PluginManager(self)
|
|
130
166
|
|
|
131
167
|
self._recent_visual_summaries = []
|
|
132
168
|
|
|
@@ -166,7 +202,7 @@ class SyntaxMUI:
|
|
|
166
202
|
if not hasattr(self, "_recent_visual_summaries"):
|
|
167
203
|
self._recent_visual_summaries = []
|
|
168
204
|
# keep last 6
|
|
169
|
-
self._recent_visual_summaries = (self._recent_visual_summaries + [summary])[-6:]
|
|
205
|
+
self._recent_visual_summaries = (self._recent_visual_summaries + [summary]) # [-6:]
|
|
170
206
|
|
|
171
207
|
def set_plottings(self, fig_or_html, note=None):
|
|
172
208
|
# prefer current chat id; fall back to per-browser sid; finally "default"
|
|
@@ -324,18 +360,29 @@ class SyntaxMUI:
|
|
|
324
360
|
DEFAULT_THEMES[theme_name] = theme
|
|
325
361
|
self.theme = DEFAULT_THEMES[theme_name]
|
|
326
362
|
else:
|
|
327
|
-
self.theme = DEFAULT_THEMES["
|
|
328
|
-
self.error("Theme must be 'light', 'dark', or a custom dict.")
|
|
363
|
+
self.theme = DEFAULT_THEMES["chark"]
|
|
364
|
+
self.error("Theme must be 'chark', 'light', 'dark', or a custom dict.")
|
|
329
365
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
self.theme_toggle_enabled
|
|
333
|
-
|
|
334
|
-
def enable_user_files(self):
|
|
335
|
-
self.user_files_enabled =
|
|
366
|
+
def enable_theme_toggle(self, bul: bool = True):
|
|
367
|
+
self.theme_toggle_enabled = bool(bul)
|
|
368
|
+
return self.theme_toggle_enabled
|
|
369
|
+
|
|
370
|
+
def enable_user_files(self, bul: bool = True):
|
|
371
|
+
self.user_files_enabled = bool(bul)
|
|
372
|
+
return self.user_files_enabled
|
|
373
|
+
|
|
374
|
+
def enable_registration(self, bul: bool = True):
|
|
375
|
+
self.registration_enabled = bool(bul)
|
|
376
|
+
return self.registration_enabled
|
|
377
|
+
|
|
378
|
+
def enable_site_documentation(self, bul: bool = True):
|
|
379
|
+
self.site_documentation_enabled = bool(bul)
|
|
380
|
+
return self.site_documentation_enabled
|
|
381
|
+
|
|
382
|
+
def enable_ml_lab(self, bul: bool = True):
|
|
383
|
+
self.ml_lab_enabled = bool(bul)
|
|
384
|
+
return self.ml_lab_enabled
|
|
336
385
|
|
|
337
|
-
def enable_registration(self):
|
|
338
|
-
self.registration_enabled = True
|
|
339
386
|
|
|
340
387
|
def _apply_feature_flags_from_db(self):
|
|
341
388
|
"""
|
|
@@ -345,11 +392,28 @@ class SyntaxMUI:
|
|
|
345
392
|
return str(v or "").strip().lower() in ("1", "true", "yes", "on")
|
|
346
393
|
|
|
347
394
|
try:
|
|
348
|
-
stream_v = db.get_setting("feature.stream_mode", "
|
|
349
|
-
user_files_v = db.get_setting("feature.user_files", "
|
|
395
|
+
stream_v = db.get_setting("feature.stream_mode", "1")
|
|
396
|
+
user_files_v = db.get_setting("feature.user_files", "1")
|
|
397
|
+
|
|
398
|
+
# NEW defaults are all False (0)
|
|
399
|
+
docs_v = db.get_setting("feature.site_documentation", "0")
|
|
400
|
+
ml_v = db.get_setting("feature.ml_lab", "0")
|
|
401
|
+
reg_v = db.get_setting("feature.registration", "0")
|
|
402
|
+
theme_toggle_v = db.get_setting("feature.theme_toggle", "0")
|
|
403
|
+
|
|
404
|
+
# Theme is a choice (default light)
|
|
405
|
+
theme_name = db.get_setting("branding.theme_name", "light")
|
|
350
406
|
|
|
351
407
|
self.is_streaming = _truthy(stream_v)
|
|
352
408
|
self.user_files_enabled = _truthy(user_files_v)
|
|
409
|
+
|
|
410
|
+
self.site_documentation_enabled = _truthy(docs_v)
|
|
411
|
+
self.ml_lab_enabled = _truthy(ml_v)
|
|
412
|
+
self.registration_enabled = _truthy(reg_v)
|
|
413
|
+
self.theme_toggle_enabled = _truthy(theme_toggle_v)
|
|
414
|
+
|
|
415
|
+
# Apply the chosen theme to the instance
|
|
416
|
+
self.set_theme(str(theme_name or "light").strip().lower() or "light")
|
|
353
417
|
except Exception:
|
|
354
418
|
# Keep defaults if DB isn't ready for any reason
|
|
355
419
|
pass
|
|
@@ -383,6 +447,31 @@ class SyntaxMUI:
|
|
|
383
447
|
use it; otherwise keep the framework defaults.
|
|
384
448
|
Also pulls site_title and project_name from app_settings.
|
|
385
449
|
"""
|
|
450
|
+
|
|
451
|
+
# Premium gating: ignore custom branding unless entitled.
|
|
452
|
+
try:
|
|
453
|
+
if hasattr(self, "feature_gate") and self.feature_gate and (not self.feature_gate.enabled("branding_controls")):
|
|
454
|
+
self.site_logo = getattr(self, "_default_site_logo", self.site_logo)
|
|
455
|
+
self.favicon = getattr(self, "_default_favicon", self.favicon)
|
|
456
|
+
self.bot_icon = getattr(self, "_default_bot_icon", self.bot_icon)
|
|
457
|
+
try:
|
|
458
|
+
self.set_smxai_identity("")
|
|
459
|
+
self.set_smxai_instructions("")
|
|
460
|
+
self.set_website_description(getattr(self, "_default_website_description", self.website_description))
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
self.site_title = getattr(self, "_default_site_title", self.site_title)
|
|
464
|
+
self.project_name = getattr(self, "_default_project_name", self.project_name)
|
|
465
|
+
return
|
|
466
|
+
except Exception:
|
|
467
|
+
# Fail closed: if we cannot resolve entitlements, do not apply custom branding.
|
|
468
|
+
self.site_logo = getattr(self, "_default_site_logo", self.site_logo)
|
|
469
|
+
self.favicon = getattr(self, "_default_favicon", self.favicon)
|
|
470
|
+
self.bot_icon = getattr(self, "_default_bot_icon", self.bot_icon)
|
|
471
|
+
self.site_title = getattr(self, "_default_site_title", self.site_title)
|
|
472
|
+
self.project_name = getattr(self, "_default_project_name", self.project_name)
|
|
473
|
+
return
|
|
474
|
+
|
|
386
475
|
branding_dir = os.path.join(_CLIENT_DIR, "branding")
|
|
387
476
|
|
|
388
477
|
def _pick_any(*basenames: str):
|
|
@@ -416,6 +505,18 @@ class SyntaxMUI:
|
|
|
416
505
|
self.bot_icon = f"<img src='/branding/{bot_fn}' width='20' alt='bot'/>"
|
|
417
506
|
else:
|
|
418
507
|
self.bot_icon = getattr(self, "_default_bot_icon", self.bot_icon)
|
|
508
|
+
# AI branding (stored in DB; blank value means "use framework default")
|
|
509
|
+
ident = (self.db.get_setting("branding.smxai_identity", "") or "").strip()
|
|
510
|
+
instr = (self.db.get_setting("branding.smxai_instructions", "") or "").strip()
|
|
511
|
+
wdesc = (self.db.get_setting("branding.website_description", "") or "").strip()
|
|
512
|
+
|
|
513
|
+
# Use the setters so behaviour is consistent across the framework.
|
|
514
|
+
self.set_smxai_identity(ident)
|
|
515
|
+
self.set_smxai_instructions(instr)
|
|
516
|
+
if wdesc:
|
|
517
|
+
self.set_website_description(wdesc)
|
|
518
|
+
else:
|
|
519
|
+
self.set_website_description(getattr(self, "_default_website_description", self.website_description))
|
|
419
520
|
|
|
420
521
|
# Site title + project name (DB settings; fall back to defaults)
|
|
421
522
|
try:
|
|
@@ -603,17 +704,20 @@ class SyntaxMUI:
|
|
|
603
704
|
# *********** LLM CLIENT HELPERS **********************
|
|
604
705
|
# ──────────────────────────────────────────────────────────────
|
|
605
706
|
def set_smxai_identity(self, profile):
|
|
606
|
-
|
|
607
|
-
|
|
707
|
+
"""Set the system identity/profile used by the chat model."""
|
|
708
|
+
profile = (profile or "").strip()
|
|
709
|
+
self.smxai_identity = profile if profile else getattr(self, "_default_smxai_identity", self.smxai_identity)
|
|
608
710
|
|
|
609
711
|
def set_smxai_instructions(self, instructions):
|
|
610
|
-
|
|
712
|
+
"""Set additional system instructions used by the chat model."""
|
|
713
|
+
instructions = (instructions or "").strip()
|
|
714
|
+
self.smxai_instructions = instructions if instructions else getattr(self, "_default_smxai_instructions", self.smxai_instructions)
|
|
611
715
|
|
|
612
716
|
|
|
613
717
|
def set_website_description(self, desc):
|
|
614
718
|
self.website_description = desc
|
|
615
719
|
|
|
616
|
-
|
|
720
|
+
|
|
617
721
|
def embed_query(self, q):
|
|
618
722
|
return embed_text(q)
|
|
619
723
|
|
|
@@ -689,7 +793,7 @@ class SyntaxMUI:
|
|
|
689
793
|
self.classifier_profile['client'] = _prof.get_client(classifier_profile)
|
|
690
794
|
|
|
691
795
|
_client = self.classifier_profile['client']
|
|
692
|
-
_provider = self.classifier_profile['provider']
|
|
796
|
+
_provider = self.classifier_profile['provider'].lower()
|
|
693
797
|
_model = self.classifier_profile['model']
|
|
694
798
|
|
|
695
799
|
# New instruction format with hybrid option
|
|
@@ -802,7 +906,7 @@ class SyntaxMUI:
|
|
|
802
906
|
"""
|
|
803
907
|
|
|
804
908
|
_client = self.summarizer_profile['client']
|
|
805
|
-
_provider = self.summarizer_profile['provider']
|
|
909
|
+
_provider = self.summarizer_profile['provider'].lower()
|
|
806
910
|
_model = self.summarizer_profile['model']
|
|
807
911
|
|
|
808
912
|
def google_generated_title():
|
|
@@ -813,7 +917,7 @@ class SyntaxMUI:
|
|
|
813
917
|
)
|
|
814
918
|
return response.text.strip()
|
|
815
919
|
except Exception as e:
|
|
816
|
-
return f"Google Summary agent error
|
|
920
|
+
return f"Google Summary agent error: {e}"
|
|
817
921
|
|
|
818
922
|
def gpt_models_latest_generated_title():
|
|
819
923
|
try:
|
|
@@ -889,16 +993,15 @@ class SyntaxMUI:
|
|
|
889
993
|
if not chat_profile:
|
|
890
994
|
yield """
|
|
891
995
|
<p style='color:red;'>
|
|
892
|
-
Error
|
|
893
|
-
|
|
894
|
-
To do that, you must login first or contact your administrator.
|
|
996
|
+
Error, Chat profile is not configured!
|
|
997
|
+
Login to the admin panel and add the LLM profile for chatting or contact your administrator.
|
|
895
998
|
</p>
|
|
896
999
|
"""
|
|
897
1000
|
return None
|
|
898
1001
|
self.chat_profile = chat_profile
|
|
899
1002
|
self.chat_profile['client'] = _prof.get_client(chat_profile)
|
|
900
1003
|
|
|
901
|
-
_provider = self.chat_profile['provider']
|
|
1004
|
+
_provider = self.chat_profile['provider'].lower()
|
|
902
1005
|
_client = self.chat_profile['client']
|
|
903
1006
|
_model = self.chat_profile['model']
|
|
904
1007
|
|
|
@@ -911,19 +1014,14 @@ class SyntaxMUI:
|
|
|
911
1014
|
"""
|
|
912
1015
|
|
|
913
1016
|
try:
|
|
914
|
-
if _provider == "google":
|
|
915
|
-
|
|
916
|
-
for chunk in _client.models.generate_content_stream(
|
|
1017
|
+
if _provider == "google":
|
|
1018
|
+
chuncks = _client.models.generate_content_stream(
|
|
917
1019
|
model=_model,
|
|
918
1020
|
contents=_contents,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
),
|
|
924
|
-
):
|
|
925
|
-
|
|
926
|
-
yield chunk.text
|
|
1021
|
+
)
|
|
1022
|
+
for chunk in chuncks:
|
|
1023
|
+
if chunk:
|
|
1024
|
+
yield chunk.text
|
|
927
1025
|
|
|
928
1026
|
elif _provider == "openai" and _model in self.get_gpt_models_latest(): # GPt 5 series
|
|
929
1027
|
input_prompt = (
|
|
@@ -970,7 +1068,7 @@ class SyntaxMUI:
|
|
|
970
1068
|
if token:
|
|
971
1069
|
yield token
|
|
972
1070
|
except Exception as e:
|
|
973
|
-
yield f"Error during streaming: {type(e).__name__}: {e}"
|
|
1071
|
+
yield f"Error during streaming: CLIENT {_client}" # {type(e).__name__}: {e}"
|
|
974
1072
|
|
|
975
1073
|
|
|
976
1074
|
def process_query(self, query, context, history, stream=False):
|
|
@@ -980,16 +1078,15 @@ class SyntaxMUI:
|
|
|
980
1078
|
if not chat_profile:
|
|
981
1079
|
yield """
|
|
982
1080
|
<p style='color:red;'>
|
|
983
|
-
Error
|
|
984
|
-
|
|
985
|
-
To do that, you must login first or contact your administrator.
|
|
1081
|
+
Error, Chat profile is not configured!
|
|
1082
|
+
Login to the admin panel and add the LLM profile for chatting or contact your administrator.
|
|
986
1083
|
</p>
|
|
987
1084
|
"""
|
|
988
|
-
|
|
1085
|
+
return None
|
|
989
1086
|
|
|
990
1087
|
self.chat_profile = chat_profile
|
|
991
1088
|
self.chat_profile['client'] = _prof.get_client(chat_profile)
|
|
992
|
-
_provider = self.chat_profile['provider']
|
|
1089
|
+
_provider = self.chat_profile['provider'].lower()
|
|
993
1090
|
_client = self.chat_profile['client']
|
|
994
1091
|
_model = self.chat_profile['model']
|
|
995
1092
|
_contents = f"""
|
|
@@ -1541,7 +1638,12 @@ class SyntaxMUI:
|
|
|
1541
1638
|
current_profile['client'] = _prof.get_client(current_profile)
|
|
1542
1639
|
return current_profile
|
|
1543
1640
|
|
|
1544
|
-
def run(self):
|
|
1641
|
+
def run(self, browser: Optional[str] = None) -> None:
|
|
1545
1642
|
url = f"http://{self.host}:{self.port}/"
|
|
1546
|
-
|
|
1643
|
+
|
|
1644
|
+
if browser:
|
|
1645
|
+
webbrowser.get(browser).open(url)
|
|
1646
|
+
else:
|
|
1647
|
+
webbrowser.open(url)
|
|
1648
|
+
|
|
1547
1649
|
self.app.run(host=self.host, port=self.port, debug=False)
|
|
@@ -181,9 +181,9 @@ def _impute_for_analysis(df: pd.DataFrame) -> Tuple[pd.DataFrame, Dict[str, str]
|
|
|
181
181
|
def ensure_cleaned_df(DATA_FOLDER: str, cleaned_folder: str, df: pd.DataFrame) -> pd.DataFrame:
|
|
182
182
|
"""
|
|
183
183
|
Build (or reuse) an analysis-ready cleaned dataset and persist to:
|
|
184
|
-
f"{DATA_FOLDER}/{
|
|
184
|
+
f"{DATA_FOLDER}/{selected_dataset_processed}/cleaned_df.csv"
|
|
185
185
|
Also writes a missingness audit:
|
|
186
|
-
f"{DATA_FOLDER}/{
|
|
186
|
+
f"{DATA_FOLDER}/{selected_dataset_processed}/missingness.csv"
|
|
187
187
|
Returns the cleaned frame. Does NOT mutate the provided df.
|
|
188
188
|
"""
|
|
189
189
|
target_dir = os.path.join(DATA_FOLDER, cleaned_folder)
|
syntaxmatrix/db.py
CHANGED
|
@@ -6,6 +6,8 @@ from werkzeug.utils import secure_filename
|
|
|
6
6
|
from syntaxmatrix.project_root import detect_project_root
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
_SMX_DB_PROVIDER = (os.environ.get("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
10
|
+
|
|
9
11
|
_CLIENT_DIR = detect_project_root()
|
|
10
12
|
DB_PATH = os.path.join(_CLIENT_DIR, "data", "syntaxmatrix.db")
|
|
11
13
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
@@ -624,3 +626,61 @@ def get_setting(key: str, default: str | None = None) -> str | None:
|
|
|
624
626
|
return row[0] if row else default
|
|
625
627
|
finally:
|
|
626
628
|
conn.close()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _smx_apply_optional_backend_override() -> None:
|
|
632
|
+
import os
|
|
633
|
+
import importlib
|
|
634
|
+
|
|
635
|
+
provider = (os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
636
|
+
if provider in ("", "sqlite", "sqlite3"):
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
# Pick backend module (allow override for custom installations)
|
|
640
|
+
mod_name = (os.getenv("SMX_DB_BACKEND_MODULE") or "").strip()
|
|
641
|
+
if not mod_name:
|
|
642
|
+
if provider in ("postgres", "postgresql", "pg"):
|
|
643
|
+
mod_name = "syntaxmatrix.db_backends.postgres_backend"
|
|
644
|
+
else:
|
|
645
|
+
# Convention: syntaxmatrix.db_backends.<provider>_backend
|
|
646
|
+
mod_name = f"syntaxmatrix.db_backends.{provider}_backend"
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
mod = importlib.import_module(mod_name)
|
|
650
|
+
except Exception as e:
|
|
651
|
+
raise RuntimeError(
|
|
652
|
+
f"SMX_DB_PROVIDER='{provider}' requested, but backend module '{mod_name}' "
|
|
653
|
+
f"could not be imported. Install the premium backend package (or set "
|
|
654
|
+
f"SMX_DB_BACKEND_MODULE) and try again. Underlying error: {e}"
|
|
655
|
+
) from e
|
|
656
|
+
|
|
657
|
+
installer = getattr(mod, "install", None)
|
|
658
|
+
if callable(installer):
|
|
659
|
+
installer(globals())
|
|
660
|
+
else:
|
|
661
|
+
names = getattr(mod, "__all__", None)
|
|
662
|
+
if not names:
|
|
663
|
+
names = [n for n in dir(mod) if not n.startswith("_")]
|
|
664
|
+
for n in names:
|
|
665
|
+
globals()[n] = getattr(mod, n)
|
|
666
|
+
|
|
667
|
+
# Helpful introspection
|
|
668
|
+
globals()["_SMX_DB_PROVIDER"] = provider
|
|
669
|
+
globals()["_SMX_DB_BACKEND_MODULE"] = mod_name
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
# Apply on import
|
|
673
|
+
_smx_apply_optional_backend_override()
|
|
674
|
+
|
|
675
|
+
# If a non-SQLite backend was requested, validate the required API surface now.
|
|
676
|
+
try:
|
|
677
|
+
import os as _os
|
|
678
|
+
_provider = (globals().get("_SMX_DB_PROVIDER") or _os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
679
|
+
|
|
680
|
+
if _provider not in ("", "sqlite", "sqlite3"):
|
|
681
|
+
from .db_contract import assert_backend_implements_core_api as _assert_backend_implements_core_api
|
|
682
|
+
_assert_backend_implements_core_api(globals(), provider=_provider)
|
|
683
|
+
except Exception:
|
|
684
|
+
# Fail fast with a clear error; this is intentional for premium backends.
|
|
685
|
+
raise
|
|
686
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DB backends package
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# syntaxmatrix/db_backends/postgres_backend.py
|
|
2
|
+
"""
|
|
3
|
+
PostgreSQL backend
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def install(ns: dict) -> None:
|
|
7
|
+
raise RuntimeError(
|
|
8
|
+
"Postgres backend is not available in the free tier.\n\n"
|
|
9
|
+
"You set SMX_DB_PROVIDER=postgres, but the premium Postgres backend package "
|
|
10
|
+
"is not installed.\n\n"
|
|
11
|
+
"Fix:\n"
|
|
12
|
+
"- Install the SyntaxMatrix premium Postgres backend package, then restart.\n"
|
|
13
|
+
"- Or set SMX_DB_PROVIDER=sqlite to use the built-in SQLite backend.\n"
|
|
14
|
+
)
|