sylo-fieldbrain 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/extensions/fieldbrain-tools.ts +495 -0
- package/extensions/index.ts +12 -0
- package/extensions/python-runner.ts +56 -0
- package/package.json +44 -0
- package/scripts/__pycache__/_db_lib.cpython-312.pyc +0 -0
- package/scripts/__pycache__/_json_out.cpython-312.pyc +0 -0
- package/scripts/__pycache__/models.cpython-312.pyc +0 -0
- package/scripts/_db_lib.py +228 -0
- package/scripts/_json_out.py +19 -0
- package/scripts/alembic/env.py +53 -0
- package/scripts/alembic/versions/001_baseline.py +38 -0
- package/scripts/alembic/versions/002_core_tables.py +429 -0
- package/scripts/alembic/versions/003_durability_content_in_db.py +106 -0
- package/scripts/alembic/versions/004_pgvector_embedding.py +42 -0
- package/scripts/alembic/versions/005_project_job_number.py +37 -0
- package/scripts/alembic/versions/006_project_parent.py +62 -0
- package/scripts/alembic/versions/__init__.py +1 -0
- package/scripts/alembic.ini +38 -0
- package/scripts/db_auto_migrate.py +103 -0
- package/scripts/db_bootstrap.py +207 -0
- package/scripts/db_check.py +105 -0
- package/scripts/db_migrate.py +85 -0
- package/scripts/fieldbrain_brain_delete.py +49 -0
- package/scripts/fieldbrain_brain_read.py +61 -0
- package/scripts/fieldbrain_brain_restore.py +49 -0
- package/scripts/fieldbrain_brain_revisions.py +54 -0
- package/scripts/fieldbrain_brain_write.py +67 -0
- package/scripts/fieldbrain_document_attach.py +81 -0
- package/scripts/fieldbrain_document_catalog.py +128 -0
- package/scripts/fieldbrain_document_ingest.py +62 -0
- package/scripts/fieldbrain_document_list.py +67 -0
- package/scripts/fieldbrain_document_promote.py +43 -0
- package/scripts/fieldbrain_log_create.py +55 -0
- package/scripts/fieldbrain_log_delete.py +39 -0
- package/scripts/fieldbrain_log_restore.py +39 -0
- package/scripts/fieldbrain_log_revisions.py +39 -0
- package/scripts/fieldbrain_log_search.py +58 -0
- package/scripts/fieldbrain_log_update.py +52 -0
- package/scripts/fieldbrain_project_create.py +72 -0
- package/scripts/fieldbrain_project_list.py +53 -0
- package/scripts/fieldbrain_search.py +74 -0
- package/scripts/fieldbrain_ui_brain_list.py +60 -0
- package/scripts/fieldbrain_ui_project_create.py +95 -0
- package/scripts/fieldbrain_ui_project_list.py +49 -0
- package/scripts/models.py +355 -0
- package/scripts/pgvector_enable.py +165 -0
- package/scripts/pgvector_guide.py +44 -0
- package/scripts/pgvector_install_files.py +66 -0
- package/scripts/pgvector_install_from_folder.py +148 -0
- package/scripts/postbuild-ui.mjs +13 -0
- package/scripts/requirements.txt +7 -0
- package/scripts/services/__init__.py +1 -0
- package/scripts/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/brain_paths.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/document_formats.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/document_service.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/pgvector_windows.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/project_naming.cpython-312.pyc +0 -0
- package/scripts/services/__pycache__/project_service.cpython-312.pyc +0 -0
- package/scripts/services/brain_paths.py +81 -0
- package/scripts/services/brain_service.py +280 -0
- package/scripts/services/document_catalog.py +186 -0
- package/scripts/services/document_formats.py +116 -0
- package/scripts/services/document_ingest.py +254 -0
- package/scripts/services/document_service.py +316 -0
- package/scripts/services/embedding_service.py +72 -0
- package/scripts/services/global_brain_support.py +36 -0
- package/scripts/services/maintenance_log_service.py +258 -0
- package/scripts/services/migrate_lock.py +24 -0
- package/scripts/services/pgvector_windows.py +190 -0
- package/scripts/services/project_naming.py +72 -0
- package/scripts/services/project_service.py +368 -0
- package/scripts/services/search_service.py +250 -0
- package/scripts/status.py +39 -0
- package/shared/README.md +7 -0
- package/skills/fieldbrain/SKILL.md +197 -0
- package/skills/fieldbrain/SKILL.md.bak +83 -0
- package/skills/fieldbrain/routes/fieldbrain/assets/index-B56utPpP.css +1 -0
- package/skills/fieldbrain/routes/fieldbrain/assets/index-DO0AD0df.js +55 -0
- package/skills/fieldbrain/routes/fieldbrain/fallback.md +7 -0
- package/skills/fieldbrain/routes/fieldbrain/index.html +13 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Postgres access for sylo-fieldbrain — single funnel for all tools."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import create_engine, text
|
|
14
|
+
from sqlalchemy.engine import Engine
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Bump when Alembic head revision changes (must match latest migration).
|
|
19
|
+
SCHEMA_VERSION = 6
|
|
20
|
+
|
|
21
|
+
DEFAULT_DATABASE_URL = "postgresql://fieldbrain:fieldbrain@localhost:5432/fieldbrain"
|
|
22
|
+
DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
|
|
23
|
+
|
|
24
|
+
_engine_singleton: Engine | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def package_root() -> Path:
|
|
28
|
+
return Path(__file__).resolve().parent.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def config_dir() -> Path:
|
|
32
|
+
override = os.environ.get("SYLO_FIELDBRAIN_CONFIG_DIR", "").strip()
|
|
33
|
+
if override:
|
|
34
|
+
return Path(override).expanduser()
|
|
35
|
+
legacy = os.environ.get("SYLO_LOGICSCOUT_CONFIG_DIR", "").strip()
|
|
36
|
+
if legacy:
|
|
37
|
+
return Path(legacy).expanduser()
|
|
38
|
+
return Path.home() / ".sylo" / "fieldbrain"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def config_path() -> Path:
|
|
42
|
+
return config_dir() / "database_config.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_database_config() -> dict[str, Any] | None:
|
|
46
|
+
path = config_path()
|
|
47
|
+
if not path.is_file():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
if isinstance(data, dict):
|
|
52
|
+
return data
|
|
53
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
54
|
+
logger.warning("Failed to read %s: %s", path, exc)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_database_config(
|
|
59
|
+
host: str,
|
|
60
|
+
port: int,
|
|
61
|
+
database: str,
|
|
62
|
+
username: str,
|
|
63
|
+
password: str = "",
|
|
64
|
+
) -> Path:
|
|
65
|
+
"""Persist connection fields. Empty password keeps existing when file already exists."""
|
|
66
|
+
path = config_path()
|
|
67
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
existing = load_database_config() or {}
|
|
69
|
+
pwd = password if password else str(existing.get("password") or "")
|
|
70
|
+
payload = {
|
|
71
|
+
"host": host.strip(),
|
|
72
|
+
"port": int(port),
|
|
73
|
+
"database": database.strip(),
|
|
74
|
+
"username": username.strip(),
|
|
75
|
+
"password": pwd,
|
|
76
|
+
}
|
|
77
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_database_url() -> str:
|
|
82
|
+
env_url = os.environ.get("SYLO_FIELDBRAIN_DATABASE_URL", "").strip()
|
|
83
|
+
if not env_url:
|
|
84
|
+
env_url = os.environ.get("SYLO_LOGICSCOUT_DATABASE_URL", "").strip()
|
|
85
|
+
if env_url:
|
|
86
|
+
return env_url
|
|
87
|
+
|
|
88
|
+
config = load_database_config()
|
|
89
|
+
if config:
|
|
90
|
+
host = str(config.get("host") or "localhost")
|
|
91
|
+
port = int(config.get("port") or 5432)
|
|
92
|
+
database = str(config.get("database") or "fieldbrain")
|
|
93
|
+
username = str(config.get("username") or "fieldbrain")
|
|
94
|
+
password = str(config.get("password") or "")
|
|
95
|
+
safe_password = urllib.parse.quote_plus(password) if password else ""
|
|
96
|
+
auth = f"{username}:{safe_password}@" if safe_password else f"{username}@"
|
|
97
|
+
return f"postgresql://{auth}{host}:{port}/{database}"
|
|
98
|
+
|
|
99
|
+
legacy = os.environ.get("DATABASE_URL", "").strip()
|
|
100
|
+
if legacy.startswith("postgresql"):
|
|
101
|
+
return legacy
|
|
102
|
+
|
|
103
|
+
return DEFAULT_DATABASE_URL
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def ollama_base_url() -> str:
|
|
107
|
+
url = os.environ.get("SYLO_FIELDBRAIN_OLLAMA_URL", "").strip()
|
|
108
|
+
if not url:
|
|
109
|
+
url = os.environ.get("SYLO_LOGICSCOUT_OLLAMA_URL", "").strip()
|
|
110
|
+
return url or DEFAULT_OLLAMA_URL
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def reset_engine() -> None:
|
|
114
|
+
global _engine_singleton
|
|
115
|
+
if _engine_singleton is not None:
|
|
116
|
+
try:
|
|
117
|
+
_engine_singleton.dispose()
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
logger.warning("Engine dispose failed: %s", exc)
|
|
120
|
+
_engine_singleton = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_engine(*, echo: bool = False) -> Engine:
|
|
124
|
+
global _engine_singleton
|
|
125
|
+
if _engine_singleton is not None:
|
|
126
|
+
return _engine_singleton
|
|
127
|
+
|
|
128
|
+
url = build_database_url()
|
|
129
|
+
if not url.startswith("postgresql"):
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
f"FieldBrain requires PostgreSQL. Got {url!r}. "
|
|
132
|
+
"Set SYLO_FIELDBRAIN_DATABASE_URL or ~/.sylo/fieldbrain/database_config.json"
|
|
133
|
+
)
|
|
134
|
+
_engine_singleton = create_engine(url, pool_pre_ping=True, echo=echo)
|
|
135
|
+
return _engine_singleton
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_applied_schema_version(engine: Engine | None = None) -> int | None:
|
|
139
|
+
eng = engine or get_engine()
|
|
140
|
+
with eng.connect() as conn:
|
|
141
|
+
row = conn.execute(
|
|
142
|
+
text("SELECT version FROM logicscout_schema_meta ORDER BY id DESC LIMIT 1")
|
|
143
|
+
).fetchone()
|
|
144
|
+
if row is None:
|
|
145
|
+
return None
|
|
146
|
+
return int(row[0])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def verify_schema_version(engine: Engine | None = None) -> tuple[bool, str]:
|
|
150
|
+
"""Return (ok, message). Fail loud on mismatch or missing meta table."""
|
|
151
|
+
eng = engine or get_engine()
|
|
152
|
+
try:
|
|
153
|
+
applied = read_applied_schema_version(eng)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
return False, f"Schema meta unreadable (run fieldbrain_db_migrate): {exc}"
|
|
156
|
+
|
|
157
|
+
if applied is None:
|
|
158
|
+
return False, "Database not initialized — run fieldbrain_db_migrate before using FieldBrain tools."
|
|
159
|
+
|
|
160
|
+
if applied != SCHEMA_VERSION:
|
|
161
|
+
return (
|
|
162
|
+
False,
|
|
163
|
+
f"Schema version mismatch: database has {applied}, package expects {SCHEMA_VERSION}. "
|
|
164
|
+
"Run fieldbrain_db_migrate on one Sylo install, then restart others.",
|
|
165
|
+
)
|
|
166
|
+
return True, f"Schema version {applied} OK"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def check_pgvector(engine: Engine | None = None) -> tuple[bool, str | None]:
|
|
170
|
+
eng = engine or get_engine()
|
|
171
|
+
with eng.connect() as conn:
|
|
172
|
+
try:
|
|
173
|
+
row = conn.execute(
|
|
174
|
+
text("SELECT extversion FROM pg_extension WHERE extname = 'vector'")
|
|
175
|
+
).fetchone()
|
|
176
|
+
if row:
|
|
177
|
+
return True, str(row[0])
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
return False, str(exc)
|
|
180
|
+
return False, None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def connection_summary() -> dict[str, Any]:
|
|
184
|
+
url = build_database_url()
|
|
185
|
+
parsed = urllib.parse.urlparse(url)
|
|
186
|
+
config = load_database_config()
|
|
187
|
+
return {
|
|
188
|
+
"database_url_host": parsed.hostname or "localhost",
|
|
189
|
+
"database_url_port": parsed.port or 5432,
|
|
190
|
+
"database_url_name": (parsed.path or "/fieldbrain").lstrip("/"),
|
|
191
|
+
"database_url_user": parsed.username or "fieldbrain",
|
|
192
|
+
"config_file": str(config_path()),
|
|
193
|
+
"config_file_exists": config_path().is_file(),
|
|
194
|
+
"config_source": "env" if os.environ.get("SYLO_FIELDBRAIN_DATABASE_URL") or os.environ.get("SYLO_LOGICSCOUT_DATABASE_URL") else (
|
|
195
|
+
"file" if config else "default"
|
|
196
|
+
),
|
|
197
|
+
"ollama_url": ollama_base_url(),
|
|
198
|
+
"expected_schema_version": SCHEMA_VERSION,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_health_check() -> dict[str, Any]:
|
|
203
|
+
summary = connection_summary()
|
|
204
|
+
try:
|
|
205
|
+
engine = get_engine()
|
|
206
|
+
with engine.connect() as conn:
|
|
207
|
+
conn.execute(text("SELECT 1"))
|
|
208
|
+
summary["postgres_connected"] = True
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
summary["postgres_connected"] = False
|
|
211
|
+
summary["postgres_error"] = str(exc)
|
|
212
|
+
return summary
|
|
213
|
+
|
|
214
|
+
from services.pgvector_windows import detect_postgres_major, windows_install_status
|
|
215
|
+
|
|
216
|
+
summary["postgres_major"] = detect_postgres_major(engine)
|
|
217
|
+
install = windows_install_status(summary["postgres_major"])
|
|
218
|
+
summary.update(install)
|
|
219
|
+
|
|
220
|
+
pg_ok, pg_detail = check_pgvector(engine)
|
|
221
|
+
summary["pgvector_available"] = pg_ok
|
|
222
|
+
summary["pgvector_detail"] = pg_detail
|
|
223
|
+
|
|
224
|
+
schema_ok, schema_msg = verify_schema_version(engine)
|
|
225
|
+
summary["schema_ok"] = schema_ok
|
|
226
|
+
summary["schema_message"] = schema_msg
|
|
227
|
+
summary["applied_schema_version"] = read_applied_schema_version(engine)
|
|
228
|
+
return summary
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared JSON stdout helpers for sylo-logicscout scripts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def emit(payload: dict[str, Any]) -> None:
|
|
12
|
+
"""Print JSON to stdout and exit with code 0 or 1."""
|
|
13
|
+
print(json.dumps(payload, indent=2))
|
|
14
|
+
if payload.get("ok") is False:
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def emit_error(message: str, **extra: Any) -> None:
|
|
19
|
+
emit({"ok": False, "error": message, **extra})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Alembic environment for sylo-logicscout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from alembic import context
|
|
10
|
+
from sqlalchemy import engine_from_config, pool
|
|
11
|
+
|
|
12
|
+
scripts_dir = Path(__file__).resolve().parent.parent
|
|
13
|
+
if str(scripts_dir) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(scripts_dir))
|
|
15
|
+
|
|
16
|
+
from _db_lib import build_database_url # noqa: E402
|
|
17
|
+
|
|
18
|
+
config = context.config
|
|
19
|
+
if config.config_file_name is not None:
|
|
20
|
+
fileConfig(config.config_file_name)
|
|
21
|
+
|
|
22
|
+
config.set_main_option("sqlalchemy.url", build_database_url())
|
|
23
|
+
target_metadata = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_migrations_offline() -> None:
|
|
27
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
28
|
+
context.configure(
|
|
29
|
+
url=url,
|
|
30
|
+
target_metadata=target_metadata,
|
|
31
|
+
literal_binds=True,
|
|
32
|
+
dialect_opts={"paramstyle": "named"},
|
|
33
|
+
)
|
|
34
|
+
with context.begin_transaction():
|
|
35
|
+
context.run_migrations()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_migrations_online() -> None:
|
|
39
|
+
connectable = engine_from_config(
|
|
40
|
+
config.get_section(config.config_ini_section, {}),
|
|
41
|
+
prefix="sqlalchemy.",
|
|
42
|
+
poolclass=pool.NullPool,
|
|
43
|
+
)
|
|
44
|
+
with connectable.connect() as connection:
|
|
45
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
46
|
+
with context.begin_transaction():
|
|
47
|
+
context.run_migrations()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if context.is_offline_mode():
|
|
51
|
+
run_migrations_offline()
|
|
52
|
+
else:
|
|
53
|
+
run_migrations_online()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Baseline schema meta + pgvector extension attempt.
|
|
2
|
+
|
|
3
|
+
Revision ID: 001_baseline
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-07-04
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
revision = "001_baseline"
|
|
14
|
+
down_revision = None
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
op.execute(
|
|
21
|
+
"""
|
|
22
|
+
CREATE TABLE IF NOT EXISTS logicscout_schema_meta (
|
|
23
|
+
id SERIAL PRIMARY KEY,
|
|
24
|
+
version INTEGER NOT NULL,
|
|
25
|
+
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
)
|
|
29
|
+
op.execute(
|
|
30
|
+
"""
|
|
31
|
+
INSERT INTO logicscout_schema_meta (version)
|
|
32
|
+
SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM logicscout_schema_meta)
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def downgrade() -> None:
|
|
38
|
+
op.execute("DROP TABLE IF EXISTS logicscout_schema_meta")
|