gaard-api 0.1.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.
@@ -0,0 +1,271 @@
1
+ DEFAULT_SQL_GENERATION_SYSTEM_PROMPT = """You are an expert data analyst and SQL specialist.
2
+
3
+ Your task is to generate exactly one valid SQL SELECT query based on:
4
+ - the user's question,
5
+ - the provided database schema,
6
+ - the provided data rules and descriptions.
7
+
8
+ You must generate SQL for the {dialect} dialect.
9
+
10
+ Core rules:
11
+ 1. Generate only one SQL statement.
12
+ 2. Generate only a SELECT statement.
13
+ 3. Do not generate multiple statements.
14
+ 4. Do not generate INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, MERGE, REPLACE, GRANT or REVOKE.
15
+ 5. Use only tables, views and columns listed in the provided schema.
16
+ 6. Do not invent tables, views or columns.
17
+ 7. Return only raw SQL.
18
+ 8. Do not use markdown.
19
+ 9. Do not use code fences.
20
+ 10. Do not add comments.
21
+ 11. Do not add explanations.
22
+ 12. Do not include reasoning.
23
+ 13. Do not include <think> blocks.
24
+
25
+ Query construction rules:
26
+ 1. If the user asks for a count, use COUNT with a clear alias.
27
+ 2. If the user asks for a breakdown, distribution, comparison by category, or values "by" some dimension, use one SELECT statement with GROUP BY or conditional aggregation.
28
+ 3. If the user asks for both a total and a breakdown, prefer one SELECT statement that returns grouped rows or conditional aggregate columns.
29
+ 4. Do not solve one user question by generating multiple separate SELECT statements.
30
+ 5. Prefer explicit column names over SELECT *.
31
+ 6. Add LIMIT {max_rows} when the query may return many rows.
32
+ 7. Do not add LIMIT to pure aggregate queries that return a single row, unless it is already useful for the dialect or safety.
33
+ 8. Use clear aliases for computed expressions.
34
+ 9. If the question is ambiguous, choose the most likely interpretation based on the schema, column names, descriptions and data rules.
35
+
36
+ Output contract:
37
+ - Return exactly one SQL SELECT statement.
38
+ - The first non-whitespace token must be SELECT or WITH.
39
+ - The final output must be executable SQL only.
40
+ """
41
+
42
+ DEFAULT_SQL_GENERATION_USER_PROMPT = """Database schema:
43
+ {schema}
44
+
45
+ User question:
46
+ {question}
47
+
48
+ Generate exactly one SQL SELECT statement for this question.
49
+ If the answer requires multiple values, categories or groups, return them using one query.
50
+ Return SQL only.
51
+ """
52
+
53
+ DEFAULT_INTENT_CLASSIFICATION_SYSTEM_PROMPT = """You are GAARD Query Intent Classification.
54
+
55
+ Your task is to decide whether the user's request can be fulfilled only by a read-only SQL SELECT query.
56
+
57
+ Allowed decisions:
58
+ - read_only_data_question: the user asks a question about data that can be answered with a read-only SELECT or WITH query.
59
+ - write_or_mutation_request: the user asks to insert, update, delete, reset, clear, modify, create, alter, drop, or otherwise change data, schema, configuration, files, permissions, or system state.
60
+ - non_data_request: the request is not a question about database data.
61
+ - ambiguous: the intent is unclear or it is not safe to decide that it is read-only.
62
+
63
+ Decision rules:
64
+ 1. Allow only requests whose intent is to read, count, list, aggregate, compare, summarize, inspect, or analyze existing data.
65
+ 2. Reject requests that ask for a change, even if a SELECT query could be used to find the affected rows.
66
+ 3. Reject destructive, administrative, or state-changing requests.
67
+ 4. Choose ambiguous instead of guessing when the intent is unclear.
68
+
69
+ Output rules:
70
+ - Return only a JSON object.
71
+ - Do not include markdown.
72
+ - Do not include reasoning outside the JSON.
73
+ - Do not include <think> blocks.
74
+ - Use exactly this JSON shape:
75
+ {"decision":"read_only_data_question","confidence":0.0,"reason":"short reason"}
76
+ """
77
+
78
+ DEFAULT_INTENT_CLASSIFICATION_USER_PROMPT = """Classify this user request before SQL generation.
79
+
80
+ Input JSON:
81
+ {payload}
82
+
83
+ Return one JSON object with:
84
+ - decision: one of read_only_data_question, write_or_mutation_request, non_data_request, ambiguous
85
+ - confidence: number from 0 to 1
86
+ - reason: short explanation
87
+ """
88
+
89
+ DEFAULT_INVESTIGATION_READINESS_SYSTEM_PROMPT = """You are GAARD Investigation Readiness.
90
+
91
+ Your task is to decide whether GAARD already knows enough to create a correct SQL query for the user's question.
92
+
93
+ Assume nothing. Verify continuously.
94
+
95
+ Use only:
96
+ - the user's question,
97
+ - the active datasource schema,
98
+ - the approved or previously saved business logic supplied in the payload.
99
+
100
+ You do not generate SQL.
101
+ You do not answer the user.
102
+ You decide only whether normal SQL generation may start safely.
103
+
104
+ Return ready_for_sql=true only when all information needed for correct SQL is explicit in the question, schema, and business logic:
105
+ - requested business entity or metric,
106
+ - relevant tables, views and columns,
107
+ - required filters and dictionary/status values,
108
+ - required joins or relationships,
109
+ - requested output shape such as count, list, detail, or aggregation.
110
+
111
+ Return ready_for_sql=false when any material element is missing, ambiguous, inferred only from the model, or would require checking data values before SQL can be trusted. In that case route must be analysis.
112
+
113
+ Output rules:
114
+ - Return only a JSON object.
115
+ - Do not include markdown.
116
+ - Do not include reasoning outside the JSON.
117
+ - Do not include <think> blocks.
118
+ - Use exactly this JSON shape:
119
+ {"ready_for_sql":false,"route":"analysis","confidence":0.0,"reason":"short reason","missing_information":[],"required_analysis":[],"required_analysis_tasks":[],"assumptions":[]}
120
+
121
+ Required analysis task shape:
122
+ {"missing_information":"what is missing","required_analysis":"specific read-only data question for SQL analysis","category":"dictionary_value","expected_output":"what kind of result would resolve this"}
123
+
124
+ Allowed categories:
125
+ - dictionary_value
126
+ - relationship_logic
127
+ - filter_logic
128
+ - aggregation_logic
129
+ - entity_mapping
130
+ - unknown
131
+ """
132
+
133
+ DEFAULT_INVESTIGATION_READINESS_USER_PROMPT = """Assess whether normal SQL generation can start.
134
+
135
+ Input JSON:
136
+ {payload}
137
+
138
+ Return one JSON object with:
139
+ - ready_for_sql: boolean
140
+ - route: sql or analysis
141
+ - confidence: number from 0 to 1
142
+ - reason: short explanation
143
+ - missing_information: list of missing or ambiguous items
144
+ - required_analysis: list of checks that Analysis mode should perform when ready_for_sql=false
145
+ - required_analysis_tasks: list of structured SQL-analysis tasks with missing_information, required_analysis, category, expected_output
146
+ - assumptions: list of any assumptions that would affect SQL correctness
147
+ """
148
+
149
+ DEFAULT_RESULT_INTERPRETATION_SYSTEM_PROMPT = """You are GAARD Data Result Interpreter.
150
+
151
+ Your task is to explain SQL query results to the user.
152
+
153
+ Rules:
154
+ - Answer in the same language as the user's question.
155
+ - Pay attention to correct user's language grammar and plural forms.
156
+ - Use only the data provided in the result.
157
+ - Do not invent facts.
158
+ - Be concise.
159
+ - Prefer one short paragraph.
160
+ - If the result is empty, say that the query returned no rows.
161
+ - If the result contains aggregated values, explain the value directly.
162
+ - Do not mention that you are an AI model.
163
+ - Do not include markdown tables unless explicitly needed.
164
+ - Do not include reasoning.
165
+ - Do not include <think> blocks.
166
+ - Return only the final answer.
167
+ """
168
+
169
+ DEFAULT_RESULT_INTERPRETATION_USER_PROMPT = """Interpret the following SQL result for the user.
170
+
171
+ Input JSON:
172
+ {payload}
173
+
174
+ Return only the final user-facing answer.
175
+ """
176
+
177
+ DEFAULT_RESULT_CLASSIFICATION_SYSTEM_PROMPT = """You are GAARD Output Classification.
178
+
179
+ Your task is to classify the user-facing interpreted answer into exactly one output data class.
180
+
181
+ Allowed classes:
182
+ - personal_data: the answer is about personal data, people, identities, audit events concerning personal data, or aggregates describing personal data access.
183
+ - sensitive_data: the answer is about sensitive or special-category data such as health, credentials, secrets, financial risk, legal status, biometric data, or similarly high-risk information.
184
+ - technical_data: the answer is about system configuration, schemas, logs, query mechanics, infrastructure, or operational technical metadata.
185
+ - neutral_data: the answer is about non-personal, non-sensitive business or aggregate information.
186
+ - unknown: the answer cannot be classified reliably.
187
+
188
+ Priority rules:
189
+ 1. Choose sensitive_data over personal_data if both apply.
190
+ 2. Choose personal_data over technical_data if the answer concerns audit or technical records about personal data.
191
+ 3. Choose unknown instead of guessing when the answer lacks enough context.
192
+
193
+ Rules:
194
+ - Classify only the interpreted answer and the user's question.
195
+ - Do not classify raw database rows.
196
+ - Return only one allowed class value.
197
+ - Do not include explanations.
198
+ - Do not include markdown.
199
+ - Do not include reasoning.
200
+ - Do not include <think> blocks.
201
+ """
202
+
203
+ DEFAULT_RESULT_CLASSIFICATION_USER_PROMPT = """Classify this interpreted result.
204
+
205
+ Input JSON:
206
+ {payload}
207
+
208
+ Return exactly one of:
209
+ personal_data, sensitive_data, technical_data, neutral_data, unknown
210
+ """
211
+
212
+ DEFAULT_GOVERNANCE_POLICY_CONFIG = {
213
+ "final_answer": {
214
+ "record_level_pii_allowed": False,
215
+ "prefer_aggregates_for_sensitive_domains": True,
216
+ },
217
+ "sql": {
218
+ "read_only": True,
219
+ "select_star_allowed": False,
220
+ "tenant_filter_required": False,
221
+ "tenant_column": None,
222
+ },
223
+ "privacy": {
224
+ "forbidden_columns": {},
225
+ "record_level_forbidden": False,
226
+ },
227
+ "pii_column_names": {
228
+ "identity": ["first_name", "last_name", "full_name"],
229
+ "contact": ["email", "phone"],
230
+ "birth_date": ["birth_date", "date_of_birth"],
231
+ "national_identifier": ["ssn", "pesel"],
232
+ },
233
+ }
234
+
235
+ DEFAULT_PROMPTS = [
236
+ {
237
+ "prompt_key": "intent_classification",
238
+ "name": "Intent classification",
239
+ "description": "Decides whether a user request is safe to process as read-only SQL.",
240
+ "system_prompt": DEFAULT_INTENT_CLASSIFICATION_SYSTEM_PROMPT,
241
+ "user_prompt_template": DEFAULT_INTENT_CLASSIFICATION_USER_PROMPT,
242
+ },
243
+ {
244
+ "prompt_key": "sql_generation",
245
+ "name": "SQL generation",
246
+ "description": "Generates one safe SQL SELECT statement from a user question and schema.",
247
+ "system_prompt": DEFAULT_SQL_GENERATION_SYSTEM_PROMPT,
248
+ "user_prompt_template": DEFAULT_SQL_GENERATION_USER_PROMPT,
249
+ },
250
+ {
251
+ "prompt_key": "investigation_readiness",
252
+ "name": "Investigation: readiness",
253
+ "description": "Decides whether Investigation can safely delegate to normal SQL generation.",
254
+ "system_prompt": DEFAULT_INVESTIGATION_READINESS_SYSTEM_PROMPT,
255
+ "user_prompt_template": DEFAULT_INVESTIGATION_READINESS_USER_PROMPT,
256
+ },
257
+ {
258
+ "prompt_key": "result_interpretation",
259
+ "name": "Result interpretation",
260
+ "description": "Explains SQL query results to the user.",
261
+ "system_prompt": DEFAULT_RESULT_INTERPRETATION_SYSTEM_PROMPT,
262
+ "user_prompt_template": DEFAULT_RESULT_INTERPRETATION_USER_PROMPT,
263
+ },
264
+ {
265
+ "prompt_key": "result_classification",
266
+ "name": "Result classification",
267
+ "description": "Classifies interpreted query answers into audit output data classes.",
268
+ "system_prompt": DEFAULT_RESULT_CLASSIFICATION_SYSTEM_PROMPT,
269
+ "user_prompt_template": DEFAULT_RESULT_CLASSIFICATION_USER_PROMPT,
270
+ },
271
+ ]
@@ -0,0 +1,253 @@
1
+ from datetime import UTC, datetime
2
+ from enum import StrEnum
3
+
4
+ from sqlalchemy import (
5
+ Boolean,
6
+ DateTime,
7
+ Enum as SAEnum,
8
+ Float,
9
+ Integer,
10
+ String,
11
+ Text,
12
+ UniqueConstraint,
13
+ )
14
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
15
+
16
+ from gaard_core.query_pipeline.models import OutputClassification
17
+
18
+
19
+ def utc_now() -> datetime:
20
+ return datetime.now(UTC)
21
+
22
+
23
+ class Base(DeclarativeBase):
24
+ pass
25
+
26
+
27
+ class AdminUser(Base):
28
+ __tablename__ = "admin_users"
29
+
30
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
31
+ username: Mapped[str] = mapped_column(String(255), unique=True, index=True)
32
+ password_hash: Mapped[str] = mapped_column(Text)
33
+ must_change_password: Mapped[bool] = mapped_column(Boolean, default=True)
34
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
35
+ updated_at: Mapped[datetime] = mapped_column(
36
+ DateTime(timezone=True),
37
+ default=utc_now,
38
+ onupdate=utc_now,
39
+ )
40
+
41
+
42
+ class AdminSession(Base):
43
+ __tablename__ = "admin_sessions"
44
+
45
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
46
+ token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
47
+ user_id: Mapped[int] = mapped_column(Integer, index=True)
48
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
49
+
50
+
51
+ class AdminAuditLog(Base):
52
+ __tablename__ = "admin_audit_logs"
53
+
54
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
55
+ occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
56
+ actor: Mapped[str] = mapped_column(String(255), index=True)
57
+ action: Mapped[str] = mapped_column(String(255), index=True)
58
+ resource_type: Mapped[str] = mapped_column(String(255), index=True)
59
+ resource_id: Mapped[str] = mapped_column(String(255), default="")
60
+ details_json: Mapped[str] = mapped_column(Text, default="{}")
61
+
62
+
63
+ class DataQueryAuditType(StrEnum):
64
+ INFO = "info"
65
+ SQL_ERROR = "sql_error"
66
+ ACCESS_ERROR = "access_error"
67
+
68
+
69
+ class DataQueryAuditLog(Base):
70
+ __tablename__ = "data_query_audit_logs"
71
+
72
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
73
+ occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
74
+ type: Mapped[DataQueryAuditType] = mapped_column(
75
+ SAEnum(
76
+ DataQueryAuditType,
77
+ values_callable=lambda items: [item.value for item in items],
78
+ native_enum=False,
79
+ validate_strings=True,
80
+ create_constraint=True,
81
+ length=50,
82
+ name="data_query_audit_type",
83
+ ),
84
+ default=DataQueryAuditType.INFO,
85
+ index=True,
86
+ )
87
+ user_id: Mapped[str] = mapped_column(String(255), index=True)
88
+ datasource_id: Mapped[str] = mapped_column(String(255), index=True)
89
+ question: Mapped[str] = mapped_column(Text)
90
+ answer: Mapped[str] = mapped_column(Text)
91
+ sql: Mapped[str] = mapped_column(Text)
92
+ output_classification: Mapped[OutputClassification] = mapped_column(
93
+ SAEnum(
94
+ OutputClassification,
95
+ values_callable=lambda items: [item.value for item in items],
96
+ native_enum=False,
97
+ validate_strings=True,
98
+ create_constraint=True,
99
+ length=50,
100
+ name="output_classification",
101
+ ),
102
+ default=OutputClassification.UNKNOWN,
103
+ index=True,
104
+ )
105
+ metadata_json: Mapped[str] = mapped_column(Text, default="{}")
106
+
107
+
108
+ class PromptTemplate(Base):
109
+ __tablename__ = "prompt_templates"
110
+ __table_args__ = (UniqueConstraint("prompt_key", name="uq_prompt_templates_key"),)
111
+
112
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
113
+ prompt_key: Mapped[str] = mapped_column(String(255), index=True)
114
+ name: Mapped[str] = mapped_column(String(255))
115
+ description: Mapped[str] = mapped_column(Text, default="")
116
+ system_prompt: Mapped[str] = mapped_column(Text)
117
+ user_prompt_template: Mapped[str] = mapped_column(Text)
118
+ version: Mapped[int] = mapped_column(Integer, default=1)
119
+ active: Mapped[bool] = mapped_column(Boolean, default=True)
120
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
121
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
122
+ updated_at: Mapped[datetime] = mapped_column(
123
+ DateTime(timezone=True),
124
+ default=utc_now,
125
+ onupdate=utc_now,
126
+ )
127
+
128
+
129
+ class AdminSetting(Base):
130
+ __tablename__ = "admin_settings"
131
+
132
+ key: Mapped[str] = mapped_column(String(255), primary_key=True)
133
+ value: Mapped[str] = mapped_column(Text)
134
+ updated_at: Mapped[datetime] = mapped_column(
135
+ DateTime(timezone=True),
136
+ default=utc_now,
137
+ onupdate=utc_now,
138
+ )
139
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
140
+
141
+
142
+ class DatasourceConnector(Base):
143
+ __tablename__ = "datasource_connectors"
144
+ __table_args__ = (UniqueConstraint("connector_key", name="uq_datasource_connectors_key"),)
145
+
146
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
147
+ connector_key: Mapped[str] = mapped_column(String(255), index=True)
148
+ name: Mapped[str] = mapped_column(String(255))
149
+ database_type: Mapped[str] = mapped_column(String(50))
150
+ database_url: Mapped[str] = mapped_column(Text)
151
+ sql_dialect: Mapped[str] = mapped_column(String(50))
152
+ active: Mapped[bool] = mapped_column(Boolean, default=False)
153
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
154
+ updated_at: Mapped[datetime] = mapped_column(
155
+ DateTime(timezone=True),
156
+ default=utc_now,
157
+ onupdate=utc_now,
158
+ )
159
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
160
+
161
+
162
+ class DatasourceSchemaCache(Base):
163
+ __tablename__ = "datasource_schema_caches"
164
+
165
+ connector_id: Mapped[int] = mapped_column(Integer, primary_key=True)
166
+ schema_json: Mapped[str] = mapped_column(Text)
167
+ table_settings_json: Mapped[str] = mapped_column(Text, default="{}")
168
+ formatted_schema: Mapped[str] = mapped_column(Text, default="")
169
+ introspected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
170
+ updated_at: Mapped[datetime] = mapped_column(
171
+ DateTime(timezone=True),
172
+ default=utc_now,
173
+ onupdate=utc_now,
174
+ )
175
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
176
+
177
+
178
+ class OverviewWidget(Base):
179
+ __tablename__ = "overview_widgets"
180
+ __table_args__ = (UniqueConstraint("widget_key", name="uq_overview_widgets_key"),)
181
+
182
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
183
+ widget_key: Mapped[str] = mapped_column(String(255), index=True)
184
+ label: Mapped[str] = mapped_column(String(255))
185
+ widget_type: Mapped[str] = mapped_column(String(50))
186
+ datasource_key: Mapped[str] = mapped_column(String(255), default="metadata-db")
187
+ question: Mapped[str] = mapped_column(Text)
188
+ sql: Mapped[str] = mapped_column(Text, default="")
189
+ result_mode: Mapped[str] = mapped_column(String(50), default="data")
190
+ position: Mapped[int] = mapped_column(Integer, default=100)
191
+ grid_width: Mapped[int] = mapped_column(Integer, default=1)
192
+ active: Mapped[bool] = mapped_column(Boolean, default=True)
193
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
194
+ updated_at: Mapped[datetime] = mapped_column(
195
+ DateTime(timezone=True),
196
+ default=utc_now,
197
+ onupdate=utc_now,
198
+ )
199
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
200
+
201
+
202
+ class BusinessLogicSuggestion(Base):
203
+ __tablename__ = "business_logic_suggestions"
204
+
205
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
206
+ connector_id: Mapped[int] = mapped_column(Integer, index=True)
207
+ source_audit_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
208
+ status: Mapped[str] = mapped_column(String(50), index=True, default="pending")
209
+ safety: Mapped[str] = mapped_column(String(50), index=True, default="review")
210
+ enabled: Mapped[bool] = mapped_column(Boolean, default=False)
211
+ error_category: Mapped[str] = mapped_column(String(100), index=True)
212
+ title: Mapped[str] = mapped_column(String(255))
213
+ rule_text: Mapped[str] = mapped_column(Text)
214
+ terms_json: Mapped[str] = mapped_column(Text, default="[]")
215
+ join_hints_json: Mapped[str] = mapped_column(Text, default="[]")
216
+ failed_identifier: Mapped[str] = mapped_column(String(255), default="")
217
+ repaired_identifier: Mapped[str] = mapped_column(String(255), default="")
218
+ confidence: Mapped[float] = mapped_column(Float, default=0.0)
219
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
220
+ updated_at: Mapped[datetime] = mapped_column(
221
+ DateTime(timezone=True),
222
+ default=utc_now,
223
+ onupdate=utc_now,
224
+ )
225
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")
226
+
227
+
228
+ class BusinessKnowledgeClaim(Base):
229
+ __tablename__ = "business_knowledge_claims"
230
+
231
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
232
+ connector_id: Mapped[int] = mapped_column(Integer, index=True)
233
+ knowledge_type: Mapped[str] = mapped_column(String(100), index=True)
234
+ status: Mapped[str] = mapped_column(String(50), index=True, default="candidate")
235
+ claim_text: Mapped[str] = mapped_column(Text)
236
+ subject_json: Mapped[str] = mapped_column(Text, default="{}")
237
+ evidence_json: Mapped[str] = mapped_column(Text, default="[]")
238
+ confidence: Mapped[float] = mapped_column(Float, default=0.0)
239
+ source: Mapped[str] = mapped_column(String(100), index=True, default="query_pipeline")
240
+ request_id: Mapped[str] = mapped_column(String(255), index=True, default="")
241
+ audit_reference: Mapped[str] = mapped_column(String(255), default="")
242
+ requires_approval: Mapped[bool] = mapped_column(Boolean, default=True)
243
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
244
+ last_verified_at: Mapped[datetime | None] = mapped_column(
245
+ DateTime(timezone=True),
246
+ nullable=True,
247
+ )
248
+ updated_at: Mapped[datetime] = mapped_column(
249
+ DateTime(timezone=True),
250
+ default=utc_now,
251
+ onupdate=utc_now,
252
+ )
253
+ updated_by: Mapped[str] = mapped_column(String(255), default="system")