powerbi-ontology-extractor 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,408 @@
1
+ """
2
+ Semantic Contract → OWL Converter
3
+
4
+ Converts SemanticContract to OntoGuard-compatible OWL format.
5
+ This enables AI agent contracts to be validated by OntoGuard semantic firewall.
6
+
7
+ Key mappings:
8
+ - read_entities → ReadAction with requiresRole/appliesTo
9
+ - write_properties → WriteAction with requiresRole/appliesTo
10
+ - executable_actions → ExecuteAction with requiresRole/appliesTo
11
+ - business_rules → Action classes with constraints
12
+ - context_filters → OWL restrictions
13
+ """
14
+
15
+ import logging
16
+ from typing import Optional, Any
17
+
18
+ from rdflib import Graph, Namespace, Literal, URIRef
19
+ from rdflib.namespace import RDF, RDFS, OWL, XSD
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ContractToOWLConverter:
25
+ """
26
+ Converts SemanticContract to OntoGuard-compatible OWL format.
27
+
28
+ This generates OWL action rules that can be loaded by OntoGuard
29
+ for validating AI agent actions against their contract.
30
+
31
+ Example:
32
+ from powerbi_ontology.contract_builder import ContractBuilder, SemanticContract
33
+
34
+ contract = builder.build_contract("SalesAgent", permissions)
35
+ converter = ContractToOWLConverter(contract)
36
+ converter.save("sales_agent_contract.owl")
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ contract: Any, # SemanticContract
42
+ base_uri: Optional[str] = None,
43
+ ontology: Optional[Any] = None # Ontology for entity details
44
+ ):
45
+ """
46
+ Initialize converter.
47
+
48
+ Args:
49
+ contract: SemanticContract to convert
50
+ base_uri: Optional base URI (defaults to agent name)
51
+ ontology: Optional Ontology for additional entity details
52
+ """
53
+ self.contract = contract
54
+ self.ontology = ontology
55
+ self.graph = Graph()
56
+
57
+ # Create namespace
58
+ agent_name = contract.agent_name.replace(" ", "_").replace("-", "_")
59
+ self.base_uri = base_uri or f"http://example.org/contracts/{agent_name}#"
60
+
61
+ self.ont = Namespace(self.base_uri)
62
+
63
+ # Bind namespaces
64
+ self.graph.bind("ont", self.ont)
65
+ self.graph.bind("owl", OWL)
66
+ self.graph.bind("rdfs", RDFS)
67
+ self.graph.bind("xsd", XSD)
68
+
69
+ def convert(self, format: str = "xml") -> str:
70
+ """
71
+ Convert SemanticContract to OWL format.
72
+
73
+ Args:
74
+ format: Output format ("xml", "turtle", "json-ld", "n3")
75
+
76
+ Returns:
77
+ OWL content as string
78
+ """
79
+ logger.info(f"Converting contract '{self.contract.agent_name}' to OWL")
80
+
81
+ # Add ontology metadata
82
+ self._add_ontology_metadata()
83
+
84
+ # Add base classes
85
+ self._add_base_classes()
86
+
87
+ # Add OntoGuard properties
88
+ self._add_ontoguard_properties()
89
+
90
+ # Add entity classes from permissions
91
+ self._add_entity_classes()
92
+
93
+ # Convert read permissions to action rules
94
+ self._add_read_permissions()
95
+
96
+ # Convert write permissions to action rules
97
+ self._add_write_permissions()
98
+
99
+ # Convert executable actions
100
+ self._add_executable_actions()
101
+
102
+ # Convert business rules
103
+ self._add_business_rules()
104
+
105
+ # Add context filters as restrictions
106
+ self._add_context_filters()
107
+
108
+ # Add audit configuration
109
+ self._add_audit_config()
110
+
111
+ return self.graph.serialize(format=format)
112
+
113
+ def _add_ontology_metadata(self):
114
+ """Add OWL ontology metadata."""
115
+ ontology_uri = URIRef(self.base_uri.rstrip("#"))
116
+
117
+ self.graph.add((ontology_uri, RDF.type, OWL.Ontology))
118
+ self.graph.add((ontology_uri, RDFS.label, Literal(
119
+ f"Contract: {self.contract.agent_name}"
120
+ )))
121
+ self.graph.add((ontology_uri, RDFS.comment, Literal(
122
+ f"Semantic contract for AI agent '{self.contract.agent_name}'"
123
+ )))
124
+ self.graph.add((ontology_uri, OWL.versionInfo, Literal(
125
+ self.contract.ontology_version
126
+ )))
127
+
128
+ # Add metadata
129
+ metadata = self.contract.metadata or {}
130
+ if metadata.get("created_date"):
131
+ self.graph.add((ontology_uri, self.ont.createdDate, Literal(
132
+ metadata["created_date"], datatype=XSD.dateTime
133
+ )))
134
+ if metadata.get("ontology_source"):
135
+ self.graph.add((ontology_uri, self.ont.ontologySource, Literal(
136
+ metadata["ontology_source"]
137
+ )))
138
+
139
+ def _add_base_classes(self):
140
+ """Add base classes for OntoGuard compatibility."""
141
+ # User/Role class
142
+ user_uri = self.ont.User
143
+ self.graph.add((user_uri, RDF.type, OWL.Class))
144
+ self.graph.add((user_uri, RDFS.label, Literal("User")))
145
+
146
+ # Add the agent's required role
147
+ role = self.contract.permissions.required_role or "Agent"
148
+ role_uri = self.ont[self._safe_name(role)]
149
+ self.graph.add((role_uri, RDF.type, OWL.Class))
150
+ self.graph.add((role_uri, RDFS.subClassOf, user_uri))
151
+ self.graph.add((role_uri, RDFS.label, Literal(role)))
152
+ self.graph.add((role_uri, RDFS.comment, Literal(
153
+ f"Role required by agent {self.contract.agent_name}"
154
+ )))
155
+
156
+ # Action base class
157
+ action_uri = self.ont.Action
158
+ self.graph.add((action_uri, RDF.type, OWL.Class))
159
+ self.graph.add((action_uri, RDFS.label, Literal("Action")))
160
+
161
+ # Action subclasses
162
+ for action_type in ["ReadAction", "WriteAction", "DeleteAction", "ExecuteAction"]:
163
+ action_class = self.ont[action_type]
164
+ self.graph.add((action_class, RDF.type, OWL.Class))
165
+ self.graph.add((action_class, RDFS.subClassOf, action_uri))
166
+ self.graph.add((action_class, RDFS.label, Literal(action_type)))
167
+
168
+ def _add_ontoguard_properties(self):
169
+ """Add OntoGuard action permission properties."""
170
+ # requiresRole
171
+ requires_role = self.ont.requiresRole
172
+ self.graph.add((requires_role, RDF.type, OWL.ObjectProperty))
173
+ self.graph.add((requires_role, RDFS.label, Literal("requires role")))
174
+ self.graph.add((requires_role, RDFS.domain, self.ont.Action))
175
+ self.graph.add((requires_role, RDFS.range, self.ont.User))
176
+
177
+ # appliesTo
178
+ applies_to = self.ont.appliesTo
179
+ self.graph.add((applies_to, RDF.type, OWL.ObjectProperty))
180
+ self.graph.add((applies_to, RDFS.label, Literal("applies to")))
181
+ self.graph.add((applies_to, RDFS.domain, self.ont.Action))
182
+ self.graph.add((applies_to, RDFS.range, OWL.Thing))
183
+
184
+ # allowsAction
185
+ allows_action = self.ont.allowsAction
186
+ self.graph.add((allows_action, RDF.type, OWL.DatatypeProperty))
187
+ self.graph.add((allows_action, RDFS.label, Literal("allows action")))
188
+ self.graph.add((allows_action, RDFS.domain, self.ont.Action))
189
+ self.graph.add((allows_action, RDFS.range, XSD.string))
190
+
191
+ # appliesToProperty (for write permissions)
192
+ applies_to_prop = self.ont.appliesToProperty
193
+ self.graph.add((applies_to_prop, RDF.type, OWL.DatatypeProperty))
194
+ self.graph.add((applies_to_prop, RDFS.label, Literal("applies to property")))
195
+ self.graph.add((applies_to_prop, RDFS.domain, self.ont.Action))
196
+ self.graph.add((applies_to_prop, RDFS.range, XSD.string))
197
+
198
+ # hasContextFilter
199
+ has_filter = self.ont.hasContextFilter
200
+ self.graph.add((has_filter, RDF.type, OWL.DatatypeProperty))
201
+ self.graph.add((has_filter, RDFS.label, Literal("has context filter")))
202
+ self.graph.add((has_filter, RDFS.domain, self.ont.Action))
203
+ self.graph.add((has_filter, RDFS.range, XSD.string))
204
+
205
+ def _add_entity_classes(self):
206
+ """Add entity classes from permissions."""
207
+ # Collect all entities from permissions
208
+ entities = set(self.contract.permissions.read_entities)
209
+ entities.update(self.contract.permissions.write_properties.keys())
210
+
211
+ for entity_name in entities:
212
+ entity_uri = self.ont[self._safe_name(entity_name)]
213
+ self.graph.add((entity_uri, RDF.type, OWL.Class))
214
+ self.graph.add((entity_uri, RDFS.label, Literal(entity_name)))
215
+
216
+ # If we have the ontology, add more details
217
+ if self.ontology:
218
+ ont_entity = next(
219
+ (e for e in self.ontology.entities if e.name == entity_name),
220
+ None
221
+ )
222
+ if ont_entity and ont_entity.description:
223
+ self.graph.add((entity_uri, RDFS.comment, Literal(ont_entity.description)))
224
+
225
+ def _add_read_permissions(self):
226
+ """Convert read_entities to ReadAction rules."""
227
+ role = self.contract.permissions.required_role or "Agent"
228
+ role_uri = self.ont[self._safe_name(role)]
229
+
230
+ for entity_name in self.contract.permissions.read_entities:
231
+ entity_uri = self.ont[self._safe_name(entity_name)]
232
+
233
+ # Create action individual
234
+ action_name = f"read_{self._safe_name(entity_name)}"
235
+ action_uri = self.ont[action_name]
236
+
237
+ self.graph.add((action_uri, RDF.type, self.ont.ReadAction))
238
+ self.graph.add((action_uri, RDFS.label, Literal(f"Read {entity_name}")))
239
+ self.graph.add((action_uri, self.ont.allowsAction, Literal("read")))
240
+ self.graph.add((action_uri, self.ont.appliesTo, entity_uri))
241
+ self.graph.add((action_uri, self.ont.requiresRole, role_uri))
242
+
243
+ # Add context filter if exists
244
+ context_filter = self.contract.permissions.context_filters.get(entity_name)
245
+ if context_filter:
246
+ self.graph.add((action_uri, self.ont.hasContextFilter, Literal(context_filter)))
247
+
248
+ def _add_write_permissions(self):
249
+ """Convert write_properties to WriteAction rules."""
250
+ role = self.contract.permissions.required_role or "Agent"
251
+ role_uri = self.ont[self._safe_name(role)]
252
+
253
+ for entity_name, properties in self.contract.permissions.write_properties.items():
254
+ entity_uri = self.ont[self._safe_name(entity_name)]
255
+
256
+ # Create write action for each property
257
+ for prop_name in properties:
258
+ action_name = f"write_{self._safe_name(entity_name)}_{self._safe_name(prop_name)}"
259
+ action_uri = self.ont[action_name]
260
+
261
+ self.graph.add((action_uri, RDF.type, self.ont.WriteAction))
262
+ self.graph.add((action_uri, RDFS.label, Literal(f"Write {entity_name}.{prop_name}")))
263
+ self.graph.add((action_uri, self.ont.allowsAction, Literal("write")))
264
+ self.graph.add((action_uri, self.ont.appliesTo, entity_uri))
265
+ self.graph.add((action_uri, self.ont.appliesToProperty, Literal(prop_name)))
266
+ self.graph.add((action_uri, self.ont.requiresRole, role_uri))
267
+
268
+ # Also create general update action for entity
269
+ update_action_name = f"update_{self._safe_name(entity_name)}"
270
+ update_action_uri = self.ont[update_action_name]
271
+
272
+ self.graph.add((update_action_uri, RDF.type, self.ont.WriteAction))
273
+ self.graph.add((update_action_uri, RDFS.label, Literal(f"Update {entity_name}")))
274
+ self.graph.add((update_action_uri, self.ont.allowsAction, Literal("update")))
275
+ self.graph.add((update_action_uri, self.ont.appliesTo, entity_uri))
276
+ self.graph.add((update_action_uri, self.ont.requiresRole, role_uri))
277
+
278
+ def _add_executable_actions(self):
279
+ """Convert executable_actions to ExecuteAction rules."""
280
+ role = self.contract.permissions.required_role or "Agent"
281
+ role_uri = self.ont[self._safe_name(role)]
282
+
283
+ for action_name in self.contract.permissions.executable_actions:
284
+ safe_action = self._safe_name(action_name)
285
+
286
+ # Create action class
287
+ action_class_uri = self.ont[f"{safe_action}Action"]
288
+ self.graph.add((action_class_uri, RDF.type, OWL.Class))
289
+ self.graph.add((action_class_uri, RDFS.subClassOf, self.ont.ExecuteAction))
290
+ self.graph.add((action_class_uri, RDFS.label, Literal(action_name)))
291
+
292
+ # Create action individual
293
+ action_uri = self.ont[f"execute_{safe_action}"]
294
+ self.graph.add((action_uri, RDF.type, action_class_uri))
295
+ self.graph.add((action_uri, RDFS.label, Literal(f"Execute {action_name}")))
296
+ self.graph.add((action_uri, self.ont.allowsAction, Literal("execute")))
297
+ self.graph.add((action_uri, self.ont.requiresRole, role_uri))
298
+
299
+ def _add_business_rules(self):
300
+ """Convert business rules to OWL action rules."""
301
+ for rule in self.contract.business_rules:
302
+ safe_name = self._safe_name(rule.name)
303
+
304
+ # Create action class for the rule
305
+ rule_class_uri = self.ont[f"{safe_name}Rule"]
306
+ self.graph.add((rule_class_uri, RDF.type, OWL.Class))
307
+ self.graph.add((rule_class_uri, RDFS.subClassOf, self.ont.Action))
308
+ self.graph.add((rule_class_uri, RDFS.label, Literal(rule.name)))
309
+
310
+ if rule.description:
311
+ self.graph.add((rule_class_uri, RDFS.comment, Literal(rule.description)))
312
+
313
+ # Create rule individual
314
+ rule_uri = self.ont[f"{safe_name}RuleInstance"]
315
+ self.graph.add((rule_uri, RDF.type, rule_class_uri))
316
+
317
+ # Add entity (appliesTo)
318
+ if rule.entity:
319
+ entity_uri = self.ont[self._safe_name(rule.entity)]
320
+ self.graph.add((rule_uri, self.ont.appliesTo, entity_uri))
321
+
322
+ # Add condition as annotation
323
+ if rule.condition:
324
+ self.graph.add((rule_uri, self.ont.ruleCondition, Literal(rule.condition)))
325
+
326
+ # Add action
327
+ if rule.action:
328
+ self.graph.add((rule_uri, self.ont.ruleAction, Literal(rule.action)))
329
+
330
+ # Determine role from classification
331
+ classification = getattr(rule, 'classification', 'low')
332
+ if classification:
333
+ role_map = {
334
+ 'critical': 'Admin',
335
+ 'high': 'Admin',
336
+ 'medium': 'Editor',
337
+ 'low': 'Viewer'
338
+ }
339
+ required_role = role_map.get(classification.lower(), 'Agent')
340
+ self.graph.add((rule_uri, self.ont.requiresRole, self.ont[required_role]))
341
+
342
+ def _add_context_filters(self):
343
+ """Add context filters as OWL annotations."""
344
+ for entity_name, filter_condition in self.contract.permissions.context_filters.items():
345
+ entity_uri = self.ont[self._safe_name(entity_name)]
346
+
347
+ # Add filter as annotation
348
+ self.graph.add((entity_uri, self.ont.contextFilter, Literal(filter_condition)))
349
+
350
+ def _add_audit_config(self):
351
+ """Add audit configuration as annotations."""
352
+ ontology_uri = URIRef(self.base_uri.rstrip("#"))
353
+ audit = self.contract.audit_settings
354
+
355
+ self.graph.add((ontology_uri, self.ont.auditLogReads, Literal(
356
+ audit.log_reads, datatype=XSD.boolean
357
+ )))
358
+ self.graph.add((ontology_uri, self.ont.auditLogWrites, Literal(
359
+ audit.log_writes, datatype=XSD.boolean
360
+ )))
361
+ self.graph.add((ontology_uri, self.ont.auditLogActions, Literal(
362
+ audit.log_actions, datatype=XSD.boolean
363
+ )))
364
+ self.graph.add((ontology_uri, self.ont.alertOnViolation, Literal(
365
+ audit.alert_on_violation, datatype=XSD.boolean
366
+ )))
367
+
368
+ def _safe_name(self, name: str) -> str:
369
+ """Convert name to valid URI component."""
370
+ safe = name.replace(" ", "_").replace("-", "_").replace(".", "_")
371
+ safe = "".join(c for c in safe if c.isalnum() or c == "_")
372
+ return safe
373
+
374
+ def save(self, filepath: str, format: str = "xml"):
375
+ """
376
+ Save OWL export to file.
377
+
378
+ Args:
379
+ filepath: Path to save file
380
+ format: Output format
381
+ """
382
+ output = self.convert(format=format)
383
+ with open(filepath, "w", encoding="utf-8") as f:
384
+ f.write(output)
385
+ logger.info(f"Saved contract OWL to {filepath}")
386
+
387
+ def get_action_rules_summary(self) -> dict:
388
+ """
389
+ Get summary of generated action rules.
390
+
391
+ Returns:
392
+ Dictionary with counts of different rule types
393
+ """
394
+ self.convert() # Ensure graph is populated
395
+
396
+ read_actions = len(list(self.graph.subjects(RDF.type, self.ont.ReadAction)))
397
+ write_actions = len(list(self.graph.subjects(RDF.type, self.ont.WriteAction)))
398
+ execute_actions = len(list(self.graph.subjects(RDF.type, self.ont.ExecuteAction)))
399
+
400
+ return {
401
+ "agent_name": self.contract.agent_name,
402
+ "required_role": self.contract.permissions.required_role,
403
+ "read_actions": read_actions,
404
+ "write_actions": write_actions,
405
+ "execute_actions": execute_actions,
406
+ "business_rules": len(self.contract.business_rules),
407
+ "total_triples": len(self.graph)
408
+ }
@@ -0,0 +1,243 @@
1
+ """
2
+ Microsoft Fabric IQ Exporter
3
+
4
+ Exports ontologies to Microsoft Fabric IQ format.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Dict, List, Optional
10
+
11
+ from powerbi_ontology.ontology_generator import Ontology, OntologyEntity, OntologyRelationship, BusinessRule
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class FabricIQExporter:
17
+ """
18
+ Exports ontologies to Microsoft Fabric IQ format.
19
+
20
+ Fabric IQ uses a specific JSON schema for ontologies.
21
+ """
22
+
23
+ def __init__(self, ontology: Ontology):
24
+ """
25
+ Initialize Fabric IQ exporter.
26
+
27
+ Args:
28
+ ontology: Ontology to export
29
+ """
30
+ self.ontology = ontology
31
+
32
+ def export(self) -> Dict:
33
+ """
34
+ Export ontology to Fabric IQ JSON format.
35
+
36
+ Returns:
37
+ Dictionary in Fabric IQ format
38
+ """
39
+ logger.info(f"Exporting ontology '{self.ontology.name}' to Fabric IQ format")
40
+
41
+ fabric_iq = {
42
+ "ontologyItem": f"{self.ontology.name}_v{self.ontology.version}",
43
+ "version": self.ontology.version,
44
+ "source": self.ontology.source,
45
+ "extractedDate": datetime.now().isoformat() + "Z",
46
+ "entities": [self.format_entity(entity) for entity in self.ontology.entities],
47
+ "relationships": [
48
+ self.format_relationship(rel) for rel in self.ontology.relationships
49
+ ],
50
+ "businessRules": [
51
+ self.format_business_rule(rule) for rule in self.ontology.business_rules
52
+ ],
53
+ "dataBindings": self._generate_data_bindings(),
54
+ "metadata": self.ontology.metadata
55
+ }
56
+
57
+ # Validate export
58
+ if self.validate_export(fabric_iq):
59
+ logger.info("Fabric IQ export validated successfully")
60
+ else:
61
+ logger.warning("Fabric IQ export validation failed")
62
+
63
+ return fabric_iq
64
+
65
+ def format_entity(self, entity: OntologyEntity) -> Dict:
66
+ """
67
+ Format entity for Fabric IQ.
68
+
69
+ Args:
70
+ entity: OntologyEntity to format
71
+
72
+ Returns:
73
+ Dictionary in Fabric IQ entity format
74
+ """
75
+ return {
76
+ "name": entity.name,
77
+ "description": entity.description,
78
+ "entityType": entity.entity_type,
79
+ "properties": [
80
+ {
81
+ "name": prop.name,
82
+ "type": prop.data_type,
83
+ "required": prop.required,
84
+ "unique": prop.unique,
85
+ "description": prop.description,
86
+ "constraints": [
87
+ {
88
+ "type": constraint.type,
89
+ "value": constraint.value,
90
+ "message": constraint.message
91
+ }
92
+ for constraint in prop.constraints
93
+ ]
94
+ }
95
+ for prop in entity.properties
96
+ ],
97
+ "relationships": [
98
+ {
99
+ "type": rel.relationship_type,
100
+ "target": rel.to_entity,
101
+ "cardinality": rel.cardinality
102
+ }
103
+ for rel in self.ontology.relationships
104
+ if rel.from_entity == entity.name
105
+ ],
106
+ "source": entity.source_table
107
+ }
108
+
109
+ def format_relationship(self, rel: OntologyRelationship) -> Dict:
110
+ """
111
+ Format relationship for Fabric IQ.
112
+
113
+ Args:
114
+ rel: OntologyRelationship to format
115
+
116
+ Returns:
117
+ Dictionary in Fabric IQ relationship format
118
+ """
119
+ return {
120
+ "from": rel.from_entity,
121
+ "fromProperty": rel.from_property,
122
+ "to": rel.to_entity,
123
+ "toProperty": rel.to_property,
124
+ "type": rel.relationship_type,
125
+ "cardinality": rel.cardinality,
126
+ "description": rel.description
127
+ }
128
+
129
+ def format_business_rule(self, rule: BusinessRule) -> Dict:
130
+ """
131
+ Format business rule for Fabric IQ.
132
+
133
+ Args:
134
+ rule: BusinessRule to format
135
+
136
+ Returns:
137
+ Dictionary in Fabric IQ business rule format
138
+ """
139
+ return {
140
+ "name": rule.name,
141
+ "source": f"DAX: {rule.source_measure}" if rule.source_measure else "Manual",
142
+ "entity": rule.entity,
143
+ "condition": rule.condition,
144
+ "action": rule.action,
145
+ "classification": rule.classification,
146
+ "triggers": self._extract_triggers(rule),
147
+ "description": rule.description,
148
+ "priority": rule.priority
149
+ }
150
+
151
+ def generate_semantic_bindings(self, schema_mappings: Dict) -> Dict:
152
+ """
153
+ Generate semantic bindings for OneLake.
154
+
155
+ Args:
156
+ schema_mappings: Dictionary of entity -> physical source mappings
157
+
158
+ Returns:
159
+ Dictionary of data bindings
160
+ """
161
+ bindings = {}
162
+ for entity_name, physical_source in schema_mappings.items():
163
+ entity = next(
164
+ (e for e in self.ontology.entities if e.name == entity_name),
165
+ None
166
+ )
167
+ if entity:
168
+ bindings[entity_name] = {
169
+ "source": physical_source,
170
+ "mapping": {
171
+ prop.name: prop.name # Default mapping
172
+ for prop in entity.properties
173
+ }
174
+ }
175
+ return bindings
176
+
177
+ def validate_export(self, fabric_iq_json: Dict) -> bool:
178
+ """
179
+ Validate Fabric IQ export against schema.
180
+
181
+ Args:
182
+ fabric_iq_json: Fabric IQ JSON to validate
183
+
184
+ Returns:
185
+ True if valid
186
+ """
187
+ required_fields = ["ontologyItem", "version", "source", "entities"]
188
+ for field in required_fields:
189
+ if field not in fabric_iq_json:
190
+ logger.error(f"Missing required field: {field}")
191
+ return False
192
+
193
+ # Validate entities
194
+ if not isinstance(fabric_iq_json["entities"], list):
195
+ logger.error("Entities must be a list")
196
+ return False
197
+
198
+ for entity in fabric_iq_json["entities"]:
199
+ if "name" not in entity:
200
+ logger.error("Entity missing 'name' field")
201
+ return False
202
+
203
+ return True
204
+
205
+ def export_contract(self, contract) -> str:
206
+ """Export semantic contract to Fabric IQ format."""
207
+ import json
208
+ # Convert contract to Fabric IQ format
209
+ contract_json = {
210
+ "agentContract": contract.agent_name,
211
+ "ontologyVersion": contract.ontology_version,
212
+ "permissions": {
213
+ "readEntities": contract.permissions.read_entities,
214
+ "writeProperties": contract.permissions.write_properties,
215
+ "executableActions": contract.permissions.executable_actions
216
+ },
217
+ "businessRules": [
218
+ {
219
+ "name": rule.name,
220
+ "condition": rule.condition,
221
+ "action": rule.action
222
+ }
223
+ for rule in contract.business_rules
224
+ ]
225
+ }
226
+ return json.dumps(contract_json, indent=2)
227
+
228
+ def _generate_data_bindings(self) -> Dict:
229
+ """Generate data bindings from ontology metadata."""
230
+ # This would typically come from SchemaMapper
231
+ # For now, return empty dict
232
+ return {}
233
+
234
+ def _extract_triggers(self, rule: BusinessRule) -> List[str]:
235
+ """Extract trigger actions from business rule."""
236
+ triggers = []
237
+ if "notify" in rule.action.lower() or "alert" in rule.action.lower():
238
+ triggers.append("NotifyOperations")
239
+ if "log" in rule.action.lower() or "record" in rule.action.lower():
240
+ triggers.append("LogIncident")
241
+ if "classify" in rule.action.lower():
242
+ triggers.append("UpdateClassification")
243
+ return triggers