syntaxmatrix 2.5.6__py3-none-any.whl → 2.6.2__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 (41) hide show
  1. syntaxmatrix/agentic/agents.py +1220 -169
  2. syntaxmatrix/agentic/agents_orchestrer.py +326 -0
  3. syntaxmatrix/agentic/code_tools_registry.py +27 -32
  4. syntaxmatrix/commentary.py +16 -16
  5. syntaxmatrix/core.py +185 -81
  6. syntaxmatrix/db.py +460 -4
  7. syntaxmatrix/{display.py → display_html.py} +2 -6
  8. syntaxmatrix/gpt_models_latest.py +1 -1
  9. syntaxmatrix/media/__init__.py +0 -0
  10. syntaxmatrix/media/media_pixabay.py +277 -0
  11. syntaxmatrix/models.py +1 -1
  12. syntaxmatrix/page_builder_defaults.py +183 -0
  13. syntaxmatrix/page_builder_generation.py +1122 -0
  14. syntaxmatrix/page_layout_contract.py +644 -0
  15. syntaxmatrix/page_patch_publish.py +1471 -0
  16. syntaxmatrix/preface.py +142 -21
  17. syntaxmatrix/profiles.py +28 -10
  18. syntaxmatrix/routes.py +1740 -453
  19. syntaxmatrix/selftest_page_templates.py +360 -0
  20. syntaxmatrix/settings/client_items.py +28 -0
  21. syntaxmatrix/settings/model_map.py +1022 -207
  22. syntaxmatrix/settings/prompts.py +328 -130
  23. syntaxmatrix/static/assets/hero-default.svg +22 -0
  24. syntaxmatrix/static/icons/bot-icon.png +0 -0
  25. syntaxmatrix/static/icons/favicon.png +0 -0
  26. syntaxmatrix/static/icons/logo.png +0 -0
  27. syntaxmatrix/static/icons/logo3.png +0 -0
  28. syntaxmatrix/templates/admin_branding.html +104 -0
  29. syntaxmatrix/templates/admin_features.html +63 -0
  30. syntaxmatrix/templates/admin_secretes.html +108 -0
  31. syntaxmatrix/templates/dashboard.html +296 -133
  32. syntaxmatrix/templates/dataset_resize.html +535 -0
  33. syntaxmatrix/templates/edit_page.html +2535 -0
  34. syntaxmatrix/utils.py +2431 -2383
  35. {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/METADATA +6 -2
  36. {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/RECORD +39 -24
  37. syntaxmatrix/generate_page.py +0 -644
  38. syntaxmatrix/static/icons/hero_bg.jpg +0 -0
  39. {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/WHEEL +0 -0
  40. {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/licenses/LICENSE.txt +0 -0
  41. {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,360 @@
1
+ # syntaxmatrix/selftest_page_templates.py
2
+ from __future__ import annotations
3
+ from bs4 import BeautifulSoup
4
+ import copy
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ # Your new contract utilities (from the previous step)
10
+ from syntaxmatrix.page_layout_contract import (
11
+ normalise_layout,
12
+ validate_layout,
13
+ validate_compiled_html,
14
+ )
15
+
16
+ # Your existing patch-only publisher
17
+ from syntaxmatrix.page_patch_publish import patch_page_publish
18
+
19
+ def _pick_section_for_title_patch(sections):
20
+ """
21
+ Prefer a non-hero section that has both title+text, so we can assert the intro paragraph.
22
+ Fallback: first non-hero section.
23
+ """
24
+ non_hero = [s for s in sections if isinstance(s, dict) and (s.get("type") or "").lower() != "hero"]
25
+ for s in non_hero:
26
+ if (s.get("title") or "").strip() and (s.get("text") or "").strip():
27
+ return s
28
+ return non_hero[0] if non_hero else None
29
+
30
+
31
+ def _pick_section_for_item_patch(sections):
32
+ """
33
+ Prefer a grid-ish section with at least one item.
34
+ """
35
+ grid_types = {"values", "team", "logos", "testimonials", "faq", "cta",
36
+ "services", "offers", "comparison", "process", "proof", "case_studies"}
37
+ for s in sections:
38
+ if not isinstance(s, dict):
39
+ continue
40
+ st = (s.get("type") or "").lower()
41
+ items = s.get("items") if isinstance(s.get("items"), list) else []
42
+ if st in grid_types and len(items) > 0:
43
+ return s
44
+ return None
45
+
46
+
47
+ def _default_fixture_about_v1() -> Dict[str, Any]:
48
+ return {
49
+ "page": "about",
50
+ "category": "about",
51
+ "template": {"id": "about_glass_hero_v1", "version": "1.0.0"},
52
+ "meta": {
53
+ "pageTitle": "About SyntaxMatrix",
54
+ "summary": "AI platform framework for developer teams to provision client-ready AI platforms.",
55
+ "primaryCta": {"label": "Request a demo", "href": "#sec_cta"},
56
+ "secondaryCta": {"label": "See capabilities", "href": "#sec_values"},
57
+ },
58
+ "sections": [
59
+ {
60
+ "id": "sec_hero",
61
+ "type": "hero",
62
+ "title": "Build client-ready AI platforms with confidence",
63
+ "text": "SyntaxMatrix helps teams provision, customise, and ship robust AI products faster.",
64
+ "imageUrl": "https://example.com/hero-a.jpg",
65
+ "items": [],
66
+ },
67
+ {
68
+ "id": "sec_story",
69
+ "type": "story",
70
+ "title": "Our story",
71
+ "text": "We built SyntaxMatrix to remove repetitive engineering work that slows down AI delivery.",
72
+ },
73
+ {
74
+ "id": "sec_values",
75
+ "type": "values",
76
+ "title": "What we stand for",
77
+ "text": "Principles that guide how we design and ship.",
78
+ "cols": 3,
79
+ "items": [
80
+ {
81
+ "id": "val_1",
82
+ "type": "card",
83
+ "title": "Engineering rigour",
84
+ "text": "Clear architecture, safe defaults, predictable behaviour.",
85
+ },
86
+ {
87
+ "id": "val_2",
88
+ "type": "card",
89
+ "title": "Client readiness",
90
+ "text": "Provisioning, admin tooling, audit trails, deployment patterns.",
91
+ },
92
+ {
93
+ "id": "val_3",
94
+ "type": "card",
95
+ "title": "Practical AI",
96
+ "text": "RAG, analytics, and automation that supports real teams.",
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ "id": "sec_cta",
102
+ "type": "cta",
103
+ "title": "Ready to talk?",
104
+ "text": "Tell us what you’re building and we’ll suggest a path to a production-ready setup.",
105
+ "cols": 2,
106
+ "items": [
107
+ {"id": "cta_1", "type": "card", "title": "Book a demo", "text": "See it in action."},
108
+ {"id": "cta_2", "type": "card", "title": "Discuss requirements", "text": "We’ll map your use-case."},
109
+ ],
110
+ },
111
+ ],
112
+ }
113
+
114
+
115
+ def _compile_html(layout: Dict[str, Any]) -> Tuple[str, str]:
116
+ """
117
+ Prefer page_builder first (lighter deps). Print real import errors if compilers fail.
118
+ Returns: (html, compiler_name)
119
+ """
120
+ import importlib
121
+ import traceback
122
+
123
+ errors = []
124
+
125
+ # 1) Page builder compiler (preferred)
126
+ try:
127
+ mod = importlib.import_module("syntaxmatrix.page_builder_generation")
128
+ fn = getattr(mod, "compile_layout_to_html", None)
129
+ if callable(fn):
130
+ slug = (layout.get("page") or layout.get("category") or "page")
131
+ return fn(layout, page_slug=str(slug)), "page_builder.compile_layout_to_html"
132
+ errors.append("page_builder: compile_layout_to_html not found/callable")
133
+ except Exception:
134
+ errors.append("page_builder import failed:\n" + traceback.format_exc())
135
+
136
+ # 2) Agentic compiler (optional; may require extra deps like google.genai)
137
+ try:
138
+ mod = importlib.import_module("syntaxmatrix.agentic.agents")
139
+ fn = getattr(mod, "compile_plan_to_html", None)
140
+ if callable(fn):
141
+ return fn(layout), "agentic.compile_plan_to_html"
142
+ errors.append("agentic: compile_plan_to_html not found/callable")
143
+ except Exception:
144
+ errors.append("agentic import failed:\n" + traceback.format_exc())
145
+
146
+ raise RuntimeError(
147
+ "Could not import a compiler.\n\n" + "\n\n".join(errors)
148
+ )
149
+
150
+ def _issues_to_text(issues) -> str:
151
+ lines = []
152
+ for it in issues:
153
+ d = it.to_dict() if hasattr(it, "to_dict") else dict(it)
154
+ lines.append(f"- [{d.get('level')}] {d.get('path')}: {d.get('message')}")
155
+ return "\n".join(lines)
156
+
157
+
158
+ def run_page_template_selftest(
159
+ fixture_path: Optional[str] = None,
160
+ *,
161
+ verbose: bool = True,
162
+ ) -> int:
163
+ """
164
+ Self-test:
165
+ 1) Load fixture JSON (or use built-in About v1 fixture)
166
+ 2) normalise + validate layout
167
+ 3) compile HTML
168
+ 4) validate compiled HTML anchors
169
+ 5) patch publish with a modified layout
170
+ 6) assert that hero + section titles/intros + grid items changed
171
+
172
+ Returns 0 if pass, non-zero if fail.
173
+ """
174
+ try:
175
+ # ── 1) Load fixture
176
+ if fixture_path:
177
+ raw = json.loads(Path(fixture_path).read_text(encoding="utf-8"))
178
+ else:
179
+ raw = _default_fixture_about_v1()
180
+
181
+ # ── 2) Normalise + validate layout
182
+ layout = normalise_layout(raw, mode="draft")
183
+ issues = validate_layout(layout)
184
+ errors = [i for i in issues if i.level == "error"]
185
+ warns = [i for i in issues if i.level == "warning"]
186
+
187
+ if verbose and warns:
188
+ print("Layout warnings:")
189
+ print(_issues_to_text(warns))
190
+
191
+ if errors:
192
+ print("Layout errors:")
193
+ print(_issues_to_text(errors))
194
+ return 2
195
+
196
+ # ── 3) Compile HTML
197
+ html, compiler_name = _compile_html(layout)
198
+ if verbose:
199
+ print(f"Compiled using: {compiler_name}")
200
+ print(f"HTML length: {len(html)} chars")
201
+
202
+ # ── 4) Validate compiled HTML anchors
203
+ html_issues = validate_compiled_html(html, layout)
204
+ html_errors = [i for i in html_issues if i.level == "error"]
205
+ html_warns = [i for i in html_issues if i.level == "warning"]
206
+
207
+ if verbose and html_warns:
208
+ print("HTML warnings:")
209
+ print(_issues_to_text(html_warns))
210
+
211
+ if html_errors:
212
+ print("HTML errors:")
213
+ print(_issues_to_text(html_errors))
214
+ return 3
215
+
216
+ # ── 5) Patch publish with a modified layout
217
+ mutated = copy.deepcopy(layout)
218
+
219
+ # Change hero fields
220
+ hero = next(s for s in mutated["sections"] if (s.get("type") or "").lower() == "hero")
221
+ old_hero_title = hero.get("title", "")
222
+ old_hero_text = hero.get("text", "")
223
+ old_hero_img = hero.get("imageUrl", "")
224
+
225
+ hero["title"] = "About SyntaxMatrix (Patched)"
226
+ hero["text"] = "This hero lead was patched successfully."
227
+ hero["imageUrl"] = "https://example.com/hero-b.jpg"
228
+
229
+ # ── 5) Patch publish with a modified layout
230
+ mutated = copy.deepcopy(layout)
231
+
232
+ # Change hero fields (always)
233
+ hero = next(s for s in mutated["sections"] if (s.get("type") or "").lower() == "hero")
234
+ old_hero_title = hero.get("title", "")
235
+ old_hero_text = hero.get("text", "")
236
+ old_hero_img = hero.get("imageUrl", "")
237
+
238
+ hero["title"] = "Hero (Patched)"
239
+ hero["text"] = "This hero lead was patched successfully."
240
+ hero["imageUrl"] = "https://example.com/hero-b.jpg"
241
+
242
+ # Pick a non-hero section to patch title + intro
243
+ target_sec = _pick_section_for_title_patch(mutated["sections"])
244
+ if target_sec is None:
245
+ raise RuntimeError("No non-hero section available to test section title/intro patching.")
246
+
247
+ target_id = target_sec.get("id")
248
+ old_target_title = target_sec.get("title", "")
249
+ old_target_text = target_sec.get("text", "")
250
+
251
+ target_sec["title"] = f"{old_target_title} (Patched)".strip() if old_target_title else "Section (Patched)"
252
+ target_sec["text"] = "This intro paragraph was patched successfully."
253
+
254
+ # Pick a grid section to patch first item
255
+ items_sec = _pick_section_for_item_patch(mutated["sections"])
256
+ if items_sec is None:
257
+ raise RuntimeError("No grid-like section with items found to test item patching.")
258
+
259
+ items_sec_id = items_sec.get("id")
260
+ items_list = items_sec.get("items") if isinstance(items_sec.get("items"), list) else []
261
+ old_item_title = (items_list[0].get("title", "") if items_list else "")
262
+
263
+ items_list[0]["title"] = f"{old_item_title} (Patched)".strip() if old_item_title else "Item (Patched)"
264
+ items_list[0]["text"] = "This card body was patched successfully."
265
+
266
+ patched_html, stats = patch_page_publish(html, mutated)
267
+
268
+ if verbose:
269
+ print("Patch stats:", stats)
270
+
271
+ patched_html, stats = patch_page_publish(html, mutated)
272
+
273
+ if verbose:
274
+ print("Patch stats:", stats)
275
+
276
+ # ── 6) Assertions: ensure old content removed, new content present
277
+ # ── 6) Assertions: verify the *target nodes* changed (not substring checks)
278
+ soup = BeautifulSoup(patched_html, "html.parser")
279
+
280
+ def text_of(selector: str) -> str:
281
+ el = soup.select_one(selector)
282
+ return el.get_text(strip=True) if el else ""
283
+
284
+ # Hero checks
285
+ hero_id = hero.get("id") or "sec_hero"
286
+ hero_h1 = text_of(f"section#{hero_id} h1")
287
+ if hero_h1 != "Hero (Patched)":
288
+ raise AssertionError(f"Hero <h1> not patched. Got: {hero_h1!r}")
289
+
290
+ hero_lead = (
291
+ text_of(f"section#{hero_id} p.lead")
292
+ or text_of(f"section#{hero_id} .lead")
293
+ or text_of(f"section#{hero_id} p")
294
+ )
295
+ if hero_lead != "This hero lead was patched successfully.":
296
+ raise AssertionError(f"Hero lead not patched. Got: {hero_lead!r}")
297
+
298
+ hero_bg = soup.select_one(f"section#{hero_id} .hero-bg")
299
+ if hero_bg is None:
300
+ raise AssertionError("Hero .hero-bg not found after patch.")
301
+ style = hero_bg.get("style") or ""
302
+ if "https://example.com/hero-b.jpg" not in style:
303
+ raise AssertionError(f"Hero bg not patched. style={style!r}")
304
+
305
+ # Section title + intro checks (dynamic)
306
+ sec_tag = soup.select_one(f"section#{target_id}")
307
+ if sec_tag is None:
308
+ raise AssertionError(f"Target section '{target_id}' not found after patch.")
309
+ h2 = sec_tag.find("h2")
310
+ if h2 is None:
311
+ raise AssertionError(f"Target section '{target_id}' has no <h2> after patch.")
312
+ if h2.get_text(strip=True) != target_sec["title"]:
313
+ raise AssertionError(f"Target section title not patched. Got: {h2.get_text(strip=True)!r}")
314
+
315
+ p_after = h2.find_next_sibling()
316
+ intro_text = p_after.get_text(strip=True) if (p_after is not None and p_after.name == "p") else ""
317
+ if intro_text != "This intro paragraph was patched successfully.":
318
+ # fallback: accept if the text exists somewhere in the section
319
+ if "This intro paragraph was patched successfully." not in sec_tag.get_text(" ", strip=True):
320
+ raise AssertionError(f"Target section intro not patched. Got: {intro_text!r}")
321
+
322
+ # Item patch checks (dynamic)
323
+ items_sec_tag = soup.select_one(f"section#{items_sec_id}")
324
+ if items_sec_tag is None:
325
+ raise AssertionError(f"Items section '{items_sec_id}' not found after patch.")
326
+ card_h3 = items_sec_tag.select_one(".card h3") or items_sec_tag.find("h3")
327
+ if card_h3 is None:
328
+ raise AssertionError(f"Items section '{items_sec_id}' has no item <h3> to assert.")
329
+ if card_h3.get_text(strip=True) != items_list[0]["title"]:
330
+ raise AssertionError(f"First item title not patched. Got: {card_h3.get_text(strip=True)!r}")
331
+
332
+ # Optional: re-validate anchors still OK after patch
333
+ post_issues = validate_compiled_html(patched_html, mutated)
334
+ post_errors = [i for i in post_issues if i.level == "error"]
335
+ if post_errors:
336
+ print("Post-patch HTML errors:")
337
+ print(_issues_to_text(post_errors))
338
+ return 4
339
+
340
+ if verbose:
341
+ print("✅ Self-test passed.")
342
+ return 0
343
+
344
+ except AssertionError as e:
345
+ print(f"❌ Self-test assertion failed: {e}")
346
+ return 10
347
+ except Exception as e:
348
+ print(f"❌ Self-test crashed: {type(e).__name__}: {e}")
349
+ return 11
350
+
351
+
352
+ if __name__ == "__main__":
353
+ import argparse
354
+ parser = argparse.ArgumentParser(description="SyntaxMatrix page template self-test")
355
+ parser.add_argument("--fixture", default=None, help="Path to fixture JSON (optional)")
356
+ parser.add_argument("--quiet", action="store_true", help="Less output")
357
+ args = parser.parse_args()
358
+
359
+ code = run_page_template_selftest(args.fixture, verbose=not args.quiet)
360
+ raise SystemExit(code)
@@ -0,0 +1,28 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from syntaxmatrix.project_root import detect_project_root
4
+
5
+
6
+ def getenv_api_key(client_dir, key_name):
7
+ _CLIENT_DOTENV_PATH = os.path.join(str(client_dir.parent), ".env")
8
+ if os.path.isfile(_CLIENT_DOTENV_PATH):
9
+ load_dotenv(_CLIENT_DOTENV_PATH, override=True)
10
+
11
+ try:
12
+ return os.getenv(key_name)
13
+ except Exception:
14
+ pass
15
+
16
+
17
+ def read_client_file(client_dir, client_file):
18
+ client_file_path = os.path.join(str(client_dir.parent), client_file)
19
+ try:
20
+ with open(client_file_path, 'r') as f:
21
+ content = f.read()
22
+ return content
23
+ except Exception as e:
24
+ pass
25
+
26
+ # def client_media_path(client_dir, client_localassets_file):
27
+ # client_asset = os.path.join(str(client_dir.parent), client_localassets_file)
28
+ # return client_asset