syntaxmatrix 2.5.5.5__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 (44) hide show
  1. syntaxmatrix/__init__.py +3 -2
  2. syntaxmatrix/agentic/agents.py +1220 -169
  3. syntaxmatrix/agentic/agents_orchestrer.py +326 -0
  4. syntaxmatrix/agentic/code_tools_registry.py +27 -32
  5. syntaxmatrix/auth.py +142 -5
  6. syntaxmatrix/commentary.py +16 -16
  7. syntaxmatrix/core.py +192 -84
  8. syntaxmatrix/db.py +460 -4
  9. syntaxmatrix/{display.py → display_html.py} +2 -6
  10. syntaxmatrix/gpt_models_latest.py +1 -1
  11. syntaxmatrix/media/__init__.py +0 -0
  12. syntaxmatrix/media/media_pixabay.py +277 -0
  13. syntaxmatrix/models.py +1 -1
  14. syntaxmatrix/page_builder_defaults.py +183 -0
  15. syntaxmatrix/page_builder_generation.py +1122 -0
  16. syntaxmatrix/page_layout_contract.py +644 -0
  17. syntaxmatrix/page_patch_publish.py +1471 -0
  18. syntaxmatrix/preface.py +670 -0
  19. syntaxmatrix/profiles.py +28 -10
  20. syntaxmatrix/routes.py +1941 -593
  21. syntaxmatrix/selftest_page_templates.py +360 -0
  22. syntaxmatrix/settings/client_items.py +28 -0
  23. syntaxmatrix/settings/model_map.py +1022 -207
  24. syntaxmatrix/settings/prompts.py +328 -130
  25. syntaxmatrix/static/assets/hero-default.svg +22 -0
  26. syntaxmatrix/static/icons/bot-icon.png +0 -0
  27. syntaxmatrix/static/icons/favicon.png +0 -0
  28. syntaxmatrix/static/icons/logo.png +0 -0
  29. syntaxmatrix/static/icons/logo3.png +0 -0
  30. syntaxmatrix/templates/admin_branding.html +104 -0
  31. syntaxmatrix/templates/admin_features.html +63 -0
  32. syntaxmatrix/templates/admin_secretes.html +108 -0
  33. syntaxmatrix/templates/change_password.html +124 -0
  34. syntaxmatrix/templates/dashboard.html +296 -131
  35. syntaxmatrix/templates/dataset_resize.html +535 -0
  36. syntaxmatrix/templates/edit_page.html +2535 -0
  37. syntaxmatrix/utils.py +2728 -2835
  38. {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/METADATA +6 -2
  39. {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/RECORD +42 -25
  40. syntaxmatrix/generate_page.py +0 -634
  41. syntaxmatrix/static/icons/hero_bg.jpg +0 -0
  42. {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/WHEEL +0 -0
  43. {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/licenses/LICENSE.txt +0 -0
  44. {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import io
5
+ import os
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import requests
11
+ from PIL import Image
12
+
13
+
14
+ PIXABAY_API_URL = "https://pixabay.com/api/"
15
+
16
+
17
+ @dataclass
18
+ class PixabayHit:
19
+ id: int
20
+ page_url: str
21
+ tags: str
22
+ user: str
23
+ preview_url: str
24
+ webformat_url: str
25
+ large_image_url: str
26
+ width: int
27
+ height: int
28
+ image_type: str
29
+
30
+
31
+ def _clean_query(q: str) -> str:
32
+ q = (q or "").strip()
33
+ q = re.sub(r"\s+", " ", q)
34
+ return q[:100]
35
+
36
+
37
+ def pixabay_search(
38
+ api_key: str,
39
+ query: str,
40
+ *,
41
+ image_type: str = "photo",
42
+ orientation: str = "horizontal",
43
+ per_page: int = 24,
44
+ page: int = 1,
45
+ safesearch: bool = True,
46
+ editors_choice: bool = False,
47
+ min_width: int = 0,
48
+ min_height: int = 0,
49
+ timeout: int = 15,
50
+ ) -> List[PixabayHit]:
51
+ """Search Pixabay and return normalised hits."""
52
+ if not api_key:
53
+ return []
54
+
55
+ q = _clean_query(query)
56
+ if not q:
57
+ return []
58
+
59
+ per_page = max(3, min(200, int(per_page or 24)))
60
+ page = max(1, int(page or 1))
61
+
62
+ params = {
63
+ "key": api_key,
64
+ "q": q,
65
+ "image_type": image_type or "photo",
66
+ "orientation": orientation or "all",
67
+ "per_page": per_page,
68
+ "page": page,
69
+ "safesearch": "true" if safesearch else "false",
70
+ "editors_choice": "true" if editors_choice else "false",
71
+ "min_width": int(min_width or 0),
72
+ "min_height": int(min_height or 0),
73
+ "order": "popular",
74
+ }
75
+
76
+ r = requests.get(PIXABAY_API_URL, params=params, timeout=timeout)
77
+ r.raise_for_status()
78
+ data = r.json() or {}
79
+ hits = data.get("hits") or []
80
+
81
+ out: List[PixabayHit] = []
82
+ for h in hits:
83
+ try:
84
+ out.append(
85
+ PixabayHit(
86
+ id=int(h.get("id")),
87
+ page_url=str(h.get("pageURL") or ""),
88
+ tags=str(h.get("tags") or ""),
89
+ user=str(h.get("user") or ""),
90
+ preview_url=str(h.get("previewURL") or ""),
91
+ webformat_url=str(h.get("webformatURL") or ""),
92
+ large_image_url=str(h.get("largeImageURL") or ""),
93
+ width=int(h.get("imageWidth") or 0),
94
+ height=int(h.get("imageHeight") or 0),
95
+ image_type=str(h.get("type") or image_type or "photo"),
96
+ )
97
+ )
98
+ except Exception:
99
+ continue
100
+
101
+ return out
102
+
103
+
104
+ def _is_pixabay_url(url: str) -> bool:
105
+ url = (url or "").strip().lower()
106
+ return url.startswith("https://") and ("pixabay.com" in url)
107
+
108
+
109
+ def _sha256_bytes(b: bytes) -> str:
110
+ return hashlib.sha256(b).hexdigest()
111
+
112
+
113
+ def _dhash_hex(img: Image.Image, size: int = 8) -> str:
114
+ g = img.convert("L").resize((size + 1, size), Image.LANCZOS)
115
+ px = list(g.getdata())
116
+ rows = [px[i * (size + 1):(i + 1) * (size + 1)] for i in range(size)]
117
+ bits = []
118
+ for row in rows:
119
+ for x in range(size):
120
+ bits.append(1 if row[x] > row[x + 1] else 0)
121
+
122
+ val = 0
123
+ for b in bits:
124
+ val = (val << 1) | b
125
+ return f"{val:0{size*size//4}x}"
126
+
127
+
128
+ def download_and_store_image(
129
+ url: str,
130
+ *,
131
+ out_path_no_ext: str,
132
+ max_width: int = 1920,
133
+ thumb_dir: Optional[str] = None,
134
+ thumb_width: int = 800,
135
+ timeout: int = 20,
136
+ min_width: int = 0,
137
+ fallback_url: Optional[str] = None,
138
+ ) -> Dict[str, Any]:
139
+ """
140
+ Downloads an image from Pixabay.
141
+
142
+ Rule:
143
+ - Always try `url` first (webformat).
144
+ - If `min_width` is set and the downloaded image is narrower than min_width,
145
+ then try `fallback_url` (large) once, and keep it only if it's larger.
146
+ - Then resize down to max_width (1920).
147
+ - Save as JPG unless image has alpha channel (then PNG).
148
+ - Create thumbnail only if width > 800px (your rule).
149
+ """
150
+
151
+ def _fetch(u: str) -> bytes:
152
+ if not _is_pixabay_url(u):
153
+ raise ValueError("Only Pixabay URLs are allowed")
154
+ rr = requests.get(u, stream=True, timeout=timeout)
155
+ rr.raise_for_status()
156
+ return rr.content
157
+
158
+ # 1) Fetch webformat first
159
+ content = _fetch(url)
160
+ img = Image.open(io.BytesIO(content))
161
+ img.load()
162
+
163
+ # 2) If too small (e.g., hero) try large variant once
164
+ if min_width and img.width < int(min_width) and fallback_url:
165
+ try:
166
+ content2 = _fetch(fallback_url)
167
+ img2 = Image.open(io.BytesIO(content2))
168
+ img2.load()
169
+ if img2.width > img.width:
170
+ content = content2
171
+ img = img2
172
+ except Exception:
173
+ # If large fetch fails, keep webformat result
174
+ pass
175
+
176
+ # 3) Resize down to max_width (never upsample)
177
+ if img.width > int(max_width or 1920):
178
+ ratio = (int(max_width) / float(img.width))
179
+ new_h = max(1, int(round(img.height * ratio)))
180
+ img = img.resize((int(max_width), new_h), Image.LANCZOS)
181
+
182
+ # 4) Decide output format
183
+ has_alpha = ("A" in img.getbands())
184
+ ext = ".png" if has_alpha else ".jpg"
185
+ out_path = out_path_no_ext + ext
186
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
187
+
188
+ # 5) Encode + save
189
+ buf = io.BytesIO()
190
+ if ext == ".jpg":
191
+ rgb = img.convert("RGB") if img.mode != "RGB" else img
192
+ rgb.save(buf, "JPEG", quality=85, optimize=True, progressive=True)
193
+ mime = "image/jpeg"
194
+ else:
195
+ img.save(buf, "PNG", optimize=True)
196
+ mime = "image/png"
197
+
198
+ data = buf.getvalue()
199
+ with open(out_path, "wb") as f:
200
+ f.write(data)
201
+
202
+ meta = {
203
+ "file_path": out_path,
204
+ "width": img.width,
205
+ "height": img.height,
206
+ "sha256": _sha256_bytes(data),
207
+ "dhash": _dhash_hex(img),
208
+ "mime": mime,
209
+ }
210
+
211
+ # 6) Thumbnail only if width > 800px (your rule)
212
+ if thumb_dir and img.width > int(thumb_width or 800):
213
+ os.makedirs(thumb_dir, exist_ok=True)
214
+ thumb = img.copy()
215
+ ratio = (int(thumb_width) / float(thumb.width))
216
+ new_h = max(1, int(round(thumb.height * ratio)))
217
+ thumb = thumb.resize((int(thumb_width), new_h), Image.LANCZOS)
218
+
219
+ thumb_path = os.path.join(
220
+ thumb_dir,
221
+ os.path.basename(out_path_no_ext) + f"-t{int(thumb_width)}.jpg"
222
+ )
223
+
224
+ tb = io.BytesIO()
225
+ thumb_rgb = thumb.convert("RGB") if thumb.mode != "RGB" else thumb
226
+ thumb_rgb.save(tb, "JPEG", quality=82, optimize=True, progressive=True)
227
+ with open(thumb_path, "wb") as f:
228
+ f.write(tb.getvalue())
229
+
230
+ meta["thumb_path"] = thumb_path
231
+
232
+ return meta
233
+
234
+ def import_pixabay_hit(
235
+ hit: PixabayHit,
236
+ *,
237
+ media_images_dir: str,
238
+ thumbs_dir: Optional[str] = None,
239
+ max_width: int = 1920,
240
+ thumb_width: int = 800,
241
+ min_width: int = 0,
242
+ ) -> Dict[str, Any]:
243
+ # Efficient by default: use webformat first.
244
+ url = hit.webformat_url or hit.large_image_url
245
+ if not url:
246
+ raise ValueError("No image url")
247
+
248
+ if not _is_pixabay_url(url):
249
+ raise ValueError("Only Pixabay URLs are allowed")
250
+
251
+ # Only use large as fallback when webformat is too small for the use-case (e.g., hero)
252
+ fallback = None
253
+ if url == hit.webformat_url and hit.large_image_url and hit.large_image_url != hit.webformat_url:
254
+ fallback = hit.large_image_url
255
+
256
+ out_base = os.path.join(media_images_dir, f"pixabay-{hit.id}")
257
+
258
+ meta = download_and_store_image(
259
+ url,
260
+ out_path_no_ext=out_base,
261
+ max_width=max_width,
262
+ thumb_dir=thumbs_dir,
263
+ thumb_width=thumb_width,
264
+ min_width=min_width,
265
+ fallback_url=fallback,
266
+ )
267
+
268
+ meta.update(
269
+ {
270
+ "source": "pixabay",
271
+ "source_url": hit.page_url,
272
+ "author": hit.user,
273
+ "tags": hit.tags,
274
+ "pixabay_id": hit.id,
275
+ }
276
+ )
277
+ return meta
syntaxmatrix/models.py CHANGED
@@ -8,7 +8,7 @@ class Workspace(db.Model):
8
8
  name = db.Column(db.String(64), unique=True, nullable=False)
9
9
  llm_provider = db.Column(db.String(24), default="openai")
10
10
  llm_model = db.Column(db.String(48), default="gpt-3.5-turbo")
11
- llm_api_key = db.Column(db.LargeBinary) # we'll encrypt later
11
+ llm_api_key = db.Column(db.LargeBinary)
12
12
 
13
13
  def __repr__(self):
14
14
  return f"<Workspace {self.name}>"
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import html as _html
4
+ import re as _re
5
+ from typing import Any, Dict, List
6
+
7
+
8
+ def _title_from_slug(slug: str) -> str:
9
+ s = (slug or "").strip().replace("_", " ").replace("-", " ")
10
+ s = _re.sub(r"\s+", " ", s).strip()
11
+ if not s:
12
+ return "New page"
13
+ return s[:1].upper() + s[1:]
14
+
15
+
16
+ def make_default_layout(page_name: str, website_description: str = "") -> Dict[str, Any]:
17
+ """Create a starter layout JSON for the drag-and-drop page builder."""
18
+ page = (page_name or "page").strip().lower()
19
+ desc = (website_description or "").strip()
20
+
21
+ def sec(_id: str, _type: str, title: str, text: str, cols: int, items: List[Dict[str, Any]]):
22
+ return {
23
+ "id": _id,
24
+ "type": _type,
25
+ "title": title,
26
+ "text": text,
27
+ "cols": cols,
28
+ "items": items or [],
29
+ }
30
+
31
+ def item(_id: str, _type: str, title: str, text: str, image_url: str = ""):
32
+ return {
33
+ "id": _id,
34
+ "type": _type,
35
+ "title": title,
36
+ "text": text,
37
+ "imageUrl": image_url or "",
38
+ }
39
+
40
+ hero_title = _title_from_slug(page)
41
+ hero_text = desc or "Describe your offering here. Replace this text with your own message."
42
+
43
+ return {
44
+ "page": page,
45
+ "sections": [
46
+ sec(
47
+ "sec_hero",
48
+ "hero",
49
+ hero_title,
50
+ hero_text,
51
+ 1,
52
+ [
53
+ item("item_hero_1", "card", "Get started", "Add a short call-to-action here.", ""),
54
+ ],
55
+ ),
56
+ sec(
57
+ "sec_features",
58
+ "features",
59
+ "Features",
60
+ "Three quick reasons people choose you.",
61
+ 3,
62
+ [
63
+ item("item_feat_1", "card", "Fast setup", "Be up and running quickly.", ""),
64
+ item("item_feat_2", "card", "Secure by design", "Built with sensible defaults.", ""),
65
+ item("item_feat_3", "card", "Support that cares", "We help you succeed.", ""),
66
+ ],
67
+ ),
68
+ sec(
69
+ "sec_gallery",
70
+ "gallery",
71
+ "Gallery",
72
+ "Add images that show your product, team, or work.",
73
+ 3,
74
+ [
75
+ item("item_gal_1", "card", "Image", "Drop an image onto this card.", ""),
76
+ item("item_gal_2", "card", "Image", "Drop an image onto this card.", ""),
77
+ item("item_gal_3", "card", "Image", "Drop an image onto this card.", ""),
78
+ ],
79
+ ),
80
+ sec(
81
+ "sec_testimonials",
82
+ "testimonials",
83
+ "Testimonials",
84
+ "A couple of short quotes from customers.",
85
+ 2,
86
+ [
87
+ item("item_test_1", "quote", "Customer name", "“A short customer quote goes here.”", ""),
88
+ item("item_test_2", "quote", "Customer name", "“Another short quote goes here.”", ""),
89
+ ],
90
+ ),
91
+ sec(
92
+ "sec_faq",
93
+ "faq",
94
+ "FAQ",
95
+ "Answer the most common questions upfront.",
96
+ 2,
97
+ [
98
+ item("item_faq_1", "faq", "What do you offer?", "Explain in one or two sentences.", ""),
99
+ item("item_faq_2", "faq", "How do I get started?", "Tell them the first step.", ""),
100
+ ],
101
+ ),
102
+ sec(
103
+ "sec_cta",
104
+ "cta",
105
+ "Ready to talk?",
106
+ "Add a clear call-to-action with next steps.",
107
+ 2,
108
+ [
109
+ item("item_cta_1", "card", "Book a demo", "Invite people to contact you.", ""),
110
+ item("item_cta_2", "card", "Email us", "Add your preferred contact method.", ""),
111
+ ],
112
+ ),
113
+ ],
114
+ }
115
+
116
+
117
+ def _esc(s: str) -> str:
118
+ return _html.escape(s or "", quote=True)
119
+
120
+
121
+ def layout_to_html(st: Dict[str, Any]) -> str:
122
+ """Render layout JSON into HTML snippets used by /page/<page_name>."""
123
+ sections = st.get("sections") if isinstance(st, dict) else None
124
+ if not isinstance(sections, list):
125
+ return ""
126
+
127
+ blocks: List[str] = []
128
+
129
+ for s in sections:
130
+ if not isinstance(s, dict):
131
+ continue
132
+
133
+ title = _esc(s.get("title") or "")
134
+ text = _esc(s.get("text") or "")
135
+
136
+ try:
137
+ cols = int(s.get("cols") or 1)
138
+ except Exception:
139
+ cols = 1
140
+ cols = max(1, min(5, cols))
141
+
142
+ items_html: List[str] = []
143
+ for it in (s.get("items") or []):
144
+ if not isinstance(it, dict):
145
+ continue
146
+
147
+ it_title = _esc(it.get("title") or "")
148
+ it_text = _esc(it.get("text") or "")
149
+ img = (it.get("imageUrl") or "").strip()
150
+
151
+ img_html = (
152
+ f'<img src="{_esc(img)}" alt="{it_title}" style="width:100%;height:auto;border-radius:12px;">'
153
+ if img else ""
154
+ )
155
+
156
+ items_html.append(
157
+ '<div style="border:1px solid rgba(148,163,184,.25);border-radius:16px;'
158
+ 'padding:14px;background:rgba(15,23,42,.35);">'
159
+ f'{img_html}'
160
+ f'<h3 style="margin:10px 0 6px;font-size:1.05rem;">{it_title}</h3>'
161
+ f'<p style="margin:0;color:#cbd5e1;line-height:1.5;">{it_text}</p>'
162
+ '</div>'
163
+ )
164
+
165
+ grid = ""
166
+ if items_html:
167
+ joined = "\n".join(items_html)
168
+ grid = (
169
+ f'<div style="display:grid;gap:12px;grid-template-columns:repeat({cols}, minmax(0,1fr));">'
170
+ f'{joined}</div>'
171
+ )
172
+
173
+ p = f'<p style="margin:0 0 14px;color:#cbd5e1;line-height:1.55;">{text}</p>' if text else ""
174
+
175
+ blocks.append(
176
+ '<section style="max-width:1100px;margin:22px auto;padding:0 14px;">'
177
+ f'<h2 style="margin:0 0 8px;font-size:1.6rem;">{title}</h2>'
178
+ f'{p}'
179
+ f'{grid}'
180
+ '</section>'
181
+ )
182
+
183
+ return "\n\n".join(blocks)