syntaxmatrix 2.6.4.2__py3-none-any.whl → 2.6.4.4__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/core.py +32 -5
- syntaxmatrix/db.py +77 -0
- syntaxmatrix/db_backends/__init__.py +1 -0
- syntaxmatrix/db_backends/postgres_backend.py +14 -0
- syntaxmatrix/db_backends/sqlite_backend.py +258 -0
- syntaxmatrix/db_contract.py +71 -0
- syntaxmatrix/plugin_manager.py +114 -0
- syntaxmatrix/premium/__init__.py +10 -0
- syntaxmatrix/premium/gate.py +107 -0
- syntaxmatrix/routes.py +10 -11
- syntaxmatrix/settings/model_map.py +30 -30
- {syntaxmatrix-2.6.4.2.dist-info → syntaxmatrix-2.6.4.4.dist-info}/METADATA +3 -2
- {syntaxmatrix-2.6.4.2.dist-info → syntaxmatrix-2.6.4.4.dist-info}/RECORD +16 -9
- {syntaxmatrix-2.6.4.2.dist-info → syntaxmatrix-2.6.4.4.dist-info}/WHEEL +0 -0
- {syntaxmatrix-2.6.4.2.dist-info → syntaxmatrix-2.6.4.4.dist-info}/licenses/LICENSE.txt +0 -0
- {syntaxmatrix-2.6.4.2.dist-info → syntaxmatrix-2.6.4.4.dist-info}/top_level.txt +0 -0
syntaxmatrix/core.py
CHANGED
|
@@ -34,6 +34,10 @@ from syntaxmatrix.settings.prompts import(
|
|
|
34
34
|
)
|
|
35
35
|
from syntaxmatrix.settings.client_items import read_client_file, getenv_api_key
|
|
36
36
|
|
|
37
|
+
from .premium import FeatureGate
|
|
38
|
+
from .plugin_manager import PluginManager
|
|
39
|
+
|
|
40
|
+
|
|
37
41
|
# ──────── framework‐local storage paths ────────
|
|
38
42
|
# this ensures the key & data always live under the package dir,
|
|
39
43
|
_CLIENT_DIR = detect_project_root()
|
|
@@ -127,6 +131,18 @@ class SyntaxMUI:
|
|
|
127
131
|
self.is_streaming = False
|
|
128
132
|
self.stream_args = {}
|
|
129
133
|
self._apply_feature_flags_from_db()
|
|
134
|
+
# Premium (entitlements + plugins). Safe no-op unless configured.
|
|
135
|
+
try:
|
|
136
|
+
self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR, db=db)
|
|
137
|
+
except Exception:
|
|
138
|
+
self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
self.plugins = PluginManager(self, gate=self.feature_gate, db=db)
|
|
142
|
+
self.plugins.load_all()
|
|
143
|
+
except Exception:
|
|
144
|
+
# Never break app boot because of premium plumbing
|
|
145
|
+
self.plugins = PluginManager(self)
|
|
130
146
|
|
|
131
147
|
self._recent_visual_summaries = []
|
|
132
148
|
|
|
@@ -345,8 +361,8 @@ class SyntaxMUI:
|
|
|
345
361
|
return str(v or "").strip().lower() in ("1", "true", "yes", "on")
|
|
346
362
|
|
|
347
363
|
try:
|
|
348
|
-
stream_v = db.get_setting("feature.stream_mode", "
|
|
349
|
-
user_files_v = db.get_setting("feature.user_files", "
|
|
364
|
+
stream_v = db.get_setting("feature.stream_mode", "1")
|
|
365
|
+
user_files_v = db.get_setting("feature.user_files", "1")
|
|
350
366
|
|
|
351
367
|
self.is_streaming = _truthy(stream_v)
|
|
352
368
|
self.user_files_enabled = _truthy(user_files_v)
|
|
@@ -887,7 +903,12 @@ class SyntaxMUI:
|
|
|
887
903
|
if not self.chat_profile:
|
|
888
904
|
chat_profile = _prof.get_profile("chat") or _prof.get_profile("admin")
|
|
889
905
|
if not chat_profile:
|
|
890
|
-
yield """
|
|
906
|
+
yield """
|
|
907
|
+
<p style='color:red;'>
|
|
908
|
+
Error!<br>
|
|
909
|
+
Chat profile is not configured. Add a chat profile inside the admin panel.
|
|
910
|
+
To do that, you must login first or contact your administrator.
|
|
911
|
+
</p>
|
|
891
912
|
"""
|
|
892
913
|
return None
|
|
893
914
|
self.chat_profile = chat_profile
|
|
@@ -967,14 +988,20 @@ class SyntaxMUI:
|
|
|
967
988
|
except Exception as e:
|
|
968
989
|
yield f"Error during streaming: {type(e).__name__}: {e}"
|
|
969
990
|
|
|
991
|
+
|
|
970
992
|
def process_query(self, query, context, history, stream=False):
|
|
971
993
|
|
|
972
994
|
if not self.chat_profile:
|
|
973
995
|
chat_profile = _prof.get_profile("chat") or _prof.get_profile("admin")
|
|
974
996
|
if not chat_profile:
|
|
975
|
-
|
|
997
|
+
yield """
|
|
998
|
+
<p style='color:red;'>
|
|
999
|
+
Error!<br>
|
|
1000
|
+
Chat profile is not configured. Add a chat profile inside the admin panel.
|
|
1001
|
+
To do that, you must login first or contact your administrator.
|
|
1002
|
+
</p>
|
|
976
1003
|
"""
|
|
977
|
-
|
|
1004
|
+
return None
|
|
978
1005
|
|
|
979
1006
|
self.chat_profile = chat_profile
|
|
980
1007
|
self.chat_profile['client'] = _prof.get_client(chat_profile)
|
syntaxmatrix/db.py
CHANGED
|
@@ -6,6 +6,8 @@ from werkzeug.utils import secure_filename
|
|
|
6
6
|
from syntaxmatrix.project_root import detect_project_root
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
_SMX_DB_PROVIDER = (os.environ.get("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
10
|
+
|
|
9
11
|
_CLIENT_DIR = detect_project_root()
|
|
10
12
|
DB_PATH = os.path.join(_CLIENT_DIR, "data", "syntaxmatrix.db")
|
|
11
13
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
@@ -624,3 +626,78 @@ def get_setting(key: str, default: str | None = None) -> str | None:
|
|
|
624
626
|
return row[0] if row else default
|
|
625
627
|
finally:
|
|
626
628
|
conn.close()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ============================================================================
|
|
632
|
+
# Optional DB backend override (Premium hook)
|
|
633
|
+
#
|
|
634
|
+
# Default behaviour (no env vars): SQLite stays in use exactly as before.
|
|
635
|
+
#
|
|
636
|
+
# To enable a premium backend (e.g. Postgres), set:
|
|
637
|
+
# SMX_DB_PROVIDER=postgres
|
|
638
|
+
#
|
|
639
|
+
# Optional:
|
|
640
|
+
# SMX_DB_BACKEND_MODULE=syntaxmatrix_premium.db_backends.postgres_backend
|
|
641
|
+
#
|
|
642
|
+
# The backend module should either expose:
|
|
643
|
+
# - install(target_globals: dict) -> None (preferred)
|
|
644
|
+
# OR export a compatible surface (functions/constants) that will be copied
|
|
645
|
+
# into this module namespace.
|
|
646
|
+
# ============================================================================
|
|
647
|
+
|
|
648
|
+
def _smx_apply_optional_backend_override() -> None:
|
|
649
|
+
import os
|
|
650
|
+
import importlib
|
|
651
|
+
|
|
652
|
+
provider = (os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
653
|
+
if provider in ("", "sqlite", "sqlite3"):
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
# Pick backend module (allow override for custom installations)
|
|
657
|
+
mod_name = (os.getenv("SMX_DB_BACKEND_MODULE") or "").strip()
|
|
658
|
+
if not mod_name:
|
|
659
|
+
if provider in ("postgres", "postgresql", "pg"):
|
|
660
|
+
mod_name = "syntaxmatrix.db_backends.postgres_backend"
|
|
661
|
+
else:
|
|
662
|
+
# Convention: syntaxmatrix.db_backends.<provider>_backend
|
|
663
|
+
mod_name = f"syntaxmatrix.db_backends.{provider}_backend"
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
mod = importlib.import_module(mod_name)
|
|
667
|
+
except Exception as e:
|
|
668
|
+
raise RuntimeError(
|
|
669
|
+
f"SMX_DB_PROVIDER='{provider}' requested, but backend module '{mod_name}' "
|
|
670
|
+
f"could not be imported. Install the premium backend package (or set "
|
|
671
|
+
f"SMX_DB_BACKEND_MODULE) and try again. Underlying error: {e}"
|
|
672
|
+
) from e
|
|
673
|
+
|
|
674
|
+
installer = getattr(mod, "install", None)
|
|
675
|
+
if callable(installer):
|
|
676
|
+
installer(globals())
|
|
677
|
+
else:
|
|
678
|
+
names = getattr(mod, "__all__", None)
|
|
679
|
+
if not names:
|
|
680
|
+
names = [n for n in dir(mod) if not n.startswith("_")]
|
|
681
|
+
for n in names:
|
|
682
|
+
globals()[n] = getattr(mod, n)
|
|
683
|
+
|
|
684
|
+
# Helpful introspection
|
|
685
|
+
globals()["_SMX_DB_PROVIDER"] = provider
|
|
686
|
+
globals()["_SMX_DB_BACKEND_MODULE"] = mod_name
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# Apply on import
|
|
690
|
+
_smx_apply_optional_backend_override()
|
|
691
|
+
|
|
692
|
+
# If a non-SQLite backend was requested, validate the required API surface now.
|
|
693
|
+
try:
|
|
694
|
+
import os as _os
|
|
695
|
+
_provider = (globals().get("_SMX_DB_PROVIDER") or _os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
|
|
696
|
+
|
|
697
|
+
if _provider not in ("", "sqlite", "sqlite3"):
|
|
698
|
+
from .db_contract import assert_backend_implements_core_api as _assert_backend_implements_core_api
|
|
699
|
+
_assert_backend_implements_core_api(globals(), provider=_provider)
|
|
700
|
+
except Exception:
|
|
701
|
+
# Fail fast with a clear error; this is intentional for premium backends.
|
|
702
|
+
raise
|
|
703
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DB backends package
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# syntaxmatrix/db_backends/postgres_backend.py
|
|
2
|
+
"""
|
|
3
|
+
PostgreSQL backend
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def install(ns: dict) -> None:
|
|
7
|
+
raise RuntimeError(
|
|
8
|
+
"Postgres backend is not available in the free tier.\n\n"
|
|
9
|
+
"You set SMX_DB_PROVIDER=postgres, but the premium Postgres backend package "
|
|
10
|
+
"is not installed.\n\n"
|
|
11
|
+
"Fix:\n"
|
|
12
|
+
"- Install the SyntaxMatrix premium Postgres backend package, then restart.\n"
|
|
13
|
+
"- Or set SMX_DB_PROVIDER=sqlite to use the built-in SQLite backend.\n"
|
|
14
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sqlite3
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from werkzeug.utils import secure_filename
|
|
6
|
+
from syntaxmatrix.project_root import detect_project_root
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_CLIENT_DIR = detect_project_root()
|
|
10
|
+
DB_PATH = os.path.join(_CLIENT_DIR, "data", "syntaxmatrix.db")
|
|
11
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
12
|
+
|
|
13
|
+
TEMPLATES_DIR = os.path.join(_CLIENT_DIR, "templates")
|
|
14
|
+
os.makedirs(TEMPLATES_DIR, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ------------ Utils ------------
|
|
18
|
+
def connect_db():
|
|
19
|
+
conn = sqlite3.connect(DB_PATH)
|
|
20
|
+
conn.row_factory = sqlite3.Row
|
|
21
|
+
return conn
|
|
22
|
+
|
|
23
|
+
def _col_exists(conn, table: str, col: str) -> bool:
|
|
24
|
+
cur = conn.execute(f"PRAGMA table_info({table})")
|
|
25
|
+
cols = [r["name"] for r in cur.fetchall()]
|
|
26
|
+
return col in cols
|
|
27
|
+
|
|
28
|
+
def _ensure_column(conn, table: str, col: str, col_sql: str):
|
|
29
|
+
if not _col_exists(conn, table, col):
|
|
30
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_sql}")
|
|
31
|
+
|
|
32
|
+
def _ensure_index(conn, idx_sql: str):
|
|
33
|
+
try:
|
|
34
|
+
conn.execute(idx_sql)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ------------ Schema init ------------
|
|
40
|
+
def init_db():
|
|
41
|
+
conn = connect_db()
|
|
42
|
+
conn.execute("""
|
|
43
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
44
|
+
name TEXT PRIMARY KEY,
|
|
45
|
+
content TEXT NOT NULL,
|
|
46
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
48
|
+
)
|
|
49
|
+
""")
|
|
50
|
+
conn.execute("""
|
|
51
|
+
CREATE TABLE IF NOT EXISTS page_layouts (
|
|
52
|
+
name TEXT PRIMARY KEY,
|
|
53
|
+
layout_json TEXT NOT NULL,
|
|
54
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
55
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
56
|
+
)
|
|
57
|
+
""")
|
|
58
|
+
conn.execute("""
|
|
59
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
60
|
+
key TEXT PRIMARY KEY,
|
|
61
|
+
value TEXT NOT NULL,
|
|
62
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
63
|
+
)
|
|
64
|
+
""")
|
|
65
|
+
conn.execute("""
|
|
66
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
action TEXT NOT NULL,
|
|
69
|
+
subject TEXT,
|
|
70
|
+
meta TEXT,
|
|
71
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
72
|
+
)
|
|
73
|
+
""")
|
|
74
|
+
|
|
75
|
+
_init_media_assets_table(conn)
|
|
76
|
+
|
|
77
|
+
conn.commit()
|
|
78
|
+
conn.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------ Pages ------------
|
|
82
|
+
def get_pages():
|
|
83
|
+
conn = connect_db()
|
|
84
|
+
cur = conn.execute("SELECT name FROM pages ORDER BY name")
|
|
85
|
+
rows = [r["name"] for r in cur.fetchall()]
|
|
86
|
+
conn.close()
|
|
87
|
+
return rows
|
|
88
|
+
|
|
89
|
+
def get_page(name: str):
|
|
90
|
+
conn = connect_db()
|
|
91
|
+
cur = conn.execute("SELECT name, content FROM pages WHERE name = ?", (name,))
|
|
92
|
+
row = cur.fetchone()
|
|
93
|
+
conn.close()
|
|
94
|
+
return dict(row) if row else None
|
|
95
|
+
|
|
96
|
+
def add_page(name: str, content: str):
|
|
97
|
+
conn = connect_db()
|
|
98
|
+
conn.execute("INSERT OR REPLACE INTO pages (name, content) VALUES (?, ?)", (name, content))
|
|
99
|
+
conn.commit()
|
|
100
|
+
conn.close()
|
|
101
|
+
|
|
102
|
+
def update_page(name: str, content: str):
|
|
103
|
+
conn = connect_db()
|
|
104
|
+
conn.execute("UPDATE pages SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?", (content, name))
|
|
105
|
+
conn.commit()
|
|
106
|
+
conn.close()
|
|
107
|
+
|
|
108
|
+
def delete_page(name: str):
|
|
109
|
+
conn = connect_db()
|
|
110
|
+
conn.execute("DELETE FROM pages WHERE name = ?", (name,))
|
|
111
|
+
conn.execute("DELETE FROM page_layouts WHERE name = ?", (name,))
|
|
112
|
+
conn.commit()
|
|
113
|
+
conn.close()
|
|
114
|
+
|
|
115
|
+
def rename_page(old_name: str, new_name: str):
|
|
116
|
+
conn = connect_db()
|
|
117
|
+
conn.execute("UPDATE pages SET name = ? WHERE name = ?", (new_name, old_name))
|
|
118
|
+
conn.execute("UPDATE page_layouts SET name = ? WHERE name = ?", (new_name, old_name))
|
|
119
|
+
conn.commit()
|
|
120
|
+
conn.close()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ------------ Layouts ------------
|
|
124
|
+
def upsert_page_layout(name: str, layout_json: str):
|
|
125
|
+
conn = connect_db()
|
|
126
|
+
conn.execute(
|
|
127
|
+
"INSERT OR REPLACE INTO page_layouts (name, layout_json) VALUES (?, ?)",
|
|
128
|
+
(name, layout_json)
|
|
129
|
+
)
|
|
130
|
+
conn.commit()
|
|
131
|
+
conn.close()
|
|
132
|
+
|
|
133
|
+
def get_page_layout(name: str):
|
|
134
|
+
conn = connect_db()
|
|
135
|
+
cur = conn.execute("SELECT name, layout_json FROM page_layouts WHERE name = ?", (name,))
|
|
136
|
+
row = cur.fetchone()
|
|
137
|
+
conn.close()
|
|
138
|
+
return dict(row) if row else None
|
|
139
|
+
|
|
140
|
+
def get_all_page_layouts():
|
|
141
|
+
conn = connect_db()
|
|
142
|
+
cur = conn.execute("SELECT name, layout_json FROM page_layouts ORDER BY name")
|
|
143
|
+
rows = [dict(r) for r in cur.fetchall()]
|
|
144
|
+
conn.close()
|
|
145
|
+
return rows
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ------------ Settings ------------
|
|
149
|
+
def get_setting(key: str, default=None):
|
|
150
|
+
conn = connect_db()
|
|
151
|
+
cur = conn.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
|
152
|
+
row = cur.fetchone()
|
|
153
|
+
conn.close()
|
|
154
|
+
if not row:
|
|
155
|
+
return default
|
|
156
|
+
try:
|
|
157
|
+
return json.loads(row["value"])
|
|
158
|
+
except Exception:
|
|
159
|
+
return row["value"]
|
|
160
|
+
|
|
161
|
+
def set_setting(key: str, value):
|
|
162
|
+
conn = connect_db()
|
|
163
|
+
conn.execute(
|
|
164
|
+
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
|
|
165
|
+
(key, json.dumps(value))
|
|
166
|
+
)
|
|
167
|
+
conn.commit()
|
|
168
|
+
conn.close()
|
|
169
|
+
|
|
170
|
+
def delete_setting(key: str):
|
|
171
|
+
conn = connect_db()
|
|
172
|
+
conn.execute("DELETE FROM settings WHERE key = ?", (key,))
|
|
173
|
+
conn.commit()
|
|
174
|
+
conn.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ------------ Audit log ------------
|
|
178
|
+
def audit(action: str, subject: str = "", meta: dict | None = None):
|
|
179
|
+
conn = connect_db()
|
|
180
|
+
conn.execute(
|
|
181
|
+
"INSERT INTO audit_log (action, subject, meta) VALUES (?, ?, ?)",
|
|
182
|
+
(action, subject, json.dumps(meta or {}))
|
|
183
|
+
)
|
|
184
|
+
conn.commit()
|
|
185
|
+
conn.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ------------ Media assets (dedupe + metadata) ------------
|
|
189
|
+
MEDIA_DIR = os.path.join(_CLIENT_DIR, "uploads", "media")
|
|
190
|
+
os.makedirs(MEDIA_DIR, exist_ok=True)
|
|
191
|
+
|
|
192
|
+
MEDIA_IMAGES_DIR = os.path.join(MEDIA_DIR, "images")
|
|
193
|
+
os.makedirs(MEDIA_IMAGES_DIR, exist_ok=True)
|
|
194
|
+
|
|
195
|
+
MEDIA_THUMBS_DIR = os.path.join(MEDIA_DIR, "thumbs")
|
|
196
|
+
os.makedirs(MEDIA_THUMBS_DIR, exist_ok=True)
|
|
197
|
+
|
|
198
|
+
def _init_media_assets_table(conn):
|
|
199
|
+
conn.execute("""
|
|
200
|
+
CREATE TABLE IF NOT EXISTS media_assets (
|
|
201
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
202
|
+
kind TEXT NOT NULL DEFAULT 'image',
|
|
203
|
+
rel_path TEXT NOT NULL UNIQUE,
|
|
204
|
+
thumb_path TEXT,
|
|
205
|
+
sha256 TEXT,
|
|
206
|
+
dhash TEXT,
|
|
207
|
+
width INTEGER,
|
|
208
|
+
height INTEGER,
|
|
209
|
+
bytes INTEGER,
|
|
210
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
211
|
+
)
|
|
212
|
+
""")
|
|
213
|
+
_ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_kind ON media_assets(kind)")
|
|
214
|
+
_ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_sha256 ON media_assets(sha256)")
|
|
215
|
+
_ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_dhash ON media_assets(dhash)")
|
|
216
|
+
|
|
217
|
+
def upsert_media_asset(kind: str, rel_path: str, thumb_path: str | None = None,
|
|
218
|
+
sha256: str | None = None, dhash: str | None = None,
|
|
219
|
+
width: int | None = None, height: int | None = None, bytes_: int | None = None):
|
|
220
|
+
conn = connect_db()
|
|
221
|
+
_init_media_assets_table(conn)
|
|
222
|
+
conn.execute(
|
|
223
|
+
"""
|
|
224
|
+
INSERT INTO media_assets (kind, rel_path, thumb_path, sha256, dhash, width, height, bytes)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
226
|
+
ON CONFLICT(rel_path) DO UPDATE SET
|
|
227
|
+
kind=excluded.kind,
|
|
228
|
+
thumb_path=excluded.thumb_path,
|
|
229
|
+
sha256=excluded.sha256,
|
|
230
|
+
dhash=excluded.dhash,
|
|
231
|
+
width=excluded.width,
|
|
232
|
+
height=excluded.height,
|
|
233
|
+
bytes=excluded.bytes
|
|
234
|
+
""",
|
|
235
|
+
(kind, rel_path, thumb_path, sha256, dhash, width, height, bytes_)
|
|
236
|
+
)
|
|
237
|
+
conn.commit()
|
|
238
|
+
conn.close()
|
|
239
|
+
|
|
240
|
+
def get_media_asset_by_rel_path(rel_path: str):
|
|
241
|
+
conn = connect_db()
|
|
242
|
+
cur = conn.execute("SELECT * FROM media_assets WHERE rel_path = ?", (rel_path,))
|
|
243
|
+
row = cur.fetchone()
|
|
244
|
+
conn.close()
|
|
245
|
+
return dict(row) if row else None
|
|
246
|
+
|
|
247
|
+
def list_media_assets(kind: str = "image"):
|
|
248
|
+
conn = connect_db()
|
|
249
|
+
cur = conn.execute("SELECT * FROM media_assets WHERE kind = ? ORDER BY id DESC", (kind,))
|
|
250
|
+
rows = [dict(r) for r in cur.fetchall()]
|
|
251
|
+
conn.close()
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
def normalise_media_filename(filename: str) -> str:
|
|
255
|
+
filename = secure_filename(filename or "file")
|
|
256
|
+
if not filename:
|
|
257
|
+
filename = "file"
|
|
258
|
+
return filename
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# syntaxmatrix/db_contract.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Dict, Iterable, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Keep this list tight: only functions that the framework truly depends on.
|
|
8
|
+
# If we add more later, we do it deliberately.
|
|
9
|
+
CORE_REQUIRED_FUNCTIONS = (
|
|
10
|
+
# Pages
|
|
11
|
+
"get_pages",
|
|
12
|
+
"get_page_html",
|
|
13
|
+
"add_page",
|
|
14
|
+
"update_page",
|
|
15
|
+
"delete_page",
|
|
16
|
+
|
|
17
|
+
# Secrets
|
|
18
|
+
"get_secrets",
|
|
19
|
+
"set_secret",
|
|
20
|
+
"delete_secret",
|
|
21
|
+
|
|
22
|
+
# Nav
|
|
23
|
+
"get_nav_links",
|
|
24
|
+
"set_nav_links",
|
|
25
|
+
|
|
26
|
+
# Page layouts (builder)
|
|
27
|
+
"get_page_layout",
|
|
28
|
+
"upsert_page_layout",
|
|
29
|
+
"delete_page_layout",
|
|
30
|
+
|
|
31
|
+
# Media library
|
|
32
|
+
"add_media_file",
|
|
33
|
+
"list_media_files",
|
|
34
|
+
"delete_media_file",
|
|
35
|
+
|
|
36
|
+
# Generic settings (used by branding + profiles + other admin toggles)
|
|
37
|
+
"get_setting",
|
|
38
|
+
"set_setting",
|
|
39
|
+
|
|
40
|
+
# Optional: some backends may want init, but we do not force it here.
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def assert_backend_implements_core_api(ns: Dict[str, object], *, provider: str = "") -> None:
|
|
45
|
+
"""
|
|
46
|
+
Validate that the loaded backend provides the minimum required surface.
|
|
47
|
+
|
|
48
|
+
`ns` is usually `globals()` from syntaxmatrix.db (the facade module).
|
|
49
|
+
|
|
50
|
+
We keep this strict for non-SQLite providers, because:
|
|
51
|
+
- SQLite is the built-in reference backend.
|
|
52
|
+
- Premium/Cloud backends must be complete, or we fail fast with a clear error.
|
|
53
|
+
"""
|
|
54
|
+
missing = []
|
|
55
|
+
for fn in CORE_REQUIRED_FUNCTIONS:
|
|
56
|
+
obj = ns.get(fn)
|
|
57
|
+
if not callable(obj):
|
|
58
|
+
missing.append(fn)
|
|
59
|
+
|
|
60
|
+
if missing:
|
|
61
|
+
prov = provider or "unknown"
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"SyntaxMatrix DB backend validation failed.\n"
|
|
64
|
+
f"Provider: {prov}\n"
|
|
65
|
+
"Missing required functions:\n"
|
|
66
|
+
f" - " + "\n - ".join(missing) + "\n\n"
|
|
67
|
+
"Fix:\n"
|
|
68
|
+
"- If you are using the premium Postgres backend, ensure the premium package is installed\n"
|
|
69
|
+
" and that your backend module's install(ns) correctly injects these functions.\n"
|
|
70
|
+
"- If you are writing your own backend, implement the missing functions.\n"
|
|
71
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _safe_json_loads(raw: str, *, default: Any) -> Any:
|
|
11
|
+
try:
|
|
12
|
+
return json.loads(raw)
|
|
13
|
+
except Exception:
|
|
14
|
+
return default
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PluginSpec:
|
|
19
|
+
"""A single plugin to load.
|
|
20
|
+
|
|
21
|
+
module: python module path (e.g. 'syntaxmatrix_premium_cloud_db')
|
|
22
|
+
name: entitlement key (e.g. 'cloud_db')
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
module: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginManager:
|
|
30
|
+
"""Loads optional plugins (typically premium) in a controlled, safe way."""
|
|
31
|
+
|
|
32
|
+
ENV_PLUGINS = "SMX_PREMIUM_PLUGINS" # JSON list of {name,module}
|
|
33
|
+
DB_PLUGINS_KEY = "premium.plugins" # JSON list of {name,module}
|
|
34
|
+
|
|
35
|
+
def __init__(self, smx: object, *, gate: Optional[object] = None, db: Optional[object] = None):
|
|
36
|
+
self._smx = smx
|
|
37
|
+
self._gate = gate
|
|
38
|
+
self._db = db
|
|
39
|
+
self.loaded: Dict[str, str] = {} # name -> module
|
|
40
|
+
self.errors: List[str] = []
|
|
41
|
+
|
|
42
|
+
def _specs_from_env(self) -> List[PluginSpec]:
|
|
43
|
+
raw = os.environ.get(self.ENV_PLUGINS, "").strip()
|
|
44
|
+
if not raw:
|
|
45
|
+
return []
|
|
46
|
+
data = _safe_json_loads(raw, default=[])
|
|
47
|
+
return self._coerce_specs(data)
|
|
48
|
+
|
|
49
|
+
def _specs_from_db(self) -> List[PluginSpec]:
|
|
50
|
+
if not self._db:
|
|
51
|
+
return []
|
|
52
|
+
get_setting = getattr(self._db, "get_setting", None)
|
|
53
|
+
if not callable(get_setting):
|
|
54
|
+
return []
|
|
55
|
+
raw = get_setting(self.DB_PLUGINS_KEY, "[]")
|
|
56
|
+
data = _safe_json_loads(str(raw or "[]"), default=[])
|
|
57
|
+
return self._coerce_specs(data)
|
|
58
|
+
|
|
59
|
+
def _coerce_specs(self, data: Any) -> List[PluginSpec]:
|
|
60
|
+
out: List[PluginSpec] = []
|
|
61
|
+
if isinstance(data, list):
|
|
62
|
+
for row in data:
|
|
63
|
+
if not isinstance(row, dict):
|
|
64
|
+
continue
|
|
65
|
+
name = str(row.get("name") or "").strip()
|
|
66
|
+
module = str(row.get("module") or "").strip()
|
|
67
|
+
if name and module:
|
|
68
|
+
out.append(PluginSpec(name=name, module=module))
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
def _entitled(self, name: str) -> bool:
|
|
72
|
+
if not self._gate:
|
|
73
|
+
return True # if no gate configured, don't block
|
|
74
|
+
enabled = getattr(self._gate, "enabled", None)
|
|
75
|
+
if not callable(enabled):
|
|
76
|
+
return True
|
|
77
|
+
try:
|
|
78
|
+
return bool(enabled(name))
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def load_all(self) -> Tuple[Dict[str, str], List[str]]:
|
|
83
|
+
"""Load all configured plugins. Returns (loaded, errors)."""
|
|
84
|
+
specs = self._specs_from_env()
|
|
85
|
+
if not specs:
|
|
86
|
+
specs = self._specs_from_db()
|
|
87
|
+
|
|
88
|
+
for spec in specs:
|
|
89
|
+
if not self._entitled(spec.name):
|
|
90
|
+
continue
|
|
91
|
+
if spec.name in self.loaded:
|
|
92
|
+
continue
|
|
93
|
+
self._load_one(spec)
|
|
94
|
+
|
|
95
|
+
return self.loaded, self.errors
|
|
96
|
+
|
|
97
|
+
def _load_one(self, spec: PluginSpec) -> None:
|
|
98
|
+
try:
|
|
99
|
+
mod = importlib.import_module(spec.module)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.errors.append(f"{spec.name}: import failed: {e}")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Plugin contract: module exposes register(smx) -> None
|
|
105
|
+
reg = getattr(mod, "register", None)
|
|
106
|
+
if not callable(reg):
|
|
107
|
+
self.errors.append(f"{spec.name}: missing register(smx) function")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
reg(self._smx)
|
|
112
|
+
self.loaded[spec.name] = spec.module
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self.errors.append(f"{spec.name}: register() failed: {e}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Premium support.
|
|
2
|
+
|
|
3
|
+
This package contains runtime plumbing for premium features (entitlements +
|
|
4
|
+
plugin loading). The actual premium implementations should live in separate,
|
|
5
|
+
private distributions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .gate import FeatureGate
|
|
9
|
+
|
|
10
|
+
__all__ = ["FeatureGate"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _safe_json_loads(raw: str, *, default: Any) -> Any:
|
|
10
|
+
try:
|
|
11
|
+
return json.loads(raw)
|
|
12
|
+
except Exception:
|
|
13
|
+
return default
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class GateSources:
|
|
18
|
+
"""Where the gate reads entitlements from.
|
|
19
|
+
|
|
20
|
+
Precedence (highest first):
|
|
21
|
+
1) env_json
|
|
22
|
+
2) db_setting_key
|
|
23
|
+
3) licence_file
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
env_json: str = "SMX_PREMIUM_ENTITLEMENTS"
|
|
27
|
+
db_setting_key: str = "premium.entitlements"
|
|
28
|
+
licence_file_relpath: str = os.path.join("premium", "licence.json")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FeatureGate:
|
|
32
|
+
"""Runtime entitlement checks for premium features.
|
|
33
|
+
|
|
34
|
+
This is intentionally small, dependency-free, and safe:
|
|
35
|
+
- If anything fails, it returns 'not entitled' rather than crashing the app.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
client_dir: str,
|
|
42
|
+
db: Optional[object] = None,
|
|
43
|
+
sources: Optional[GateSources] = None,
|
|
44
|
+
):
|
|
45
|
+
self._client_dir = client_dir
|
|
46
|
+
self._db = db
|
|
47
|
+
self._sources = sources or GateSources()
|
|
48
|
+
self._cache: Optional[Dict[str, Any]] = None
|
|
49
|
+
|
|
50
|
+
def _load_from_env(self) -> Optional[Dict[str, Any]]:
|
|
51
|
+
raw = os.environ.get(self._sources.env_json)
|
|
52
|
+
if not raw:
|
|
53
|
+
return None
|
|
54
|
+
data = _safe_json_loads(raw, default=None)
|
|
55
|
+
return data if isinstance(data, dict) else None
|
|
56
|
+
|
|
57
|
+
def _load_from_db(self) -> Optional[Dict[str, Any]]:
|
|
58
|
+
if not self._db:
|
|
59
|
+
return None
|
|
60
|
+
get_setting = getattr(self._db, "get_setting", None)
|
|
61
|
+
if not callable(get_setting):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
raw = get_setting(self._sources.db_setting_key, "{}")
|
|
65
|
+
data = _safe_json_loads(str(raw or "{}"), default={})
|
|
66
|
+
return data if isinstance(data, dict) else None
|
|
67
|
+
|
|
68
|
+
def _load_from_file(self) -> Optional[Dict[str, Any]]:
|
|
69
|
+
p = os.path.join(self._client_dir, self._sources.licence_file_relpath)
|
|
70
|
+
if not os.path.exists(p):
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
74
|
+
data = json.load(f)
|
|
75
|
+
return data if isinstance(data, dict) else None
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def entitlements(self, *, refresh: bool = False) -> Dict[str, Any]:
|
|
80
|
+
"""Returns entitlement dict (possibly empty)."""
|
|
81
|
+
if self._cache is not None and not refresh:
|
|
82
|
+
return self._cache
|
|
83
|
+
|
|
84
|
+
ent = self._load_from_env()
|
|
85
|
+
if ent is None:
|
|
86
|
+
ent = self._load_from_db()
|
|
87
|
+
if ent is None:
|
|
88
|
+
ent = self._load_from_file()
|
|
89
|
+
if ent is None:
|
|
90
|
+
ent = {}
|
|
91
|
+
|
|
92
|
+
self._cache = ent
|
|
93
|
+
return ent
|
|
94
|
+
|
|
95
|
+
def enabled(self, key: str) -> bool:
|
|
96
|
+
"""True if entitlement exists and is truthy."""
|
|
97
|
+
key = (key or "").strip()
|
|
98
|
+
if not key:
|
|
99
|
+
return False
|
|
100
|
+
ent = self.entitlements()
|
|
101
|
+
return bool(ent.get(key))
|
|
102
|
+
|
|
103
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
104
|
+
key = (key or "").strip()
|
|
105
|
+
if not key:
|
|
106
|
+
return default
|
|
107
|
+
return self.entitlements().get(key, default)
|
syntaxmatrix/routes.py
CHANGED
|
@@ -5715,8 +5715,8 @@ def setup_routes(smx):
|
|
|
5715
5715
|
flash("Settings updated ✓")
|
|
5716
5716
|
return redirect(url_for("admin_features"))
|
|
5717
5717
|
|
|
5718
|
-
stream_mode = _truthy(db.get_setting("feature.stream_mode", "
|
|
5719
|
-
user_files = _truthy(db.get_setting("feature.user_files", "
|
|
5718
|
+
stream_mode = _truthy(db.get_setting("feature.stream_mode", "1"))
|
|
5719
|
+
user_files = _truthy(db.get_setting("feature.user_files", "1"))
|
|
5720
5720
|
|
|
5721
5721
|
return render_template(
|
|
5722
5722
|
"admin_features.html",
|
|
@@ -7964,7 +7964,6 @@ def setup_routes(smx):
|
|
|
7964
7964
|
nav = _generate_nav()
|
|
7965
7965
|
footer = footer_html()
|
|
7966
7966
|
|
|
7967
|
-
# now use render_template_string so we can drop the same head/nav/footer
|
|
7968
7967
|
return render_template_string(f"""
|
|
7969
7968
|
{head}
|
|
7970
7969
|
<body>
|
|
@@ -7982,17 +7981,17 @@ def setup_routes(smx):
|
|
|
7982
7981
|
<pre style="background:#f4f4f4;padding:1rem;
|
|
7983
7982
|
border-radius:4px;text-align:left;
|
|
7984
7983
|
overflow-x:auto;max-height:200px;">
|
|
7985
|
-
|
|
7984
|
+
{{ error_message }}
|
|
7986
7985
|
</pre>
|
|
7987
7986
|
<p>
|
|
7988
|
-
<a href="{{
|
|
7987
|
+
<a href="{{ url_for('home') }}"
|
|
7989
7988
|
style="display:inline-block;
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7989
|
+
margin-top:2rem;
|
|
7990
|
+
padding:0.75rem 1.25rem;
|
|
7991
|
+
background:#007acc;
|
|
7992
|
+
color:#fff;
|
|
7993
|
+
text-decoration:none;
|
|
7994
|
+
border-radius:4px;">
|
|
7996
7995
|
← Back to Home
|
|
7997
7996
|
</a>
|
|
7998
7997
|
</p>
|
|
@@ -5,23 +5,23 @@ import os
|
|
|
5
5
|
PROVIDERS_MODELS = {
|
|
6
6
|
#1
|
|
7
7
|
"OpenAI": [
|
|
8
|
-
"gpt-5.2",
|
|
9
|
-
"gpt-5.2-chat-latest",
|
|
10
|
-
"gpt-5.2-pro",
|
|
11
|
-
"gpt-5.1",
|
|
12
|
-
"gpt-5.1-chat-latest",
|
|
8
|
+
"gpt-5.2",
|
|
9
|
+
"gpt-5.2-chat-latest",
|
|
10
|
+
"gpt-5.2-pro",
|
|
11
|
+
"gpt-5.1",
|
|
12
|
+
"gpt-5.1-chat-latest",
|
|
13
13
|
"gpt-5.1-codex-mini",
|
|
14
|
-
"gpt-5.1-codex-max",
|
|
15
|
-
"gpt-5",
|
|
16
|
-
"gpt-5-nano",
|
|
17
|
-
"gpt-5-mini",
|
|
18
|
-
"gpt-5-pro",
|
|
19
|
-
"gpt-4.1",
|
|
20
|
-
"gpt-4.1-nano",
|
|
21
|
-
"gpt-4.1-mini",
|
|
22
|
-
"gpt-4o",
|
|
23
|
-
|
|
24
|
-
# "gpt-4o-mini-search-preview",
|
|
14
|
+
"gpt-5.1-codex-max",
|
|
15
|
+
"gpt-5",
|
|
16
|
+
"gpt-5-nano",
|
|
17
|
+
"gpt-5-mini",
|
|
18
|
+
"gpt-5-pro",
|
|
19
|
+
"gpt-4.1",
|
|
20
|
+
"gpt-4.1-nano",
|
|
21
|
+
"gpt-4.1-mini",
|
|
22
|
+
"gpt-4o",
|
|
23
|
+
"gpt-4o-mini",
|
|
24
|
+
# "gpt-4o-mini-search-preview",
|
|
25
25
|
],
|
|
26
26
|
#2
|
|
27
27
|
"Google": [
|
|
@@ -175,7 +175,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
175
175
|
# - Summarizer
|
|
176
176
|
# """,
|
|
177
177
|
|
|
178
|
-
#1.
|
|
178
|
+
#1.3 OpenAI
|
|
179
179
|
"gpt-4.1":"""
|
|
180
180
|
Model: GPT 4.1
|
|
181
181
|
Cost:
|
|
@@ -199,7 +199,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
199
199
|
- Coder
|
|
200
200
|
""",
|
|
201
201
|
|
|
202
|
-
#1.
|
|
202
|
+
#1.4 OpenAI
|
|
203
203
|
"gpt-4.1-nano":"""
|
|
204
204
|
Model: GPT 4.1 Nano
|
|
205
205
|
Cost:
|
|
@@ -226,7 +226,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
226
226
|
- ImageTexter
|
|
227
227
|
""",
|
|
228
228
|
|
|
229
|
-
#1.
|
|
229
|
+
#1.5 OpenAI
|
|
230
230
|
"gpt-4.1-mini":"""
|
|
231
231
|
Model: GPT 4.1 Mini
|
|
232
232
|
Cost:
|
|
@@ -254,7 +254,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
254
254
|
- ImageTexter
|
|
255
255
|
""",
|
|
256
256
|
|
|
257
|
-
#1.
|
|
257
|
+
#1.6 OpenAI
|
|
258
258
|
"gpt-5":"""
|
|
259
259
|
Model: GPT 5
|
|
260
260
|
Cost:
|
|
@@ -279,7 +279,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
279
279
|
- Coder
|
|
280
280
|
""",
|
|
281
281
|
|
|
282
|
-
#1.
|
|
282
|
+
#1.7 OpenAI
|
|
283
283
|
"gpt-5-nano":"""
|
|
284
284
|
Model: GPT 5 Nano
|
|
285
285
|
Cost:
|
|
@@ -307,7 +307,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
307
307
|
- ImageTexter
|
|
308
308
|
""",
|
|
309
309
|
|
|
310
|
-
#1.
|
|
310
|
+
#1.8 OpenAI
|
|
311
311
|
"gpt-5-mini":"""
|
|
312
312
|
Model: GPT 5 Mini
|
|
313
313
|
Cost:
|
|
@@ -342,7 +342,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
342
342
|
|
|
343
343
|
#1.8 OpenAI
|
|
344
344
|
|
|
345
|
-
#1.
|
|
345
|
+
#1.9 OpenAI
|
|
346
346
|
"gpt-5-pro":"""
|
|
347
347
|
Model: GPT 5 Pro
|
|
348
348
|
Cost:
|
|
@@ -367,7 +367,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
367
367
|
- Coder
|
|
368
368
|
""",
|
|
369
369
|
|
|
370
|
-
#1.
|
|
370
|
+
#1.10 OpenAI
|
|
371
371
|
"gpt-5.1":"""
|
|
372
372
|
Model: GPT 5.1
|
|
373
373
|
Cost:
|
|
@@ -393,7 +393,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
393
393
|
- Coder
|
|
394
394
|
""",
|
|
395
395
|
|
|
396
|
-
#1.
|
|
396
|
+
#1.11 OpenAI
|
|
397
397
|
"gpt-5.1-chat-latest":"""
|
|
398
398
|
Model: GPT 5.1 Chat
|
|
399
399
|
Cost:
|
|
@@ -419,7 +419,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
419
419
|
- Chat
|
|
420
420
|
""",
|
|
421
421
|
|
|
422
|
-
#1.
|
|
422
|
+
#1.12 OpenAI
|
|
423
423
|
"gpt-5.1-codex-mini":"""
|
|
424
424
|
Model: GPT 5.1 Codex Mini
|
|
425
425
|
Cost:
|
|
@@ -444,7 +444,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
444
444
|
- Coder
|
|
445
445
|
""",
|
|
446
446
|
|
|
447
|
-
#1.
|
|
447
|
+
#1.13 OpenAI
|
|
448
448
|
"gpt-5.1-codex-max":"""
|
|
449
449
|
Model: GPT 5.1 Codex Max
|
|
450
450
|
Cost:
|
|
@@ -469,7 +469,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
469
469
|
- Coder
|
|
470
470
|
""",
|
|
471
471
|
|
|
472
|
-
#1.
|
|
472
|
+
#1.14 OpenAI
|
|
473
473
|
"gpt-5.2":"""
|
|
474
474
|
Model: GPT 5.2
|
|
475
475
|
Cost:
|
|
@@ -494,7 +494,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
494
494
|
- Coder
|
|
495
495
|
""",
|
|
496
496
|
|
|
497
|
-
#1.
|
|
497
|
+
#1.15 OpenAI
|
|
498
498
|
"gpt-5.2-chat-latest":"""
|
|
499
499
|
Model: GPT 5.2 Chat
|
|
500
500
|
Cost:
|
|
@@ -519,7 +519,7 @@ MODEL_DESCRIPTIONS = {
|
|
|
519
519
|
- Admin
|
|
520
520
|
""",
|
|
521
521
|
|
|
522
|
-
#1.
|
|
522
|
+
#1.16 OpenAI
|
|
523
523
|
"gpt-5.2-pro":"""
|
|
524
524
|
Model: GPT 5.2 Pro
|
|
525
525
|
Cost:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syntaxmatrix
|
|
3
|
-
Version: 2.6.4.
|
|
3
|
+
Version: 2.6.4.4
|
|
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
|
|
7
|
+
License: MIT
|
|
7
8
|
Classifier: Programming Language :: Python :: 3.9
|
|
8
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
@@ -48,6 +48,7 @@ Dynamic: author-email
|
|
|
48
48
|
Dynamic: classifier
|
|
49
49
|
Dynamic: description
|
|
50
50
|
Dynamic: description-content-type
|
|
51
|
+
Dynamic: license
|
|
51
52
|
Dynamic: license-file
|
|
52
53
|
Dynamic: requires-dist
|
|
53
54
|
Dynamic: requires-python
|
|
@@ -2,9 +2,10 @@ 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=3c8qBAKJI2IcYd9PBZrFEwmv-c4_tfa3ebEoPa5vW7U,12428
|
|
5
|
-
syntaxmatrix/core.py,sha256=
|
|
5
|
+
syntaxmatrix/core.py,sha256=IKcvPJMbTMwv0eqkeLx4se3AU_Qx7LNNbS5sz-VWVf4,67485
|
|
6
6
|
syntaxmatrix/dataset_preprocessing.py,sha256=wtV4MWzkyfOsBHTsS0H1gqHho77ZQHGDI9skJryyZWA,8732
|
|
7
|
-
syntaxmatrix/db.py,sha256=
|
|
7
|
+
syntaxmatrix/db.py,sha256=Q3b_Dr8LifWpGQyBsA-MLc8pBQjFJbc0ox0t_sbdECE,23722
|
|
8
|
+
syntaxmatrix/db_contract.py,sha256=N7WvdTgRH87XX7K5cPSDXvNfHNsSZ4xxZO5DL5vrpVA,2141
|
|
8
9
|
syntaxmatrix/display_html.py,sha256=tBeeHcRbmAOKqRTXY0hUehThFspCDsvjW4myi2zj0iU,3568
|
|
9
10
|
syntaxmatrix/emailer.py,sha256=KazaSY0ieE5kC5nTVmh-O2N3gjfeG_oBnl4pl_UHEws,949
|
|
10
11
|
syntaxmatrix/file_processor.py,sha256=9-TT20qfhZ7Q0eWCJpdxDA54jWM9g56A6VJNFme2azY,2863
|
|
@@ -18,10 +19,11 @@ syntaxmatrix/page_builder_generation.py,sha256=EuRVsHLxYDo0SQUo9apMY0ML0-OnaZ9kD
|
|
|
18
19
|
syntaxmatrix/page_layout_contract.py,sha256=4i18ireN2wigWD3mGIA5A6RMw-0DCxnEXpKuu3UrHog,25175
|
|
19
20
|
syntaxmatrix/page_patch_publish.py,sha256=pakIo8tA2y_2zulrU7aBqbqXkmgJLc2-YwghDvYKbgs,50473
|
|
20
21
|
syntaxmatrix/plottings.py,sha256=MjHQ9T1_oC5oyr4_wkM2GJDrpjp0sbvudbs2lGaMyzk,6103
|
|
22
|
+
syntaxmatrix/plugin_manager.py,sha256=sRpm6xGU8AQSDU_bnh1aHHARwl4uqNYgYXgKlFGHYm4,3747
|
|
21
23
|
syntaxmatrix/preface.py,sha256=tCm0C0BhY_SOntQT5I7cOJr6TB5mVDAeL9i8UmHLu5g,21237
|
|
22
24
|
syntaxmatrix/profiles.py,sha256=hPg27IQjl8-Tpo3BanQQsByeAgcizqIA2I_IKKNZ0TI,2900
|
|
23
25
|
syntaxmatrix/project_root.py,sha256=1ckvbFVV1szHtHsfSCoGcImHkRwbfszmPG1kGh9ZZlE,2227
|
|
24
|
-
syntaxmatrix/routes.py,sha256=
|
|
26
|
+
syntaxmatrix/routes.py,sha256=CMBWRB7b1ju1odxWT6K7b8njkjz__f8XlX0biKSozKA,356892
|
|
25
27
|
syntaxmatrix/selftest_page_templates.py,sha256=JY1i2xu7FBkN0TIPiAXhEk_iIjdOBmfc1g9aX98iqhw,14833
|
|
26
28
|
syntaxmatrix/session.py,sha256=v0qgxnVM_LEaNvZQJSa-13Q2eiwc3RDnjd2SahNnHQk,599
|
|
27
29
|
syntaxmatrix/smiv.py,sha256=1lSN3UYpXvYoVNd6VrkY5iZuF_nDxD6xxvLnTn9wcbQ,1405
|
|
@@ -38,13 +40,18 @@ syntaxmatrix/agentic/agents.py,sha256=IVqF2VLl-80nx75FW0-RK2-2nfhbQjVwyGgplgVOea
|
|
|
38
40
|
syntaxmatrix/agentic/agents_orchestrer.py,sha256=NMC0Mr1zRxxWBr-KRZxu1iLMBJowqesNIkuNwY1AlQA,14681
|
|
39
41
|
syntaxmatrix/agentic/code_tools_registry.py,sha256=rV0sA1qf_a9A4mmJXGuLnPD6qzAtTBjVgViYpwykfRU,1489
|
|
40
42
|
syntaxmatrix/agentic/model_templates.py,sha256=A3ROE3BHkvnU9cxqSGjlCBIw9U15zRaTKgK-WxcZtUI,76033
|
|
43
|
+
syntaxmatrix/db_backends/__init__.py,sha256=ocEqHP5enZCCpurdITAucrrWbw2peihBC6AM3Ck9AAI,23
|
|
44
|
+
syntaxmatrix/db_backends/postgres_backend.py,sha256=vveUAz8Ss2KSFQRpKVxjBPpgqabw-Z5VVKHHdKxsuSU,519
|
|
45
|
+
syntaxmatrix/db_backends/sqlite_backend.py,sha256=DJ70rB7NCRpcYZ6heGnrunt9lxxFgAaidLcio-8y8U0,8384
|
|
41
46
|
syntaxmatrix/media/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
47
|
syntaxmatrix/media/media_pixabay.py,sha256=_gkntpbK7HzyLOtzoyWLjXXWtCXUFknD4hFHn_qHfRY,8195
|
|
48
|
+
syntaxmatrix/premium/__init__.py,sha256=ab4z0PWcMSbnIIqyRBHWTqoE1NyhhW5kzIHNcdhZ4J4,269
|
|
49
|
+
syntaxmatrix/premium/gate.py,sha256=-ko1x4iNZZxe6DYTZdW9Cni38NAbfdZoGJ5Co_813l0,3273
|
|
43
50
|
syntaxmatrix/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
51
|
syntaxmatrix/settings/client_items.py,sha256=jtJ2k5r_Roq42Pumor_VEsYVTIdbFVP2l61MOeMAPUc,868
|
|
45
52
|
syntaxmatrix/settings/default.yaml,sha256=BznvF1D06VMPbT6UX3MQ4zUkXxTXLnAA53aUu8G4O38,569
|
|
46
53
|
syntaxmatrix/settings/logging.py,sha256=U8iTDFv0H1ECdIzH9He2CtOVlK1x5KHCk126Zn5Vi7M,1362
|
|
47
|
-
syntaxmatrix/settings/model_map.py,sha256=
|
|
54
|
+
syntaxmatrix/settings/model_map.py,sha256=P3RsBGkq36ozADjZOY_p8bjvLgaleiZynMgl4HP1qR8,26385
|
|
48
55
|
syntaxmatrix/settings/prompts.py,sha256=Gni--SPxFfqbQXPJqkK0tdViBtAMtUBp185i5WdYux4,25888
|
|
49
56
|
syntaxmatrix/settings/string_navbar.py,sha256=NqgTzo3J9rRI4c278VG6kpoViFfmi2FKmL6sO0R-bus,83
|
|
50
57
|
syntaxmatrix/static/docs.md,sha256=rWlKjNcpS2cs5DElGNYuaA-XXdGZnRGMXx62nACvDwE,11105
|
|
@@ -78,8 +85,8 @@ syntaxmatrix/vectordb/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
78
85
|
syntaxmatrix/vectordb/adapters/milvus_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
86
|
syntaxmatrix/vectordb/adapters/pgvector_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
87
|
syntaxmatrix/vectordb/adapters/sqlite_adapter.py,sha256=L8M2qHfwZRAFVxWeurUVdHaJXz6F5xTUSWh3uy6TSUs,6035
|
|
81
|
-
syntaxmatrix-2.6.4.
|
|
82
|
-
syntaxmatrix-2.6.4.
|
|
83
|
-
syntaxmatrix-2.6.4.
|
|
84
|
-
syntaxmatrix-2.6.4.
|
|
85
|
-
syntaxmatrix-2.6.4.
|
|
88
|
+
syntaxmatrix-2.6.4.4.dist-info/licenses/LICENSE.txt,sha256=j1P8naTdy1JMxTC80XYQjbyAQnuOlpDusCUhncrvpy8,1083
|
|
89
|
+
syntaxmatrix-2.6.4.4.dist-info/METADATA,sha256=UZK3_MnFpYk0wb3AUfEJx100W6v4ra2oHM_9Wt54DyE,18201
|
|
90
|
+
syntaxmatrix-2.6.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
91
|
+
syntaxmatrix-2.6.4.4.dist-info/top_level.txt,sha256=HKP_zkl4V_nt7osC15DlacoBZktHrbZYOqf_pPkF3T8,13
|
|
92
|
+
syntaxmatrix-2.6.4.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|