Fix 3 bugs in notebook 13 (manual ontology + Snowflake mapping)

- Bug 1: replace dict .get() with dataclass attribute access on
  AssociativeClass (name/connects/temporal/properties)
- Bug 2: add full URI to every ontology property and use BASE_URI-prefixed
  URIs for all relationship types so TripletStore stores hr:<name>
  instead of urn:property:<name>, fixing SPARQL PREFIX hr: queries
- Bug 3: filter None values from EmploymentEvent properties dict so
  open-ended employment does not store the literal string "None" as endDate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
KaifAhmad1
2026-04-09 15:23:19 +05:30
co-authored by Claude Sonnet 4.6
parent 665f9c080e
commit c1f0cf6f34
@@ -105,48 +105,7 @@
"id": "cell-4",
"metadata": {},
"outputs": [],
"source": [
"BASE_URI = \"https://example.com/hr/\"\n",
"\n",
"# Your ontology — designed by you, not inferred by Semantica.\n",
"ontology: Dict[str, Any] = {\n",
" \"name\": \"EmploymentDomainOntology\",\n",
" \"uri\": f\"{BASE_URI}EmploymentDomainOntology\",\n",
" \"namespace\": {\"base_uri\": BASE_URI},\n",
"\n",
" # You decide the class taxonomy\n",
" \"classes\": [\n",
" {\"name\": \"Person\", \"uri\": f\"{BASE_URI}Person\"},\n",
" {\"name\": \"Organization\", \"uri\": f\"{BASE_URI}Organization\"},\n",
" {\"name\": \"Role\", \"uri\": f\"{BASE_URI}Role\"},\n",
" # EmploymentEvent is a reification node.\n",
" # It connects Person + Organization + Role and carries salary/date context.\n",
" {\"name\": \"EmploymentEvent\", \"uri\": f\"{BASE_URI}EmploymentEvent\"},\n",
" ],\n",
"\n",
" # You decide every property — type, domain, range, cardinality\n",
" \"properties\": [\n",
" # Datatype properties\n",
" {\"name\": \"name\", \"type\": \"datatype\", \"domain\": \"Person\", \"range\": \"string\", \"required\": True},\n",
" {\"name\": \"legalName\", \"type\": \"datatype\", \"domain\": \"Organization\", \"range\": \"string\", \"required\": True},\n",
" {\"name\": \"title\", \"type\": \"datatype\", \"domain\": \"Role\", \"range\": \"string\", \"required\": True},\n",
" {\"name\": \"startDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n",
" {\"name\": \"endDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n",
" {\"name\": \"salary\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"decimal\"},\n",
"\n",
" # Object properties — reification spokes (required)\n",
" {\"name\": \"employee\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Person\", \"required\": True},\n",
" {\"name\": \"employer\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Organization\", \"required\": True},\n",
" {\"name\": \"role\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Role\", \"required\": True},\n",
"\n",
" # Shortcut edges — direct person→org / person→role without traversing the event node\n",
" {\"name\": \"worksFor\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Organization\"},\n",
" {\"name\": \"hasRole\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Role\"},\n",
" ],\n",
"}\n",
"\n",
"ontology"
]
"source": "BASE_URI = \"https://example.com/hr/\"\n\n# Your ontology — designed by you, not inferred by Semantica.\nontology: Dict[str, Any] = {\n \"name\": \"EmploymentDomainOntology\",\n \"uri\": f\"{BASE_URI}EmploymentDomainOntology\",\n \"namespace\": {\"base_uri\": BASE_URI},\n\n # You decide the class taxonomy\n \"classes\": [\n {\"name\": \"Person\", \"uri\": f\"{BASE_URI}Person\"},\n {\"name\": \"Organization\", \"uri\": f\"{BASE_URI}Organization\"},\n {\"name\": \"Role\", \"uri\": f\"{BASE_URI}Role\"},\n # EmploymentEvent is a reification node.\n # It connects Person + Organization + Role and carries salary/date context.\n {\"name\": \"EmploymentEvent\", \"uri\": f\"{BASE_URI}EmploymentEvent\"},\n ],\n\n # Each property carries a full URI so TripletStore stores it as hr:<name>\n # rather than the default urn:property:<name>.\n # This ensures SPARQL queries using PREFIX hr: match what is actually stored.\n \"properties\": [\n # Datatype properties\n {\"name\": \"name\", \"uri\": f\"{BASE_URI}name\", \"type\": \"datatype\", \"domain\": \"Person\", \"range\": \"string\", \"required\": True},\n {\"name\": \"legalName\", \"uri\": f\"{BASE_URI}legalName\", \"type\": \"datatype\", \"domain\": \"Organization\", \"range\": \"string\", \"required\": True},\n {\"name\": \"title\", \"uri\": f\"{BASE_URI}title\", \"type\": \"datatype\", \"domain\": \"Role\", \"range\": \"string\", \"required\": True},\n {\"name\": \"startDate\", \"uri\": f\"{BASE_URI}startDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"endDate\", \"uri\": f\"{BASE_URI}endDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"salary\", \"uri\": f\"{BASE_URI}salary\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"decimal\"},\n\n # Object properties — reification spokes (required)\n {\"name\": \"employee\", \"uri\": f\"{BASE_URI}employee\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Person\", \"required\": True},\n {\"name\": \"employer\", \"uri\": f\"{BASE_URI}employer\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Organization\", \"required\": True},\n {\"name\": \"role\", \"uri\": f\"{BASE_URI}role\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Role\", \"required\": True},\n\n # Shortcut edges — direct person→org / person→role without traversing the event node\n {\"name\": \"worksFor\", \"uri\": f\"{BASE_URI}worksFor\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Organization\"},\n {\"name\": \"hasRole\", \"uri\": f\"{BASE_URI}hasRole\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Role\"},\n ],\n}\n\nontology"
},
{
"cell_type": "markdown",
@@ -186,29 +145,7 @@
"id": "cell-6",
"metadata": {},
"outputs": [],
"source": [
"assoc_builder = AssociativeClassBuilder()\n",
"\n",
"employment_assoc = assoc_builder.create_associative_class(\n",
" name=\"EmploymentEvent\",\n",
" connects=[\"Person\", \"Organization\", \"Role\"],\n",
" temporal=True, # adds startDate / endDate handling\n",
" properties={\n",
" \"startDate\": \"xsd:date\",\n",
" \"endDate\": \"xsd:date\",\n",
" \"salary\": \"xsd:decimal\",\n",
" },\n",
")\n",
"\n",
"validation_result = assoc_builder.validate_associative_class(employment_assoc)\n",
"\n",
"print(\"AssociativeClass structure:\")\n",
"print(f\" name: {employment_assoc.get('name')}\")\n",
"print(f\" connects: {employment_assoc.get('connects')}\")\n",
"print(f\" temporal: {employment_assoc.get('temporal')}\")\n",
"print(f\" properties: {list(employment_assoc.get('properties', {}).keys())}\")\n",
"print(f\"\\nValidation passed: {validation_result}\")"
]
"source": "assoc_builder = AssociativeClassBuilder()\n\nemployment_assoc = assoc_builder.create_associative_class(\n name=\"EmploymentEvent\",\n connects=[\"Person\", \"Organization\", \"Role\"],\n temporal=True, # adds startDate / endDate handling\n properties={\n \"startDate\": \"xsd:date\",\n \"endDate\": \"xsd:date\",\n \"salary\": \"xsd:decimal\",\n },\n)\n\nvalidation_result = assoc_builder.validate_associative_class(employment_assoc)\n\n# AssociativeClass is a dataclass — use attribute access, not .get()\nprint(\"AssociativeClass structure:\")\nprint(f\" name: {employment_assoc.name}\")\nprint(f\" connects: {employment_assoc.connects}\")\nprint(f\" temporal: {employment_assoc.temporal}\")\nprint(f\" properties: {list(employment_assoc.properties.keys())}\")\nprint(f\"\\nValidation passed: {validation_result}\")"
},
{
"cell_type": "markdown",
@@ -309,67 +246,7 @@
"id": "cell-10",
"metadata": {},
"outputs": [],
"source": [
"def map_rows_to_kg(rows: List[Dict[str, Any]]) -> Dict[str, Any]:\n",
" entities: Dict[str, Dict[str, Any]] = {}\n",
" relationships: List[Dict[str, Any]] = []\n",
"\n",
" for row in rows:\n",
" # Stable, deterministic node IDs derived from business keys\n",
" person_id = f\"person:{row['EMPLOYEE_ID']}\"\n",
" org_id = f\"org:{row['ORG_ID']}\"\n",
" role_id = f\"role:{row['ROLE_ID']}\"\n",
" # Event ID includes all three participants + start date so that\n",
" # a re-hired employee gets a distinct event node, not an overwrite.\n",
" event_id = f\"employment:{row['EMPLOYEE_ID']}:{row['ORG_ID']}:{row['START_DATE']}\"\n",
"\n",
" # Entities — \"type\" must match a class name from Step 1\n",
" entities[person_id] = {\n",
" \"id\": person_id,\n",
" \"type\": \"Person\",\n",
" \"properties\": {\"name\": row[\"EMPLOYEE_NAME\"]},\n",
" }\n",
" entities[org_id] = {\n",
" \"id\": org_id,\n",
" \"type\": \"Organization\",\n",
" \"properties\": {\"legalName\": row[\"ORG_NAME\"]},\n",
" }\n",
" entities[role_id] = {\n",
" \"id\": role_id,\n",
" \"type\": \"Role\",\n",
" \"properties\": {\"title\": row[\"ROLE_TITLE\"]},\n",
" }\n",
" # Reification node — carries the n-ary context\n",
" entities[event_id] = {\n",
" \"id\": event_id,\n",
" \"type\": \"EmploymentEvent\",\n",
" \"properties\": {\n",
" \"startDate\": row[\"START_DATE\"],\n",
" \"endDate\": row[\"END_DATE\"], # None = still employed\n",
" \"salary\": row[\"SALARY\"],\n",
" },\n",
" }\n",
"\n",
" relationships.extend([\n",
" # Shortcut edges — fast SPARQL when context is not needed\n",
" {\"source\": person_id, \"target\": org_id, \"type\": \"worksFor\"},\n",
" {\"source\": person_id, \"target\": role_id, \"type\": \"hasRole\"},\n",
" # Reification spokes — full context via the event node\n",
" {\"source\": event_id, \"target\": person_id, \"type\": \"employee\"},\n",
" {\"source\": event_id, \"target\": org_id, \"type\": \"employer\"},\n",
" {\"source\": event_id, \"target\": role_id, \"type\": \"role\"},\n",
" ])\n",
"\n",
" return build_kg([{\"entities\": list(entities.values()), \"relationships\": relationships}])\n",
"\n",
"\n",
"kg = map_rows_to_kg(rows)\n",
"print(f\"Entities built: {len(kg.get('entities', []))}\")\n",
"print(f\"Relationships built: {len(kg.get('relationships', []))}\")\n",
"\n",
"sample = next((e for e in kg[\"entities\"] if e[\"type\"] == \"EmploymentEvent\"), None)\n",
"print(f\"\\nSample EmploymentEvent node: {sample}\")"
]
"source": "def map_rows_to_kg(rows: List[Dict[str, Any]]) -> Dict[str, Any]:\n entities: Dict[str, Dict[str, Any]] = {}\n relationships: List[Dict[str, Any]] = []\n\n for row in rows:\n # Stable, deterministic node IDs derived from business keys\n person_id = f\"person:{row['EMPLOYEE_ID']}\"\n org_id = f\"org:{row['ORG_ID']}\"\n role_id = f\"role:{row['ROLE_ID']}\"\n # Event ID includes all three participants + start date so that\n # a re-hired employee gets a distinct event node, not an overwrite.\n event_id = f\"employment:{row['EMPLOYEE_ID']}:{row['ORG_ID']}:{row['START_DATE']}\"\n\n # Entities — \"type\" must match a class name from Step 1\n entities[person_id] = {\n \"id\": person_id,\n \"type\": \"Person\",\n \"properties\": {\"name\": row[\"EMPLOYEE_NAME\"]},\n }\n entities[org_id] = {\n \"id\": org_id,\n \"type\": \"Organization\",\n \"properties\": {\"legalName\": row[\"ORG_NAME\"]},\n }\n entities[role_id] = {\n \"id\": role_id,\n \"type\": \"Role\",\n \"properties\": {\"title\": row[\"ROLE_TITLE\"]},\n }\n\n # Reification node — filter out None values so TripletStore does not\n # stringify None as the literal \"None\" for open-ended employment.\n event_props = {\n \"startDate\": row[\"START_DATE\"],\n \"endDate\": row[\"END_DATE\"],\n \"salary\": row[\"SALARY\"],\n }\n entities[event_id] = {\n \"id\": event_id,\n \"type\": \"EmploymentEvent\",\n \"properties\": {k: v for k, v in event_props.items() if v is not None},\n }\n\n # Full URIs for relationship types so TripletStore stores hr:<type>\n # instead of the default urn:property:<type>, keeping SPARQL consistent.\n relationships.extend([\n # Shortcut edges — fast SPARQL when context is not needed\n {\"source\": person_id, \"target\": org_id, \"type\": f\"{BASE_URI}worksFor\"},\n {\"source\": person_id, \"target\": role_id, \"type\": f\"{BASE_URI}hasRole\"},\n # Reification spokes — full context via the event node\n {\"source\": event_id, \"target\": person_id, \"type\": f\"{BASE_URI}employee\"},\n {\"source\": event_id, \"target\": org_id, \"type\": f\"{BASE_URI}employer\"},\n {\"source\": event_id, \"target\": role_id, \"type\": f\"{BASE_URI}role\"},\n ])\n\n return build_kg([{\"entities\": list(entities.values()), \"relationships\": relationships}])\n\n\nkg = map_rows_to_kg(rows)\nprint(f\"Entities built: {len(kg.get('entities', []))}\")\nprint(f\"Relationships built: {len(kg.get('relationships', []))}\")\n\nsample = next((e for e in kg[\"entities\"] if e[\"type\"] == \"EmploymentEvent\"), None)\nprint(f\"\\nSample EmploymentEvent node: {sample}\")"
},
{
"cell_type": "markdown",
@@ -555,4 +432,4 @@
]
}
]
}
}