This document provides implementation instructions for building a proof-of-concept that demonstrates the core value proposition of an enterprise semantic layer. The POC consists of four parts:
- Source databases: Realistic tables for three enterprise applications (HubSpot CRM, Jira, Dynamics 365 Sales) populated with synthetic data for one fictional company
- Ontology: A Schema.org-based enterprise vocabulary extending Schema.org where needed
- TMCF mappings: Declarative mappings from each application's native schema to the shared ontology
- NL-to-semantic translator: A system that takes English questions and translates them into queries expressed against the common ontology, which then get compiled to SQL against the source tables using the TMCF mappings
The result should be a working demo where a user asks "which deals are at risk based on open support tickets?" and the system joins across HubSpot, Jira, and Dynamics 365 data using the semantic layer — without any hand-coded cross-system glue.
Use SQLite for all three databases. Each gets its own .db file, simulating three separate applications with no shared schema.
Use Microsoft's standard fictional company "Contoso" as the demo organization. Contoso Ltd is a mid-size B2B technology company that sells software products and professional services. They have:
- ~50 customer accounts (companies they sell to)
- ~200 contacts (people at those companies)
- ~80 deals/opportunities in various stages
- ~120 products/line items
- ~300 Jira issues across 4 projects
- ~150 support tickets tracked in Jira Service Management
All three systems share the same universe of customers, contacts, and employees — but use different IDs, different column names, and different schema conventions. This is the core challenge the semantic layer solves.
Shared identity bridge: To keep the base case simple (per the assumption of common IDs), generate a master entity list first, then distribute entities across the three systems with a shared contoso_id that appears in each system (as different column names). In a real deployment, this would be the entity resolution problem; here we assume it's been solved.
File: hubspot.db
HubSpot is the marketing and early-pipeline CRM. It tracks contacts, companies, deals (in early stages), and marketing engagement.
| Column | Type | Description | Example |
|---|---|---|---|
| company_id | INTEGER PRIMARY KEY | HubSpot internal ID | 1001 |
| contoso_id | TEXT | Cross-system identifier | "CUST-0042" |
| name | TEXT | Company name | "Northwind Traders" |
| domain | TEXT | Website domain | "northwindtraders.com" |
| industry | TEXT | Industry classification | "Technology" |
| city | TEXT | City | "Seattle" |
| state | TEXT | State/province | "WA" |
| country | TEXT | Country | "US" |
| num_employees | INTEGER | Employee count | 500 |
| annual_revenue | REAL | Annual revenue (USD) | 5000000.00 |
| lifecycle_stage | TEXT | HubSpot lifecycle stage | "customer" |
| hs_lead_status | TEXT | Lead status | "OPEN" |
| createdate | TEXT | ISO 8601 creation date | "2024-03-15T10:30:00Z" |
| lastmodifieddate | TEXT | Last modified | "2025-11-02T14:20:00Z" |
| hubspot_owner_id | INTEGER | Assigned HubSpot owner | 201 |
Lifecycle stages: "subscriber", "lead", "marketingqualifiedlead", "salesqualifiedlead", "opportunity", "customer", "evangelist", "other"
| Column | Type | Description | Example |
|---|---|---|---|
| contact_id | INTEGER PRIMARY KEY | HubSpot contact ID | 5001 |
| contoso_id | TEXT | Cross-system person ID | "PER-0117" |
| TEXT UNIQUE | Email address | "j.smith@northwind.com" | |
| firstname | TEXT | First name | "Jordan" |
| lastname | TEXT | Last name | "Smith" |
| phone | TEXT | Phone | "+1-206-555-0142" |
| jobtitle | TEXT | Job title | "VP of Engineering" |
| company_id | INTEGER | FK to hs_companies | 1001 |
| lifecyclestage | TEXT | Lifecycle stage | "customer" |
| hs_lead_status | TEXT | Lead status | "CONNECTED" |
| lastmodifieddate | TEXT | Last modified | "2025-10-15T09:00:00Z" |
| createdate | TEXT | Created | "2024-01-20T08:00:00Z" |
| Column | Type | Description | Example |
|---|---|---|---|
| deal_id | INTEGER PRIMARY KEY | HubSpot deal ID | 8001 |
| contoso_id | TEXT | Cross-system deal ID | "DEAL-0033" |
| dealname | TEXT | Deal name | "Northwind Enterprise License" |
| amount | REAL | Deal value (USD) | 150000.00 |
| dealstage | TEXT | Pipeline stage | "contractsent" |
| pipeline | TEXT | Pipeline name | "default" |
| closedate | TEXT | Expected close date | "2026-03-30" |
| createdate | TEXT | Created | "2025-08-01T10:00:00Z" |
| company_id | INTEGER | FK to hs_companies | 1001 |
| hubspot_owner_id | INTEGER | Deal owner | 201 |
| deal_type | TEXT | New or existing business | "existingbusiness" |
| hs_priority | TEXT | Priority | "high" |
Deal stages: "appointmentscheduled", "qualifiedtobuy", "presentationscheduled", "decisionmakerboughtin", "contractsent", "closedwon", "closedlost"
| Column | Type |
|---|---|
| deal_id | INTEGER |
| contact_id | INTEGER |
| role | TEXT |
Roles: "DECISION_MAKER", "CHAMPION", "INFLUENCER", "BLOCKER", "END_USER"
| Column | Type | Description |
|---|---|---|
| owner_id | INTEGER PRIMARY KEY | Owner ID |
| TEXT | Owner email | |
| firstname | TEXT | First name |
| lastname | TEXT | Last name |
Generate 8-12 HubSpot owners (these are Contoso sales/marketing employees).
| Column | Type | Description | Example |
|---|---|---|---|
| event_id | INTEGER PRIMARY KEY | Event ID | 30001 |
| contact_id | INTEGER | FK to hs_contacts | 5001 |
| email_campaign_id | INTEGER | Campaign ID | 601 |
| event_type | TEXT | Event type | "OPEN" |
| event_timestamp | TEXT | When it happened | "2025-09-15T14:22:00Z" |
Event types: "SENT", "DELIVERED", "OPEN", "CLICK", "BOUNCE", "UNSUBSCRIBE"
| Column | Type | Description |
|---|---|---|
| campaign_id | INTEGER PRIMARY KEY | Campaign ID |
| name | TEXT | Campaign name |
| type | TEXT | "EMAIL", "SOCIAL", "CONTENT" |
| start_date | TEXT | Start date |
| end_date | TEXT | End date |
Generate 8-10 marketing campaigns.
File: jira.db
Jira tracks engineering work (bugs, features, tasks) and support tickets (via Jira Service Management). This is where product development and customer support issues live.
| Column | Type | Description | Example |
|---|---|---|---|
| project_id | INTEGER PRIMARY KEY | Project ID | 10001 |
| project_key | TEXT UNIQUE | Short key | "ENG" |
| name | TEXT | Project name | "Engineering" |
| project_type | TEXT | Type | "software" |
| lead_account_id | TEXT | Project lead | "contoso-emp-005" |
Generate 4 projects:
- ENG (Engineering): software development issues
- PLATFORM (Platform): infrastructure/DevOps
- SUP (Support): customer support tickets (Jira Service Management style)
- DOCS (Documentation): documentation tasks
| Column | Type | Description | Example |
|---|---|---|---|
| issue_id | INTEGER PRIMARY KEY | Issue ID | 20001 |
| issue_key | TEXT UNIQUE | Display key | "SUP-142" |
| project_id | INTEGER | FK to jira_projects | 10001 |
| summary | TEXT | Issue title | "Login fails for SSO users" |
| description | TEXT | Full description | "When users attempt..." |
| issue_type | TEXT | Type of issue | "Bug" |
| status | TEXT | Current status | "In Progress" |
| priority | TEXT | Priority | "High" |
| assignee_account_id | TEXT | Assigned employee | "contoso-emp-012" |
| reporter_account_id | TEXT | Reporter | "contoso-emp-005" |
| created | TEXT | Created date | "2025-09-20T11:00:00Z" |
| updated | TEXT | Last updated | "2025-11-01T16:30:00Z" |
| resolved | TEXT | Resolution date (nullable) | "2025-10-15T09:00:00Z" |
| resolution | TEXT | Resolution type (nullable) | "Fixed" |
| story_points | INTEGER | Story points (nullable) | 5 |
| sprint_id | INTEGER | FK to jira_sprints (nullable) | 401 |
| labels | TEXT | Comma-separated labels | "customer-reported,regression" |
| components | TEXT | Comma-separated components | "auth,sso" |
Issue types: "Bug", "Story", "Task", "Epic", "Sub-task", "Service Request", "Incident"
Statuses: "To Do", "In Progress", "In Review", "Done", "Closed", "Waiting for Customer", "Escalated"
Priorities: "Highest", "High", "Medium", "Low", "Lowest"
Resolutions: "Fixed", "Won't Fix", "Duplicate", "Cannot Reproduce", "Done", NULL (unresolved)
This is how Jira connects issues to customers. In Jira Service Management, support tickets are linked to requesting organizations.
| Column | Type | Description | Example |
|---|---|---|---|
| issue_id | INTEGER | FK to jira_issues | 20001 |
| contoso_customer_id | TEXT | Cross-system customer ID | "CUST-0042" |
| customer_name | TEXT | Denormalized name | "Northwind Traders" |
| link_type | TEXT | Relationship type | "REPORTED_BY" |
Link types: "REPORTED_BY", "AFFECTS", "REQUESTED_BY"
This table is critical — it connects Jira support tickets to the same customers tracked in HubSpot and Dynamics 365.
| Column | Type | Description |
|---|---|---|
| account_id | TEXT PRIMARY KEY | Jira account ID (e.g. "contoso-emp-005") |
| display_name | TEXT | Full name |
| TEXT | ||
| active | INTEGER | 1=active, 0=inactive |
Generate 15-20 Jira users (Contoso employees — engineers, support staff, PMs).
| Column | Type | Description |
|---|---|---|
| sprint_id | INTEGER PRIMARY KEY | Sprint ID |
| name | TEXT | Sprint name |
| state | TEXT | "active", "closed", "future" |
| start_date | TEXT | Start date |
| end_date | TEXT | End date |
| project_id | INTEGER | FK to jira_projects |
Generate 6-8 sprints for ENG and PLATFORM projects.
| Column | Type | Description |
|---|---|---|
| comment_id | INTEGER PRIMARY KEY | Comment ID |
| issue_id | INTEGER | FK to jira_issues |
| author_account_id | TEXT | Who wrote it |
| body | TEXT | Comment text |
| created | TEXT | Created timestamp |
Generate 2-4 comments per issue on average.
File: dynamics365.db
Dynamics 365 is the formal sales and order management system. It has the "system of record" for accounts, opportunities, quotes, and orders. It represents the later-stage pipeline and post-sale relationship.
| Column | Type | Description | Example |
|---|---|---|---|
| accountid | TEXT PRIMARY KEY | D365 GUID | "a1b2c3d4-..." |
| contoso_id | TEXT | Cross-system ID | "CUST-0042" |
| name | TEXT | Account name | "Northwind Traders" |
| accountnumber | TEXT | Account number | "ACC-NWT-001" |
| revenue | REAL | Annual revenue | 5000000.00 |
| numberofemployees | INTEGER | Headcount | 500 |
| industrycode | INTEGER | Industry code | 12 |
| address1_city | TEXT | City | "Seattle" |
| address1_stateorprovince | TEXT | State | "WA" |
| address1_country | TEXT | Country | "US" |
| telephone1 | TEXT | Main phone | "+1-206-555-0100" |
| websiteurl | TEXT | Website | "https://northwindtraders.com" |
| ownerid | TEXT | Account owner GUID | "emp-guid-005" |
| statecode | INTEGER | 0=Active, 1=Inactive | 0 |
| createdon | TEXT | Created | "2023-06-15T10:00:00Z" |
| modifiedon | TEXT | Modified | "2025-10-28T11:30:00Z" |
| customertypecode | INTEGER | 1=Competitor,3=Customer,11=Prospect | 3 |
| accountratingcode | INTEGER | 1=Default,2=High,3=Low | 2 |
Industry codes (subset): 1=Accounting, 4=Agriculture, 7=Consulting, 12=Technology, 15=Financial Services, 18=Healthcare, 21=Manufacturing, 24=Retail, 27=Education
| Column | Type | Description | Example |
|---|---|---|---|
| contactid | TEXT PRIMARY KEY | D365 GUID | "c5d6e7f8-..." |
| contoso_id | TEXT | Cross-system person ID | "PER-0117" |
| firstname | TEXT | First name | "Jordan" |
| lastname | TEXT | Last name | "Smith" |
| emailaddress1 | TEXT | Primary email | "j.smith@northwind.com" |
| telephone1 | TEXT | Phone | "+1-206-555-0142" |
| jobtitle | TEXT | Job title | "VP of Engineering" |
| parentcustomerid | TEXT | FK to d365_accounts.accountid | "a1b2c3d4-..." |
| ownerid | TEXT | Contact owner | "emp-guid-005" |
| statecode | INTEGER | 0=Active, 1=Inactive | 0 |
| createdon | TEXT | Created | "2024-01-20T08:00:00Z" |
| Column | Type | Description | Example |
|---|---|---|---|
| opportunityid | TEXT PRIMARY KEY | D365 GUID | "op-guid-033" |
| contoso_id | TEXT | Cross-system deal ID | "DEAL-0033" |
| name | TEXT | Opportunity name | "Northwind Enterprise License" |
| estimatedvalue | REAL | Estimated revenue | 150000.00 |
| actualvalue | REAL | Actual value (if won) | NULL |
| estimatedclosedate | TEXT | Expected close | "2026-03-30" |
| actualclosedate | TEXT | Actual close (if closed) | NULL |
| stepname | TEXT | Sales stage | "Propose" |
| statecode | INTEGER | 0=Open, 1=Won, 2=Lost | 0 |
| statuscode | INTEGER | Sub-status | 1 |
| parentaccountid | TEXT | FK to d365_accounts | "a1b2c3d4-..." |
| parentcontactid | TEXT | FK to d365_contacts | "c5d6e7f8-..." |
| ownerid | TEXT | Opportunity owner | "emp-guid-005" |
| createdon | TEXT | Created | "2025-08-01T10:00:00Z" |
| modifiedon | TEXT | Modified | "2025-11-01T09:00:00Z" |
| closeprobability | INTEGER | Win probability % | 70 |
| budgetamount | REAL | Customer budget | 200000.00 |
| purchaseprocess | TEXT | "Individual","Committee","Unknown" | "Committee" |
| purchasetimeframe | TEXT | "Immediate","ThisQuarter","ThisYear","Unknown" | "ThisQuarter" |
Step names: "Qualify", "Develop", "Propose", "Close"
| Column | Type | Description |
|---|---|---|
| productid | TEXT PRIMARY KEY | Product GUID |
| name | TEXT | Product name |
| productnumber | TEXT | SKU |
| description | TEXT | Description |
| price | REAL | List price |
| productstructure | TEXT | "Product" or "ProductFamily" |
| statecode | INTEGER | 0=Active, 1=Retired, 2=Draft |
Generate 10-15 products: software licenses (per-seat, enterprise), professional services (implementation, training, support tiers), add-on modules.
| Column | Type | Description |
|---|---|---|
| opportunityproductid | TEXT PRIMARY KEY | Line item GUID |
| opportunityid | TEXT | FK to d365_opportunities |
| productid | TEXT | FK to d365_products |
| quantity | REAL | Quantity |
| priceperunit | REAL | Price per unit |
| extendedamount | REAL | quantity * priceperunit |
| description | TEXT | Line item description |
| Column | Type | Description |
|---|---|---|
| salesorderid | TEXT PRIMARY KEY | Order GUID |
| name | TEXT | Order name |
| ordernumber | TEXT | Order number |
| totalamount | REAL | Total value |
| customerid | TEXT | FK to d365_accounts |
| opportunityid | TEXT | FK to d365_opportunities (nullable) |
| statecode | INTEGER | 0=Active, 1=Submitted, 2=Canceled, 3=Fulfilled |
| createdon | TEXT | Created |
| submitdate | TEXT | Submitted date |
| Column | Type | Description |
|---|---|---|
| systemuserid | TEXT PRIMARY KEY | User GUID (e.g. "emp-guid-005") |
| fullname | TEXT | Full name |
| internalemailaddress | TEXT | |
| title | TEXT | Job title |
| businessunitid | TEXT | Business unit |
| isdisabled | INTEGER | 0=active, 1=disabled |
Generate 15-20 D365 users (overlapping with Jira users where appropriate — same Contoso employees appear in both systems).
Implement as a Python script: generate_data.py
Generate these first, then distribute across all three systems:
# Master lists — generate once, reference everywhere
CUSTOMERS = [
{"contoso_id": "CUST-0001", "name": "Northwind Traders", "domain": "northwindtraders.com",
"industry": "Technology", "city": "Seattle", "state": "WA", "employees": 500, "revenue": 5000000},
{"contoso_id": "CUST-0002", "name": "Adventure Works", "domain": "adventureworks.com",
"industry": "Manufacturing", "city": "Portland", "state": "OR", "employees": 1200, "revenue": 15000000},
# ... ~50 total
]
CONTACTS = [
{"contoso_id": "PER-0001", "first": "Jordan", "last": "Smith",
"email": "j.smith@northwindtraders.com", "title": "VP of Engineering", "customer_id": "CUST-0001"},
# ... ~200 total, 3-6 per customer
]
EMPLOYEES = [
{"contoso_id": "EMP-0001", "first": "Alex", "last": "Johnson",
"email": "alex.johnson@contoso.com", "title": "Account Executive", "dept": "Sales"},
# ... ~20 total, appearing across all three systems
]
DEALS = [
{"contoso_id": "DEAL-0001", "name": "Northwind Enterprise License",
"customer_id": "CUST-0001", "amount": 150000, "stage": "Propose", "close_date": "2026-03-30"},
# ... ~80 total
]These rules ensure the demo tells a coherent story:
- Every customer in HubSpot also appears in D365 (same contoso_id). Some customers in D365 may NOT be in HubSpot (legacy accounts).
- Every deal in HubSpot has a corresponding opportunity in D365 (same contoso_id). HubSpot has the marketing-side view; D365 has the formal opportunity record.
- ~30% of customers should have open support tickets in Jira linked via
jira_issue_customer_link. This enables the cross-system "deals at risk" query. - Some Contoso employees appear in all three systems under different ID schemes:
- HubSpot:
hubspot_owner_id(integer) - Jira:
account_id(string like "contoso-emp-005") - D365:
systemuserid(GUID-like string like "emp-guid-005") - Create a mapping: EMP-0005 -> hubspot owner 205, jira "contoso-emp-005", d365 "emp-guid-005"
- HubSpot:
- Temporal coherence:
- Deals created 3-12 months ago
- Support tickets created 1-6 months ago
- Some support tickets reference deals or products by name in their description
- Recent activity (comments, status changes) within last 2 months
- Interesting patterns to embed (these make the demo queries compelling):
- 3-5 customers with high-value open deals AND multiple escalated support tickets -> "deals at risk"
- 2-3 customers with recent deal closure AND surge in support tickets -> "onboarding problems"
- A few customers with declining marketing engagement (no email opens in 3+ months) AND upcoming renewals -> "churn risk"
- One customer with a closed-lost deal who then filed several support tickets referencing competitor features -> "competitive intelligence"
| Entity | Count | Notes |
|---|---|---|
| Customers/Accounts | ~50 | Same entities across all 3 systems |
| Contacts | ~200 | 3-6 per customer |
| Employees | ~20 | Appear across systems with different IDs |
| Deals/Opportunities | ~80 | 60% open, 25% won, 15% lost |
| Products | 10-15 | Software + services |
| Jira issues (engineering) | ~200 | Bugs, stories, tasks |
| Jira issues (support) | ~150 | Service requests, incidents |
| Jira comments | ~800 | 2-4 per issue average |
| Marketing email events | ~2000 | Across 8-10 campaigns |
| D365 orders | ~30 | For won deals |
| D365 opportunity line items | ~150 | 1-3 per opportunity |
Use the faker library for realistic names, addresses, phone numbers, and email addresses. Use deterministic seeding (random.seed(42), Faker.seed_instance(42)) so the data is reproducible.
Define the ontology as a set of MCF (Meta Content Framework) files, following Data Commons conventions. The ontology extends Schema.org into enterprise territory.
File: ontology/enterprise_schema.mcf
- Reuse Schema.org types and properties wherever they exist.
schema:Organization,schema:Person,schema:Product,schema:Order,schema:OrderItemalready cover a lot of ground. - Extend with
ent:namespace for enterprise-specific types and properties that Schema.org doesn't cover (pipeline stages, support tickets, sprints, etc.). - Keep the core tight. Define only what's needed to map the three source systems. Don't speculate about types we won't use.
- Properties should be precise. "revenue" must distinguish
annual_revenuefromdeal_valuefromorder_total. Each is a different property.
These exist in Schema.org and need no extension:
- schema:Organization — customer accounts/companies. Properties: name, url, telephone, address, numberOfEmployees, industry (Note: Schema.org doesn't have industry directly — it has naics code. We'll add
ent:industryas a Text property.) - schema:Person — contacts at customer organizations AND internal employees. Properties: givenName, familyName, email, telephone, jobTitle, worksFor (-> Organization)
- schema:Product — products being sold. Properties: name, productID, description, offers -> schema:Offer with price, priceCurrency
- schema:Order — fulfilled sales orders. Properties: orderNumber, orderDate, customer (-> Organization), orderedItem (-> OrderItem), orderStatus
- schema:OrderItem — line items on orders. Properties: orderedItem (-> Product), orderQuantity, price
These types don't exist in Schema.org and must be defined:
# === Enterprise Extension Schema ===
# --- SalesOpportunity ---
# A potential sale being tracked through a pipeline.
# Schema.org has nothing for sales pipeline management.
Node: ent:SalesOpportunity
typeOf: schema:Class
subClassOf: schema:Intangible
name: "SalesOpportunity"
description: "A potential sale to a customer being tracked through a sales pipeline."
Node: ent:opportunityName
typeOf: schema:Property
name: "opportunityName"
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Text
Node: ent:estimatedValue
typeOf: schema:Property
name: "estimatedValue"
description: "The estimated monetary value of this opportunity."
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:MonetaryAmount
Node: ent:actualValue
typeOf: schema:Property
name: "actualValue"
description: "The actual monetary value when the opportunity closes."
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:MonetaryAmount
Node: ent:estimatedCloseDate
typeOf: schema:Property
name: "estimatedCloseDate"
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Date
Node: ent:actualCloseDate
typeOf: schema:Property
name: "actualCloseDate"
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Date
Node: ent:pipelineStage
typeOf: schema:Property
name: "pipelineStage"
description: "Current stage in the sales pipeline."
domainIncludes: ent:SalesOpportunity
rangeIncludes: ent:PipelineStageEnum
Node: ent:opportunityStatus
typeOf: schema:Property
name: "opportunityStatus"
description: "Whether the opportunity is open, won, or lost."
domainIncludes: ent:SalesOpportunity
rangeIncludes: ent:OpportunityStatusEnum
Node: ent:closeProbability
typeOf: schema:Property
name: "closeProbability"
description: "Estimated probability of winning, as a percentage 0-100."
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Number
Node: ent:customer
typeOf: schema:Property
name: "customer"
description: "The customer organization for this opportunity."
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Organization
Node: ent:primaryContact
typeOf: schema:Property
name: "primaryContact"
domainIncludes: ent:SalesOpportunity
rangeIncludes: schema:Person
Node: ent:owner
typeOf: schema:Property
name: "owner"
description: "The employee who owns/is responsible for this entity."
domainIncludes: ent:SalesOpportunity, ent:SupportTicket
rangeIncludes: schema:Person
# Pipeline Stage Enumeration
Node: ent:PipelineStageEnum
typeOf: schema:Class
subClassOf: schema:Enumeration
Node: ent:PipelineStage_Qualify
typeOf: ent:PipelineStageEnum
name: "Qualify"
Node: ent:PipelineStage_Develop
typeOf: ent:PipelineStageEnum
name: "Develop"
Node: ent:PipelineStage_Propose
typeOf: ent:PipelineStageEnum
name: "Propose"
Node: ent:PipelineStage_Close
typeOf: ent:PipelineStageEnum
name: "Close"
# Opportunity Status Enumeration
Node: ent:OpportunityStatusEnum
typeOf: schema:Class
subClassOf: schema:Enumeration
Node: ent:OpportunityStatus_Open
typeOf: ent:OpportunityStatusEnum
name: "Open"
Node: ent:OpportunityStatus_Won
typeOf: ent:OpportunityStatusEnum
name: "Won"
Node: ent:OpportunityStatus_Lost
typeOf: ent:OpportunityStatusEnum
name: "Lost"
# --- SupportTicket ---
# A customer support issue or service request.
# Schema.org has no support/ticketing concept.
Node: ent:SupportTicket
typeOf: schema:Class
subClassOf: schema:Intangible
name: "SupportTicket"
description: "A customer support issue, incident, or service request."
Node: ent:ticketId
typeOf: schema:Property
name: "ticketId"
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Text
Node: ent:ticketSummary
typeOf: schema:Property
name: "ticketSummary"
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Text
Node: ent:ticketDescription
typeOf: schema:Property
name: "ticketDescription"
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Text
Node: ent:ticketStatus
typeOf: schema:Property
name: "ticketStatus"
domainIncludes: ent:SupportTicket
rangeIncludes: ent:TicketStatusEnum
Node: ent:ticketPriority
typeOf: schema:Property
name: "ticketPriority"
domainIncludes: ent:SupportTicket
rangeIncludes: ent:PriorityEnum
Node: ent:ticketType
typeOf: schema:Property
name: "ticketType"
description: "Whether this is a service request, incident, bug report, etc."
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Text
Node: ent:affectedCustomer
typeOf: schema:Property
name: "affectedCustomer"
description: "The customer organization affected by this ticket."
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Organization
Node: ent:reportedBy
typeOf: schema:Property
name: "reportedBy"
domainIncludes: ent:SupportTicket
rangeIncludes: schema:Person
Node: ent:assignee
typeOf: schema:Property
name: "assignee"
domainIncludes: ent:SupportTicket, ent:EngineeringIssue
rangeIncludes: schema:Person
Node: ent:dateCreated
typeOf: schema:Property
name: "dateCreated"
domainIncludes: ent:SupportTicket, ent:EngineeringIssue, ent:SalesOpportunity
rangeIncludes: schema:DateTime
Node: ent:dateResolved
typeOf: schema:Property
name: "dateResolved"
domainIncludes: ent:SupportTicket, ent:EngineeringIssue
rangeIncludes: schema:DateTime
Node: ent:resolution
typeOf: schema:Property
name: "resolution"
domainIncludes: ent:SupportTicket, ent:EngineeringIssue
rangeIncludes: schema:Text
# Ticket Status Enumeration
Node: ent:TicketStatusEnum
typeOf: schema:Class
subClassOf: schema:Enumeration
Node: ent:TicketStatus_Open
typeOf: ent:TicketStatusEnum
name: "Open"
Node: ent:TicketStatus_InProgress
typeOf: ent:TicketStatusEnum
name: "InProgress"
Node: ent:TicketStatus_WaitingOnCustomer
typeOf: ent:TicketStatusEnum
name: "WaitingOnCustomer"
Node: ent:TicketStatus_Escalated
typeOf: ent:TicketStatusEnum
name: "Escalated"
Node: ent:TicketStatus_Resolved
typeOf: ent:TicketStatusEnum
name: "Resolved"
Node: ent:TicketStatus_Closed
typeOf: ent:TicketStatusEnum
name: "Closed"
# Priority Enumeration (shared across types)
Node: ent:PriorityEnum
typeOf: schema:Class
subClassOf: schema:Enumeration
Node: ent:Priority_Highest
typeOf: ent:PriorityEnum
name: "Highest"
Node: ent:Priority_High
typeOf: ent:PriorityEnum
name: "High"
Node: ent:Priority_Medium
typeOf: ent:PriorityEnum
name: "Medium"
Node: ent:Priority_Low
typeOf: ent:PriorityEnum
name: "Low"
# --- EngineeringIssue ---
# A software development work item (bug, feature, task).
Node: ent:EngineeringIssue
typeOf: schema:Class
subClassOf: schema:Intangible
name: "EngineeringIssue"
description: "A software development work item such as a bug, feature request, or task."
Node: ent:issueKey
typeOf: schema:Property
name: "issueKey"
domainIncludes: ent:EngineeringIssue
rangeIncludes: schema:Text
Node: ent:issueSummary
typeOf: schema:Property
name: "issueSummary"
domainIncludes: ent:EngineeringIssue
rangeIncludes: schema:Text
Node: ent:issueType
typeOf: schema:Property
name: "issueType"
description: "Bug, Story, Task, Epic, etc."
domainIncludes: ent:EngineeringIssue
rangeIncludes: schema:Text
Node: ent:issueStatus
typeOf: schema:Property
name: "issueStatus"
domainIncludes: ent:EngineeringIssue
rangeIncludes: schema:Text
Node: ent:issuePriority
typeOf: schema:Property
name: "issuePriority"
domainIncludes: ent:EngineeringIssue
rangeIncludes: ent:PriorityEnum
Node: ent:storyPoints
typeOf: schema:Property
name: "storyPoints"
domainIncludes: ent:EngineeringIssue
rangeIncludes: schema:Number
Node: ent:project
typeOf: schema:Property
name: "project"
domainIncludes: ent:EngineeringIssue, ent:SupportTicket
rangeIncludes: ent:Project
# --- Project ---
Node: ent:Project
typeOf: schema:Class
subClassOf: schema:Intangible
name: "Project"
description: "A project grouping related work items."
Node: ent:projectKey
typeOf: schema:Property
name: "projectKey"
domainIncludes: ent:Project
rangeIncludes: schema:Text
Node: ent:projectName
typeOf: schema:Property
name: "projectName"
domainIncludes: ent:Project
rangeIncludes: schema:Text
# --- Additional Organization properties ---
Node: ent:annualRevenue
typeOf: schema:Property
name: "annualRevenue"
description: "Annual revenue of the organization."
domainIncludes: schema:Organization
rangeIncludes: schema:MonetaryAmount
Node: ent:industry
typeOf: schema:Property
name: "industry"
description: "Industry classification."
domainIncludes: schema:Organization
rangeIncludes: schema:Text
Node: ent:lifecycleStage
typeOf: schema:Property
name: "lifecycleStage"
description: "CRM lifecycle stage (lead, customer, etc.)."
domainIncludes: schema:Organization, schema:Person
rangeIncludes: schema:Text
Node: ent:customerType
typeOf: schema:Property
name: "customerType"
description: "Classification as prospect, customer, competitor, etc."
domainIncludes: schema:Organization
rangeIncludes: schema:Text
# --- MarketingCampaign ---
Node: ent:MarketingCampaign
typeOf: schema:Class
subClassOf: schema:Intangible
name: "MarketingCampaign"
description: "A marketing campaign targeting contacts."
Node: ent:campaignName
typeOf: schema:Property
name: "campaignName"
domainIncludes: ent:MarketingCampaign
rangeIncludes: schema:Text
Node: ent:campaignType
typeOf: schema:Property
name: "campaignType"
domainIncludes: ent:MarketingCampaign
rangeIncludes: schema:Text
Node: ent:startDate
typeOf: schema:Property
name: "startDate"
domainIncludes: ent:MarketingCampaign
rangeIncludes: schema:Date
Node: ent:endDate
typeOf: schema:Property
name: "endDate"
domainIncludes: ent:MarketingCampaign
rangeIncludes: schema:Date
# --- MarketingEngagement ---
Node: ent:MarketingEngagement
typeOf: schema:Class
subClassOf: schema:Intangible
name: "MarketingEngagement"
description: "A marketing interaction event (email open, click, etc.)."
Node: ent:engagementType
typeOf: schema:Property
name: "engagementType"
description: "Type of engagement: SENT, DELIVERED, OPEN, CLICK, BOUNCE, UNSUBSCRIBE"
domainIncludes: ent:MarketingEngagement
rangeIncludes: schema:Text
Node: ent:engagementDate
typeOf: schema:Property
name: "engagementDate"
domainIncludes: ent:MarketingEngagement
rangeIncludes: schema:DateTime
Node: ent:engagementContact
typeOf: schema:Property
name: "engagementContact"
domainIncludes: ent:MarketingEngagement
rangeIncludes: schema:Person
Node: ent:engagementCampaign
typeOf: schema:Property
name: "engagementCampaign"
domainIncludes: ent:MarketingEngagement
rangeIncludes: ent:MarketingCampaign
The ontology must also include mapping rules for enumerated values that differ across systems. Implement these as JSON lookup files:
File: ontology/enum_mappings.json
{
"pipeline_stage": {
"ontology_type": "ent:PipelineStageEnum",
"mappings": {
"hubspot": {
"appointmentscheduled": "Qualify",
"qualifiedtobuy": "Qualify",
"presentationscheduled": "Develop",
"decisionmakerboughtin": "Develop",
"contractsent": "Propose",
"closedwon": "Close",
"closedlost": "Close"
},
"dynamics365": {
"Qualify": "Qualify",
"Develop": "Develop",
"Propose": "Propose",
"Close": "Close"
}
}
},
"opportunity_status": {
"ontology_type": "ent:OpportunityStatusEnum",
"mappings": {
"hubspot": {
"closedwon": "Won",
"closedlost": "Lost",
"_default": "Open"
},
"dynamics365_statecode": {
"0": "Open",
"1": "Won",
"2": "Lost"
}
}
},
"ticket_status": {
"ontology_type": "ent:TicketStatusEnum",
"mappings": {
"jira": {
"To Do": "Open",
"In Progress": "InProgress",
"In Review": "InProgress",
"Waiting for Customer": "WaitingOnCustomer",
"Escalated": "Escalated",
"Done": "Resolved",
"Closed": "Closed"
}
}
},
"priority": {
"ontology_type": "ent:PriorityEnum",
"mappings": {
"jira": {
"Highest": "Highest",
"High": "High",
"Medium": "Medium",
"Low": "Low",
"Lowest": "Low"
},
"hubspot": {
"high": "High",
"medium": "Medium",
"low": "Low"
}
}
}
}Template MCF files declare how each source table maps to the ontology. These follow the Data Commons TMCF format adapted for the enterprise context.
The key syntax:
Node: E:<DatasetName>->E<N>declares an entitytypeOf:declares its ontology typeC:<DatasetName>-><ColumnName>references a column value- Properties on the left are ontology properties; column references on the right are source columns
File: mappings/hubspot.tmcf
# === HubSpot Companies -> schema:Organization ===
Node: E:hs_companies->E1
typeOf: schema:Organization
identifier: C:hs_companies->contoso_id
name: C:hs_companies->name
url: C:hs_companies->domain
ent:industry: C:hs_companies->industry
ent:annualRevenue: C:hs_companies->annual_revenue
schema:numberOfEmployees: C:hs_companies->num_employees
schema:address: C:hs_companies->city
ent:lifecycleStage: C:hs_companies->lifecycle_stage
ent:customerType: C:hs_companies->lifecycle_stage
# === HubSpot Contacts -> schema:Person ===
Node: E:hs_contacts->E1
typeOf: schema:Person
identifier: C:hs_contacts->contoso_id
schema:givenName: C:hs_contacts->firstname
schema:familyName: C:hs_contacts->lastname
schema:email: C:hs_contacts->email
schema:telephone: C:hs_contacts->phone
schema:jobTitle: C:hs_contacts->jobtitle
schema:worksFor: C:hs_contacts->company_id
ent:lifecycleStage: C:hs_contacts->lifecyclestage
# === HubSpot Deals -> ent:SalesOpportunity ===
Node: E:hs_deals->E1
typeOf: ent:SalesOpportunity
identifier: C:hs_deals->contoso_id
ent:opportunityName: C:hs_deals->dealname
ent:estimatedValue: C:hs_deals->amount
ent:pipelineStage: C:hs_deals->dealstage
ent:estimatedCloseDate: C:hs_deals->closedate
ent:customer: C:hs_deals->company_id
ent:dateCreated: C:hs_deals->createdate
ent:owner: C:hs_deals->hubspot_owner_id
# === HubSpot Campaigns -> ent:MarketingCampaign ===
Node: E:hs_campaigns->E1
typeOf: ent:MarketingCampaign
ent:campaignName: C:hs_campaigns->name
ent:campaignType: C:hs_campaigns->type
ent:startDate: C:hs_campaigns->start_date
ent:endDate: C:hs_campaigns->end_date
# === HubSpot Email Events -> ent:MarketingEngagement ===
Node: E:hs_marketing_emails->E1
typeOf: ent:MarketingEngagement
ent:engagementType: C:hs_marketing_emails->event_type
ent:engagementDate: C:hs_marketing_emails->event_timestamp
ent:engagementContact: C:hs_marketing_emails->contact_id
ent:engagementCampaign: C:hs_marketing_emails->email_campaign_id
File: mappings/jira.tmcf
# === Jira Projects -> ent:Project ===
Node: E:jira_projects->E1
typeOf: ent:Project
ent:projectKey: C:jira_projects->project_key
ent:projectName: C:jira_projects->name
# === Jira Support Issues -> ent:SupportTicket ===
# Only issues from the SUP project are SupportTickets.
# Filter: project_key = "SUP"
Node: E:jira_issues_support->E1
typeOf: ent:SupportTicket
ent:ticketId: C:jira_issues->issue_key
ent:ticketSummary: C:jira_issues->summary
ent:ticketDescription: C:jira_issues->description
ent:ticketStatus: C:jira_issues->status
ent:ticketPriority: C:jira_issues->priority
ent:ticketType: C:jira_issues->issue_type
ent:assignee: C:jira_issues->assignee_account_id
ent:reportedBy: C:jira_issues->reporter_account_id
ent:dateCreated: C:jira_issues->created
ent:dateResolved: C:jira_issues->resolved
ent:resolution: C:jira_issues->resolution
# Customer linkage comes from join with jira_issue_customer_link
ent:affectedCustomer: C:jira_issue_customer_link->contoso_customer_id
# === Jira Engineering Issues -> ent:EngineeringIssue ===
# Issues from ENG, PLATFORM, DOCS projects.
Node: E:jira_issues_eng->E1
typeOf: ent:EngineeringIssue
ent:issueKey: C:jira_issues->issue_key
ent:issueSummary: C:jira_issues->summary
ent:issueType: C:jira_issues->issue_type
ent:issueStatus: C:jira_issues->status
ent:issuePriority: C:jira_issues->priority
ent:assignee: C:jira_issues->assignee_account_id
ent:dateCreated: C:jira_issues->created
ent:dateResolved: C:jira_issues->resolved
ent:resolution: C:jira_issues->resolution
ent:storyPoints: C:jira_issues->story_points
ent:project: C:jira_issues->project_id
File: mappings/dynamics365.tmcf
# === D365 Accounts -> schema:Organization ===
Node: E:d365_accounts->E1
typeOf: schema:Organization
identifier: C:d365_accounts->contoso_id
name: C:d365_accounts->name
schema:telephone: C:d365_accounts->telephone1
url: C:d365_accounts->websiteurl
schema:numberOfEmployees: C:d365_accounts->numberofemployees
ent:annualRevenue: C:d365_accounts->revenue
schema:address: C:d365_accounts->address1_city
ent:customerType: C:d365_accounts->customertypecode
# === D365 Contacts -> schema:Person ===
Node: E:d365_contacts->E1
typeOf: schema:Person
identifier: C:d365_contacts->contoso_id
schema:givenName: C:d365_contacts->firstname
schema:familyName: C:d365_contacts->lastname
schema:email: C:d365_contacts->emailaddress1
schema:telephone: C:d365_contacts->telephone1
schema:jobTitle: C:d365_contacts->jobtitle
schema:worksFor: C:d365_contacts->parentcustomerid
# === D365 Opportunities -> ent:SalesOpportunity ===
Node: E:d365_opportunities->E1
typeOf: ent:SalesOpportunity
identifier: C:d365_opportunities->contoso_id
ent:opportunityName: C:d365_opportunities->name
ent:estimatedValue: C:d365_opportunities->estimatedvalue
ent:actualValue: C:d365_opportunities->actualvalue
ent:estimatedCloseDate: C:d365_opportunities->estimatedclosedate
ent:actualCloseDate: C:d365_opportunities->actualclosedate
ent:pipelineStage: C:d365_opportunities->stepname
ent:opportunityStatus: C:d365_opportunities->statecode
ent:closeProbability: C:d365_opportunities->closeprobability
ent:customer: C:d365_opportunities->parentaccountid
ent:primaryContact: C:d365_opportunities->parentcontactid
ent:owner: C:d365_opportunities->ownerid
ent:dateCreated: C:d365_opportunities->createdon
# === D365 Products -> schema:Product ===
Node: E:d365_products->E1
typeOf: schema:Product
schema:productID: C:d365_products->productnumber
schema:name: C:d365_products->name
schema:description: C:d365_products->description
# === D365 Orders -> schema:Order ===
Node: E:d365_orders->E1
typeOf: schema:Order
schema:orderNumber: C:d365_orders->ordernumber
schema:orderDate: C:d365_orders->createdon
schema:customer: C:d365_orders->customerid
schema:orderStatus: C:d365_orders->statecode
# === D365 Opportunity Line Items -> schema:OrderItem ===
# (Used as proposed line items on opportunities, before they become actual order items)
Node: E:d365_opportunityproducts->E1
typeOf: schema:OrderItem
schema:orderedItem: C:d365_opportunityproducts->productid
schema:orderQuantity: C:d365_opportunityproducts->quantity
schema:price: C:d365_opportunityproducts->priceperunit
This is the component that takes a natural language question and translates it into a structured query against the shared ontology, which then gets compiled to SQL against the source databases using the TMCF mappings.
User Question (English)
|
v
+----------------------+
| NL -> Semantic Query | (LLM call)
| "which deals are |
| at risk..." |
| | |
| SemanticQuery{ |
| find: SalesOpp |
| where: status=Open|
| join: SupportTkt |
| on: customer |
| having: count>2 |
| } |
+----------------------+
|
v
+----------------------+
| Semantic -> SQL | (deterministic compiler)
| Uses TMCF mappings |
| + enum mappings |
| | |
| SELECT ... FROM |
| d365_opportunities |
| JOIN jira_issues... |
+----------------------+
|
v
+----------------------+
| SQL Execution |
| Against source DBs |
| | |
| Results |
+----------------------+
|
v
+----------------------+
| Result -> English | (LLM call)
| Natural language |
| summary of findings |
+----------------------+
Define a JSON-based intermediate representation that the LLM outputs. This is the "common language" between natural language and SQL.
{
"description": "Find open deals where the customer has more than 2 escalated support tickets",
"primary_entity": "ent:SalesOpportunity",
"select": [
"ent:opportunityName",
"ent:estimatedValue",
"ent:estimatedCloseDate",
"ent:customer.schema:name",
"ent:pipelineStage"
],
"filters": [
{
"property": "ent:opportunityStatus",
"operator": "eq",
"value": "Open"
}
],
"joins": [
{
"entity": "ent:SupportTicket",
"on": {
"left": "ent:SalesOpportunity.ent:customer",
"right": "ent:SupportTicket.ent:affectedCustomer"
},
"type": "inner"
}
],
"aggregations": [
{
"function": "count",
"entity": "ent:SupportTicket",
"alias": "open_ticket_count",
"filters": [
{
"property": "ent:ticketStatus",
"operator": "in",
"value": ["Escalated", "Open", "InProgress"]
}
]
}
],
"having": [
{
"alias": "open_ticket_count",
"operator": "gt",
"value": 2
}
],
"order_by": [
{"property": "ent:estimatedValue", "direction": "desc"}
]
}Implement as a Python module: translator/nl_to_semantic.py
The LLM prompt must include:
- The complete ontology (all types, properties, and enumerations)
- The semantic query language spec with examples
- The user's question
SYSTEM_PROMPT = """You are a semantic query translator. You translate natural language questions
about enterprise data into structured semantic queries.
You have access to the following ontology:
## Entity Types
### schema:Organization
Properties: name (Text), url (Text), telephone (Text), numberOfEmployees (Number),
ent:annualRevenue (MonetaryAmount), ent:industry (Text), ent:lifecycleStage (Text),
ent:customerType (Text), address (Text)
### schema:Person
Properties: givenName (Text), familyName (Text), email (Text), telephone (Text),
jobTitle (Text), worksFor (-> Organization), ent:lifecycleStage (Text)
### ent:SalesOpportunity
Properties: opportunityName (Text), estimatedValue (MonetaryAmount), actualValue (MonetaryAmount),
estimatedCloseDate (Date), actualCloseDate (Date), pipelineStage (PipelineStageEnum),
opportunityStatus (OpportunityStatusEnum: Open/Won/Lost), closeProbability (Number),
customer (-> Organization), primaryContact (-> Person), owner (-> Person), dateCreated (DateTime)
### ent:SupportTicket
Properties: ticketId (Text), ticketSummary (Text), ticketDescription (Text),
ticketStatus (TicketStatusEnum: Open/InProgress/WaitingOnCustomer/Escalated/Resolved/Closed),
ticketPriority (PriorityEnum: Highest/High/Medium/Low), ticketType (Text),
affectedCustomer (-> Organization), reportedBy (-> Person), assignee (-> Person),
dateCreated (DateTime), dateResolved (DateTime), resolution (Text)
### ent:EngineeringIssue
Properties: issueKey (Text), issueSummary (Text), issueType (Text), issueStatus (Text),
issuePriority (PriorityEnum), assignee (-> Person), dateCreated (DateTime),
dateResolved (DateTime), resolution (Text), storyPoints (Number), project (-> Project)
### schema:Product
Properties: name (Text), productID (Text), description (Text)
### schema:Order
Properties: orderNumber (Text), orderDate (Date), customer (-> Organization), orderStatus (Text)
### ent:MarketingCampaign
Properties: campaignName (Text), campaignType (Text), startDate (Date), endDate (Date)
### ent:MarketingEngagement
Properties: engagementType (Text: SENT/DELIVERED/OPEN/CLICK/BOUNCE/UNSUBSCRIBE),
engagementDate (DateTime), engagementContact (-> Person), engagementCampaign (-> MarketingCampaign)
## Semantic Query Format
Respond ONLY with a JSON object (no markdown, no explanation) following this schema:
{
"description": "Plain English restatement of what the query finds",
"primary_entity": "ontology type to query (e.g. ent:SalesOpportunity)",
"select": ["list of ontology properties to return"],
"filters": [{"property": "...", "operator": "eq|neq|gt|lt|gte|lte|in|like|is_null|is_not_null", "value": "..."}],
"joins": [{"entity": "type to join", "on": {"left": "property path", "right": "property path"}, "type": "inner|left"}],
"aggregations": [{"function": "count|sum|avg|min|max", "entity": "type", "alias": "name", "filters": [...]}],
"having": [{"alias": "...", "operator": "...", "value": ...}],
"order_by": [{"property": "...", "direction": "asc|desc"}],
"limit": null
}
Omit any top-level keys that are empty or not needed.
## Key Relationships for Cross-System Joins
- SalesOpportunity.customer -> Organization <- SupportTicket.affectedCustomer
(join deals to tickets through the shared customer)
- SalesOpportunity.customer -> Organization <- MarketingEngagement.engagementContact.worksFor
(join deals to marketing engagement through contact's employer)
- Person.worksFor -> Organization
(connect people to companies)
## Examples
User: "Show me all open high-value deals closing this quarter"
{
"description": "Open opportunities with estimated value over $100,000 closing in Q1 2026",
"primary_entity": "ent:SalesOpportunity",
"select": ["ent:opportunityName", "ent:estimatedValue", "ent:estimatedCloseDate", "ent:customer.schema:name", "ent:pipelineStage"],
"filters": [
{"property": "ent:opportunityStatus", "operator": "eq", "value": "Open"},
{"property": "ent:estimatedValue", "operator": "gt", "value": 100000},
{"property": "ent:estimatedCloseDate", "operator": "gte", "value": "2026-01-01"},
{"property": "ent:estimatedCloseDate", "operator": "lte", "value": "2026-03-31"}
],
"order_by": [{"property": "ent:estimatedValue", "direction": "desc"}]
}
User: "Which customers have the most unresolved support tickets?"
{
"description": "Organizations ranked by count of non-closed support tickets",
"primary_entity": "schema:Organization",
"select": ["schema:name", "ent:annualRevenue"],
"joins": [
{"entity": "ent:SupportTicket", "on": {"left": "schema:Organization.identifier", "right": "ent:SupportTicket.ent:affectedCustomer"}, "type": "inner"}
],
"aggregations": [
{"function": "count", "entity": "ent:SupportTicket", "alias": "ticket_count", "filters": [{"property": "ent:ticketStatus", "operator": "in", "value": ["Open", "InProgress", "Escalated"]}]}
],
"order_by": [{"property": "ticket_count", "direction": "desc"}],
"limit": 20
}
"""Implement as: translator/semantic_to_sql.py
This is the deterministic (non-LLM) component that reads the TMCF mappings and enum mappings, and compiles a semantic query into SQL.
Input: SemanticQuery (JSON), TMCF mappings, enum_mappings
Output: SQL query string, list of (database_file, table_name) to execute against
1. RESOLVE PRIMARY TABLE
- Look up primary_entity in TMCF mappings
- Find which source database + table maps to this type
- If multiple sources map to the same type (e.g., Organization appears in
both HubSpot and D365), pick the "richest" source or the one that has
the most requested select properties. For the POC, use a priority order:
SalesOpportunity -> dynamics365.d365_opportunities (canonical)
SupportTicket -> jira.jira_issues (canonical)
Organization -> dynamics365.d365_accounts (canonical)
Person -> dynamics365.d365_contacts (canonical)
MarketingCampaign -> hubspot.hs_campaigns (canonical)
MarketingEngagement -> hubspot.hs_marketing_emails (canonical)
2. RESOLVE SELECT COLUMNS
- For each property in select[], look up the TMCF mapping to find the
source column name
- For dotted paths like "ent:customer.schema:name", resolve the FK chain:
ent:customer maps to parentaccountid in d365_opportunities
schema:name maps to name in d365_accounts
-> generates a JOIN and selects d365_accounts.name
3. RESOLVE JOINS
- For each join in joins[], find the target table from TMCF
- Resolve the "on" properties to actual columns using TMCF
- The join key is the shared entity identifier (contoso_id)
- Cross-database joins: since we use SQLite, ATTACH the other database
files and reference tables as db_alias.table_name
4. RESOLVE FILTERS
- Map ontology property to source column via TMCF
- Map enum values through enum_mappings.json
e.g., ent:opportunityStatus "Open" -> statecode 0 (for D365)
- Generate WHERE clauses
5. RESOLVE AGGREGATIONS
- Map to SQL aggregate functions
- Apply sub-filters within the aggregation
- Generate GROUP BY as needed
6. GENERATE SQL
- Combine all parts into a valid SQL statement
- Use ATTACH DATABASE for cross-database queries
- Return the SQL plus execution instructions
Since the three systems are separate SQLite databases, use ATTACH:
-- Attach all databases
ATTACH DATABASE 'hubspot.db' AS hubspot;
ATTACH DATABASE 'jira.db' AS jira;
ATTACH DATABASE 'dynamics365.db' AS d365;
-- Example: deals at risk (opportunities with escalated tickets)
SELECT
d365.d365_opportunities.name AS opportunity_name,
d365.d365_opportunities.estimatedvalue AS estimated_value,
d365.d365_opportunities.estimatedclosedate AS close_date,
d365.d365_accounts.name AS customer_name,
COUNT(jira.jira_issues.issue_id) AS escalated_ticket_count
FROM d365.d365_opportunities
JOIN d365.d365_accounts
ON d365.d365_opportunities.parentaccountid = d365.d365_accounts.accountid
JOIN jira.jira_issue_customer_link
ON d365.d365_accounts.contoso_id = jira.jira_issue_customer_link.contoso_customer_id
JOIN jira.jira_issues
ON jira.jira_issue_customer_link.issue_id = jira.jira_issues.issue_id
WHERE d365.d365_opportunities.statecode = 0 -- Open
AND jira.jira_issues.status IN ('Escalated', 'In Progress', 'To Do')
AND jira.jira_issues.project_id IN (
SELECT project_id FROM jira.jira_projects WHERE project_key = 'SUP'
)
GROUP BY d365.d365_opportunities.opportunityid
HAVING COUNT(jira.jira_issues.issue_id) > 2
ORDER BY d365.d365_opportunities.estimatedvalue DESC;Implement as: translator/summarize_results.py
After SQL execution, pass the results back through an LLM to generate a natural language summary.
SUMMARIZE_PROMPT = """You are summarizing query results for a business user.
The user asked: "{original_question}"
The query found the following results:
{results_as_table}
Provide a concise, actionable summary. Highlight the most important findings.
If there are concerning patterns (e.g., high-value deals with many support issues),
call them out explicitly. Use specific numbers and names from the data.
Keep it to 2-4 paragraphs."""Implement as: demo.py
A simple CLI that ties everything together:
"""
Usage: python demo.py "which deals are at risk based on support tickets?"
"""
import sys
from translator.nl_to_semantic import translate_to_semantic
from translator.semantic_to_sql import compile_to_sql
from translator.execute import run_query
from translator.summarize_results import summarize
def main():
question = sys.argv[1] if len(sys.argv) > 1 else input("Ask a question: ")
# Step 1: NL -> Semantic Query (LLM)
print("\n Translating to semantic query...")
semantic_query = translate_to_semantic(question)
print(f"Semantic query:\n{json.dumps(semantic_query, indent=2)}")
# Step 2: Semantic Query -> SQL (deterministic)
print("\n Compiling to SQL...")
sql, db_config = compile_to_sql(semantic_query)
print(f"Generated SQL:\n{sql}")
# Step 3: Execute
print("\n Executing query...")
results = run_query(sql, db_config)
print(f"Found {len(results)} results")
# Step 4: Summarize (LLM)
print("\n Summarizing results...")
summary = summarize(question, results)
print(f"\n{summary}")
if __name__ == "__main__":
main()The system should handle at least these queries:
-
"Which deals are at risk based on open support tickets?"
- Joins: SalesOpportunity -> Organization <- SupportTicket
- Cross-system: D365 opportunities + Jira support tickets
-
"Show me customers with declining marketing engagement who have upcoming renewals"
- Joins: Organization <- MarketingEngagement, Organization <- SalesOpportunity
- Cross-system: HubSpot engagement + D365 opportunities
-
"What's the average time to resolve support tickets for our top 10 customers by deal value?"
- Joins: Organization <- SalesOpportunity, Organization <- SupportTicket
- Aggregations: avg resolution time, sum deal value
- Cross-system: all three databases
-
"Which sales reps have the most deals with customers who have escalated tickets?"
- Joins: Person (as owner) -> SalesOpportunity -> Organization <- SupportTicket
- Cross-system: D365 + Jira
-
"List all products sold to customers with more than 5 open support tickets"
- Joins: Product -> OrderItem -> Order -> Organization <- SupportTicket
- Cross-system: D365 + Jira
-
"How many engineering issues are customer-reported versus internally found?"
- Filter on labels containing "customer-reported" in Jira
- Single system but uses ontology vocabulary
-
"What's our total pipeline value by industry?"
- Joins: SalesOpportunity -> Organization
- Aggregation: sum estimatedValue, group by industry
- Single system (D365) but expressed in ontology terms
-
"Show me contacts at customers with both open deals and escalated tickets who haven't opened a marketing email in 3 months"
- Joins across all three systems
- The "holy grail" cross-system query
enterprise-semantic-layer/
├── README.md
├── requirements.txt # faker, anthropic (or openai), sqlite3 (stdlib)
├── generate_data.py # Synthetic data generator
├── databases/
│ ├── hubspot.db # Generated
│ ├── jira.db # Generated
│ └── dynamics365.db # Generated
├── ontology/
│ ├── enterprise_schema.mcf # The ontology
│ └── enum_mappings.json # Enumeration value mappings
├── mappings/
│ ├── hubspot.tmcf # HubSpot -> ontology mappings
│ ├── jira.tmcf # Jira -> ontology mappings
│ └── dynamics365.tmcf # D365 -> ontology mappings
├── translator/
│ ├── __init__.py
│ ├── nl_to_semantic.py # LLM: English -> SemanticQuery JSON
│ ├── semantic_to_sql.py # Deterministic: SemanticQuery -> SQL
│ ├── tmcf_parser.py # Reads TMCF files into lookup structures
│ ├── execute.py # Runs SQL against SQLite databases
│ └── summarize_results.py # LLM: Results -> English summary
├── demo.py # CLI entry point
└── tests/
├── test_data_generation.py # Verify data consistency
├── test_tmcf_parser.py # Verify TMCF parsing
├── test_semantic_to_sql.py # Verify SQL compilation
└── test_example_queries.py # End-to-end tests for the 8 example queries
Build in this sequence, testing at each step:
- Implement
generate_data.py - Create master entity lists with cross-system IDs
- Populate all three SQLite databases
- Test: Verify row counts match targets, verify cross-system IDs are consistent, run basic SELECT queries
- Write
enterprise_schema.mcf - Write
enum_mappings.json - Write the three TMCF files
- Test: Parse all MCF/TMCF files, verify all referenced properties exist in ontology
- Implement
tmcf_parser.pythat reads TMCF files and produces lookup dictionaries:ontology_type -> [(database, table, column_mappings)]ontology_property -> [(database, table, column)]
- Test: Parser correctly resolves SalesOpportunity to d365_opportunities, SupportTicket to jira_issues, etc.
- Implement
semantic_to_sql.py - Handle: single-table queries, cross-database joins via ATTACH, enum value translation, aggregations, HAVING clauses
- Test: Hand-write semantic query JSON for each of the 8 example queries, verify generated SQL is valid and returns expected results
- Implement
nl_to_semantic.pyusing Anthropic API (Claude) - The prompt includes the full ontology and examples
- Test: Feed each of the 8 example questions, verify the semantic query JSON is correct
- Implement
summarize_results.py - Implement
demo.py - Test: End-to-end with all 8 example queries
- Unit tests for TMCF parser
- Unit tests for SQL compiler (given known semantic query -> expected SQL)
- Integration tests that run each example query end-to-end
- Data consistency tests (every contoso_id in Jira customer links exists in D365 accounts, etc.)
- Model: claude-sonnet-4-20250514
- Two LLM calls per query: NL->semantic and results->summary
- The SQL compilation step is purely deterministic (no LLM) — this is important because it demonstrates that the semantic layer does the heavy lifting, not prompt engineering
- Given the same semantic query JSON, it must always produce the same SQL
- No LLM in the loop for SQL generation
- This is a key part of the argument: the semantic layer makes the data legible enough that a deterministic compiler can do cross-system joins. You don't need an LLM to figure out that Salesforce Account = SAP Customer.
- Data Commons TMCF supports complex features (quantities, nested entities, provenance). For this POC, we use a simplified subset:
- One entity per Node block
- Column references (C:) to simple column values
- No nested entities (handle FK references at query compilation time)
- Filter expressions as comments (e.g., "# Filter: project_key = 'SUP'") that the parser reads
- This is the "assume common IDs" simplification
- In the real system, entity resolution (Part 3 of the proposal) would establish these common IDs
- For the POC, we pre-generate them and plant them in all three databases
- TMCF maps columns to ontology properties
- enum_mappings.json maps source values to ontology enumeration values
- The SQL compiler uses both: TMCF for column names, enum_mappings for WHERE clause values