syntaxmatrix 2.6.4.4__py3-none-any.whl → 3.0.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.
- syntaxmatrix/__init__.py +6 -4
- syntaxmatrix/agentic/agents.py +206 -26
- syntaxmatrix/agentic/agents_orchestrer.py +16 -10
- syntaxmatrix/client_docs.py +237 -0
- syntaxmatrix/commentary.py +96 -25
- syntaxmatrix/core.py +142 -56
- syntaxmatrix/dataset_preprocessing.py +2 -2
- syntaxmatrix/db.py +0 -17
- syntaxmatrix/kernel_manager.py +174 -150
- syntaxmatrix/page_builder_generation.py +656 -63
- syntaxmatrix/page_layout_contract.py +25 -3
- syntaxmatrix/page_patch_publish.py +368 -15
- syntaxmatrix/plugins/__init__.py +0 -0
- syntaxmatrix/premium/__init__.py +10 -2
- syntaxmatrix/premium/catalogue/__init__.py +121 -0
- syntaxmatrix/premium/gate.py +15 -3
- syntaxmatrix/premium/state.py +507 -0
- syntaxmatrix/premium/verify.py +222 -0
- syntaxmatrix/profiles.py +1 -1
- syntaxmatrix/routes.py +9847 -8004
- syntaxmatrix/settings/model_map.py +50 -65
- syntaxmatrix/settings/prompts.py +1186 -414
- 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.1.dist-info/METADATA +219 -0
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/RECORD +38 -33
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/WHEEL +1 -1
- syntaxmatrix/settings/default.yaml +0 -13
- syntaxmatrix-2.6.4.4.dist-info/METADATA +0 -539
- syntaxmatrix-2.6.4.4.dist-info/licenses/LICENSE.txt +0 -21
- /syntaxmatrix/{plugin_manager.py → plugins/plugin_manager.py} +0 -0
- /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# syntaxmatrix/client_docs.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import html
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
from flask import abort, render_template
|
|
11
|
+
from markupsafe import Markup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TocItem:
|
|
16
|
+
level: int
|
|
17
|
+
id: str
|
|
18
|
+
text: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_slug_rx = re.compile(r"[^a-z0-9\- ]+")
|
|
22
|
+
_ws_rx = re.compile(r"\s+")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _slugify(text: str) -> str:
|
|
26
|
+
t = text.strip().lower()
|
|
27
|
+
t = _slug_rx.sub("", t)
|
|
28
|
+
t = _ws_rx.sub("-", t)
|
|
29
|
+
t = t.strip("-")
|
|
30
|
+
return t or "section"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_headings(md: str) -> List[Tuple[int, str]]:
|
|
34
|
+
"""
|
|
35
|
+
Extract ATX-style markdown headings (#, ##, ###, ...), ignoring fenced code blocks.
|
|
36
|
+
"""
|
|
37
|
+
headings: List[Tuple[int, str]] = []
|
|
38
|
+
in_code = False
|
|
39
|
+
for line in md.splitlines():
|
|
40
|
+
if line.strip().startswith("```"):
|
|
41
|
+
in_code = not in_code
|
|
42
|
+
continue
|
|
43
|
+
if in_code:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
m = re.match(r"^(#{1,6})\s+(.+?)\s*$", line)
|
|
47
|
+
if not m:
|
|
48
|
+
continue
|
|
49
|
+
level = len(m.group(1))
|
|
50
|
+
title = m.group(2).strip()
|
|
51
|
+
# Avoid weird headings like "### ----"
|
|
52
|
+
if title and not all(ch in "-_=*" for ch in title):
|
|
53
|
+
headings.append((level, title))
|
|
54
|
+
return headings
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _render_markdown_minimal(md: str, heading_ids: dict[str, str]) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Minimal markdown renderer (safe-by-default):
|
|
60
|
+
- headings, paragraphs, bullet lists, code fences, inline code, links, images
|
|
61
|
+
- everything else is HTML-escaped
|
|
62
|
+
"""
|
|
63
|
+
lines = md.splitlines()
|
|
64
|
+
out: List[str] = []
|
|
65
|
+
|
|
66
|
+
in_code = False
|
|
67
|
+
code_lang = ""
|
|
68
|
+
code_buf: List[str] = []
|
|
69
|
+
|
|
70
|
+
in_ul = False
|
|
71
|
+
|
|
72
|
+
def flush_ul():
|
|
73
|
+
nonlocal in_ul
|
|
74
|
+
if in_ul:
|
|
75
|
+
out.append("</ul>")
|
|
76
|
+
in_ul = False
|
|
77
|
+
|
|
78
|
+
def flush_code():
|
|
79
|
+
nonlocal in_code, code_lang, code_buf
|
|
80
|
+
if not in_code:
|
|
81
|
+
return
|
|
82
|
+
code_text = "\n".join(code_buf)
|
|
83
|
+
out.append(
|
|
84
|
+
f'<pre class="smx-code"><code class="language-{html.escape(code_lang)}">'
|
|
85
|
+
f"{html.escape(code_text)}</code></pre>"
|
|
86
|
+
)
|
|
87
|
+
in_code = False
|
|
88
|
+
code_lang = ""
|
|
89
|
+
code_buf = []
|
|
90
|
+
|
|
91
|
+
def inline_fmt(s: str) -> str:
|
|
92
|
+
s = html.escape(s)
|
|
93
|
+
|
|
94
|
+
# inline code: `code`
|
|
95
|
+
s = re.sub(r"`([^`]+)`", lambda m: f"<code>{html.escape(m.group(1))}</code>", s)
|
|
96
|
+
|
|
97
|
+
# images: 
|
|
98
|
+
s = re.sub(
|
|
99
|
+
r"!\[([^\]]*)\]\(([^)]+)\)",
|
|
100
|
+
lambda m: f'<img alt="{html.escape(m.group(1))}" src="{html.escape(m.group(2))}" />',
|
|
101
|
+
s,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# links: [text](url)
|
|
105
|
+
s = re.sub(
|
|
106
|
+
r"\[([^\]]+)\]\(([^)]+)\)",
|
|
107
|
+
lambda m: f'<a href="{html.escape(m.group(2))}" target="_blank" rel="noopener noreferrer">{html.escape(m.group(1))}</a>',
|
|
108
|
+
s,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# bold **text**
|
|
112
|
+
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
|
|
113
|
+
|
|
114
|
+
# italics *text* (simple)
|
|
115
|
+
s = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", s)
|
|
116
|
+
|
|
117
|
+
return s
|
|
118
|
+
|
|
119
|
+
# Pre-map heading text to stable IDs (supports duplicates)
|
|
120
|
+
# heading_ids is already built with de-duplication.
|
|
121
|
+
for raw in lines:
|
|
122
|
+
line = raw.rstrip("\n")
|
|
123
|
+
|
|
124
|
+
# fenced code blocks
|
|
125
|
+
m_code = re.match(r"^\s*```(\w+)?\s*$", line)
|
|
126
|
+
if m_code:
|
|
127
|
+
flush_ul()
|
|
128
|
+
if in_code:
|
|
129
|
+
flush_code()
|
|
130
|
+
else:
|
|
131
|
+
in_code = True
|
|
132
|
+
code_lang = (m_code.group(1) or "").strip()
|
|
133
|
+
code_buf = []
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if in_code:
|
|
137
|
+
code_buf.append(line)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# headings
|
|
141
|
+
m_h = re.match(r"^(#{1,6})\s+(.+?)\s*$", line)
|
|
142
|
+
if m_h:
|
|
143
|
+
flush_ul()
|
|
144
|
+
level = len(m_h.group(1))
|
|
145
|
+
text = m_h.group(2).strip()
|
|
146
|
+
hid = heading_ids.get(text) or _slugify(text)
|
|
147
|
+
out.append(
|
|
148
|
+
f'<h{level} id="{html.escape(hid)}" class="smx-h">{inline_fmt(text)}</h{level}>'
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# bullet list items (supports "-" or "*")
|
|
153
|
+
m_li = re.match(r"^\s*[-*]\s+(.+)\s*$", line)
|
|
154
|
+
if m_li:
|
|
155
|
+
if not in_ul:
|
|
156
|
+
out.append("<ul>")
|
|
157
|
+
in_ul = True
|
|
158
|
+
out.append(f"<li>{inline_fmt(m_li.group(1).strip())}</li>")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# blank line ends lists/paragraph chunks
|
|
162
|
+
if line.strip() == "":
|
|
163
|
+
flush_ul()
|
|
164
|
+
out.append("")
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# blockquote (single-line)
|
|
168
|
+
m_bq = re.match(r"^\s*>\s+(.+)\s*$", line)
|
|
169
|
+
if m_bq:
|
|
170
|
+
flush_ul()
|
|
171
|
+
out.append(f"<blockquote>{inline_fmt(m_bq.group(1).strip())}</blockquote>")
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
# horizontal rule
|
|
175
|
+
if re.match(r"^\s*---\s*$", line):
|
|
176
|
+
flush_ul()
|
|
177
|
+
out.append("<hr/>")
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# normal paragraph line
|
|
181
|
+
flush_ul()
|
|
182
|
+
out.append(f"<p>{inline_fmt(line.strip())}</p>")
|
|
183
|
+
|
|
184
|
+
flush_ul()
|
|
185
|
+
flush_code()
|
|
186
|
+
|
|
187
|
+
return "\n".join(out)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_docs_html_and_toc(md: str) -> Tuple[str, List[TocItem]]:
|
|
191
|
+
headings = _extract_headings(md)
|
|
192
|
+
|
|
193
|
+
# Build stable unique IDs
|
|
194
|
+
used: dict[str, int] = {}
|
|
195
|
+
heading_ids: dict[str, str] = {}
|
|
196
|
+
|
|
197
|
+
toc: List[TocItem] = []
|
|
198
|
+
for level, title in headings:
|
|
199
|
+
base = _slugify(title)
|
|
200
|
+
n = used.get(base, 0)
|
|
201
|
+
used[base] = n + 1
|
|
202
|
+
hid = base if n == 0 else f"{base}-{n+1}"
|
|
203
|
+
|
|
204
|
+
# map by raw title (good enough for this README)
|
|
205
|
+
# if duplicates exist with same title, only first maps here; duplicates are still in TOC correctly
|
|
206
|
+
if title not in heading_ids:
|
|
207
|
+
heading_ids[title] = hid
|
|
208
|
+
|
|
209
|
+
toc.append(TocItem(level=level, id=hid, text=title))
|
|
210
|
+
|
|
211
|
+
html_content = _render_markdown_minimal(md, heading_ids)
|
|
212
|
+
return html_content, toc
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def register_client_docs_routes(app, client_dir: str) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Call this once during app initialisation.
|
|
218
|
+
Exposes:
|
|
219
|
+
GET /docs
|
|
220
|
+
"""
|
|
221
|
+
@app.get("/docs")
|
|
222
|
+
def smx_client_docs():
|
|
223
|
+
readme_path = os.path.join(client_dir, "README.md")
|
|
224
|
+
if not os.path.exists(readme_path):
|
|
225
|
+
abort(404, description="README.md not found in client root")
|
|
226
|
+
|
|
227
|
+
with open(readme_path, "r", encoding="utf-8") as f:
|
|
228
|
+
md = f.read()
|
|
229
|
+
|
|
230
|
+
docs_html, toc = build_docs_html_and_toc(md)
|
|
231
|
+
|
|
232
|
+
return render_template(
|
|
233
|
+
"client_docs.html",
|
|
234
|
+
page_title="System Documentation",
|
|
235
|
+
toc=toc,
|
|
236
|
+
docs_html=Markup(docs_html),
|
|
237
|
+
)
|
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")
|