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,355 @@
|
|
|
1
|
+
"""SQLAlchemy models for sylo-logicscout shared Postgres data.
|
|
2
|
+
|
|
3
|
+
Auth, chat, and batch L5X analysis job tables are intentionally omitted.
|
|
4
|
+
Sylo SQLite owns chats; LogicForge owns L5X parse tooling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import (
|
|
10
|
+
Boolean,
|
|
11
|
+
Column,
|
|
12
|
+
DateTime,
|
|
13
|
+
Float,
|
|
14
|
+
ForeignKey,
|
|
15
|
+
Index,
|
|
16
|
+
Integer,
|
|
17
|
+
LargeBinary,
|
|
18
|
+
String,
|
|
19
|
+
Text,
|
|
20
|
+
UniqueConstraint,
|
|
21
|
+
func,
|
|
22
|
+
text as sql_text,
|
|
23
|
+
)
|
|
24
|
+
from sqlalchemy.dialects.postgresql import TSVECTOR
|
|
25
|
+
from sqlalchemy.orm import declarative_base
|
|
26
|
+
|
|
27
|
+
Base = declarative_base()
|
|
28
|
+
|
|
29
|
+
DEFAULT_OLLAMA_ENDPOINT = "http://localhost:11434"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Project(Base):
|
|
33
|
+
"""Project metadata (L5X path/hash point at LogicForge-managed files)."""
|
|
34
|
+
|
|
35
|
+
__tablename__ = "projects"
|
|
36
|
+
|
|
37
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
38
|
+
name = Column(String(255), nullable=False, index=True)
|
|
39
|
+
controller_name = Column(String(255), nullable=True)
|
|
40
|
+
processor_type = Column(String(100), nullable=True)
|
|
41
|
+
software_revision = Column(String(50), nullable=True)
|
|
42
|
+
l5x_file_path = Column(String(500), nullable=True)
|
|
43
|
+
l5x_file_hash = Column(String(64), nullable=True, index=True)
|
|
44
|
+
plc_ip = Column(String(45), nullable=True)
|
|
45
|
+
plc_read_enabled = Column(Boolean, default=False, nullable=False)
|
|
46
|
+
max_alert_chats = Column(Integer, nullable=False, default=10)
|
|
47
|
+
hmi_embed_token = Column(String(255), nullable=True)
|
|
48
|
+
cross_project_share = Column(String(20), nullable=False, default="share")
|
|
49
|
+
cross_project_search = Column(Boolean, nullable=False, default=True)
|
|
50
|
+
org_id = Column(String(100), nullable=False, default="default", index=True)
|
|
51
|
+
project_tags_json = Column(Text, nullable=True)
|
|
52
|
+
domain = Column(String(100), nullable=True)
|
|
53
|
+
controller_family = Column(String(100), nullable=True)
|
|
54
|
+
reminders_muted = Column(Boolean, nullable=False, default=False)
|
|
55
|
+
is_system_hidden = Column(Boolean, nullable=False, default=False, server_default="false")
|
|
56
|
+
job_number = Column(String(20), nullable=True, index=True)
|
|
57
|
+
parent_project_id = Column(Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
58
|
+
l5x_status = Column(String(20), nullable=False, default="analyzed")
|
|
59
|
+
created_at = Column(DateTime, default=func.now())
|
|
60
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Document(Base):
|
|
64
|
+
"""Uploaded document metadata; ingestion pipeline fills analysis rows later."""
|
|
65
|
+
|
|
66
|
+
__tablename__ = "documents"
|
|
67
|
+
|
|
68
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
69
|
+
scope = Column(String(20), nullable=False, default="project_local", index=True)
|
|
70
|
+
owner_project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
71
|
+
title = Column(String(255), nullable=False)
|
|
72
|
+
original_filename = Column(String(255), nullable=False)
|
|
73
|
+
stored_path = Column(String(500), nullable=False)
|
|
74
|
+
file_hash = Column(String(64), nullable=False, index=True)
|
|
75
|
+
mime_type = Column(String(100), nullable=True)
|
|
76
|
+
file_size = Column(Integer, nullable=True)
|
|
77
|
+
category = Column(String(20), nullable=False, default="other", index=True)
|
|
78
|
+
tags_json = Column(Text, nullable=True)
|
|
79
|
+
manufacturer = Column(String(100), nullable=True)
|
|
80
|
+
model = Column(String(100), nullable=True)
|
|
81
|
+
version = Column(String(50), nullable=True)
|
|
82
|
+
archived = Column(Boolean, nullable=False, default=False)
|
|
83
|
+
file_content = Column(LargeBinary, nullable=True)
|
|
84
|
+
created_at = Column(DateTime, default=func.now())
|
|
85
|
+
|
|
86
|
+
__table_args__ = (UniqueConstraint("file_hash", name="uq_documents_file_hash"),)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ProjectDocumentLink(Base):
|
|
90
|
+
"""Attach a document to a project."""
|
|
91
|
+
|
|
92
|
+
__tablename__ = "project_document_links"
|
|
93
|
+
|
|
94
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
95
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
96
|
+
document_id = Column(Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
97
|
+
role = Column(String(20), nullable=False, default="reference", index=True)
|
|
98
|
+
attached_at = Column(DateTime, default=func.now())
|
|
99
|
+
|
|
100
|
+
__table_args__ = (UniqueConstraint("project_id", "document_id", name="uq_project_document_link"),)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GlobalSettings(Base):
|
|
104
|
+
"""Server-wide settings singleton."""
|
|
105
|
+
|
|
106
|
+
__tablename__ = "global_settings"
|
|
107
|
+
|
|
108
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
109
|
+
ollama_endpoint = Column(String(500), nullable=True, default=DEFAULT_OLLAMA_ENDPOINT)
|
|
110
|
+
llm_model = Column(String(100), nullable=True)
|
|
111
|
+
summarizer_model = Column(String(100), nullable=True)
|
|
112
|
+
vision_model = Column(String(100), nullable=True)
|
|
113
|
+
doc_vision_model = Column(String(100), nullable=True)
|
|
114
|
+
analysis_model = Column(String(100), nullable=True)
|
|
115
|
+
embedding_model = Column(String(100), nullable=True)
|
|
116
|
+
search_index_schema_version = Column(Integer, nullable=False, default=1, server_default="1")
|
|
117
|
+
search_query_expansion = Column(Boolean, nullable=False, default=True)
|
|
118
|
+
search_reranking_enabled = Column(Boolean, nullable=False, default=True)
|
|
119
|
+
search_rerank_pool = Column(Integer, nullable=False, default=15)
|
|
120
|
+
search_context_window = Column(Integer, nullable=False, default=1)
|
|
121
|
+
search_rerank_backend = Column(String(20), nullable=False, default="ollama")
|
|
122
|
+
search_rerank_model = Column(String(100), nullable=True)
|
|
123
|
+
ollama_num_ctx = Column(Integer, nullable=True)
|
|
124
|
+
ollama_num_ctx_agents_json = Column(Text, nullable=True)
|
|
125
|
+
ollama_profiles_json = Column(Text, nullable=True)
|
|
126
|
+
ollama_agent_endpoints_json = Column(Text, nullable=True)
|
|
127
|
+
marker_pdf_enabled = Column(Boolean, nullable=False, default=True, server_default="true")
|
|
128
|
+
marker_pdf_use_llm = Column(Boolean, nullable=False, default=False, server_default="false")
|
|
129
|
+
marker_pdf_ollama_model = Column(String(100), nullable=True)
|
|
130
|
+
vision_render_dpi = Column(Integer, nullable=False, default=200, server_default="200")
|
|
131
|
+
max_inflight_vision = Column(Integer, nullable=False, default=1, server_default="1")
|
|
132
|
+
max_inflight_text = Column(Integer, nullable=False, default=1, server_default="1")
|
|
133
|
+
l5x_analysis_concurrency = Column(Integer, nullable=False, default=1, server_default="1")
|
|
134
|
+
vision_timeout_seconds = Column(Integer, nullable=False, default=300, server_default="300")
|
|
135
|
+
text_timeout_seconds = Column(Integer, nullable=False, default=180, server_default="180")
|
|
136
|
+
find_logic_llm_model = Column(String(100), nullable=True)
|
|
137
|
+
documentation_search_llm_model = Column(String(100), nullable=True)
|
|
138
|
+
orchestrator_temperature = Column(Float, nullable=True)
|
|
139
|
+
summarizer_temperature = Column(Float, nullable=True)
|
|
140
|
+
summarizer_max_summary_chars = Column(Integer, nullable=True)
|
|
141
|
+
vision_temperature = Column(Float, nullable=True)
|
|
142
|
+
doc_vision_temperature = Column(Float, nullable=True)
|
|
143
|
+
analysis_temperature = Column(Float, nullable=True)
|
|
144
|
+
documentation_search_temperature = Column(Float, nullable=True)
|
|
145
|
+
find_logic_temperature = Column(Float, nullable=True)
|
|
146
|
+
search_rerank_temperature = Column(Float, nullable=True)
|
|
147
|
+
orchestrator_max_iterations = Column(Integer, nullable=True)
|
|
148
|
+
find_logic_default_max_iterations = Column(Integer, nullable=True)
|
|
149
|
+
documentation_search_default_max_iterations = Column(Integer, nullable=True)
|
|
150
|
+
find_logic_effort_low_max_iterations = Column(Integer, nullable=True)
|
|
151
|
+
find_logic_effort_medium_max_iterations = Column(Integer, nullable=True)
|
|
152
|
+
find_logic_effort_high_max_iterations = Column(Integer, nullable=True)
|
|
153
|
+
find_logic_system_prompt_extra = Column(Text, nullable=True)
|
|
154
|
+
find_logic_inner_user_message = Column(Text, nullable=True)
|
|
155
|
+
find_logic_allowed_tools_json = Column(Text, nullable=True)
|
|
156
|
+
ollama_warmup_on_start = Column(Boolean, nullable=False, default=False, server_default=sql_text("false"))
|
|
157
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class DocumentAnalysis(Base):
|
|
161
|
+
"""LLM-generated analysis for an uploaded document."""
|
|
162
|
+
|
|
163
|
+
__tablename__ = "document_analyses"
|
|
164
|
+
|
|
165
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
166
|
+
document_id = Column(Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
|
|
167
|
+
llm_index_description = Column(Text, nullable=False)
|
|
168
|
+
parsed_text_with_images = Column(Text, nullable=True)
|
|
169
|
+
analyzed_at = Column(DateTime, default=func.now())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class DocumentPageImage(Base):
|
|
173
|
+
"""Vision-model description for a document page containing images or diagrams."""
|
|
174
|
+
|
|
175
|
+
__tablename__ = "document_page_images"
|
|
176
|
+
|
|
177
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
178
|
+
document_id = Column(Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
179
|
+
page_number = Column(Integer, nullable=False)
|
|
180
|
+
has_images = Column(Boolean, default=True, nullable=False)
|
|
181
|
+
llm_image_description = Column(Text, nullable=True)
|
|
182
|
+
llm_page_detail = Column(Text, nullable=True)
|
|
183
|
+
ocr_text = Column(Text, nullable=True)
|
|
184
|
+
created_at = Column(DateTime, default=func.now())
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class DocumentPdfOutline(Base):
|
|
188
|
+
"""Per-document TOC entries (physical_pdf_page is 0-based)."""
|
|
189
|
+
|
|
190
|
+
__tablename__ = "document_pdf_outline"
|
|
191
|
+
|
|
192
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
193
|
+
document_id = Column(Integer, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
194
|
+
level = Column(Integer, nullable=False)
|
|
195
|
+
title = Column(Text, nullable=False)
|
|
196
|
+
toc_page_claimed = Column(Integer, nullable=True)
|
|
197
|
+
physical_pdf_page = Column(Integer, nullable=False)
|
|
198
|
+
verification = Column(String(32), nullable=False)
|
|
199
|
+
sequence = Column(Integer, nullable=False)
|
|
200
|
+
created_at = Column(DateTime, default=func.now())
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Alert(Base):
|
|
204
|
+
"""Tag-monitoring alert definition."""
|
|
205
|
+
|
|
206
|
+
__tablename__ = "alerts"
|
|
207
|
+
|
|
208
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
209
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
210
|
+
tag_name = Column(String(255), nullable=False)
|
|
211
|
+
trigger_value_type = Column(String(20), nullable=False)
|
|
212
|
+
trigger_operator = Column(String(20), nullable=False, default="eq")
|
|
213
|
+
trigger_value = Column(Text, nullable=False)
|
|
214
|
+
command = Column(Text, nullable=False)
|
|
215
|
+
enabled = Column(Boolean, default=True, nullable=False)
|
|
216
|
+
created_at = Column(DateTime, default=func.now())
|
|
217
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class AlertResponse(Base):
|
|
221
|
+
"""Agent response to a triggered alert."""
|
|
222
|
+
|
|
223
|
+
__tablename__ = "alert_responses"
|
|
224
|
+
|
|
225
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
226
|
+
alert_id = Column(Integer, ForeignKey("alerts.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
227
|
+
tag_value = Column(Text, nullable=True)
|
|
228
|
+
agent_response = Column(Text, nullable=False, default="")
|
|
229
|
+
is_processing = Column(Boolean, default=False, nullable=False)
|
|
230
|
+
triggered_at = Column(DateTime, default=func.now())
|
|
231
|
+
responded_at = Column(DateTime, nullable=True)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TagTroubleshootingSolution(Base):
|
|
235
|
+
"""Human-approved resolution for a project tag."""
|
|
236
|
+
|
|
237
|
+
__tablename__ = "tag_troubleshooting_solutions"
|
|
238
|
+
|
|
239
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
240
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
241
|
+
tag_name = Column(String(255), nullable=False, index=True)
|
|
242
|
+
solution_text = Column(Text, nullable=False)
|
|
243
|
+
source_ref = Column(String(255), nullable=True)
|
|
244
|
+
logged_by = Column(String(100), nullable=True)
|
|
245
|
+
created_at = Column(DateTime, default=func.now())
|
|
246
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
247
|
+
|
|
248
|
+
__table_args__ = (Index("ix_tag_trouble_solutions_project_tag", "project_id", "tag_name"),)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class BrainDraft(Base):
|
|
252
|
+
"""Brain change audit record."""
|
|
253
|
+
|
|
254
|
+
__tablename__ = "brain_drafts"
|
|
255
|
+
|
|
256
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
257
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
258
|
+
source_ref = Column(String(255), nullable=True)
|
|
259
|
+
status = Column(String(20), nullable=False, default="pending")
|
|
260
|
+
op = Column(String(50), nullable=False)
|
|
261
|
+
target_path = Column(String(500), nullable=False)
|
|
262
|
+
payload_json = Column(Text, nullable=False)
|
|
263
|
+
content_hash_before = Column(String(64), nullable=True)
|
|
264
|
+
created_at = Column(DateTime, default=func.now())
|
|
265
|
+
expires_at = Column(DateTime, nullable=True)
|
|
266
|
+
applied_at = Column(DateTime, nullable=True)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class MaintenanceLogEntry(Base):
|
|
270
|
+
"""Shop maintenance log entry (retrieval-first)."""
|
|
271
|
+
|
|
272
|
+
__tablename__ = "maintenance_log_entries"
|
|
273
|
+
|
|
274
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
275
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True)
|
|
276
|
+
title = Column(String(255), nullable=False)
|
|
277
|
+
body = Column(Text, nullable=False)
|
|
278
|
+
fault_code = Column(String(100), nullable=True, index=True)
|
|
279
|
+
equipment_tag = Column(String(255), nullable=True, index=True)
|
|
280
|
+
logged_by = Column(String(100), nullable=True)
|
|
281
|
+
deleted_at = Column(DateTime, nullable=True, index=True)
|
|
282
|
+
created_at = Column(DateTime, default=func.now(), index=True)
|
|
283
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
284
|
+
search_vector = Column(TSVECTOR, nullable=True)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class MaintenanceLogRevision(Base):
|
|
288
|
+
"""Prior version of a maintenance log entry."""
|
|
289
|
+
|
|
290
|
+
__tablename__ = "maintenance_log_revisions"
|
|
291
|
+
|
|
292
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
293
|
+
log_entry_id = Column(
|
|
294
|
+
Integer, ForeignKey("maintenance_log_entries.id", ondelete="CASCADE"), nullable=False, index=True
|
|
295
|
+
)
|
|
296
|
+
title = Column(String(255), nullable=False)
|
|
297
|
+
body = Column(Text, nullable=False)
|
|
298
|
+
fault_code = Column(String(100), nullable=True)
|
|
299
|
+
equipment_tag = Column(String(255), nullable=True)
|
|
300
|
+
logged_by = Column(String(100), nullable=True)
|
|
301
|
+
revision_number = Column(Integer, nullable=False)
|
|
302
|
+
created_at = Column(DateTime, default=func.now())
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class BrainDocument(Base):
|
|
306
|
+
"""Brain markdown stored in Postgres (canonical; local disk is optional cache)."""
|
|
307
|
+
|
|
308
|
+
__tablename__ = "brain_documents"
|
|
309
|
+
|
|
310
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
311
|
+
scope = Column(String(20), nullable=False, index=True)
|
|
312
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=True, index=True)
|
|
313
|
+
relative_path = Column(String(500), nullable=False)
|
|
314
|
+
content = Column(Text, nullable=False, default="")
|
|
315
|
+
deleted_at = Column(DateTime, nullable=True, index=True)
|
|
316
|
+
created_at = Column(DateTime, default=func.now())
|
|
317
|
+
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class BrainDocumentRevision(Base):
|
|
321
|
+
"""Prior version of a brain document."""
|
|
322
|
+
|
|
323
|
+
__tablename__ = "brain_document_revisions"
|
|
324
|
+
|
|
325
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
326
|
+
brain_document_id = Column(
|
|
327
|
+
Integer, ForeignKey("brain_documents.id", ondelete="CASCADE"), nullable=False, index=True
|
|
328
|
+
)
|
|
329
|
+
relative_path = Column(String(500), nullable=False)
|
|
330
|
+
content = Column(Text, nullable=False)
|
|
331
|
+
revision_number = Column(Integer, nullable=False)
|
|
332
|
+
created_at = Column(DateTime, default=func.now())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class SearchIndex(Base):
|
|
336
|
+
"""Hybrid search projection (tsvector + optional pgvector embedding).
|
|
337
|
+
|
|
338
|
+
The ``embedding`` column is ``vector(768)`` when pgvector is installed;
|
|
339
|
+
created in migration 002, not mapped here.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
__tablename__ = "search_index"
|
|
343
|
+
|
|
344
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
345
|
+
project_id = Column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
346
|
+
source_type = Column(String(50), nullable=False, index=True)
|
|
347
|
+
source_id = Column(String(500), nullable=True)
|
|
348
|
+
label = Column(String(500), nullable=True)
|
|
349
|
+
path = Column(String(500), nullable=True)
|
|
350
|
+
content = Column(Text, nullable=True)
|
|
351
|
+
chunk_index = Column(Integer, default=0)
|
|
352
|
+
section_id = Column(Integer, nullable=False, default=0)
|
|
353
|
+
entry_share = Column(String(20), nullable=False, default="project_only")
|
|
354
|
+
brain_type = Column(String(50), nullable=True)
|
|
355
|
+
brain_status = Column(String(50), nullable=True)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Enable pgvector in the FieldBrain database and ensure embedding schema."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import create_engine, text
|
|
11
|
+
|
|
12
|
+
from _db_lib import SCHEMA_VERSION, check_pgvector, load_database_config, run_health_check
|
|
13
|
+
from _json_out import emit, emit_error
|
|
14
|
+
from services.pgvector_windows import layman_pgvector_steps, windows_install_status
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_admin_url(host: str, port: int, database: str, user: str, password: str) -> str:
|
|
18
|
+
return (
|
|
19
|
+
f"postgresql://{urllib.parse.quote(user)}:{urllib.parse.quote(password)}"
|
|
20
|
+
f"@{host}:{port}/{urllib.parse.quote(database)}"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _try_create_extension(app_url: str) -> tuple[bool, str | None]:
|
|
25
|
+
engine = create_engine(app_url, isolation_level="AUTOCOMMIT", pool_pre_ping=True)
|
|
26
|
+
try:
|
|
27
|
+
with engine.connect() as conn:
|
|
28
|
+
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
|
29
|
+
with engine.connect() as conn:
|
|
30
|
+
row = conn.execute(
|
|
31
|
+
text("SELECT extversion FROM pg_extension WHERE extname = 'vector'")
|
|
32
|
+
).fetchone()
|
|
33
|
+
if row:
|
|
34
|
+
return True, str(row[0])
|
|
35
|
+
return False, "CREATE EXTENSION ran but vector extension not visible"
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
return False, str(exc)
|
|
38
|
+
finally:
|
|
39
|
+
engine.dispose()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_embedding_schema(app_url: str) -> tuple[bool, str | None]:
|
|
43
|
+
engine = create_engine(app_url, isolation_level="AUTOCOMMIT", pool_pre_ping=True)
|
|
44
|
+
try:
|
|
45
|
+
with engine.connect() as conn:
|
|
46
|
+
pg_ok = conn.execute(
|
|
47
|
+
text("SELECT 1 FROM pg_extension WHERE extname = 'vector'")
|
|
48
|
+
).fetchone()
|
|
49
|
+
if not pg_ok:
|
|
50
|
+
return False, "pgvector extension not enabled in this database"
|
|
51
|
+
conn.execute(
|
|
52
|
+
text("ALTER TABLE search_index ADD COLUMN IF NOT EXISTS embedding vector(768)")
|
|
53
|
+
)
|
|
54
|
+
conn.execute(
|
|
55
|
+
text(
|
|
56
|
+
"""
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_search_index_embedding
|
|
58
|
+
ON search_index USING hnsw (embedding vector_cosine_ops)
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return True, None
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
return False, str(exc)
|
|
65
|
+
finally:
|
|
66
|
+
engine.dispose()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _run_migrate(app_url: str) -> tuple[int | None, str | None]:
|
|
70
|
+
from alembic import command
|
|
71
|
+
|
|
72
|
+
from _db_lib import read_applied_schema_version, reset_engine
|
|
73
|
+
from db_migrate import alembic_config
|
|
74
|
+
|
|
75
|
+
os.environ["SYLO_FIELDBRAIN_DATABASE_URL"] = app_url
|
|
76
|
+
reset_engine()
|
|
77
|
+
try:
|
|
78
|
+
command.upgrade(alembic_config(), "head")
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
return None, str(exc)
|
|
81
|
+
reset_engine()
|
|
82
|
+
try:
|
|
83
|
+
applied = read_applied_schema_version()
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
return None, f"Migration ran but schema version unreadable: {exc}"
|
|
86
|
+
if applied != SCHEMA_VERSION:
|
|
87
|
+
return applied, f"Expected schema {SCHEMA_VERSION}, got {applied}"
|
|
88
|
+
return applied, None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> None:
|
|
92
|
+
parser = argparse.ArgumentParser()
|
|
93
|
+
parser.add_argument("--admin-user", default="postgres")
|
|
94
|
+
parser.add_argument("--admin-password", default="")
|
|
95
|
+
parser.add_argument("--host", default="")
|
|
96
|
+
parser.add_argument("--port", type=int, default=0)
|
|
97
|
+
parser.add_argument("--database", default="")
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
|
|
100
|
+
admin_password = (args.admin_password or os.environ.get("SYLO_FIELDBRAIN_BOOTSTRAP_ADMIN_PASSWORD") or "").strip()
|
|
101
|
+
if not admin_password:
|
|
102
|
+
emit_error("Postgres superuser password is required (not saved).")
|
|
103
|
+
|
|
104
|
+
cfg = load_database_config() or {}
|
|
105
|
+
host = (args.host or str(cfg.get("host") or "localhost")).strip()
|
|
106
|
+
db_mode = "remote" if host not in ("localhost", "127.0.0.1") else "local"
|
|
107
|
+
port = args.port or int(cfg.get("port") or 5432)
|
|
108
|
+
database = (args.database or str(cfg.get("database") or "fieldbrain")).strip()
|
|
109
|
+
admin_user = (args.admin_user or "postgres").strip()
|
|
110
|
+
|
|
111
|
+
health = run_health_check()
|
|
112
|
+
if health.get("postgres_connected") is not True:
|
|
113
|
+
emit_error(health.get("postgres_error", "Connect to Postgres first (Test connection)."))
|
|
114
|
+
|
|
115
|
+
major = health.get("postgres_major")
|
|
116
|
+
if isinstance(major, str) and major.isdigit():
|
|
117
|
+
major = int(major)
|
|
118
|
+
install = windows_install_status(major if isinstance(major, int) else None)
|
|
119
|
+
|
|
120
|
+
admin_url = _build_admin_url(host, port, database, admin_user, admin_password)
|
|
121
|
+
ext_ok, ext_detail = _try_create_extension(admin_url)
|
|
122
|
+
if not ext_ok:
|
|
123
|
+
guide = layman_pgvector_steps(
|
|
124
|
+
major if isinstance(major, int) else None,
|
|
125
|
+
files_installed=bool(install.get("vector_files_installed")),
|
|
126
|
+
db_mode=db_mode,
|
|
127
|
+
)
|
|
128
|
+
emit_error(
|
|
129
|
+
f"Could not enable pgvector: {ext_detail}",
|
|
130
|
+
pgvector_setup=guide,
|
|
131
|
+
**install,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
app_user = str(cfg.get("username") or "fieldbrain")
|
|
135
|
+
app_password = str(cfg.get("password") or "")
|
|
136
|
+
app_url = _build_admin_url(host, port, database, app_user, app_password)
|
|
137
|
+
schema_ok, schema_err = _ensure_embedding_schema(app_url)
|
|
138
|
+
if not schema_ok:
|
|
139
|
+
emit_error(f"pgvector enabled but schema update failed: {schema_err}", pgvector_detail=ext_detail)
|
|
140
|
+
|
|
141
|
+
applied, migrate_err = _run_migrate(app_url)
|
|
142
|
+
if migrate_err:
|
|
143
|
+
emit_error(migrate_err, pgvector_detail=ext_detail)
|
|
144
|
+
|
|
145
|
+
pg_ok, pg_ver = check_pgvector()
|
|
146
|
+
lines = [
|
|
147
|
+
f"Semantic search enabled (pgvector {ext_detail}).",
|
|
148
|
+
f"Schema version {applied}.",
|
|
149
|
+
"Superuser password was not saved.",
|
|
150
|
+
"Re-ingest documents later if you want embeddings on existing files.",
|
|
151
|
+
]
|
|
152
|
+
emit(
|
|
153
|
+
{
|
|
154
|
+
"ok": True,
|
|
155
|
+
"operator_chat": "\n".join(lines),
|
|
156
|
+
"pgvector_ok": pg_ok,
|
|
157
|
+
"pgvector_detail": pg_ver,
|
|
158
|
+
"applied_schema_version": applied,
|
|
159
|
+
**install,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Layman pgvector setup guide for FieldBrain."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from _db_lib import load_database_config, run_health_check
|
|
7
|
+
from _json_out import emit
|
|
8
|
+
from services.pgvector_windows import layman_pgvector_steps, windows_install_status
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
summary = run_health_check()
|
|
13
|
+
cfg = load_database_config() or {}
|
|
14
|
+
host = str(cfg.get("host") or "localhost").strip()
|
|
15
|
+
db_mode = "remote" if host not in ("localhost", "127.0.0.1") else "local"
|
|
16
|
+
|
|
17
|
+
major = summary.get("postgres_major")
|
|
18
|
+
if isinstance(major, str) and major.isdigit():
|
|
19
|
+
major = int(major)
|
|
20
|
+
install = windows_install_status(major if isinstance(major, int) else None)
|
|
21
|
+
pg_ok = summary.get("pgvector_available") is True
|
|
22
|
+
steps = layman_pgvector_steps(
|
|
23
|
+
major if isinstance(major, int) else None,
|
|
24
|
+
files_installed=bool(install.get("vector_files_installed")),
|
|
25
|
+
db_mode=db_mode,
|
|
26
|
+
)
|
|
27
|
+
if pg_ok:
|
|
28
|
+
steps = [
|
|
29
|
+
"pgvector is already enabled — semantic search is available when Ollama is running.",
|
|
30
|
+
]
|
|
31
|
+
emit(
|
|
32
|
+
{
|
|
33
|
+
"ok": True,
|
|
34
|
+
"pgvector_ok": pg_ok,
|
|
35
|
+
"operator_chat": "\n".join(steps),
|
|
36
|
+
"pgvector_setup": steps,
|
|
37
|
+
**summary,
|
|
38
|
+
**install,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Install pgvector files from an operator-selected folder or zip."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from _json_out import emit, emit_error
|
|
11
|
+
from services.pgvector_windows import (
|
|
12
|
+
guess_pgroot_for_major,
|
|
13
|
+
install_pgvector_files,
|
|
14
|
+
resolve_source_dir,
|
|
15
|
+
windows_install_status,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
parser = argparse.ArgumentParser()
|
|
21
|
+
parser.add_argument("--source", required=True, help="Folder or .zip containing pgvector files")
|
|
22
|
+
parser.add_argument("--postgres-major", type=int, default=0)
|
|
23
|
+
parser.add_argument("--pgroot", default="", help="Override PostgreSQL install directory")
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
major = args.postgres_major or int(os.environ.get("SYLO_FIELDBRAIN_POSTGRES_MAJOR") or 0) or None
|
|
27
|
+
pgroot_raw = (args.pgroot or os.environ.get("SYLO_FIELDBRAIN_PGROOT") or "").strip()
|
|
28
|
+
if pgroot_raw:
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
pgroot = Path(pgroot_raw)
|
|
32
|
+
elif major:
|
|
33
|
+
pgroot = guess_pgroot_for_major(major)
|
|
34
|
+
else:
|
|
35
|
+
pgroot = None
|
|
36
|
+
|
|
37
|
+
if pgroot is None:
|
|
38
|
+
emit_error("Could not determine PostgreSQL install folder. Connect to Postgres and retry.")
|
|
39
|
+
|
|
40
|
+
source_dir, tmp, err = resolve_source_dir(args.source)
|
|
41
|
+
if err or source_dir is None:
|
|
42
|
+
emit_error(err or "Invalid source path")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
result = install_pgvector_files(source_dir, pgroot)
|
|
46
|
+
finally:
|
|
47
|
+
if tmp is not None:
|
|
48
|
+
tmp.cleanup()
|
|
49
|
+
|
|
50
|
+
status = windows_install_status(major)
|
|
51
|
+
payload = {**result, **status}
|
|
52
|
+
if result.get("ok"):
|
|
53
|
+
emit(
|
|
54
|
+
{
|
|
55
|
+
**payload,
|
|
56
|
+
"operator_chat": (
|
|
57
|
+
f"Copied pgvector files into {result.get('pgroot')}.\n"
|
|
58
|
+
"Next: Enable semantic search in FieldBrain Settings."
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
emit_error(result.get("error", "Install failed"), **payload)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|