syntaxmatrix 2.5.7__py3-none-any.whl → 2.5.8.1__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.
@@ -139,7 +139,7 @@ def mlearning_agent(user_prompt, system_prompt, coding_profile):
139
139
  reasoning_effort, verbosity = "medium", "medium"
140
140
  if _model == "gpt-5-nano":
141
141
  reasoning_effort, verbosity = "low", "low"
142
- elif _model in ["gpt-5-mini", "gpt-5-codex-mini"]:
142
+ elif _model in ["gpt-5-mini", "gpt-5-mini-codex"]:
143
143
  reasoning_effort, verbosity = "medium", "medium"
144
144
  elif _model in ["gpt-5", "gpt-5-codex", "gpt-5-pro"]:
145
145
  reasoning_effort, verbosity = "high", "high"
@@ -163,19 +163,7 @@ def mlearning_agent(user_prompt, system_prompt, coding_profile):
163
163
 
164
164
  code = _out(resp).strip()
165
165
  if code:
166
- return code
167
-
168
- # Try to surface any block reason (safety / policy / etc.)
169
- block_reason = None
170
- output = resp.get("output")
171
- for item in output:
172
- fr = getattr(item, "finish_reason", None)
173
- if fr and fr != "stop":
174
- block_reason = fr
175
- break
176
- if block_reason:
177
- raise RuntimeError(f"{_model} stopped with reason: {block_reason}")
178
- raise RuntimeError(f"{_model} returned an empty response in this section due to insufficient data.")
166
+ return code
179
167
 
180
168
  except APIError as e:
181
169
  # IMPORTANT: return VALID PYTHON so the dashboard can show the error
@@ -263,7 +251,6 @@ def mlearning_agent(user_prompt, system_prompt, coding_profile):
263
251
  {"role": "system", "content": system_prompt},
264
252
  {"role": "user", "content": user_prompt},
265
253
  ],
266
- extra_body={"thinking": {"type": "enabled"}},
267
254
  temperature=0,
268
255
  stream=False
269
256
  )
@@ -301,6 +288,7 @@ def mlearning_agent(user_prompt, system_prompt, coding_profile):
301
288
  else:
302
289
  code = openai_sdk_generate_code()
303
290
 
291
+ code = str(code or "")
304
292
  return code, usage
305
293
 
306
294
 
@@ -397,9 +385,11 @@ def refine_question_agent(raw_question: str, dataset_context: str | None = None)
397
385
 
398
386
  system_prompt = ("""
399
387
  - You are a Machine Learning (ML) and Data Science (DS) expert.
400
- - You rewrite user questions into clear ML job specifications to help AI assistant generate Python code that provides solution to the user question when it is run. Most user questions are vague. So, your goal is to ensure that your output guards the assistant agains making potential errors that you anticipated could arise due to the nature of the question.
401
- - If a dataset summary is provided, use it to respect column and help you rewrite the question properly.
402
- - DO NOT write andy prelude or preamble"
388
+ - Your goal is to use the provided dataset summary to convert given question into clear ML job specifications.
389
+ - Use the provided dataset summary to respect columns and aid you in properly refining the user question.
390
+ - Include chronological outline in order to guide a code generator to avoid falling off tracks.
391
+ - DO NOT include any prelude or preamble. Just the refined tasks.
392
+ - If and only if the dataset summary columns are not relevant to your desired columns that you deduced by analysing the question, and you suspect that the wrong dataset was used in the dataset summary, stop and just say: 'incompatible'.
403
393
  """)
404
394
 
405
395
  user_prompt = f"User question:\n{raw_question}\n\n"
@@ -532,9 +522,7 @@ def classify_ml_job_agent(refined_question, dataset_profile):
532
522
  system_prompt = ("""
533
523
  You are a strict machine learning task classifier for an ML workbench.
534
524
  Your goal is to correctly label the user's task specifications with the most relevant tags from a fixed list.
535
- You Must always have 'data_preprocessing' as the 1st tag. Then add up to 4 more, as needed, to make 5 max.
536
- Your list should be 2-5 tags long. If no relevant tag, default to ["data_preprocessing"]
537
- If tasks specs and `df` don't match (of different industries, return ['context mismatch']
525
+ You Must always have 'data_preprocessing' as the 1st tag. Then add all other relevant tags.
538
526
  You should return only your list of tags, no prelude or preamble.
539
527
  """)
540
528
 
@@ -554,8 +542,7 @@ def classify_ml_job_agent(refined_question, dataset_profile):
554
542
  "generative_modeling", "causal_inference", "risk_modeling", "graph_analysis",
555
543
 
556
544
  # Foundational/Pipeline Steps
557
- "data_preprocessing", "feature_engineering", "statistical_inference",
558
- "model_validation", "hyperparameter_tuning"
545
+ "data_preprocessing", "feature_engineering", "statistical_inference", "clustering", "hyperparameter_tuning"
559
546
  ]
560
547
 
561
548
  # --- 2. Construct the Generalized Prompt for the LLM ---
@@ -575,7 +562,7 @@ def classify_ml_job_agent(refined_question, dataset_profile):
575
562
  ML Jobs List: {', '.join(ml_task_list)}
576
563
 
577
564
  Respond ONLY with a valid JSON array of strings containing the selected ML job names.
578
- Example Response: ["natural_language_processing", "classification", "feature_engineering"]
565
+ Example Response: ["data_preprocessing", "regression", "classification", "feature_engineering"]
579
566
  """
580
567
 
581
568
  if dataset_profile:
@@ -583,7 +570,12 @@ def classify_ml_job_agent(refined_question, dataset_profile):
583
570
 
584
571
  llm_profile = _prof.get_profile("classification") or _prof.get_profile("admin")
585
572
  if not llm_profile:
586
- return "ERROR"
573
+ return (
574
+ "<div class='smx-alert smx-alert-warn'>"
575
+ "No LLM profile is configured for Classification. Please, do that in the Admin panel or contact your Administrator."
576
+ "</div>"
577
+ )
578
+
587
579
 
588
580
  llm_profile['client'] = _prof.get_client(llm_profile)
589
581
 
syntaxmatrix/core.py CHANGED
@@ -54,7 +54,7 @@ class SyntaxMUI:
54
54
  port="5080",
55
55
  user_icon="👩🏿‍🦲",
56
56
  bot_icon="<img src='/static/icons/favicon.png' width=20' alt='bot'/>",
57
- favicon="", # /static/icons/favicon.png",
57
+ favicon="/static/icons/favicon.png",
58
58
  site_logo="<img src='/static/icons/logo.png' width='30' alt='logo'/>",
59
59
  site_title="SyntaxMatrix",
60
60
  project_name="smxAI",
syntaxmatrix/routes.py CHANGED
@@ -65,6 +65,7 @@ _CLIENT_DIR = detect_project_root()
65
65
  _stream_q = queue.Queue()
66
66
  _stream_cancelled = {}
67
67
  _last_result_html = {} # { session_id: html_doc }
68
+ _last_resized_csv = {} # { resize_id: bytes for last resized CSV per browser session }
68
69
 
69
70
  # single, reused formatter: inline styles, padding, rounded corners, scroll
70
71
  _FMT = _HtmlFmt(
@@ -5610,8 +5611,19 @@ def setup_routes(smx):
5610
5611
  dataset_profile = f"modality: tabular; columns: {columns_summary}"
5611
5612
 
5612
5613
  refined_question = refine_question_agent(askai_question, dataset_context)
5613
- tags = classify_ml_job_agent(refined_question, dataset_profile)
5614
-
5614
+ tags = []
5615
+ if refined_question.lower() == "incompatible" or refined_question.lower() == "mismatch":
5616
+ return ("""
5617
+ <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
5618
+ <h1 style="margin: 0 0 10px 0;">Oops: Context mismatch</h1>
5619
+ <p style="margin: 0;">Please, upload the proper dataset for solution to your query.</p>
5620
+ <br>
5621
+ <a class='button' href='/dashboard' style='text-decoration:none;'>Return</a>
5622
+ </div>
5623
+ """)
5624
+ else:
5625
+ tags = classify_ml_job_agent(refined_question, dataset_profile)
5626
+
5615
5627
  ai_code = smx.ai_generate_code(refined_question, tags, df)
5616
5628
  llm_usage = smx.get_last_llm_usage()
5617
5629
  ai_code = auto_inject_template(ai_code, tags, df)
@@ -6513,6 +6525,7 @@ def setup_routes(smx):
6513
6525
  cell["highlighted_code"] = Markup(_pygmentize(cell["code"]))
6514
6526
 
6515
6527
  highlighted_ai_code = _pygmentize(ai_code)
6528
+ smxAI = "smxAI"
6516
6529
 
6517
6530
  return render_template(
6518
6531
  "dashboard.html",
@@ -6525,6 +6538,7 @@ def setup_routes(smx):
6525
6538
  askai_question=smx.sanitize_rough_to_markdown_task(askai_question),
6526
6539
  refined_question=refined_question,
6527
6540
  tasks=tags,
6541
+ smxAI=smxAI,
6528
6542
  data_cells=data_cells,
6529
6543
  session_id=session_id,
6530
6544
  llm_usage=llm_usage
@@ -6588,6 +6602,179 @@ def setup_routes(smx):
6588
6602
  # go back to the dashboard; dashboard() will auto-select the next file
6589
6603
  return redirect(url_for("dashboard"))
6590
6604
 
6605
+ # ── DATASET RESIZE (independent helper page) -------------------------
6606
+
6607
+
6608
+ @smx.app.route("/dataset/resize", methods=["GET", "POST"])
6609
+ def dataset_resize():
6610
+ """
6611
+ User uploads any CSV and picks a target size (percentage of rows).
6612
+ We keep the last resized CSV in memory and expose a download link.
6613
+ """
6614
+ # One id per browser session to index _last_resized_csv
6615
+ resize_id = session.get("dataset_resize_id")
6616
+ if not resize_id:
6617
+ resize_id = str(uuid.uuid4())
6618
+ session["dataset_resize_id"] = resize_id
6619
+
6620
+ resize_info = None # stats we pass down to the template
6621
+
6622
+ if request.method == "POST":
6623
+ file = request.files.get("dataset_file")
6624
+ target_pct_raw = (request.form.get("target_pct") or "").strip()
6625
+ strat_col = (request.form.get("strat_col") or "").strip()
6626
+
6627
+ error_msg = None
6628
+ df = None
6629
+
6630
+ # --- Basic validation ---
6631
+ if not file or file.filename == "":
6632
+ error_msg = "Please choose a CSV file."
6633
+ elif not file.filename.lower().endswith(".csv"):
6634
+ error_msg = "Only CSV files are supported."
6635
+
6636
+ # --- Read CSV into a DataFrame ---
6637
+ if not error_msg:
6638
+ try:
6639
+ df = pd.read_csv(file)
6640
+ except Exception as e:
6641
+ error_msg = f"Could not read CSV: {e}"
6642
+
6643
+ # --- Parse target percentage ---
6644
+ pct = None
6645
+ if not error_msg:
6646
+ try:
6647
+ pct = float(target_pct_raw)
6648
+ except Exception:
6649
+ error_msg = "Target size must be a number between 1 and 100."
6650
+
6651
+ if not error_msg and (pct <= 0 or pct > 100):
6652
+ error_msg = "Target size must be between 1 and 100."
6653
+
6654
+ if error_msg:
6655
+ flash(error_msg, "error")
6656
+ else:
6657
+ frac = pct / 100.0
6658
+ n_orig = len(df)
6659
+ n_target = max(1, int(round(n_orig * frac)))
6660
+
6661
+ df_resized = None
6662
+ used_strat = False
6663
+
6664
+ # --- Advanced: stratified sampling by a column (behind 'Show advanced options') ---
6665
+ if strat_col and strat_col in df.columns and n_orig > 0:
6666
+ used_strat = True
6667
+ groups = df.groupby(strat_col, sort=False)
6668
+
6669
+ # First pass: proportional allocation with rounding and minimum 1 per non-empty group
6670
+ allocations = {}
6671
+ total_alloc = 0
6672
+ for key, group in groups:
6673
+ size = len(group)
6674
+ if size <= 0:
6675
+ allocations[key] = 0
6676
+ continue
6677
+ alloc = int(round(size * frac))
6678
+ if alloc == 0 and size > 0:
6679
+ alloc = 1
6680
+ if alloc > size:
6681
+ alloc = size
6682
+ allocations[key] = alloc
6683
+ total_alloc += alloc
6684
+
6685
+ keys = list(allocations.keys())
6686
+
6687
+ # Adjust downwards if we overshot
6688
+ if total_alloc > n_target:
6689
+ idx = 0
6690
+ while total_alloc > n_target and any(v > 1 for v in allocations.values()):
6691
+ k = keys[idx % len(keys)]
6692
+ if allocations[k] > 1:
6693
+ allocations[k] -= 1
6694
+ total_alloc -= 1
6695
+ idx += 1
6696
+
6697
+ # Adjust upwards if we undershot and we still have room in groups
6698
+ if total_alloc < n_target and keys:
6699
+ idx = 0
6700
+ while total_alloc < n_target:
6701
+ k = keys[idx % len(keys)]
6702
+ group_size = len(groups.get_group(k))
6703
+ if allocations[k] < group_size:
6704
+ allocations[k] += 1
6705
+ total_alloc += 1
6706
+ idx += 1
6707
+ if idx > len(keys) * 3:
6708
+ break
6709
+
6710
+ sampled_parts = []
6711
+ for key, group in groups:
6712
+ n_g = allocations.get(key, 0)
6713
+ if n_g > 0:
6714
+ sampled_parts.append(group.sample(n=n_g, random_state=0))
6715
+
6716
+ if sampled_parts:
6717
+ df_resized = (
6718
+ pd.concat(sampled_parts, axis=0)
6719
+ .sample(frac=1.0, random_state=0)
6720
+ .reset_index(drop=True)
6721
+ )
6722
+
6723
+ # --- Default: simple random sample over all rows ---
6724
+ if df_resized is None:
6725
+ if n_target >= n_orig:
6726
+ df_resized = df.copy()
6727
+ else:
6728
+ df_resized = df.sample(n=n_target, random_state=0).reset_index(drop=True)
6729
+ if strat_col and strat_col not in df.columns:
6730
+ flash(
6731
+ f"Column '{strat_col}' not found. Used simple random sampling instead.",
6732
+ "warning",
6733
+ )
6734
+
6735
+ # --- Serialise to CSV in memory and stash in _last_resized_csv ---
6736
+ buf = _std_io.BytesIO()
6737
+ df_resized.to_csv(buf, index=False)
6738
+ buf.seek(0)
6739
+ _last_resized_csv[resize_id] = buf.getvalue()
6740
+
6741
+ resize_info = {
6742
+ "rows_in": n_orig,
6743
+ "rows_out": len(df_resized),
6744
+ "pct": pct,
6745
+ "used_strat": used_strat,
6746
+ "strat_col": strat_col if used_strat else "",
6747
+ }
6748
+ flash("Dataset resized successfully. Use the download link below.", "success")
6749
+
6750
+ return render_template("dataset_resize.html", resize_info=resize_info)
6751
+
6752
+ @smx.app.route("/dataset/resize/download", methods=["GET"])
6753
+ def download_resized_dataset():
6754
+ """Download the last resized dataset for this browser session as a CSV."""
6755
+ resize_id = session.get("dataset_resize_id")
6756
+ if not resize_id:
6757
+ return ("No resized dataset available.", 404)
6758
+
6759
+ data = _last_resized_csv.get(resize_id)
6760
+ if not data:
6761
+ return ("No resized dataset available.", 404)
6762
+
6763
+ buf = _std_io.BytesIO(data)
6764
+ buf.seek(0)
6765
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
6766
+ filename = f"resized_dataset_{stamp}.csv"
6767
+
6768
+ # Drop it from memory once downloaded
6769
+ _last_resized_csv.pop(resize_id, None)
6770
+
6771
+ return send_file(
6772
+ buf,
6773
+ mimetype="text/csv; charset=utf-8",
6774
+ as_attachment=True,
6775
+ download_name=filename,
6776
+ )
6777
+
6591
6778
 
6592
6779
  def _pdf_fallback_reportlab(full_html: str):
6593
6780
  """ReportLab fallback: extract text + base64 <img> and lay them out."""
@@ -43,7 +43,7 @@ PROVIDERS_MODELS = {
43
43
  #5
44
44
  "moonshot": [
45
45
  "kimi-k2-thinking",
46
- "kimi-k2",
46
+ "kimi-k2-instruct",
47
47
  ],
48
48
  #6
49
49
  "alibaba": [
@@ -59,7 +59,6 @@ PROVIDERS_MODELS = {
59
59
  "claude-sonnet-4-5",
60
60
  "claude-sonnet-4-0",
61
61
  "claude-3-5-haiku-latest",
62
- "claude-3-haiku-20240307",
63
62
  ]
64
63
  }
65
64
 
@@ -160,31 +160,18 @@
160
160
  padding:0.2rem;
161
161
  }
162
162
  .del-btn:hover { opacity:0.8; background:red; }
163
-
164
- /* full-screen overlay */
165
- #loader-overlay {
166
- position: fixed;
167
- top: 0; left: 0;
168
- width: 100%; height: 100%;
169
- background: rgba(241, 235, 235, 0);
170
- display: none;
163
+
164
+ /* Make the Explore Data submit button compact instead of full-width */
165
+ .eda-submit-btn {
166
+ align-self: flex-start; /* stop flex from stretching it to 100% */
167
+ width: auto; /* shrink to content */
168
+ min-width: 7.5rem; /* tweak this if you want it smaller/larger */
169
+ padding: 6px 16px; /* a bit tighter than the global button */
170
+ display: inline-flex;
171
171
  align-items: center;
172
172
  justify-content: center;
173
- z-index: 9999;
174
- }
175
- /* simple spinner */
176
- .loader {
177
- border: 8px solid #eee;
178
- border-top: 8px solid #333;
179
- border-radius: 50%;
180
- width: 60px; height: 60px;
181
- animation: spin 1s linear infinite;
182
173
  }
183
- @keyframes spin {
184
- 0% { transform: rotate(0deg); }
185
- 100% { transform: rotate(360deg); }
186
- }
187
-
174
+
188
175
  /* --- Mobile fixes --- */
189
176
  .dashboard-content img,
190
177
  .dashboard-content canvas,
@@ -618,24 +605,92 @@
618
605
  details > summary::-webkit-details-marker {
619
606
  display: none;
620
607
  }
608
+
609
+ /* Spinner that sits inside the Explore Data submit button */
610
+ .eda-btn-spinner {
611
+ display: none;
612
+ width: 1.1rem;
613
+ height: 1.1rem;
614
+ border-radius: 999px;
615
+ border: 2px solid currentColor;
616
+ border-top-color: transparent;
617
+ border-right-color: transparent;
618
+ animation: edaBtnSpin 0.7s linear infinite;
619
+ margin-left: 0; /* was 0.5rem */
620
+ vertical-align: middle;
621
+ box-sizing: border-box;
622
+ }
623
+ .eda-btn-label {
624
+ display: inline-block;
625
+ margin-right: 0.5rem; /* space between text and spinner when both are visible */
626
+ }
627
+
628
+ /* While loading: hide the label, show the spinner */
629
+ .eda-btn-loading .eda-btn-label {
630
+ display: none; /* keeps button width stable */
631
+ }
632
+
633
+ .eda-btn-loading .eda-btn-spinner {
634
+ display: inline-block;
635
+ }
636
+
637
+ @keyframes edaBtnSpin {
638
+ to {
639
+ transform: rotate(360deg);
640
+ }
641
+ }
642
+ .sidebar-links {
643
+ margin-top: 22px;
644
+ }
645
+ .sidebar-links a {
646
+ display: block;
647
+ padding: 12px 10px 12px 0;
648
+ color: #333;
649
+ text-decoration: none;
650
+ font-size: clamp(0.93rem, 2vw, 1.08rem);
651
+ border-radius: 6px;
652
+ margin-bottom: 6px;
653
+ transition: background 0.2s, color 0.2s;
654
+ }
655
+ .sidebar-links a.active,
656
+ .sidebar-links a:hover {
657
+ background: #e0e5ee;
658
+ color: #007acc;
659
+ font-weight: bold;
660
+ }
661
+ /* Sub-links under a main section (e.g. Explore → Resize dataset) */
662
+ .sidebar-links a.sidebar-sub-link {
663
+ font-size: clamp(0.80rem, 1.4vw, 0.95rem); /* smaller than main items */
664
+ padding: 6px 10px 6px 14px; /* slight indent to show hierarchy */
665
+ margin-bottom: 4px;
666
+ opacity: 0.95;
667
+ }
668
+
669
+ /* Optional: visual cue arrow for sub-items */
670
+ .sidebar-links a.sidebar-sub-link::before {
671
+ content: "↳ ";
672
+ font-size: 0.8em;
673
+ opacity: 0.7;
674
+ }
621
675
  </style>
622
676
 
623
677
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
624
678
  </head>
625
679
  <body>
626
680
  <div id="sidebarScrim" class="sidebar-scrim" aria-hidden="true"></div>
627
- <div id="loader-overlay">
628
- <div class="loader"></div>
629
- </div>
630
681
  <div class="dashboard-sidebar">
631
682
  <button class="sidebar-close" aria-label="Close menu">✕</button>
632
683
  <h2>ML&nbsp;Lab</h2>
633
684
  <a href="/">return to home</a>
634
685
  <div class="sidebar-links">
635
686
  <a href="/dashboard?section=explore"{% if section == 'explore' %} class="active"{% endif %}>Explore</a>
636
-
687
+
688
+ <!-- Explore subsets -->
689
+ <a href="{{ url_for('dataset_resize') }}" class="sidebar-sub-link">Resize dataset</a>
690
+
637
691
  <!-- Future: more links here -->
638
692
  </div>
693
+
639
694
  </div>
640
695
  <div class="dashboard-main">
641
696
  <button id="sidebarToggle" class="sidebar-toggle" aria-label="Open menu"></button>
@@ -691,11 +746,18 @@
691
746
  <h2>Explore Data</h2>
692
747
  <form id="form-askai" method="post" action="/dashboard?section=explore" style="margin-top:5px;padding:12px; border:1px solid grey;border-radius:5px;width:70vw;">
693
748
  <input type="hidden" name="dataset" value="{{ selected_dataset }}">
694
- <label for="askai"><strong>Ask smxAI:</strong></label>
695
- <textarea id="askai" name="askai_question" type="text" rows="4"
696
- style="position:relative; width:90%; padding:8px; font-size:0.8em; border-radius:8px;"
697
- placeholder="Ask me about {{ (selected_dataset or 'your dataset. Upload it first.').replace('_', ' ').replace('.csv', '') }}" required></textarea>
698
- <button type="submit" style="font-size:1.2rem; width:8rem; padding:4px;">Submit</button>
749
+ <label for="askai"><strong>Ask {{ smxAI }}:</strong></label>
750
+ <textarea id="askai" name="askai_question" type="text" rows="5"
751
+ style="position:relative; width:90%; padding:16px; font-size:0.8em; border-radius:8px;"
752
+ placeholder="Ask me about {{ (selected_dataset or 'your dataset\n. But upload it first.').replace('_', ' ').replace('.csv', '') }}" required></textarea>
753
+ <!-- <button type="submit" style="font-size:1.2rem; width:8rem; padding:4px;">Submit</button> -->
754
+ <button
755
+ type="submit"
756
+ class="btn btn-primary eda-submit-btn"
757
+ >
758
+ <span class="eda-btn-label">Submit</span>
759
+ <span class="eda-btn-spinner" aria-hidden="true"></span>
760
+ </button>
699
761
  </form>
700
762
  <div style="margin-bottom: 36px;">
701
763
  {% if askai_question %}
@@ -735,6 +797,7 @@
735
797
  {% endif %}
736
798
  {% if ai_outputs %}
737
799
  <div class="d-flex align-items-center justify-content-between" style="margin: 12px;">
800
+ <br>
738
801
  <h3 class="m-0">Result</h3>
739
802
  {% for html_block in ai_outputs %}
740
803
  <div class="ai-output" style="margin-bottom:18px;overflow-x:auto; max-width:100%;">
@@ -787,31 +850,31 @@
787
850
  </div>
788
851
  </div>
789
852
  <script>
790
- async function copyCode(btn){
791
- const pre = btn.parentElement.querySelector('pre');
792
- if(!pre) return;
793
- const text = pre.innerText;
794
-
795
- try {
796
- if (navigator.clipboard && navigator.clipboard.writeText) {
797
- await navigator.clipboard.writeText(text);
798
- } else {
799
- // fallback
800
- const range = document.createRange();
801
- range.selectNodeContents(pre);
802
- const sel = window.getSelection();
803
- sel.removeAllRanges(); sel.addRange(range);
804
- document.execCommand('copy');
805
- sel.removeAllRanges();
806
- }
807
- btn.textContent = 'Copied!';
808
- setTimeout(()=>btn.textContent='Copy', 1200);
809
- } catch(e){
810
- btn.textContent = 'Failed';
811
- setTimeout(()=>btn.textContent='Copy', 1200);
853
+ async function copyCode(btn){
854
+ const pre = btn.parentElement.querySelector('pre');
855
+ if(!pre) return;
856
+ const text = pre.innerText;
857
+
858
+ try {
859
+ if (navigator.clipboard && navigator.clipboard.writeText) {
860
+ await navigator.clipboard.writeText(text);
861
+ } else {
862
+ // fallback
863
+ const range = document.createRange();
864
+ range.selectNodeContents(pre);
865
+ const sel = window.getSelection();
866
+ sel.removeAllRanges(); sel.addRange(range);
867
+ document.execCommand('copy');
868
+ sel.removeAllRanges();
869
+ }
870
+ btn.textContent = 'Copied!';
871
+ setTimeout(()=>btn.textContent='Copy', 1200);
872
+ } catch(e){
873
+ btn.textContent = 'Failed';
874
+ setTimeout(()=>btn.textContent='Copy', 1200);
875
+ }
812
876
  }
813
- }
814
- </script>
877
+ </script>
815
878
  <script>
816
879
  function toggleCodeCell(link) {
817
880
  var cell = link.nextElementSibling;
@@ -824,6 +887,7 @@
824
887
  link.querySelector("span").innerText = "Show Code";
825
888
  }
826
889
  }
890
+
827
891
  function copyCodeToClipboard(btn) {
828
892
  var pre = btn.parentElement.querySelector("pre");
829
893
  if (!pre) return;
@@ -842,13 +906,36 @@
842
906
  }
843
907
  sel.removeAllRanges();
844
908
  }
845
-
846
- document.addEventListener("DOMContentLoaded", () => {
847
- const form = document.getElementById("form-askai");
848
- const overlay = document.getElementById("loader-overlay");
849
- form.addEventListener("submit", () => {
850
- overlay.style.display = "flex";
909
+
910
+ document.addEventListener("DOMContentLoaded", function () {
911
+ var form = document.getElementById("form-askai");
912
+ if (!form) return;
913
+
914
+ var submitBtn = form.querySelector(".eda-submit-btn");
915
+ if (!submitBtn) return;
916
+
917
+ // When the form actually submits, show the spinner and disable the button
918
+ form.addEventListener("submit", function () {
919
+ submitBtn.classList.add("eda-btn-loading");
920
+ submitBtn.disabled = true;
921
+ // IMPORTANT: no preventDefault here – the browser/htmx still submits normally
851
922
  });
923
+
924
+ // If htmx is used on this form, reset the spinner once the request completes or errors
925
+ if (window.htmx) {
926
+ form.addEventListener("htmx:afterRequest", function () {
927
+ submitBtn.classList.remove("eda-btn-loading");
928
+ submitBtn.disabled = false;
929
+ });
930
+ form.addEventListener("htmx:responseError", function () {
931
+ submitBtn.classList.remove("eda-btn-loading");
932
+ submitBtn.disabled = false;
933
+ });
934
+ form.addEventListener("htmx:sendError", function () {
935
+ submitBtn.classList.remove("eda-btn-loading");
936
+ submitBtn.disabled = false;
937
+ });
938
+ }
852
939
  });
853
940
  </script>
854
941
  <script>
@@ -0,0 +1,535 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Dataset Resizer · SyntaxMatrix ML Lab</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+
8
+ <style>
9
+ :root {
10
+ --smx-bg: #0f172a;
11
+ --smx-bg-soft: #020617;
12
+ --smx-card: #020617;
13
+ --smx-card-soft: #020617;
14
+ --smx-border: #1f2937;
15
+ --smx-border-soft: #111827;
16
+ --smx-accent: #38bdf8;
17
+ --smx-accent-soft: rgba(56, 189, 248, 0.16);
18
+ --smx-accent-strong: #0ea5e9;
19
+ --smx-text-main: #e5e7eb;
20
+ --smx-text-soft: #9ca3af;
21
+ --smx-danger: #f97373;
22
+ --smx-success: #22c55e;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ margin: 0;
31
+ padding: 0;
32
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
33
+ background: radial-gradient(circle at 0 0, #1e293b 0, #020617 55%, #000000 100%);
34
+ color: var(--smx-text-main);
35
+ min-height: 100vh;
36
+ }
37
+
38
+ .smx-shell {
39
+ max-width: 1120px;
40
+ margin: 0 auto;
41
+ padding: 24px 16px 32px;
42
+ }
43
+
44
+ .smx-topbar {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 12px;
49
+ margin-bottom: 20px;
50
+ }
51
+
52
+ .smx-topbar-left {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 10px;
56
+ min-width: 0;
57
+ }
58
+
59
+ .smx-logo-pill {
60
+ width: 32px;
61
+ height: 32px;
62
+ border-radius: 999px;
63
+ background: radial-gradient(circle at 30% 20%, #38bdf8 0, #0ea5e9 35%, #22c55e 80%);
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ font-size: 0.85rem;
68
+ font-weight: 700;
69
+ colour: #0b1120;
70
+ }
71
+
72
+ .smx-topbar-title {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 2px;
76
+ }
77
+
78
+ .smx-topbar-title-main {
79
+ font-size: 1.1rem;
80
+ font-weight: 600;
81
+ letter-spacing: 0.02em;
82
+ }
83
+
84
+ .smx-topbar-title-sub {
85
+ font-size: 0.8rem;
86
+ colour: var(--smx-text-soft);
87
+ }
88
+
89
+ .smx-back-link {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: 6px;
93
+ padding: 6px 12px;
94
+ border-radius: 999px;
95
+ border: 1px solid rgba(148, 163, 184, 0.5);
96
+ text-decoration: none;
97
+ font-size: 0.8rem;
98
+ colour: var(--smx-text-main);
99
+ background: rgba(15, 23, 42, 0.7);
100
+ backdrop-filter: blur(10px);
101
+ }
102
+
103
+ .smx-back-link:hover {
104
+ border-colour: var(--smx-accent);
105
+ colour: var(--smx-accent-strong);
106
+ }
107
+
108
+ .smx-layout {
109
+ display: grid;
110
+ grid-template-columns: minmax(0, 1.7fr) minmax(0, 1.1fr);
111
+ gap: 18px;
112
+ align-items: flex-start;
113
+ }
114
+
115
+ .smx-card {
116
+ background: radial-gradient(circle at 0 0, rgba(56, 189, 248, 0.08), rgba(15, 23, 42, 0.98));
117
+ border-radius: 16px;
118
+ border: 1px solid rgba(15, 23, 42, 0.9);
119
+ padding: 18px 18px 18px;
120
+ box-shadow:
121
+ 0 18px 40px rgba(15, 23, 42, 0.80),
122
+ 0 0 0 0.5px rgba(148, 163, 184, 0.35);
123
+ }
124
+
125
+ .smx-card-secondary {
126
+ background: radial-gradient(circle at 100% 0, rgba(56, 189, 248, 0.15), rgba(15, 23, 42, 0.98));
127
+ border-radius: 16px;
128
+ border: 1px solid rgba(30, 64, 175, 0.7);
129
+ padding: 18px 18px;
130
+ box-shadow:
131
+ 0 16px 32px rgba(15, 23, 42, 0.85),
132
+ 0 0 0 0.5px rgba(30, 64, 175, 0.55);
133
+ }
134
+
135
+ .smx-card-header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ gap: 10px;
140
+ margin-bottom: 12px;
141
+ }
142
+
143
+ .smx-card-title {
144
+ font-size: 1rem;
145
+ font-weight: 600;
146
+ display: inline-flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+
151
+ .smx-pill {
152
+ padding: 2px 8px;
153
+ border-radius: 999px;
154
+ background: rgba(56, 189, 248, 0.12);
155
+ border: 1px solid rgba(56, 189, 248, 0.5);
156
+ font-size: 0.7rem;
157
+ font-weight: 500;
158
+ letter-spacing: 0.04em;
159
+ text-transform: uppercase;
160
+ colour: var(--smx-accent-strong);
161
+ }
162
+
163
+ .smx-card-body {
164
+ font-size: 0.85rem;
165
+ colour: var(--smx-text-soft);
166
+ }
167
+
168
+ .smx-description {
169
+ margin: 0 0 10px;
170
+ line-height: 1.5;
171
+ }
172
+
173
+ label {
174
+ display: block;
175
+ font-weight: 500;
176
+ font-size: 0.82rem;
177
+ margin-bottom: 4px;
178
+ }
179
+
180
+ .smx-field {
181
+ margin-bottom: 10px;
182
+ }
183
+
184
+ input[type="file"],
185
+ input[type="number"],
186
+ input[type="text"] {
187
+ width: 100%;
188
+ padding: 7px 10px;
189
+ border-radius: 10px;
190
+ border: 1px solid rgba(148, 163, 184, 0.55);
191
+ background: rgba(15, 23, 42, 0.9);
192
+ colour: var(--smx-text-main);
193
+ font-size: 0.85rem;
194
+ color: #fff5;
195
+ }
196
+
197
+ input[type="file"] {
198
+ padding: 6px 0;
199
+ }
200
+
201
+ input[type="number"]:focus,
202
+ input[type="text"]:focus {
203
+ outline: none;
204
+ border-colour: var(--smx-accent);
205
+ box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.45);
206
+ }
207
+
208
+ .smx-hint {
209
+ font-size: 0.75rem;
210
+ colour: var(--smx-text-soft);
211
+ margin: 0 0 6px;
212
+ }
213
+
214
+ .smx-actions {
215
+ margin-top: 10px;
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: flex-start;
219
+ gap: 10px;
220
+ }
221
+
222
+ .btn-primary {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ padding: 7px 16px;
227
+ border-radius: 999px;
228
+ border: none;
229
+ background: linear-gradient(135deg, #0ea5e9, #22c55e);
230
+ colour: #0b1120;
231
+ font-size: 0.85rem;
232
+ font-weight: 600;
233
+ cursor: pointer;
234
+ box-shadow: 0 10px 22px rgba(8, 47, 73, 0.7);
235
+ }
236
+
237
+ .btn-primary:hover {
238
+ filter: brightness(1.05);
239
+ box-shadow: 0 14px 28px rgba(8, 47, 73, 0.85);
240
+ }
241
+
242
+ .smx-adv-toggle {
243
+ font-size: 0.8rem;
244
+ colour: var(--smx-accent);
245
+ cursor: pointer;
246
+ text-decoration: none;
247
+ display: inline-flex;
248
+ align-items: centre;
249
+ gap: 4px;
250
+ }
251
+
252
+ .smx-adv-toggle:hover {
253
+ text-decoration: underline;
254
+ }
255
+
256
+ #advanced-box {
257
+ display: none;
258
+ margin-top: 8px;
259
+ padding: 10px 10px 8px;
260
+ border-radius: 10px;
261
+ border: 1px dashed rgba(148, 163, 184, 0.6);
262
+ background: rgba(15, 23, 42, 0.8);
263
+ }
264
+
265
+ .flashes {
266
+ list-style: none;
267
+ padding-left: 0;
268
+ margin: 0 0 10px;
269
+ font-size: 0.78rem;
270
+ }
271
+
272
+ .flashes li {
273
+ margin-bottom: 4px;
274
+ padding: 6px 8px;
275
+ border-radius: 8px;
276
+ border: 1px solid rgba(250, 204, 21, 0.8);
277
+ background: rgba(251, 191, 36, 0.06);
278
+ colour: #facc15;
279
+ }
280
+
281
+ .flashes li.error {
282
+ border-colour: rgba(248, 113, 113, 0.9);
283
+ background: rgba(248, 113, 113, 0.08);
284
+ colour: #fecaca;
285
+ }
286
+
287
+ .flashes li.success {
288
+ border-colour: rgba(34, 197, 94, 0.9);
289
+ background: rgba(22, 163, 74, 0.08);
290
+ colour: #bbf7d0;
291
+ }
292
+
293
+ .flashes li.warning {
294
+ border-colour: rgba(234, 179, 8, 0.9);
295
+ background: rgba(234, 179, 8, 0.08);
296
+ colour: #fef3c7;
297
+ }
298
+
299
+ .meta-list {
300
+ list-style: none;
301
+ padding-left: 0;
302
+ margin: 0 0 10px;
303
+ font-size: 0.8rem;
304
+ colour: var(--smx-text-soft);
305
+ }
306
+
307
+ .meta-list li {
308
+ margin-bottom: 3px;
309
+ }
310
+
311
+ .download-link {
312
+ display: inline-flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ padding: 7px 16px;
316
+ border-radius: 999px;
317
+ border: 1px solid rgba(34, 197, 94, 0.9);
318
+ colour: #bbf7d0;
319
+ font-weight: 600;
320
+ font-size: 0.85rem;
321
+ text-decoration: none;
322
+ background: rgba(22, 163, 74, 0.23);
323
+ box-shadow: 0 10px 22px rgba(4, 52, 35, 0.8);
324
+ }
325
+
326
+ .download-link:hover {
327
+ background: rgba(21, 128, 61, 0.9);
328
+ colour: #ecfdf5;
329
+ }
330
+
331
+ .smx-side-note-title {
332
+ font-size: 0.9rem;
333
+ font-weight: 600;
334
+ margin-bottom: 6px;
335
+ }
336
+
337
+ .smx-side-note-text {
338
+ font-size: 0.8rem;
339
+ colour: var(--smx-text-soft);
340
+ margin: 0 0 10px;
341
+ line-height: 1.5;
342
+ }
343
+
344
+ .smx-badge-dot {
345
+ display: inline-block;
346
+ width: 7px;
347
+ height: 7px;
348
+ border-radius: 999px;
349
+ background: var(--smx-accent);
350
+ margin-right: 6px;
351
+ }
352
+
353
+ @media (max-width: 880px) {
354
+ .smx-layout {
355
+ grid-template-columns: minmax(0, 1fr);
356
+ }
357
+ }
358
+
359
+ @media (max-width: 640px) {
360
+ .smx-shell {
361
+ padding-top: 16px;
362
+ }
363
+ .smx-card,
364
+ .smx-card-secondary {
365
+ border-radius: 12px;
366
+ }
367
+ }
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <div class="smx-shell">
372
+ <div class="smx-topbar">
373
+ <div class="smx-topbar-left">
374
+ <div class="smx-logo-pill">SM</div>
375
+ <div class="smx-topbar-title">
376
+ <div class="smx-topbar-title-main">Dataset Resizer</div>
377
+ <div class="smx-topbar-title-sub">Subset of Explore · ML Lab</div>
378
+ </div>
379
+ </div>
380
+ <a href="{{ url_for('dashboard', section='explore') }}" class="smx-back-link">
381
+ <span aria-hidden="true">←</span>
382
+ <span>Back to ML Lab</span>
383
+ </a>
384
+ </div>
385
+
386
+ <div class="smx-layout">
387
+ <!-- Main card: form -->
388
+ <div class="smx-card">
389
+ {% with msgs = get_flashed_messages(with_categories=true) %}
390
+ {% if msgs %}
391
+ <ul class="flashes">
392
+ {% for category, msg in msgs %}
393
+ <li class="{{ category }}">{{ msg }}</li>
394
+ {% endfor %}
395
+ </ul>
396
+ {% endif %}
397
+ {% endwith %}
398
+
399
+ <div class="smx-card-header">
400
+ <div class="smx-card-title">
401
+ <span>Resize a dataset</span>
402
+ <span class="smx-pill">Explore subset</span>
403
+ </div>
404
+ </div>
405
+ <div class="smx-card-body">
406
+ <p class="smx-description">
407
+ Upload any CSV that feels too large to work with. Choose the percentage you want to keep,
408
+ and the resizer will return a smaller sample that preserves the overall feel of the data.
409
+ </p>
410
+
411
+ <form method="post" enctype="multipart/form-data">
412
+ <div class="smx-field">
413
+ <label for="dataset_file">CSV file</label>
414
+ <input id="dataset_file" type="file" name="dataset_file" accept=".csv" required>
415
+ <p class="smx-hint">
416
+ This is independent of the datasets listed in the ML Lab sidebar. You can bring in any CSV here.
417
+ </p>
418
+ </div>
419
+
420
+ <div class="smx-field">
421
+ <label for="target_pct">Target size (%)</label>
422
+ <input
423
+ id="target_pct"
424
+ name="target_pct"
425
+ type="number"
426
+ min="1"
427
+ max="100"
428
+ step="1"
429
+ placeholder="For example: 20 for 20% of the rows"
430
+ required
431
+ >
432
+ <p class="smx-hint">
433
+ We will sample roughly this share of rows. For very large files, even 5–10&nbsp;% can be enough
434
+ to explore patterns.
435
+ </p>
436
+ </div>
437
+
438
+ <a href="#" id="advToggle" class="smx-adv-toggle">
439
+ <span aria-hidden="true">▸</span>
440
+ <span>Show advanced options</span>
441
+ </a>
442
+
443
+ <div id="advanced-box">
444
+ <div class="smx-field" style="margin-top:4px;">
445
+ <label for="strat_col">Stratify by column (optional)</label>
446
+ <input
447
+ id="strat_col"
448
+ name="strat_col"
449
+ type="text"
450
+ placeholder="Type a label/segment column to keep class balance"
451
+ >
452
+ <p class="smx-hint">
453
+ If you supply a valid column name (for example a target label), the resizer will allocate rows per
454
+ class so proportions remain close to the original.
455
+ </p>
456
+ </div>
457
+ </div>
458
+
459
+ <div class="smx-actions">
460
+ <button type="submit" class="btn-primary">
461
+ Create resized CSV
462
+ </button>
463
+ </div>
464
+ </form>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- Side card: summary + download -->
469
+ <div class="smx-card-secondary">
470
+ <div class="smx-card-header">
471
+ <div class="smx-card-title">
472
+ <span class="smx-badge-dot"></span>
473
+ <span>Resize summary</span>
474
+ </div>
475
+ </div>
476
+ <div class="smx-card-body">
477
+ {% if resize_info %}
478
+ <p class="smx-side-note-text">
479
+ Here is a quick view of what was produced from your last run. You can download the resized CSV
480
+ and pass it straight into your modelling workflow.
481
+ </p>
482
+ <ul class="meta-list">
483
+ <li><strong>Original rows:</strong> {{ resize_info.rows_in }}</li>
484
+ <li><strong>Resized rows:</strong> {{ resize_info.rows_out }}</li>
485
+ <li><strong>Target size:</strong> {{ "%.1f"|format(resize_info.pct) }}&nbsp;%</li>
486
+ {% if resize_info.used_strat and resize_info.strat_col %}
487
+ <li><strong>Stratified by:</strong> {{ resize_info.strat_col }}</li>
488
+ {% endif %}
489
+ </ul>
490
+ <a href="{{ url_for('download_resized_dataset') }}" class="download-link">
491
+ Download resized CSV
492
+ </a>
493
+ {% else %}
494
+ <p class="smx-side-note-title">No resized dataset yet</p>
495
+ <p class="smx-side-note-text">
496
+ Once you upload a file and choose a percentage, this panel will display the row counts and a download
497
+ button for the resized sample.
498
+ </p>
499
+ {% endif %}
500
+ </div>
501
+ </div>
502
+ </div>
503
+ </div>
504
+
505
+ <script>
506
+ (function () {
507
+ const toggle = document.getElementById("advToggle");
508
+ const box = document.getElementById("advanced-box");
509
+ if (!toggle || !box) return;
510
+
511
+ let open = false;
512
+ const labelSpan = toggle.querySelector("span:nth-child(2)");
513
+ const arrowSpan = toggle.querySelector("span:nth-child(1)");
514
+
515
+ function refresh() {
516
+ box.style.display = open ? "block" : "none";
517
+ if (labelSpan) {
518
+ labelSpan.textContent = open ? "Hide advanced options" : "Show advanced options";
519
+ }
520
+ if (arrowSpan) {
521
+ arrowSpan.textContent = open ? "▾" : "▸";
522
+ }
523
+ }
524
+
525
+ toggle.addEventListener("click", function (ev) {
526
+ ev.preventDefault();
527
+ open = !open;
528
+ refresh();
529
+ });
530
+
531
+ refresh();
532
+ })();
533
+ </script>
534
+ </body>
535
+ </html>
syntaxmatrix/utils.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from __future__ import annotations
2
- import re, textwrap
3
- import pandas as pd
4
- import numpy as np
2
+ import re, textwrap, ast
3
+ import pandas as pd, numpy as np
5
4
  import warnings
6
-
7
5
  from difflib import get_close_matches
8
6
  from typing import Iterable, Tuple, Dict
9
7
  import inspect
@@ -629,6 +627,15 @@ def harden_ai_code(code: str) -> str:
629
627
  # 6) Final safety wrapper
630
628
  fixed = fixed.replace("\t", " ")
631
629
  fixed = textwrap.dedent(fixed).strip("\n")
630
+
631
+ # Normalise any mistaken template imports the LLM may have invented.
632
+ # If the model writes "from syntaxmatrix.templates import viz_count_bar",
633
+ # redirect that import to the real template module.
634
+ fixed = re.sub(
635
+ r"from\s+syntaxmatrix\.templates\s+import\s+([^\n]+)",
636
+ r"from syntaxmatrix.agentic.model_templates import \1",
637
+ fixed,
638
+ )
632
639
  fixed = _ensure_metrics_imports(fixed)
633
640
  fixed = _strip_stray_backrefs(fixed)
634
641
  fixed = _wrap_metric_calls(fixed)
@@ -636,6 +643,18 @@ def harden_ai_code(code: str) -> str:
636
643
  fixed = _patch_feature_coef_dataframe(fixed)
637
644
  fixed = _strip_file_io_ops(fixed)
638
645
 
646
+ metric_defaults = "\n".join([
647
+ "acc = None",
648
+ "accuracy = None",
649
+ "r2 = None",
650
+ "mae = None",
651
+ "rmse = None",
652
+ "f1 = None",
653
+ "precision = None",
654
+ "recall = None",
655
+ ]) + "\n"
656
+ fixed = metric_defaults + fixed
657
+
639
658
  # Import shared preface helpers once and wrap the LLM body safely
640
659
  header = "from syntaxmatrix.preface import *\n\n"
641
660
  wrapped = header + wrap_llm_code_safe(fixed)
@@ -1613,6 +1632,44 @@ def clean_llm_code(code: str) -> str:
1613
1632
  """
1614
1633
  code = str(code or "")
1615
1634
 
1635
+ # Special case: sometimes the OpenAI SDK object repr (e.g. ChatCompletion(...))
1636
+ # is accidentally passed here as `code`. In that case, extract the actual
1637
+ # Python code from the ChatCompletionMessage(content=...) field.
1638
+ if "ChatCompletion(" in code and "ChatCompletionMessage" in code and "content=" in code:
1639
+ try:
1640
+ extracted = None
1641
+
1642
+ class _ChatCompletionVisitor(ast.NodeVisitor):
1643
+ def visit_Call(self, node):
1644
+ nonlocal extracted
1645
+ func = node.func
1646
+ fname = getattr(func, "id", None) or getattr(func, "attr", None)
1647
+ if fname == "ChatCompletionMessage":
1648
+ for kw in node.keywords:
1649
+ if kw.arg == "content" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
1650
+ extracted = kw.value.value
1651
+ self.generic_visit(node)
1652
+
1653
+ tree = ast.parse(code, mode="exec")
1654
+ _ChatCompletionVisitor().visit(tree)
1655
+ if extracted:
1656
+ code = extracted
1657
+ except Exception:
1658
+ # Best-effort regex fallback if AST parsing fails
1659
+ m = re.search(r"content=(?P<q>['\\\"])(?P<body>.*?)(?P=q)", code, flags=re.S)
1660
+ if m:
1661
+ code = m.group("body")
1662
+
1663
+ # Existing logic continues unchanged below...
1664
+ # Extract fenced blocks (```python ... ``` or ``` ... ```)
1665
+ blocks = re.findall(r"```(?:python)?\s*(.*?)```", code, flags=re.I | re.S)
1666
+
1667
+ if blocks:
1668
+ # pick the largest block; small trailing blocks are usually garbage
1669
+ largest = max(blocks, key=lambda b: len(b.strip()))
1670
+ if len(largest.strip().splitlines()) >= 10:
1671
+ code = largest
1672
+
1616
1673
  # Extract fenced blocks (```python ... ``` or ``` ... ```)
1617
1674
  blocks = re.findall(r"```(?:python)?\s*(.*?)```", code, flags=re.I | re.S)
1618
1675
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.5.7
3
+ Version: 2.5.8.1
4
4
  Summary: SyntaxMUI: A customizable framework for Python AI Assistant Projects.
5
5
  Author: Bob Nti
6
6
  Author-email: bob.nti@syntaxmatrix.net
@@ -2,7 +2,7 @@ syntaxmatrix/__init__.py,sha256=_LnTrYAW2tbYA37Y233Vv4OMOk8NUnoJi-1yzFyHxEI,2573
2
2
  syntaxmatrix/auth.py,sha256=SCD6uWojXjj9yjUTKzgV5kBYe6ZkXASEG2VopLFkEtM,18140
3
3
  syntaxmatrix/bootstrap.py,sha256=Y7ZNg-Z3ecrr1iYem5EMzPmGstXnEKmO9kqKVoOoljo,817
4
4
  syntaxmatrix/commentary.py,sha256=3uSlbaQ1zl-gYtEtEpFbv2M-IH-HSdFdMvhxa7UCNHk,12025
5
- syntaxmatrix/core.py,sha256=7o5givPq7Io9DCnJQjTy-izgYvJivE2POxvG45i3dBk,61338
5
+ syntaxmatrix/core.py,sha256=hebGEXJLL4q9X7jQkl_OJptIh_5AEIAy7T3HchIcjdI,61332
6
6
  syntaxmatrix/dataset_preprocessing.py,sha256=wtV4MWzkyfOsBHTsS0H1gqHho77ZQHGDI9skJryyZWA,8732
7
7
  syntaxmatrix/db.py,sha256=xkCpyhFxnAwrnZCTd13NkJsahVze0i4egjMcbB7kPfs,5000
8
8
  syntaxmatrix/display.py,sha256=TgMrE5WW80VlLcL_XvEz936mekFccJgLTfzbCIozSc8,3728
@@ -18,25 +18,25 @@ syntaxmatrix/plottings.py,sha256=MjHQ9T1_oC5oyr4_wkM2GJDrpjp0sbvudbs2lGaMyzk,610
18
18
  syntaxmatrix/preface.py,sha256=EOK3lflMJ-0B6SRJtVXhzZjhvu-bfXzw-sy1TbTYOVs,17009
19
19
  syntaxmatrix/profiles.py,sha256=0-lky7Wj-WQlP5CbvTyw1tI2M0FiqhhTkLZYLRhD5AU,2251
20
20
  syntaxmatrix/project_root.py,sha256=1ckvbFVV1szHtHsfSCoGcImHkRwbfszmPG1kGh9ZZlE,2227
21
- syntaxmatrix/routes.py,sha256=kkLn6uOfOiD8-Kl2_JKs0qcxBHkuJd6cFih9v8KuTWk,302912
21
+ syntaxmatrix/routes.py,sha256=WRLKCtwGFzIetMFUyvE5cb9tl2-j54qx-JA-VlDwcQM,310998
22
22
  syntaxmatrix/session.py,sha256=v0qgxnVM_LEaNvZQJSa-13Q2eiwc3RDnjd2SahNnHQk,599
23
23
  syntaxmatrix/smiv.py,sha256=1lSN3UYpXvYoVNd6VrkY5iZuF_nDxD6xxvLnTn9wcbQ,1405
24
24
  syntaxmatrix/smpv.py,sha256=rrCgYqfjBaK2n5qzfQyXK3bHFMvgNcCIqPaXquOLtDM,3600
25
25
  syntaxmatrix/themes.py,sha256=qa90vPZTuNNKB37loZhChQfu5QqkaJG4JxgI_4QgCxw,3576
26
26
  syntaxmatrix/ui_modes.py,sha256=5lfKK3AKAB-JQCWfi1GRYp4sQqg4Z0fC3RJ8G3VGCMw,152
27
- syntaxmatrix/utils.py,sha256=0iTu9XbUN1HsZModWmyexYrXAzjox7gpHyYV7SmW-PM,123555
27
+ syntaxmatrix/utils.py,sha256=deyPiM-sHaqudspFKX9i7VPdmZlWMKcEx-PxjYOZ_lg,126045
28
28
  syntaxmatrix/vector_db.py,sha256=ozvOcMHt52xFAvcp-vAqT69kECPq9BwL8Rzgq3AJaMs,5824
29
29
  syntaxmatrix/vectorizer.py,sha256=5w_UQiUIirm_W-Q9TcaEI8LTcTYIuDBdKfz79T1aZ8g,1366
30
30
  syntaxmatrix/workspace_db.py,sha256=Xu9OlW8wo3iaH5Y88ZMdLOf-fiZxF1NBb5rAw3KcbfY,4715
31
31
  syntaxmatrix/agentic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  syntaxmatrix/agentic/agent_tools.py,sha256=yQwavONP23ziMxNQf3j2Y4TVo_LxEsiAWecKuBK8WDg,866
33
- syntaxmatrix/agentic/agents.py,sha256=m4eqeCf4fVRodCa1Pgc3LwT4EPGip4E-7n1k1HxeKxA,29898
33
+ syntaxmatrix/agentic/agents.py,sha256=_dCEuAMV-OWZ-omXUVPgDpfJkzg9AtIb48ZK5s8TICE,29408
34
34
  syntaxmatrix/agentic/code_tools_registry.py,sha256=Wp4-KHtp0BUVciqSbmionBsQMVFOnvJPruBJeNiuwkk,1564
35
35
  syntaxmatrix/agentic/model_templates.py,sha256=A3ROE3BHkvnU9cxqSGjlCBIw9U15zRaTKgK-WxcZtUI,76033
36
36
  syntaxmatrix/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  syntaxmatrix/settings/default.yaml,sha256=BznvF1D06VMPbT6UX3MQ4zUkXxTXLnAA53aUu8G4O38,569
38
38
  syntaxmatrix/settings/logging.py,sha256=U8iTDFv0H1ECdIzH9He2CtOVlK1x5KHCk126Zn5Vi7M,1362
39
- syntaxmatrix/settings/model_map.py,sha256=M6EPTPw2m88RFLNlzxt6vQzEtdr66NcHz0t4zB7QfkU,12028
39
+ syntaxmatrix/settings/model_map.py,sha256=wuVfjAs6R35ILdi384fJtOfdOGwV7LNkMjcIUkNFIdc,12001
40
40
  syntaxmatrix/settings/prompts.py,sha256=dLNijnw9UHlAg5qxcSaLPhTmR7SdDDyOFcMKhlCA4eQ,21695
41
41
  syntaxmatrix/settings/string_navbar.py,sha256=NqgTzo3J9rRI4c278VG6kpoViFfmi2FKmL6sO0R-bus,83
42
42
  syntaxmatrix/static/docs.md,sha256=rWlKjNcpS2cs5DElGNYuaA-XXdGZnRGMXx62nACvDwE,11105
@@ -51,7 +51,8 @@ syntaxmatrix/static/js/sidebar.js,sha256=zHp4skKLY2Dlqx7aLPQ8_cR0iTRT17W0SC2TR38
51
51
  syntaxmatrix/static/js/widgets.js,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  syntaxmatrix/templates/change_password.html,sha256=YWEcnwJLccLyKGzQxIrc0xuP-p00BtEIwcYq4oFvJ-0,3332
53
53
  syntaxmatrix/templates/code_cell.html,sha256=LOr9VjvNQcOGKKJ1ecpcZh3C3qsUxBHueg2iQtpdxl8,638
54
- syntaxmatrix/templates/dashboard.html,sha256=lR0wUtozTh5bDHbPSiywJiypiT_nNfzvEJJLfWckE0w,32272
54
+ syntaxmatrix/templates/dashboard.html,sha256=_uYNtfUzO71ZMRduQ07vB9CkYa2BYB7Ed3k7Kf_Dsb0,35599
55
+ syntaxmatrix/templates/dataset_resize.html,sha256=MRDbEGTjuMASdMn1yhnwKRyyM70-bL7u05i5EtrNVQg,14909
55
56
  syntaxmatrix/templates/docs.html,sha256=KVi5JrZD3gwOduiZhAz7hQrKY9SrQ_bsHOODj0Nj09s,3552
56
57
  syntaxmatrix/templates/error.html,sha256=Iu5ykHnhw8jrxVBNn6B95e90W5u9I2hySCiLtaoOJMs,3290
57
58
  syntaxmatrix/templates/login.html,sha256=V_bWHozS1xCeHPsvAAfaGG-_2lAE7K8d05IarQN1PS8,2677
@@ -63,8 +64,8 @@ syntaxmatrix/vectordb/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
63
64
  syntaxmatrix/vectordb/adapters/milvus_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
65
  syntaxmatrix/vectordb/adapters/pgvector_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
66
  syntaxmatrix/vectordb/adapters/sqlite_adapter.py,sha256=L8M2qHfwZRAFVxWeurUVdHaJXz6F5xTUSWh3uy6TSUs,6035
66
- syntaxmatrix-2.5.7.dist-info/licenses/LICENSE.txt,sha256=j1P8naTdy1JMxTC80XYQjbyAQnuOlpDusCUhncrvpy8,1083
67
- syntaxmatrix-2.5.7.dist-info/METADATA,sha256=2_xno8Sgx4iERRKmk5GfgluVQDY_94aPFdmx3NC85dw,18090
68
- syntaxmatrix-2.5.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- syntaxmatrix-2.5.7.dist-info/top_level.txt,sha256=HKP_zkl4V_nt7osC15DlacoBZktHrbZYOqf_pPkF3T8,13
70
- syntaxmatrix-2.5.7.dist-info/RECORD,,
67
+ syntaxmatrix-2.5.8.1.dist-info/licenses/LICENSE.txt,sha256=j1P8naTdy1JMxTC80XYQjbyAQnuOlpDusCUhncrvpy8,1083
68
+ syntaxmatrix-2.5.8.1.dist-info/METADATA,sha256=6TyiQ5hh35pNXj0PvjF7Tk0mcG3hTO920PeRBEoOOWk,18092
69
+ syntaxmatrix-2.5.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ syntaxmatrix-2.5.8.1.dist-info/top_level.txt,sha256=HKP_zkl4V_nt7osC15DlacoBZktHrbZYOqf_pPkF3T8,13
71
+ syntaxmatrix-2.5.8.1.dist-info/RECORD,,