solidworks-mcp-python 1.5.0__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.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
"""SQLModel persistence for agent runs and tool-error cataloging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy import inspect as sa_inspect
|
|
11
|
+
from sqlalchemy import text as sa_text
|
|
12
|
+
from sqlalchemy.pool import NullPool
|
|
13
|
+
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
|
14
|
+
|
|
15
|
+
DEFAULT_DB_PATH = Path(".solidworks_mcp") / "agent_memory.sqlite3"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorRecord(BaseModel):
|
|
19
|
+
"""A normalized error record from an MCP call or planning step.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
error_message (str): The error message value.
|
|
23
|
+
error_type (str): The error type value.
|
|
24
|
+
remediation (str): The remediation value.
|
|
25
|
+
root_cause (str): The root cause value.
|
|
26
|
+
source (str): The source value.
|
|
27
|
+
tool_name (str): The tool name value.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
source: str
|
|
31
|
+
tool_name: str
|
|
32
|
+
error_type: str
|
|
33
|
+
error_message: str
|
|
34
|
+
root_cause: str
|
|
35
|
+
remediation: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _utc_now_iso() -> str:
|
|
39
|
+
"""Build internal utc now iso.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: The resulting text value.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return datetime.now(UTC).isoformat()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentRun(SQLModel, table=True):
|
|
49
|
+
"""One recorded agent run.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
agent_name (str): The agent name value.
|
|
53
|
+
created_at (str): The created at value.
|
|
54
|
+
id (int | None): The id value.
|
|
55
|
+
model_name (str | None): The model name value.
|
|
56
|
+
output_json (str | None): The output json value.
|
|
57
|
+
prompt (str): The prompt value.
|
|
58
|
+
run_id (str): The run id value.
|
|
59
|
+
status (str): The status value.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
63
|
+
run_id: str
|
|
64
|
+
agent_name: str
|
|
65
|
+
prompt: str
|
|
66
|
+
model_name: str | None = None
|
|
67
|
+
status: str
|
|
68
|
+
output_json: str | None = None
|
|
69
|
+
created_at: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ToolEvent(SQLModel, table=True):
|
|
73
|
+
"""One tool lifecycle event linked to a run.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
created_at (str): The created at value.
|
|
77
|
+
id (int | None): The id value.
|
|
78
|
+
payload_json (str | None): The payload json value.
|
|
79
|
+
phase (str): The phase value.
|
|
80
|
+
run_id (str): The run id value.
|
|
81
|
+
tool_name (str): The tool name value.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
85
|
+
run_id: str
|
|
86
|
+
tool_name: str
|
|
87
|
+
phase: str
|
|
88
|
+
payload_json: str | None = None
|
|
89
|
+
created_at: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ErrorCatalog(SQLModel, table=True):
|
|
93
|
+
"""Persisted error records for recovery recommendations.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
created_at (str): The created at value.
|
|
97
|
+
error_message (str): The error message value.
|
|
98
|
+
error_type (str): The error type value.
|
|
99
|
+
id (int | None): The id value.
|
|
100
|
+
remediation (str): The remediation value.
|
|
101
|
+
root_cause (str): The root cause value.
|
|
102
|
+
run_id (str | None): The run id value.
|
|
103
|
+
source (str): The source value.
|
|
104
|
+
tool_name (str): The tool name value.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
108
|
+
run_id: str | None = None
|
|
109
|
+
source: str
|
|
110
|
+
tool_name: str
|
|
111
|
+
error_type: str
|
|
112
|
+
error_message: str
|
|
113
|
+
root_cause: str
|
|
114
|
+
remediation: str
|
|
115
|
+
created_at: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ConversationEvent(SQLModel, table=True):
|
|
119
|
+
"""One message or system event in a conversation, linked to a run context.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
content_snippet (str): The content snippet value.
|
|
123
|
+
conversation_id (str): The conversation id value.
|
|
124
|
+
created_at (str): The created at value.
|
|
125
|
+
event_type (str): The event type value.
|
|
126
|
+
id (int | None): The id value.
|
|
127
|
+
metadata_json (str | None): The metadata json value.
|
|
128
|
+
role (str | None): The role value.
|
|
129
|
+
run_id (str | None): The run id value.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
133
|
+
conversation_id: str
|
|
134
|
+
run_id: str | None = None
|
|
135
|
+
event_type: str # "user_message", "assistant_message", "system_event", "tool_call"
|
|
136
|
+
role: str | None = None # "user", "assistant", "system"
|
|
137
|
+
content_snippet: str # truncated for privacy; full content may be elsewhere
|
|
138
|
+
metadata_json: str | None = None # tool_name, phase, status, etc.
|
|
139
|
+
created_at: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DesignSession(SQLModel, table=True):
|
|
143
|
+
"""Persistent interactive design session metadata.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
accepted_family (str | None): The accepted family value.
|
|
147
|
+
created_at (str): The created at value.
|
|
148
|
+
current_checkpoint_index (int): The current checkpoint index value.
|
|
149
|
+
id (int | None): The id value.
|
|
150
|
+
metadata_json (str | None): The metadata json value.
|
|
151
|
+
session_id (str): The session id value.
|
|
152
|
+
source_mode (str): The source mode value.
|
|
153
|
+
status (str): The status value.
|
|
154
|
+
updated_at (str): The updated at value.
|
|
155
|
+
user_goal (str): The user goal value.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
159
|
+
session_id: str
|
|
160
|
+
user_goal: str
|
|
161
|
+
source_mode: str = "model" # model/image/text
|
|
162
|
+
accepted_family: str | None = None
|
|
163
|
+
status: str = "active"
|
|
164
|
+
current_checkpoint_index: int = 0
|
|
165
|
+
metadata_json: str | None = None
|
|
166
|
+
created_at: str
|
|
167
|
+
updated_at: str
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PlanCheckpoint(SQLModel, table=True):
|
|
171
|
+
"""One approval boundary in an interactive design session.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
approved_by_user (bool): The approved by user value.
|
|
175
|
+
checkpoint_index (int): The checkpoint index value.
|
|
176
|
+
created_at (str): The created at value.
|
|
177
|
+
executed (bool): The executed value.
|
|
178
|
+
id (int | None): The id value.
|
|
179
|
+
planned_action_json (str): The planned action json value.
|
|
180
|
+
result_json (str | None): The result json value.
|
|
181
|
+
rollback_snapshot_id (int | None): The rollback snapshot id value.
|
|
182
|
+
session_id (str): The session id value.
|
|
183
|
+
title (str): The title value.
|
|
184
|
+
updated_at (str): The updated at value.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
188
|
+
session_id: str
|
|
189
|
+
checkpoint_index: int
|
|
190
|
+
title: str
|
|
191
|
+
planned_action_json: str
|
|
192
|
+
approved_by_user: bool = False
|
|
193
|
+
executed: bool = False
|
|
194
|
+
result_json: str | None = None
|
|
195
|
+
rollback_snapshot_id: int | None = None
|
|
196
|
+
created_at: str
|
|
197
|
+
updated_at: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ToolCallRecord(SQLModel, table=True):
|
|
201
|
+
"""Execution log for tool calls scoped to session/checkpoint.
|
|
202
|
+
|
|
203
|
+
Attributes:
|
|
204
|
+
checkpoint_id (int | None): The checkpoint id value.
|
|
205
|
+
created_at (str): The created at value.
|
|
206
|
+
id (int | None): The id value.
|
|
207
|
+
input_json (str | None): The input json value.
|
|
208
|
+
latency_ms (float | None): The latency ms value.
|
|
209
|
+
output_json (str | None): The output json value.
|
|
210
|
+
run_id (str | None): The run id value.
|
|
211
|
+
session_id (str): The session id value.
|
|
212
|
+
status (str | None): Lifecycle status — None (active), 'reverted'.
|
|
213
|
+
success (bool): The success value.
|
|
214
|
+
tool_name (str): The tool name value.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
218
|
+
session_id: str
|
|
219
|
+
checkpoint_id: int | None = None
|
|
220
|
+
run_id: str | None = None
|
|
221
|
+
tool_name: str
|
|
222
|
+
input_json: str | None = None
|
|
223
|
+
output_json: str | None = None
|
|
224
|
+
success: bool = True
|
|
225
|
+
latency_ms: float | None = None
|
|
226
|
+
status: str | None = None
|
|
227
|
+
# Rendered SolidWorks-as-Code Python for this call, computed at write time
|
|
228
|
+
# (issue #28). None for rows written before the column existed — session
|
|
229
|
+
# export re-renders those on demand.
|
|
230
|
+
script_line: str | None = None
|
|
231
|
+
created_at: str
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class EvidenceLink(SQLModel, table=True):
|
|
235
|
+
"""Evidence references used to justify planning decisions.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
checkpoint_id (int | None): The checkpoint id value.
|
|
239
|
+
created_at (str): The created at value.
|
|
240
|
+
id (int | None): The id value.
|
|
241
|
+
payload_json (str | None): The payload json value.
|
|
242
|
+
rationale (str | None): The rationale value.
|
|
243
|
+
relevance_score (float | None): The relevance score value.
|
|
244
|
+
session_id (str): The session id value.
|
|
245
|
+
source_id (str): The source id value.
|
|
246
|
+
source_type (str): The source type value.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
250
|
+
session_id: str
|
|
251
|
+
checkpoint_id: int | None = None
|
|
252
|
+
source_type: str
|
|
253
|
+
source_id: str
|
|
254
|
+
relevance_score: float | None = None
|
|
255
|
+
rationale: str | None = None
|
|
256
|
+
payload_json: str | None = None
|
|
257
|
+
created_at: str
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ModelStateSnapshot(SQLModel, table=True):
|
|
261
|
+
"""Rollback/diff snapshot of model state at a checkpoint.
|
|
262
|
+
|
|
263
|
+
Attributes:
|
|
264
|
+
checkpoint_id (int | None): The checkpoint id value.
|
|
265
|
+
created_at (str): The created at value.
|
|
266
|
+
feature_tree_json (str | None): The feature tree json value.
|
|
267
|
+
id (int | None): The id value.
|
|
268
|
+
mass_properties_json (str | None): The mass properties json value.
|
|
269
|
+
model_path (str | None): The model path value.
|
|
270
|
+
screenshot_path (str | None): The screenshot path value.
|
|
271
|
+
session_id (str): The session id value.
|
|
272
|
+
state_fingerprint (str | None): The state fingerprint value.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
276
|
+
session_id: str
|
|
277
|
+
checkpoint_id: int | None = None
|
|
278
|
+
model_path: str | None = None
|
|
279
|
+
feature_tree_json: str | None = None
|
|
280
|
+
mass_properties_json: str | None = None
|
|
281
|
+
screenshot_path: str | None = None
|
|
282
|
+
state_fingerprint: str | None = None
|
|
283
|
+
created_at: str
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class SoCCheckpoint(SQLModel, table=True):
|
|
287
|
+
"""Named save point in a SolidWorks-as-Code session.
|
|
288
|
+
|
|
289
|
+
A checkpoint ties together a human label, the saved .sldprt file that
|
|
290
|
+
represents the model state at that point, and the range of ToolCallRecord
|
|
291
|
+
rows that produced it. The exporter uses this to emit structured comment
|
|
292
|
+
blocks; soc_rewind uses the file_path to restore the model; soc_pickup
|
|
293
|
+
uses snapshot_id to diff against the current feature tree.
|
|
294
|
+
|
|
295
|
+
Attributes:
|
|
296
|
+
file_path (str): Path to the .sldprt saved at this checkpoint.
|
|
297
|
+
first_record_id (int | None): First ToolCallRecord.id in this block.
|
|
298
|
+
id (int | None): Primary key.
|
|
299
|
+
label (str): Short human name for this checkpoint.
|
|
300
|
+
last_record_id (int | None): Last ToolCallRecord.id in this block.
|
|
301
|
+
session_id (str): Owning SoC session.
|
|
302
|
+
snapshot_id (int | None): FK to ModelStateSnapshot (feature tree, mass props).
|
|
303
|
+
created_at (str): ISO timestamp.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
307
|
+
session_id: str
|
|
308
|
+
label: str
|
|
309
|
+
file_path: str
|
|
310
|
+
first_record_id: int | None = None
|
|
311
|
+
last_record_id: int | None = None
|
|
312
|
+
snapshot_id: int | None = None
|
|
313
|
+
created_at: str
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class SketchGraphSnapshot(SQLModel, table=True):
|
|
317
|
+
"""Lightweight relational sketch graph storage (Section F).
|
|
318
|
+
|
|
319
|
+
Attributes:
|
|
320
|
+
created_at (str): The created at value.
|
|
321
|
+
edges_json (str): The edges json value.
|
|
322
|
+
graph_format (str): The graph format value.
|
|
323
|
+
id (int | None): The id value.
|
|
324
|
+
metadata_json (str | None): The metadata json value.
|
|
325
|
+
model_path (str | None): The model path value.
|
|
326
|
+
nodes_json (str): The nodes json value.
|
|
327
|
+
session_id (str): The session id value.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
331
|
+
session_id: str
|
|
332
|
+
model_path: str | None = None
|
|
333
|
+
graph_format: str = "json"
|
|
334
|
+
nodes_json: str
|
|
335
|
+
edges_json: str
|
|
336
|
+
metadata_json: str | None = None
|
|
337
|
+
created_at: str
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _build_engine(db_path: Path | None = None) -> Any:
|
|
341
|
+
"""Build a local SQLite engine from the configured path.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Any: SQLAlchemy engine instance for the resolved SQLite path.
|
|
348
|
+
"""
|
|
349
|
+
resolved = db_path or DEFAULT_DB_PATH
|
|
350
|
+
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
# NullPool closes sqlite connections immediately after each session/transaction,
|
|
352
|
+
# which prevents a long-lived pool from surfacing ResourceWarning noise at process exit.
|
|
353
|
+
return create_engine(f"sqlite:///{resolved}", echo=False, poolclass=NullPool)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def init_db(db_path: Path | None = None) -> Path:
|
|
357
|
+
"""Create SQLModel tables used by the lightweight agent memory system.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Path: The result produced by the operation.
|
|
364
|
+
"""
|
|
365
|
+
resolved = db_path or DEFAULT_DB_PATH
|
|
366
|
+
engine = _build_engine(resolved)
|
|
367
|
+
try:
|
|
368
|
+
SQLModel.metadata.create_all(engine)
|
|
369
|
+
_ensure_columns(engine)
|
|
370
|
+
finally:
|
|
371
|
+
engine.dispose()
|
|
372
|
+
return resolved
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Columns added to existing tables after their first release. This repo has no
|
|
376
|
+
# migration framework (issue #28): create_all() makes new tables but never
|
|
377
|
+
# alters an existing one, so a pre-existing local DB would raise
|
|
378
|
+
# "no such column" on the next insert. Checked and patched on every init_db().
|
|
379
|
+
_ADDED_COLUMNS: dict[str, dict[str, str]] = {
|
|
380
|
+
"toolcallrecord": {"script_line": "TEXT"},
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _ensure_columns(engine: Any) -> None:
|
|
385
|
+
"""Add any missing post-release columns to already-existing tables."""
|
|
386
|
+
inspector = sa_inspect(engine)
|
|
387
|
+
for table, columns in _ADDED_COLUMNS.items():
|
|
388
|
+
present = {col["name"] for col in inspector.get_columns(table)}
|
|
389
|
+
missing = {
|
|
390
|
+
name: sql_type for name, sql_type in columns.items() if name not in present
|
|
391
|
+
}
|
|
392
|
+
if not missing:
|
|
393
|
+
continue
|
|
394
|
+
with engine.begin() as conn:
|
|
395
|
+
for name, sql_type in missing.items():
|
|
396
|
+
conn.execute(
|
|
397
|
+
sa_text(f"ALTER TABLE {table} ADD COLUMN {name} {sql_type}")
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def insert_run(
|
|
402
|
+
*,
|
|
403
|
+
run_id: str,
|
|
404
|
+
agent_name: str,
|
|
405
|
+
prompt: str,
|
|
406
|
+
status: str,
|
|
407
|
+
output_json: str | None,
|
|
408
|
+
model_name: str | None,
|
|
409
|
+
db_path: Path | None = None,
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Record one prompt run and optionally the validated output payload.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
run_id (str): The run id value.
|
|
415
|
+
agent_name (str): The agent name value.
|
|
416
|
+
prompt (str): The prompt value.
|
|
417
|
+
status (str): The status value.
|
|
418
|
+
output_json (str | None): The output json value.
|
|
419
|
+
model_name (str | None): Embedding model name to use.
|
|
420
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
None: None.
|
|
424
|
+
"""
|
|
425
|
+
resolved = init_db(db_path)
|
|
426
|
+
engine = _build_engine(resolved)
|
|
427
|
+
with Session(engine) as session:
|
|
428
|
+
session.add(
|
|
429
|
+
AgentRun(
|
|
430
|
+
run_id=run_id,
|
|
431
|
+
agent_name=agent_name,
|
|
432
|
+
prompt=prompt,
|
|
433
|
+
model_name=model_name,
|
|
434
|
+
status=status,
|
|
435
|
+
output_json=output_json,
|
|
436
|
+
created_at=_utc_now_iso(),
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
session.commit()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def insert_tool_event(
|
|
443
|
+
*,
|
|
444
|
+
run_id: str,
|
|
445
|
+
tool_name: str,
|
|
446
|
+
phase: str,
|
|
447
|
+
payload_json: str | None,
|
|
448
|
+
db_path: Path | None = None,
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Store lifecycle events around MCP tool usage to aid troubleshooting.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
run_id (str): The run id value.
|
|
454
|
+
tool_name (str): The tool name value.
|
|
455
|
+
phase (str): The phase value.
|
|
456
|
+
payload_json (str | None): The payload json value.
|
|
457
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
None: None.
|
|
461
|
+
"""
|
|
462
|
+
resolved = init_db(db_path)
|
|
463
|
+
engine = _build_engine(resolved)
|
|
464
|
+
with Session(engine) as session:
|
|
465
|
+
session.add(
|
|
466
|
+
ToolEvent(
|
|
467
|
+
run_id=run_id,
|
|
468
|
+
tool_name=tool_name,
|
|
469
|
+
phase=phase,
|
|
470
|
+
payload_json=payload_json,
|
|
471
|
+
created_at=_utc_now_iso(),
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
session.commit()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def insert_error(
|
|
478
|
+
record: ErrorRecord, run_id: str | None = None, db_path: Path | None = None
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Persist an error with normalized root cause and remediation guidance.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
record (ErrorRecord): The record value.
|
|
484
|
+
run_id (str | None): The run id value. Defaults to None.
|
|
485
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
None: None.
|
|
489
|
+
"""
|
|
490
|
+
resolved = init_db(db_path)
|
|
491
|
+
engine = _build_engine(resolved)
|
|
492
|
+
with Session(engine) as session:
|
|
493
|
+
session.add(
|
|
494
|
+
ErrorCatalog(
|
|
495
|
+
run_id=run_id,
|
|
496
|
+
source=record.source,
|
|
497
|
+
tool_name=record.tool_name,
|
|
498
|
+
error_type=record.error_type,
|
|
499
|
+
error_message=record.error_message,
|
|
500
|
+
root_cause=record.root_cause,
|
|
501
|
+
remediation=record.remediation,
|
|
502
|
+
created_at=_utc_now_iso(),
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
session.commit()
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def find_recent_errors(
|
|
509
|
+
limit: int = 20, db_path: Path | None = None
|
|
510
|
+
) -> list[dict[str, Any]]:
|
|
511
|
+
"""Return recent errors so agents can avoid repeated failing states.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
limit (int): The limit value. Defaults to 20.
|
|
515
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
519
|
+
"""
|
|
520
|
+
resolved = init_db(db_path)
|
|
521
|
+
engine = _build_engine(resolved)
|
|
522
|
+
with Session(engine) as session:
|
|
523
|
+
rows = session.exec(
|
|
524
|
+
select(ErrorCatalog).order_by(ErrorCatalog.id.desc()).limit(limit) # type: ignore[union-attr]
|
|
525
|
+
).all()
|
|
526
|
+
|
|
527
|
+
return [
|
|
528
|
+
{
|
|
529
|
+
"run_id": row.run_id,
|
|
530
|
+
"source": row.source,
|
|
531
|
+
"tool_name": row.tool_name,
|
|
532
|
+
"error_type": row.error_type,
|
|
533
|
+
"error_message": row.error_message,
|
|
534
|
+
"root_cause": row.root_cause,
|
|
535
|
+
"remediation": row.remediation,
|
|
536
|
+
"created_at": row.created_at,
|
|
537
|
+
}
|
|
538
|
+
for row in rows
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def insert_conversation_event(
|
|
543
|
+
*,
|
|
544
|
+
conversation_id: str,
|
|
545
|
+
event_type: str,
|
|
546
|
+
content_snippet: str,
|
|
547
|
+
role: str | None = None,
|
|
548
|
+
run_id: str | None = None,
|
|
549
|
+
metadata_json: str | None = None,
|
|
550
|
+
db_path: Path | None = None,
|
|
551
|
+
) -> None:
|
|
552
|
+
"""Record a conversation event (message, system event, or tool call) linked to a run.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
conversation_id (str): The conversation id value.
|
|
556
|
+
event_type (str): The event type value.
|
|
557
|
+
content_snippet (str): The content snippet value.
|
|
558
|
+
role (str | None): The role value. Defaults to None.
|
|
559
|
+
run_id (str | None): The run id value. Defaults to None.
|
|
560
|
+
metadata_json (str | None): The metadata json value. Defaults to None.
|
|
561
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
None: None.
|
|
565
|
+
"""
|
|
566
|
+
resolved = init_db(db_path)
|
|
567
|
+
engine = _build_engine(resolved)
|
|
568
|
+
with Session(engine) as session:
|
|
569
|
+
session.add(
|
|
570
|
+
ConversationEvent(
|
|
571
|
+
conversation_id=conversation_id,
|
|
572
|
+
run_id=run_id,
|
|
573
|
+
event_type=event_type,
|
|
574
|
+
role=role,
|
|
575
|
+
content_snippet=content_snippet,
|
|
576
|
+
metadata_json=metadata_json,
|
|
577
|
+
created_at=_utc_now_iso(),
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
session.commit()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def find_conversation_events(
|
|
584
|
+
conversation_id: str, db_path: Path | None = None
|
|
585
|
+
) -> list[dict[str, Any]]:
|
|
586
|
+
"""Retrieve all events for a conversation, ordered by creation time.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
conversation_id (str): The conversation id value.
|
|
590
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
594
|
+
"""
|
|
595
|
+
resolved = init_db(db_path)
|
|
596
|
+
engine = _build_engine(resolved)
|
|
597
|
+
with Session(engine) as session:
|
|
598
|
+
rows = session.exec(
|
|
599
|
+
select(ConversationEvent)
|
|
600
|
+
.where(ConversationEvent.conversation_id == conversation_id)
|
|
601
|
+
.order_by(ConversationEvent.id.asc()) # type: ignore[union-attr]
|
|
602
|
+
).all()
|
|
603
|
+
|
|
604
|
+
return [
|
|
605
|
+
{
|
|
606
|
+
"id": row.id,
|
|
607
|
+
"conversation_id": row.conversation_id,
|
|
608
|
+
"run_id": row.run_id,
|
|
609
|
+
"event_type": row.event_type,
|
|
610
|
+
"role": row.role,
|
|
611
|
+
"content_snippet": row.content_snippet,
|
|
612
|
+
"metadata_json": row.metadata_json,
|
|
613
|
+
"created_at": row.created_at,
|
|
614
|
+
}
|
|
615
|
+
for row in rows
|
|
616
|
+
]
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def find_run_timeline(run_id: str, db_path: Path | None = None) -> dict[str, Any]:
|
|
620
|
+
"""Reconstruct a complete timeline for one run, joining runs, tool events, and conversation events.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
run_id (str): The run id value.
|
|
624
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
dict[str, Any]: A dictionary containing the resulting values.
|
|
628
|
+
"""
|
|
629
|
+
resolved = init_db(db_path)
|
|
630
|
+
engine = _build_engine(resolved)
|
|
631
|
+
|
|
632
|
+
timeline: dict[str, Any] = {
|
|
633
|
+
"run_id": run_id,
|
|
634
|
+
"run_info": None,
|
|
635
|
+
"events": [],
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
with Session(engine) as session:
|
|
639
|
+
run_row = session.exec(
|
|
640
|
+
select(AgentRun).where(AgentRun.run_id == run_id)
|
|
641
|
+
).first()
|
|
642
|
+
|
|
643
|
+
if run_row:
|
|
644
|
+
timeline["run_info"] = {
|
|
645
|
+
"agent_name": run_row.agent_name,
|
|
646
|
+
"prompt_preview": run_row.prompt[:200] if run_row.prompt else None,
|
|
647
|
+
"model_name": run_row.model_name,
|
|
648
|
+
"status": run_row.status,
|
|
649
|
+
"created_at": run_row.created_at,
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
tool_events = session.exec(
|
|
653
|
+
select(ToolEvent)
|
|
654
|
+
.where(ToolEvent.run_id == run_id)
|
|
655
|
+
.order_by(ToolEvent.id.asc()) # type: ignore[union-attr]
|
|
656
|
+
).all()
|
|
657
|
+
|
|
658
|
+
convo_events = session.exec(
|
|
659
|
+
select(ConversationEvent)
|
|
660
|
+
.where(ConversationEvent.run_id == run_id)
|
|
661
|
+
.order_by(ConversationEvent.id.asc()) # type: ignore[union-attr]
|
|
662
|
+
).all()
|
|
663
|
+
|
|
664
|
+
events = []
|
|
665
|
+
for evt in tool_events:
|
|
666
|
+
events.append(
|
|
667
|
+
{
|
|
668
|
+
"timestamp": evt.created_at,
|
|
669
|
+
"event_type": "tool",
|
|
670
|
+
"tool_name": evt.tool_name,
|
|
671
|
+
"phase": evt.phase,
|
|
672
|
+
"payload_preview": evt.payload_json[:100]
|
|
673
|
+
if evt.payload_json
|
|
674
|
+
else None,
|
|
675
|
+
}
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
for conv_evt in convo_events:
|
|
679
|
+
events.append(
|
|
680
|
+
{
|
|
681
|
+
"timestamp": conv_evt.created_at,
|
|
682
|
+
"event_type": "message",
|
|
683
|
+
"role": conv_evt.role,
|
|
684
|
+
"content_preview": conv_evt.content_snippet[:100],
|
|
685
|
+
"metadata": conv_evt.metadata_json,
|
|
686
|
+
}
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
events.sort(key=lambda e: e["timestamp"] or "")
|
|
690
|
+
timeline["events"] = events
|
|
691
|
+
|
|
692
|
+
return timeline
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def upsert_design_session(
|
|
696
|
+
*,
|
|
697
|
+
session_id: str,
|
|
698
|
+
user_goal: str,
|
|
699
|
+
source_mode: str = "model",
|
|
700
|
+
accepted_family: str | None = None,
|
|
701
|
+
status: str = "active",
|
|
702
|
+
current_checkpoint_index: int = 0,
|
|
703
|
+
metadata_json: str | None = None,
|
|
704
|
+
db_path: Path | None = None,
|
|
705
|
+
) -> None:
|
|
706
|
+
"""Create or update one interactive design session row.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
session_id (str): The session id value.
|
|
710
|
+
user_goal (str): The user goal value.
|
|
711
|
+
source_mode (str): The source mode value. Defaults to "model".
|
|
712
|
+
accepted_family (str | None): The accepted family value. Defaults to None.
|
|
713
|
+
status (str): The status value. Defaults to "active".
|
|
714
|
+
current_checkpoint_index (int): The current checkpoint index value. Defaults to 0.
|
|
715
|
+
metadata_json (str | None): The metadata json value. Defaults to None.
|
|
716
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
None: None.
|
|
720
|
+
"""
|
|
721
|
+
resolved = init_db(db_path)
|
|
722
|
+
engine = _build_engine(resolved)
|
|
723
|
+
now = _utc_now_iso()
|
|
724
|
+
with Session(engine) as session:
|
|
725
|
+
row = session.exec(
|
|
726
|
+
select(DesignSession).where(DesignSession.session_id == session_id)
|
|
727
|
+
).first()
|
|
728
|
+
if row is None:
|
|
729
|
+
session.add(
|
|
730
|
+
DesignSession(
|
|
731
|
+
session_id=session_id,
|
|
732
|
+
user_goal=user_goal,
|
|
733
|
+
source_mode=source_mode,
|
|
734
|
+
accepted_family=accepted_family,
|
|
735
|
+
status=status,
|
|
736
|
+
current_checkpoint_index=current_checkpoint_index,
|
|
737
|
+
metadata_json=metadata_json,
|
|
738
|
+
created_at=now,
|
|
739
|
+
updated_at=now,
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
else:
|
|
743
|
+
row.user_goal = user_goal
|
|
744
|
+
row.source_mode = source_mode
|
|
745
|
+
row.accepted_family = accepted_family
|
|
746
|
+
row.status = status
|
|
747
|
+
row.current_checkpoint_index = current_checkpoint_index
|
|
748
|
+
row.metadata_json = metadata_json
|
|
749
|
+
row.updated_at = now
|
|
750
|
+
session.add(row)
|
|
751
|
+
session.commit()
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def get_design_session(
|
|
755
|
+
session_id: str, db_path: Path | None = None
|
|
756
|
+
) -> dict[str, Any] | None:
|
|
757
|
+
"""Return one session row as a dictionary.
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
session_id (str): The session id value.
|
|
761
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
dict[str, Any] | None: A dictionary containing the resulting values.
|
|
765
|
+
"""
|
|
766
|
+
resolved = init_db(db_path)
|
|
767
|
+
engine = _build_engine(resolved)
|
|
768
|
+
with Session(engine) as session:
|
|
769
|
+
row = session.exec(
|
|
770
|
+
select(DesignSession).where(DesignSession.session_id == session_id)
|
|
771
|
+
).first()
|
|
772
|
+
|
|
773
|
+
if row is None:
|
|
774
|
+
return None
|
|
775
|
+
return {
|
|
776
|
+
"session_id": row.session_id,
|
|
777
|
+
"user_goal": row.user_goal,
|
|
778
|
+
"source_mode": row.source_mode,
|
|
779
|
+
"accepted_family": row.accepted_family,
|
|
780
|
+
"status": row.status,
|
|
781
|
+
"current_checkpoint_index": row.current_checkpoint_index,
|
|
782
|
+
"metadata_json": row.metadata_json,
|
|
783
|
+
"created_at": row.created_at,
|
|
784
|
+
"updated_at": row.updated_at,
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def insert_plan_checkpoint(
|
|
789
|
+
*,
|
|
790
|
+
session_id: str,
|
|
791
|
+
checkpoint_index: int,
|
|
792
|
+
title: str,
|
|
793
|
+
planned_action_json: str,
|
|
794
|
+
approved_by_user: bool = False,
|
|
795
|
+
executed: bool = False,
|
|
796
|
+
result_json: str | None = None,
|
|
797
|
+
rollback_snapshot_id: int | None = None,
|
|
798
|
+
db_path: Path | None = None,
|
|
799
|
+
) -> int:
|
|
800
|
+
"""Insert a new checkpoint and return its ID.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
session_id (str): The session id value.
|
|
804
|
+
checkpoint_index (int): The checkpoint index value.
|
|
805
|
+
title (str): The title value.
|
|
806
|
+
planned_action_json (str): The planned action json value.
|
|
807
|
+
approved_by_user (bool): The approved by user value. Defaults to False.
|
|
808
|
+
executed (bool): The executed value. Defaults to False.
|
|
809
|
+
result_json (str | None): The result json value. Defaults to None.
|
|
810
|
+
rollback_snapshot_id (int | None): The rollback snapshot id value. Defaults to None.
|
|
811
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
int: The computed numeric result.
|
|
815
|
+
"""
|
|
816
|
+
resolved = init_db(db_path)
|
|
817
|
+
engine = _build_engine(resolved)
|
|
818
|
+
now = _utc_now_iso()
|
|
819
|
+
with Session(engine) as session:
|
|
820
|
+
row = PlanCheckpoint(
|
|
821
|
+
session_id=session_id,
|
|
822
|
+
checkpoint_index=checkpoint_index,
|
|
823
|
+
title=title,
|
|
824
|
+
planned_action_json=planned_action_json,
|
|
825
|
+
approved_by_user=approved_by_user,
|
|
826
|
+
executed=executed,
|
|
827
|
+
result_json=result_json,
|
|
828
|
+
rollback_snapshot_id=rollback_snapshot_id,
|
|
829
|
+
created_at=now,
|
|
830
|
+
updated_at=now,
|
|
831
|
+
)
|
|
832
|
+
session.add(row)
|
|
833
|
+
session.commit()
|
|
834
|
+
session.refresh(row)
|
|
835
|
+
return int(row.id or 0)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def update_plan_checkpoint(
|
|
839
|
+
checkpoint_id: int,
|
|
840
|
+
*,
|
|
841
|
+
approved_by_user: bool | None = None,
|
|
842
|
+
executed: bool | None = None,
|
|
843
|
+
result_json: str | None = None,
|
|
844
|
+
rollback_snapshot_id: int | None = None,
|
|
845
|
+
db_path: Path | None = None,
|
|
846
|
+
) -> None:
|
|
847
|
+
"""Patch checkpoint approval/execution fields.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
checkpoint_id (int): The checkpoint id value.
|
|
851
|
+
approved_by_user (bool | None): The approved by user value. Defaults to None.
|
|
852
|
+
executed (bool | None): The executed value. Defaults to None.
|
|
853
|
+
result_json (str | None): The result json value. Defaults to None.
|
|
854
|
+
rollback_snapshot_id (int | None): The rollback snapshot id value. Defaults to None.
|
|
855
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
None: None.
|
|
859
|
+
"""
|
|
860
|
+
resolved = init_db(db_path)
|
|
861
|
+
engine = _build_engine(resolved)
|
|
862
|
+
with Session(engine) as session:
|
|
863
|
+
row = session.exec(
|
|
864
|
+
select(PlanCheckpoint).where(PlanCheckpoint.id == checkpoint_id)
|
|
865
|
+
).first()
|
|
866
|
+
if row is None:
|
|
867
|
+
return
|
|
868
|
+
if approved_by_user is not None:
|
|
869
|
+
row.approved_by_user = approved_by_user
|
|
870
|
+
if executed is not None:
|
|
871
|
+
row.executed = executed
|
|
872
|
+
if result_json is not None:
|
|
873
|
+
row.result_json = result_json
|
|
874
|
+
if rollback_snapshot_id is not None:
|
|
875
|
+
row.rollback_snapshot_id = rollback_snapshot_id
|
|
876
|
+
row.updated_at = _utc_now_iso()
|
|
877
|
+
session.add(row)
|
|
878
|
+
session.commit()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def update_plan_checkpoint_planned_action(
|
|
882
|
+
checkpoint_id: int,
|
|
883
|
+
*,
|
|
884
|
+
planned_action_json: str,
|
|
885
|
+
executed: bool | None = None,
|
|
886
|
+
result_json: str | None = None,
|
|
887
|
+
db_path: Path | None = None,
|
|
888
|
+
) -> None:
|
|
889
|
+
"""Update checkpoint planned-action JSON and optional execution status.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
checkpoint_id: Target checkpoint row ID.
|
|
893
|
+
planned_action_json: New planned action payload encoded as JSON string.
|
|
894
|
+
executed: Optional executed flag override.
|
|
895
|
+
result_json: Optional result payload override.
|
|
896
|
+
db_path: Optional SQLite path override.
|
|
897
|
+
"""
|
|
898
|
+
resolved = init_db(db_path)
|
|
899
|
+
engine = _build_engine(resolved)
|
|
900
|
+
with Session(engine) as session:
|
|
901
|
+
row = session.exec(
|
|
902
|
+
select(PlanCheckpoint).where(PlanCheckpoint.id == checkpoint_id)
|
|
903
|
+
).first()
|
|
904
|
+
if row is None:
|
|
905
|
+
return
|
|
906
|
+
|
|
907
|
+
row.planned_action_json = planned_action_json
|
|
908
|
+
if executed is not None:
|
|
909
|
+
row.executed = executed
|
|
910
|
+
if result_json is not None:
|
|
911
|
+
row.result_json = result_json
|
|
912
|
+
row.updated_at = _utc_now_iso()
|
|
913
|
+
|
|
914
|
+
session.add(row)
|
|
915
|
+
session.commit()
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def list_plan_checkpoints(
|
|
919
|
+
session_id: str,
|
|
920
|
+
db_path: Path | None = None,
|
|
921
|
+
) -> list[dict[str, Any]]:
|
|
922
|
+
"""List all checkpoints for a session.
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
session_id (str): The session id value.
|
|
926
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
930
|
+
"""
|
|
931
|
+
resolved = init_db(db_path)
|
|
932
|
+
engine = _build_engine(resolved)
|
|
933
|
+
with Session(engine) as session:
|
|
934
|
+
rows = session.exec(
|
|
935
|
+
select(PlanCheckpoint)
|
|
936
|
+
.where(PlanCheckpoint.session_id == session_id)
|
|
937
|
+
.order_by(PlanCheckpoint.checkpoint_index.asc()) # type: ignore[attr-defined]
|
|
938
|
+
).all()
|
|
939
|
+
|
|
940
|
+
return [
|
|
941
|
+
{
|
|
942
|
+
"id": row.id,
|
|
943
|
+
"session_id": row.session_id,
|
|
944
|
+
"checkpoint_index": row.checkpoint_index,
|
|
945
|
+
"title": row.title,
|
|
946
|
+
"planned_action_json": row.planned_action_json,
|
|
947
|
+
"approved_by_user": row.approved_by_user,
|
|
948
|
+
"executed": row.executed,
|
|
949
|
+
"result_json": row.result_json,
|
|
950
|
+
"rollback_snapshot_id": row.rollback_snapshot_id,
|
|
951
|
+
"created_at": row.created_at,
|
|
952
|
+
"updated_at": row.updated_at,
|
|
953
|
+
}
|
|
954
|
+
for row in rows
|
|
955
|
+
]
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def replace_plan_checkpoints(
|
|
959
|
+
*,
|
|
960
|
+
session_id: str,
|
|
961
|
+
checkpoints: list[dict[str, Any]],
|
|
962
|
+
db_path: Path | None = None,
|
|
963
|
+
) -> None:
|
|
964
|
+
"""Replace all session checkpoints with a new ordered checkpoint list.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
session_id (str): The session id value.
|
|
968
|
+
checkpoints (list[dict[str, Any]]): Replacement checkpoint records.
|
|
969
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
970
|
+
|
|
971
|
+
Returns:
|
|
972
|
+
None: None.
|
|
973
|
+
"""
|
|
974
|
+
resolved = init_db(db_path)
|
|
975
|
+
engine = _build_engine(resolved)
|
|
976
|
+
now = _utc_now_iso()
|
|
977
|
+
|
|
978
|
+
with Session(engine) as session:
|
|
979
|
+
existing_rows = session.exec(
|
|
980
|
+
select(PlanCheckpoint).where(PlanCheckpoint.session_id == session_id)
|
|
981
|
+
).all()
|
|
982
|
+
for existing in existing_rows:
|
|
983
|
+
session.delete(existing)
|
|
984
|
+
|
|
985
|
+
for index, item in enumerate(checkpoints, start=1):
|
|
986
|
+
planned_action_json = str(item.get("planned_action_json") or "{}")
|
|
987
|
+
title = str(item.get("title") or f"Checkpoint {index}")
|
|
988
|
+
approved_by_user = bool(item.get("approved_by_user", index == 1))
|
|
989
|
+
row = PlanCheckpoint(
|
|
990
|
+
session_id=session_id,
|
|
991
|
+
checkpoint_index=int(item.get("checkpoint_index") or index),
|
|
992
|
+
title=title,
|
|
993
|
+
planned_action_json=planned_action_json,
|
|
994
|
+
approved_by_user=approved_by_user,
|
|
995
|
+
executed=False,
|
|
996
|
+
result_json=None,
|
|
997
|
+
rollback_snapshot_id=None,
|
|
998
|
+
created_at=now,
|
|
999
|
+
updated_at=now,
|
|
1000
|
+
)
|
|
1001
|
+
session.add(row)
|
|
1002
|
+
|
|
1003
|
+
session.commit()
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def insert_tool_call_record(
|
|
1007
|
+
*,
|
|
1008
|
+
session_id: str,
|
|
1009
|
+
tool_name: str,
|
|
1010
|
+
checkpoint_id: int | None = None,
|
|
1011
|
+
run_id: str | None = None,
|
|
1012
|
+
input_json: str | None = None,
|
|
1013
|
+
output_json: str | None = None,
|
|
1014
|
+
success: bool = True,
|
|
1015
|
+
latency_ms: float | None = None,
|
|
1016
|
+
script_line: str | None = None,
|
|
1017
|
+
db_path: Path | None = None,
|
|
1018
|
+
) -> None:
|
|
1019
|
+
"""Insert one tool call execution record.
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
session_id (str): The session id value.
|
|
1023
|
+
tool_name (str): The tool name value.
|
|
1024
|
+
checkpoint_id (int | None): The checkpoint id value. Defaults to None.
|
|
1025
|
+
run_id (str | None): The run id value. Defaults to None.
|
|
1026
|
+
input_json (str | None): The input json value. Defaults to None.
|
|
1027
|
+
output_json (str | None): The output json value. Defaults to None.
|
|
1028
|
+
success (bool): The success value. Defaults to True.
|
|
1029
|
+
latency_ms (float | None): The latency ms value. Defaults to None.
|
|
1030
|
+
script_line (str | None): Rendered SolidWorks-as-Code Python for this
|
|
1031
|
+
call (issue #28). Computed from the tool name and payloads when
|
|
1032
|
+
not supplied. Defaults to None.
|
|
1033
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
None: None.
|
|
1037
|
+
"""
|
|
1038
|
+
if script_line is None:
|
|
1039
|
+
# Render at write time so every row is self-describing. Import here to
|
|
1040
|
+
# avoid a module-level cycle (soc_exporter imports from history_db).
|
|
1041
|
+
try:
|
|
1042
|
+
from .soc_exporter import render_single
|
|
1043
|
+
|
|
1044
|
+
script_line = render_single(tool_name, input_json, output_json) or None
|
|
1045
|
+
except Exception: # noqa: BLE001 - never let rendering block a log write
|
|
1046
|
+
script_line = None
|
|
1047
|
+
|
|
1048
|
+
resolved = init_db(db_path)
|
|
1049
|
+
engine = _build_engine(resolved)
|
|
1050
|
+
with Session(engine) as session:
|
|
1051
|
+
session.add(
|
|
1052
|
+
ToolCallRecord(
|
|
1053
|
+
session_id=session_id,
|
|
1054
|
+
checkpoint_id=checkpoint_id,
|
|
1055
|
+
run_id=run_id,
|
|
1056
|
+
tool_name=tool_name,
|
|
1057
|
+
input_json=input_json,
|
|
1058
|
+
output_json=output_json,
|
|
1059
|
+
success=success,
|
|
1060
|
+
latency_ms=latency_ms,
|
|
1061
|
+
script_line=script_line,
|
|
1062
|
+
created_at=_utc_now_iso(),
|
|
1063
|
+
)
|
|
1064
|
+
)
|
|
1065
|
+
session.commit()
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def list_tool_call_records(
|
|
1069
|
+
session_id: str,
|
|
1070
|
+
checkpoint_id: int | None = None,
|
|
1071
|
+
db_path: Path | None = None,
|
|
1072
|
+
include_reverted: bool = False,
|
|
1073
|
+
) -> list[dict[str, Any]]:
|
|
1074
|
+
"""List tool call records for a session and optional checkpoint.
|
|
1075
|
+
|
|
1076
|
+
Args:
|
|
1077
|
+
session_id (str): The session id value.
|
|
1078
|
+
checkpoint_id (int | None): The checkpoint id value. Defaults to None.
|
|
1079
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1080
|
+
include_reverted (bool): Include records with status='reverted'. Defaults to False.
|
|
1081
|
+
|
|
1082
|
+
Returns:
|
|
1083
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
1084
|
+
"""
|
|
1085
|
+
resolved = init_db(db_path)
|
|
1086
|
+
engine = _build_engine(resolved)
|
|
1087
|
+
with Session(engine) as session:
|
|
1088
|
+
query = select(ToolCallRecord).where(ToolCallRecord.session_id == session_id)
|
|
1089
|
+
if checkpoint_id is not None:
|
|
1090
|
+
query = query.where(ToolCallRecord.checkpoint_id == checkpoint_id)
|
|
1091
|
+
if not include_reverted:
|
|
1092
|
+
query = query.where(
|
|
1093
|
+
(ToolCallRecord.status == None) | (ToolCallRecord.status != "reverted") # noqa: E711
|
|
1094
|
+
)
|
|
1095
|
+
rows = session.exec(query.order_by(ToolCallRecord.id.asc())).all() # type: ignore[union-attr]
|
|
1096
|
+
|
|
1097
|
+
return [
|
|
1098
|
+
{
|
|
1099
|
+
"id": row.id,
|
|
1100
|
+
"session_id": row.session_id,
|
|
1101
|
+
"checkpoint_id": row.checkpoint_id,
|
|
1102
|
+
"run_id": row.run_id,
|
|
1103
|
+
"tool_name": row.tool_name,
|
|
1104
|
+
"input_json": row.input_json,
|
|
1105
|
+
"output_json": row.output_json,
|
|
1106
|
+
"success": row.success,
|
|
1107
|
+
"latency_ms": row.latency_ms,
|
|
1108
|
+
"status": row.status,
|
|
1109
|
+
"created_at": row.created_at,
|
|
1110
|
+
}
|
|
1111
|
+
for row in rows
|
|
1112
|
+
]
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def revert_tool_call_records(
|
|
1116
|
+
session_id: str,
|
|
1117
|
+
from_record_id: int,
|
|
1118
|
+
db_path: Path | None = None,
|
|
1119
|
+
) -> int:
|
|
1120
|
+
"""Mark ToolCallRecords as reverted from from_record_id onward.
|
|
1121
|
+
|
|
1122
|
+
Sets status='reverted' on all records for the session with id >= from_record_id.
|
|
1123
|
+
Reverted records are excluded from list_tool_call_records() by default and from
|
|
1124
|
+
the soc_exporter output, so re-generating the script omits the rolled-back work.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
session_id (str): The SoC session whose records should be reverted.
|
|
1128
|
+
from_record_id (int): First record ID to mark as reverted (inclusive).
|
|
1129
|
+
db_path (Path | None): Override default SQLite DB path.
|
|
1130
|
+
|
|
1131
|
+
Returns:
|
|
1132
|
+
int: Number of records marked reverted.
|
|
1133
|
+
"""
|
|
1134
|
+
resolved = init_db(db_path)
|
|
1135
|
+
engine = _build_engine(resolved)
|
|
1136
|
+
with Session(engine) as db_session:
|
|
1137
|
+
rows = db_session.exec(
|
|
1138
|
+
select(ToolCallRecord)
|
|
1139
|
+
.where(ToolCallRecord.session_id == session_id)
|
|
1140
|
+
.where(ToolCallRecord.id >= from_record_id) # type: ignore[operator]
|
|
1141
|
+
).all()
|
|
1142
|
+
for row in rows:
|
|
1143
|
+
row.status = "reverted"
|
|
1144
|
+
db_session.commit()
|
|
1145
|
+
return len(rows)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def insert_evidence_link(
|
|
1149
|
+
*,
|
|
1150
|
+
session_id: str,
|
|
1151
|
+
source_type: str,
|
|
1152
|
+
source_id: str,
|
|
1153
|
+
checkpoint_id: int | None = None,
|
|
1154
|
+
relevance_score: float | None = None,
|
|
1155
|
+
rationale: str | None = None,
|
|
1156
|
+
payload_json: str | None = None,
|
|
1157
|
+
db_path: Path | None = None,
|
|
1158
|
+
) -> None:
|
|
1159
|
+
"""Insert one evidence row used by planning/classification.
|
|
1160
|
+
|
|
1161
|
+
Args:
|
|
1162
|
+
session_id (str): The session id value.
|
|
1163
|
+
source_type (str): The source type value.
|
|
1164
|
+
source_id (str): The source id value.
|
|
1165
|
+
checkpoint_id (int | None): The checkpoint id value. Defaults to None.
|
|
1166
|
+
relevance_score (float | None): The relevance score value. Defaults to None.
|
|
1167
|
+
rationale (str | None): The rationale value. Defaults to None.
|
|
1168
|
+
payload_json (str | None): The payload json value. Defaults to None.
|
|
1169
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1170
|
+
|
|
1171
|
+
Returns:
|
|
1172
|
+
None: None.
|
|
1173
|
+
"""
|
|
1174
|
+
resolved = init_db(db_path)
|
|
1175
|
+
engine = _build_engine(resolved)
|
|
1176
|
+
with Session(engine) as session:
|
|
1177
|
+
session.add(
|
|
1178
|
+
EvidenceLink(
|
|
1179
|
+
session_id=session_id,
|
|
1180
|
+
checkpoint_id=checkpoint_id,
|
|
1181
|
+
source_type=source_type,
|
|
1182
|
+
source_id=source_id,
|
|
1183
|
+
relevance_score=relevance_score,
|
|
1184
|
+
rationale=rationale,
|
|
1185
|
+
payload_json=payload_json,
|
|
1186
|
+
created_at=_utc_now_iso(),
|
|
1187
|
+
)
|
|
1188
|
+
)
|
|
1189
|
+
session.commit()
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def list_evidence_links(
|
|
1193
|
+
session_id: str,
|
|
1194
|
+
checkpoint_id: int | None = None,
|
|
1195
|
+
db_path: Path | None = None,
|
|
1196
|
+
) -> list[dict[str, Any]]:
|
|
1197
|
+
"""List evidence rows for a session and optional checkpoint.
|
|
1198
|
+
|
|
1199
|
+
Args:
|
|
1200
|
+
session_id (str): The session id value.
|
|
1201
|
+
checkpoint_id (int | None): The checkpoint id value. Defaults to None.
|
|
1202
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1203
|
+
|
|
1204
|
+
Returns:
|
|
1205
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
1206
|
+
"""
|
|
1207
|
+
resolved = init_db(db_path)
|
|
1208
|
+
engine = _build_engine(resolved)
|
|
1209
|
+
with Session(engine) as session:
|
|
1210
|
+
query = select(EvidenceLink).where(EvidenceLink.session_id == session_id)
|
|
1211
|
+
if checkpoint_id is not None:
|
|
1212
|
+
query = query.where(EvidenceLink.checkpoint_id == checkpoint_id)
|
|
1213
|
+
rows = session.exec(query.order_by(EvidenceLink.id.asc())).all() # type: ignore[union-attr]
|
|
1214
|
+
|
|
1215
|
+
return [
|
|
1216
|
+
{
|
|
1217
|
+
"id": row.id,
|
|
1218
|
+
"session_id": row.session_id,
|
|
1219
|
+
"checkpoint_id": row.checkpoint_id,
|
|
1220
|
+
"source_type": row.source_type,
|
|
1221
|
+
"source_id": row.source_id,
|
|
1222
|
+
"relevance_score": row.relevance_score,
|
|
1223
|
+
"rationale": row.rationale,
|
|
1224
|
+
"payload_json": row.payload_json,
|
|
1225
|
+
"created_at": row.created_at,
|
|
1226
|
+
}
|
|
1227
|
+
for row in rows
|
|
1228
|
+
]
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def insert_model_state_snapshot(
|
|
1232
|
+
*,
|
|
1233
|
+
session_id: str,
|
|
1234
|
+
checkpoint_id: int | None = None,
|
|
1235
|
+
model_path: str | None = None,
|
|
1236
|
+
feature_tree_json: str | None = None,
|
|
1237
|
+
mass_properties_json: str | None = None,
|
|
1238
|
+
screenshot_path: str | None = None,
|
|
1239
|
+
state_fingerprint: str | None = None,
|
|
1240
|
+
db_path: Path | None = None,
|
|
1241
|
+
) -> int:
|
|
1242
|
+
"""Insert model snapshot row and return snapshot ID for rollback tracking.
|
|
1243
|
+
|
|
1244
|
+
Args:
|
|
1245
|
+
session_id (str): The session id value.
|
|
1246
|
+
checkpoint_id (int | None): The checkpoint id value. Defaults to None.
|
|
1247
|
+
model_path (str | None): The model path value. Defaults to None.
|
|
1248
|
+
feature_tree_json (str | None): The feature tree json value. Defaults to None.
|
|
1249
|
+
mass_properties_json (str | None): The mass properties json value. Defaults to None.
|
|
1250
|
+
screenshot_path (str | None): The screenshot path value. Defaults to None.
|
|
1251
|
+
state_fingerprint (str | None): The state fingerprint value. Defaults to None.
|
|
1252
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1253
|
+
|
|
1254
|
+
Returns:
|
|
1255
|
+
int: The computed numeric result.
|
|
1256
|
+
"""
|
|
1257
|
+
resolved = init_db(db_path)
|
|
1258
|
+
engine = _build_engine(resolved)
|
|
1259
|
+
with Session(engine) as session:
|
|
1260
|
+
row = ModelStateSnapshot(
|
|
1261
|
+
session_id=session_id,
|
|
1262
|
+
checkpoint_id=checkpoint_id,
|
|
1263
|
+
model_path=model_path,
|
|
1264
|
+
feature_tree_json=feature_tree_json,
|
|
1265
|
+
mass_properties_json=mass_properties_json,
|
|
1266
|
+
screenshot_path=screenshot_path,
|
|
1267
|
+
state_fingerprint=state_fingerprint,
|
|
1268
|
+
created_at=_utc_now_iso(),
|
|
1269
|
+
)
|
|
1270
|
+
session.add(row)
|
|
1271
|
+
session.commit()
|
|
1272
|
+
session.refresh(row)
|
|
1273
|
+
return int(row.id or 0)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def list_model_state_snapshots(
|
|
1277
|
+
session_id: str,
|
|
1278
|
+
db_path: Path | None = None,
|
|
1279
|
+
) -> list[dict[str, Any]]:
|
|
1280
|
+
"""List snapshots for a session newest first for diff/rollback flows.
|
|
1281
|
+
|
|
1282
|
+
Args:
|
|
1283
|
+
session_id (str): The session id value.
|
|
1284
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1285
|
+
|
|
1286
|
+
Returns:
|
|
1287
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
1288
|
+
"""
|
|
1289
|
+
resolved = init_db(db_path)
|
|
1290
|
+
engine = _build_engine(resolved)
|
|
1291
|
+
with Session(engine) as session:
|
|
1292
|
+
rows = session.exec(
|
|
1293
|
+
select(ModelStateSnapshot)
|
|
1294
|
+
.where(ModelStateSnapshot.session_id == session_id)
|
|
1295
|
+
.order_by(ModelStateSnapshot.id.desc()) # type: ignore[union-attr]
|
|
1296
|
+
).all()
|
|
1297
|
+
|
|
1298
|
+
return [
|
|
1299
|
+
{
|
|
1300
|
+
"id": row.id,
|
|
1301
|
+
"session_id": row.session_id,
|
|
1302
|
+
"checkpoint_id": row.checkpoint_id,
|
|
1303
|
+
"model_path": row.model_path,
|
|
1304
|
+
"feature_tree_json": row.feature_tree_json,
|
|
1305
|
+
"mass_properties_json": row.mass_properties_json,
|
|
1306
|
+
"screenshot_path": row.screenshot_path,
|
|
1307
|
+
"state_fingerprint": row.state_fingerprint,
|
|
1308
|
+
"created_at": row.created_at,
|
|
1309
|
+
}
|
|
1310
|
+
for row in rows
|
|
1311
|
+
]
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def create_soc_checkpoint(
|
|
1315
|
+
*,
|
|
1316
|
+
session_id: str,
|
|
1317
|
+
label: str,
|
|
1318
|
+
file_path: str,
|
|
1319
|
+
first_record_id: int | None = None,
|
|
1320
|
+
last_record_id: int | None = None,
|
|
1321
|
+
snapshot_id: int | None = None,
|
|
1322
|
+
db_path: Path | None = None,
|
|
1323
|
+
) -> int:
|
|
1324
|
+
"""Insert a SoCCheckpoint and return its id.
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
session_id (str): Owning SoC session.
|
|
1328
|
+
label (str): Short human name (e.g. "base-extrude").
|
|
1329
|
+
file_path (str): Path to the .sldprt saved at this checkpoint.
|
|
1330
|
+
first_record_id (int | None): First ToolCallRecord.id in this block.
|
|
1331
|
+
last_record_id (int | None): Last ToolCallRecord.id in this block.
|
|
1332
|
+
snapshot_id (int | None): FK to ModelStateSnapshot.
|
|
1333
|
+
db_path (Path | None): Override default SQLite path.
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
int: The new SoCCheckpoint.id.
|
|
1337
|
+
"""
|
|
1338
|
+
resolved = init_db(db_path)
|
|
1339
|
+
engine = _build_engine(resolved)
|
|
1340
|
+
with Session(engine) as session:
|
|
1341
|
+
row = SoCCheckpoint(
|
|
1342
|
+
session_id=session_id,
|
|
1343
|
+
label=label,
|
|
1344
|
+
file_path=file_path,
|
|
1345
|
+
first_record_id=first_record_id,
|
|
1346
|
+
last_record_id=last_record_id,
|
|
1347
|
+
snapshot_id=snapshot_id,
|
|
1348
|
+
created_at=_utc_now_iso(),
|
|
1349
|
+
)
|
|
1350
|
+
session.add(row)
|
|
1351
|
+
session.commit()
|
|
1352
|
+
session.refresh(row)
|
|
1353
|
+
return int(row.id) # type: ignore[arg-type]
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def list_soc_checkpoints(
|
|
1357
|
+
session_id: str,
|
|
1358
|
+
db_path: Path | None = None,
|
|
1359
|
+
) -> list[dict[str, Any]]:
|
|
1360
|
+
"""List SoCCheckpoints for a session in creation order.
|
|
1361
|
+
|
|
1362
|
+
Args:
|
|
1363
|
+
session_id (str): Owning SoC session.
|
|
1364
|
+
db_path (Path | None): Override default SQLite path.
|
|
1365
|
+
|
|
1366
|
+
Returns:
|
|
1367
|
+
list[dict[str, Any]]: Ordered list of checkpoint dicts.
|
|
1368
|
+
"""
|
|
1369
|
+
resolved = init_db(db_path)
|
|
1370
|
+
engine = _build_engine(resolved)
|
|
1371
|
+
with Session(engine) as session:
|
|
1372
|
+
rows = session.exec(
|
|
1373
|
+
select(SoCCheckpoint)
|
|
1374
|
+
.where(SoCCheckpoint.session_id == session_id)
|
|
1375
|
+
.order_by(SoCCheckpoint.id.asc()) # type: ignore[union-attr]
|
|
1376
|
+
).all()
|
|
1377
|
+
return [
|
|
1378
|
+
{
|
|
1379
|
+
"id": row.id,
|
|
1380
|
+
"session_id": row.session_id,
|
|
1381
|
+
"label": row.label,
|
|
1382
|
+
"file_path": row.file_path,
|
|
1383
|
+
"first_record_id": row.first_record_id,
|
|
1384
|
+
"last_record_id": row.last_record_id,
|
|
1385
|
+
"snapshot_id": row.snapshot_id,
|
|
1386
|
+
"created_at": row.created_at,
|
|
1387
|
+
}
|
|
1388
|
+
for row in rows
|
|
1389
|
+
]
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def get_soc_checkpoint(
|
|
1393
|
+
session_id: str,
|
|
1394
|
+
label: str,
|
|
1395
|
+
db_path: Path | None = None,
|
|
1396
|
+
) -> dict[str, Any] | None:
|
|
1397
|
+
"""Fetch a single SoCCheckpoint by session + label.
|
|
1398
|
+
|
|
1399
|
+
Args:
|
|
1400
|
+
session_id (str): Owning SoC session.
|
|
1401
|
+
label (str): Checkpoint label to look up.
|
|
1402
|
+
db_path (Path | None): Override default SQLite path.
|
|
1403
|
+
|
|
1404
|
+
Returns:
|
|
1405
|
+
dict[str, Any] | None: Checkpoint dict or None if not found.
|
|
1406
|
+
"""
|
|
1407
|
+
resolved = init_db(db_path)
|
|
1408
|
+
engine = _build_engine(resolved)
|
|
1409
|
+
with Session(engine) as session:
|
|
1410
|
+
row = session.exec(
|
|
1411
|
+
select(SoCCheckpoint)
|
|
1412
|
+
.where(SoCCheckpoint.session_id == session_id)
|
|
1413
|
+
.where(SoCCheckpoint.label == label)
|
|
1414
|
+
.order_by(SoCCheckpoint.id.desc()) # type: ignore[union-attr]
|
|
1415
|
+
).first()
|
|
1416
|
+
if row is None:
|
|
1417
|
+
return None
|
|
1418
|
+
return {
|
|
1419
|
+
"id": row.id,
|
|
1420
|
+
"session_id": row.session_id,
|
|
1421
|
+
"label": row.label,
|
|
1422
|
+
"file_path": row.file_path,
|
|
1423
|
+
"first_record_id": row.first_record_id,
|
|
1424
|
+
"last_record_id": row.last_record_id,
|
|
1425
|
+
"snapshot_id": row.snapshot_id,
|
|
1426
|
+
"created_at": row.created_at,
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def insert_sketch_graph_snapshot(
|
|
1431
|
+
*,
|
|
1432
|
+
session_id: str,
|
|
1433
|
+
nodes_json: str,
|
|
1434
|
+
edges_json: str,
|
|
1435
|
+
model_path: str | None = None,
|
|
1436
|
+
graph_format: str = "json",
|
|
1437
|
+
metadata_json: str | None = None,
|
|
1438
|
+
db_path: Path | None = None,
|
|
1439
|
+
) -> None:
|
|
1440
|
+
"""Store lightweight sketch graph snapshots in SQLite (Section F).
|
|
1441
|
+
|
|
1442
|
+
Args:
|
|
1443
|
+
session_id (str): The session id value.
|
|
1444
|
+
nodes_json (str): The nodes json value.
|
|
1445
|
+
edges_json (str): The edges json value.
|
|
1446
|
+
model_path (str | None): The model path value. Defaults to None.
|
|
1447
|
+
graph_format (str): The graph format value. Defaults to "json".
|
|
1448
|
+
metadata_json (str | None): The metadata json value. Defaults to None.
|
|
1449
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
None: None.
|
|
1453
|
+
"""
|
|
1454
|
+
resolved = init_db(db_path)
|
|
1455
|
+
engine = _build_engine(resolved)
|
|
1456
|
+
with Session(engine) as session:
|
|
1457
|
+
session.add(
|
|
1458
|
+
SketchGraphSnapshot(
|
|
1459
|
+
session_id=session_id,
|
|
1460
|
+
model_path=model_path,
|
|
1461
|
+
graph_format=graph_format,
|
|
1462
|
+
nodes_json=nodes_json,
|
|
1463
|
+
edges_json=edges_json,
|
|
1464
|
+
metadata_json=metadata_json,
|
|
1465
|
+
created_at=_utc_now_iso(),
|
|
1466
|
+
)
|
|
1467
|
+
)
|
|
1468
|
+
session.commit()
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
def list_sketch_graph_snapshots(
|
|
1472
|
+
session_id: str,
|
|
1473
|
+
model_path: str | None = None,
|
|
1474
|
+
db_path: Path | None = None,
|
|
1475
|
+
) -> list[dict[str, Any]]:
|
|
1476
|
+
"""List sketch graph snapshots for a session.
|
|
1477
|
+
|
|
1478
|
+
Args:
|
|
1479
|
+
session_id (str): The session id value.
|
|
1480
|
+
model_path (str | None): The model path value. Defaults to None.
|
|
1481
|
+
db_path (Path | None): The db path value. Defaults to None.
|
|
1482
|
+
|
|
1483
|
+
Returns:
|
|
1484
|
+
list[dict[str, Any]]: A list containing the resulting items.
|
|
1485
|
+
"""
|
|
1486
|
+
resolved = init_db(db_path)
|
|
1487
|
+
engine = _build_engine(resolved)
|
|
1488
|
+
with Session(engine) as session:
|
|
1489
|
+
query = select(SketchGraphSnapshot).where(
|
|
1490
|
+
SketchGraphSnapshot.session_id == session_id
|
|
1491
|
+
)
|
|
1492
|
+
if model_path is not None:
|
|
1493
|
+
query = query.where(SketchGraphSnapshot.model_path == model_path)
|
|
1494
|
+
rows = session.exec(query.order_by(SketchGraphSnapshot.id.desc())).all() # type: ignore[union-attr]
|
|
1495
|
+
|
|
1496
|
+
return [
|
|
1497
|
+
{
|
|
1498
|
+
"id": row.id,
|
|
1499
|
+
"session_id": row.session_id,
|
|
1500
|
+
"model_path": row.model_path,
|
|
1501
|
+
"graph_format": row.graph_format,
|
|
1502
|
+
"nodes_json": row.nodes_json,
|
|
1503
|
+
"edges_json": row.edges_json,
|
|
1504
|
+
"metadata_json": row.metadata_json,
|
|
1505
|
+
"created_at": row.created_at,
|
|
1506
|
+
}
|
|
1507
|
+
for row in rows
|
|
1508
|
+
]
|