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.
Files changed (45) hide show
  1. syntaxmatrix/__init__.py +6 -4
  2. syntaxmatrix/agentic/agents.py +195 -15
  3. syntaxmatrix/agentic/agents_orchestrer.py +16 -10
  4. syntaxmatrix/client_docs.py +237 -0
  5. syntaxmatrix/commentary.py +96 -25
  6. syntaxmatrix/core.py +156 -54
  7. syntaxmatrix/dataset_preprocessing.py +2 -2
  8. syntaxmatrix/db.py +60 -0
  9. syntaxmatrix/db_backends/__init__.py +1 -0
  10. syntaxmatrix/db_backends/postgres_backend.py +14 -0
  11. syntaxmatrix/db_backends/sqlite_backend.py +258 -0
  12. syntaxmatrix/db_contract.py +71 -0
  13. syntaxmatrix/kernel_manager.py +174 -150
  14. syntaxmatrix/page_builder_generation.py +654 -50
  15. syntaxmatrix/page_layout_contract.py +25 -3
  16. syntaxmatrix/page_patch_publish.py +368 -15
  17. syntaxmatrix/plugins/__init__.py +0 -0
  18. syntaxmatrix/plugins/plugin_manager.py +114 -0
  19. syntaxmatrix/premium/__init__.py +18 -0
  20. syntaxmatrix/premium/catalogue/__init__.py +121 -0
  21. syntaxmatrix/premium/gate.py +119 -0
  22. syntaxmatrix/premium/state.py +507 -0
  23. syntaxmatrix/premium/verify.py +222 -0
  24. syntaxmatrix/profiles.py +1 -1
  25. syntaxmatrix/routes.py +9782 -8004
  26. syntaxmatrix/settings/model_map.py +50 -65
  27. syntaxmatrix/settings/prompts.py +1435 -380
  28. syntaxmatrix/settings/string_navbar.py +4 -4
  29. syntaxmatrix/static/icons/bot_icon.png +0 -0
  30. syntaxmatrix/static/icons/bot_icon2.png +0 -0
  31. syntaxmatrix/templates/admin_billing.html +408 -0
  32. syntaxmatrix/templates/admin_branding.html +65 -2
  33. syntaxmatrix/templates/admin_features.html +54 -0
  34. syntaxmatrix/templates/dashboard.html +285 -8
  35. syntaxmatrix/templates/edit_page.html +199 -18
  36. syntaxmatrix/themes.py +17 -17
  37. syntaxmatrix/workspace_db.py +0 -23
  38. syntaxmatrix-3.0.0.dist-info/METADATA +219 -0
  39. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/RECORD +42 -30
  40. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/WHEEL +1 -1
  41. syntaxmatrix/settings/default.yaml +0 -13
  42. syntaxmatrix-2.6.4.3.dist-info/METADATA +0 -539
  43. syntaxmatrix-2.6.4.3.dist-info/licenses/LICENSE.txt +0 -21
  44. /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
  45. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os, io, re, json, base64
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
- You are a plots, graphs, and tables data analyst. You analyse and interprete in details and give your responses in plain english what the already-rendered plots and visuals mean as a response to the question. If the relevant information is made available, then, you must first answer the question explicitly and then proceed to explain the plots and tables.
164
- Use the information visible in the attached figures and the provided context strings (texts, tables, plot field names, labels).
165
- You should provide interpretations without prelude or preamble.
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
- question:
170
- {q}
171
-
172
- Visible context strings (tables, plots: titles, axes, legends, headers):
173
- {ctx}
174
-
175
- Write a comprehensive conclusion (~250-350 words) as follows:
176
- - <b>Headline</b>
177
- 2-3 sentence answering the question from an overview of all the output.
178
- - <b>Evidence</b>
179
- 8-10 bullets referencing the (output-texts/tables/panels/axes/legend groups) seen in the output.
180
- As you reference the visuals, you should interprete them in a way to show how they answer the question.
181
- - <b>Limitations</b>
182
- 1 bullet; avoid quoting numbers unless present in context.
183
- - <b>Recommendations</b>
184
- 1 bullet.
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
- ctx=json.dumps(visible, ensure_ascii=False, indent=2)
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
- from syntaxmatrix.settings.client_items import read_client_file, getenv_api_key
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
- host="127.0.0.1",
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="light",
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["light"])
105
- self.system_output_buffer = "" # Ephemeral buffer initialized
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 session
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["light"]
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
- def enable_theme_toggle(self):
332
- self.theme_toggle_enabled = True
333
-
334
- def enable_user_files(self):
335
- self.user_files_enabled = True
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", "0")
349
- user_files_v = db.get_setting("feature.user_files", "0")
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
- self.set_smxai_identity = profile
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
- self.set_smxai_instructions = instructions
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!<br>
893
- Chat profile is not configured. Add a chat profile inside the admin panel.
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": # Google, non openai skd series
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
- config=types.GenerateContentConfig(
920
- system_instruction=self.smxai_identity,
921
- temperature=0.3,
922
- max_output_tokens=1024,
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!<br>
984
- Chat profile is not configured. Add a chat profile inside the admin panel.
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
- return None
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
- webbrowser.open(url)
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}/{selected_dataset}/cleaned_df.csv"
184
+ f"{DATA_FOLDER}/{selected_dataset_processed}/cleaned_df.csv"
185
185
  Also writes a missingness audit:
186
- f"{DATA_FOLDER}/{selected_dataset}/missingness.csv"
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
+ )