Skip to content

Commit 6f7b86e

Browse files
authored
Named Entities & Navigation (#7)
1 parent 2d9e608 commit 6f7b86e

File tree

5 files changed

+149
-61
lines changed

5 files changed

+149
-61
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
),
1818
],
1919
dependencies: [
20-
.package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 7))
20+
.package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 8))
2121
],
2222
targets: [
2323
.target(

README.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,36 @@ Some examples of categorized actions include (but not limited to):
2424
### Label Scheme
2525
The base model was created with the OntoNotes 5.0 NER annotations which includes:
2626

27-
* **PERSON**: Individual names (e.g., Barack Obama).
28-
* **ORGANIZATION**: Company or institution names (e.g., Apple).
29-
* **LOCATION**: Geographical places (e.g., Tokyo).
27+
* **CARDINAL**: Cardinal numbers (e.g., 1, 2, 3).
3028
* **DATE**: Dates (e.g., May 8, 2025).
31-
* **TIME**: Times (e.g., 10:00 AM).
3229
* **EVENT**: Names of events (e.g., World Series).
33-
* **WORK\_OF\_ART**: Names of works of art (e.g., "Hamlet").
3430
* **FAC**: Buildings or facilities (e.g., White House).
3531
* **GPE**: Geo-political entities (e.g., United States).
3632
* **LANGUAGE**: Names of languages (e.g., English).
3733
* **LAW**: Legal names (e.g., The Constitution).
38-
* **NORP**: National/religious/political group (e.g., Democrats).
39-
* **CARDINAL**: Cardinal numbers (e.g., 1, 2, 3).
34+
* **LOC**: Represents locations (e.g., "New York City").
35+
* **MONEY**: Indicates monetary values (e.g., "100 dollars").
36+
* **NORP**: Represents national or political or religious groups (e.g., "Democrats", "the Catholic Church").
37+
* **ORDINAL**: Denotes ordinal numbers (e.g., "first", "second", "10th").
38+
* **ORG**: Represents organizations (e.g., "Google", "Microsoft").
39+
* **PERCENT**: Denotes percentages (e.g., "10%", "20%").
40+
* **PERSON**: Individual names (e.g., Barack Obama).
41+
* **PRODUCT**: Represents products (e.g., "iPhone", "MacBook").
42+
* **QUANTITY**: Indicates measurements or quantities (e.g., "10 kilograms").
43+
* **TIME**: Times (e.g., 10:00 AM).
44+
* **WORK\_OF\_ART**: Names of works of art (e.g., "Hamlet").
4045

4146
The trained model provides Construction NER annotations:
4247

43-
* **CON-BIM-CATG**: BIM Category - a high-level classification for families and elements, grouping them based on their functional type.
44-
* **CON-BIM-FAML**: BIM Family - a collection of elements that share common properties, behaviors, and physical characteristics.
45-
* **CON-BIM-TYPE**: BIM Type - a specific instantiation of a family that defines a unique set of parameters, essentially a variation within a family. Think of it as a specific size, material, or configuration of a particular family, such as a 3' x 6' door within a door family.
46-
* **CON-BIM-INST**: BIM Instance - a single, unique occurrence of a family type placed within a model.
47-
* **CON-BIM-LEVL**: BIM Level - a horizontal plane used to define the vertical position of elements like walls, floors, and ceilings.
48-
* **CON-BIM-VIEW**: BIM View - represents a specific way of looking at the model, whether it's a 2D plan, elevation, section, or 3D view.
49-
48+
* **CON\_BIM\_CATG**: BIM Category - a high-level classification for families and elements, grouping them based on their functional type.
49+
* **CON\_BIM\_FAML**: BIM Family - a collection of elements that share common properties, behaviors, and physical characteristics.
50+
* **CON\_BIM\_TYPE**: BIM Type - a specific instantiation of a family that defines a unique set of parameters, essentially a variation within a family. Think of it as a specific size, material, or configuration of a particular family, such as a 3' x 6' door within a door family.
51+
* **CON\_BIM\_INST**: BIM Instance - a single, unique occurrence of a family type placed within a model.
52+
* **CON\_BIM\_LEVL**: BIM Level - a horizontal plane used to define the vertical position of elements like walls, floors, and ceilings.
53+
* **CON\_BIM\_VIEW**: BIM View - represents a specific way of looking at the model, whether it's a 2D plan, elevation, section, or 3D view.
5054

5155

52-
| Component | Labels |
56+
| Component | Labels |
5357
| -------- | ------- |
54-
| named entities | CARDINAL, DATE, EVENT, FAC, GPE, LANGUAGE, LAW, LOC, MONEY, NORP, ORDINAL, ORG, PERCENT, PERSON, PRODUCT, QUANTITY, TIME, WORK_OF_ART, CON-BIM-CATG, CON-BIM-FAML, CON-BIM-TYPE, CON-BIM-INST, CON-BIM-LEVL, CON-BIM-VIEW |
55-
| categories | ISOLATE, HIDE, QUANTIFY |
58+
| named entities | CARDINAL, DATE, EVENT, FAC, GPE, LANGUAGE, LAW, LOC, MONEY, NORP, ORDINAL, ORG, PERCENT, PERSON, PRODUCT, QUANTITY, TIME, WORK\_OF\_ART, CON\_BIM\_CATG, CON\_BIM\_FAML, CON\_BIM\_TYPE, CON\_BIM\_INST, CON\_BIM\_LEVL, CON\_BIM\_VIEW |
59+
| categories | ISOLATE, HIDE, QUANTIFY, ZOOM\_IN, ZOOM\_OUT, PAN\_LEFT, PAN\_RIGHT, PAN\_UP, PAN\_DOWN, LOOK\_LEFT, LOOK\_RIGHT, LOOK\_UP, LOOK\_DOWN |

Sources/VimAssistant/Model/VimAssistant+Handler.swift

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,96 @@ public extension VimAssistant {
1919
/// - prediction: the prediction
2020
func handle(vim: Vim, prediction: VimPrediction?) {
2121
guard let prediction, let bestPrediction = prediction.bestPrediction, bestPrediction.confidence >= 0.85 else { return }
22-
guard prediction.entities.isNotEmpty else { return }
22+
let action = bestPrediction.action
23+
let ids = collect(vim: vim, prediction: prediction)
24+
Task { @MainActor in
25+
switch action {
26+
case .hide:
27+
guard ids.isNotEmpty else { return }
28+
await vim.hide(ids: ids)
29+
case .isolate:
30+
guard ids.isNotEmpty else { return }
31+
await vim.isolate(ids: ids)
32+
case .quantify:
33+
// TODO: Probably just emit an event that shows the quantities view
34+
break
35+
case .zoomIn:
36+
vim.zoom()
37+
case .zoomOut:
38+
vim.zoom(out: true)
39+
case .lookLeft:
40+
vim.look(.left)
41+
case .lookRight:
42+
vim.look(.right)
43+
case .lookUp:
44+
vim.look(.up)
45+
case .lookDown:
46+
vim.look(.down)
47+
case .panLeft:
48+
vim.pan(.left)
49+
case .panRight:
50+
vim.pan(.right)
51+
case .panUp:
52+
vim.pan(.up)
53+
case .panDown:
54+
vim.pan(.down)
55+
}
56+
}
57+
}
2358

59+
private func collect(vim: Vim, prediction: VimPrediction) -> [Int] {
60+
61+
guard let bestPrediction = prediction.bestPrediction, prediction.entities.isNotEmpty else { return []}
2462
let action = bestPrediction.action
2563

26-
print("❤️", bestPrediction)
64+
switch action {
65+
case .hide, .isolate:
66+
guard let db = vim.db, db.nodes.isNotEmpty else { return [] }
67+
let modelContext = ModelContext(db.modelContainer)
2768

28-
for entity in prediction.entities {
29-
if entity.label == "CON-BIM-CATG" {
30-
print("🚀", entity.value)
31-
perform(vim: vim, action: action, category: entity.value)
32-
} else if entity.label == "CON-BIM-FAML" {
69+
var ids: Set<Int> = .init()
3370

34-
} else if entity.label == "CON-BIM-TYPE" {
71+
// Fetch all geometry nodes
72+
let nodes = db.nodes
73+
let predicate = Database.Node.predicate(nodes: nodes)
74+
let descriptor = FetchDescriptor<Database.Node>(predicate: predicate, sortBy: [SortDescriptor(\.index)])
75+
guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return [] }
3576

36-
}
37-
}
38-
}
77+
let categoryNames = prediction.entities.filter{ $0.label == .bimCategory }.map { $0.value }
78+
let familyNames = prediction.entities.filter{ $0.label == .bimFamily }.map { $0.value }
3979

40-
private func perform(vim: Vim, action: VimPrediction.Action, category: String) {
80+
// Tuple of category names and ids
81+
let categories = results.compactMap{ $0.element?.category?.name }.uniqued().sorted{ $0 < $1 }.map { name in
82+
(name: name, ids: results.filter{ $0.element?.category?.name == name}.compactMap{ Int($0.index) })
83+
}
4184

42-
guard let db = vim.db else { return }
43-
let modelContext = ModelContext(db.modelContainer)
85+
// Tuple of family names and ids
86+
let familes = results.compactMap{ $0.element?.familyName }.uniqued().sorted{ $0 < $1 }.map { name in
87+
(name: name, ids: results.filter{ $0.element?.familyName == name}.compactMap{ Int($0.index) })
88+
}
4489

45-
let orderedSame = ComparisonResult.orderedSame
46-
let predicate = #Predicate<Database.Node>{
47-
if let element = $0.element, let cat = element.category {
48-
return cat.name.caseInsensitiveCompare(category) == orderedSame
49-
} else {
50-
return false
90+
// Collect the ids of the matching categories
91+
for name in categoryNames {
92+
let found = categories.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +)
93+
ids.formUnion(found)
5194
}
52-
}
5395

54-
let descriptor = FetchDescriptor<Database.Node>(predicate: predicate, sortBy: [SortDescriptor(\.index)])
55-
guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return }
56-
let ids = results.compactMap{ Int($0.index) }
57-
Task {
58-
switch action {
59-
case .hide:
60-
await vim.hide(ids: ids)
61-
case .isolate:
62-
await vim.isolate(ids: ids)
63-
case .quantify:
64-
break
96+
// Collect the ids of the matching families
97+
for name in familyNames {
98+
let found = familes.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +)
99+
ids.formUnion(found)
65100
}
101+
return ids.sorted()
102+
case .quantify, .zoomIn, .zoomOut, .lookLeft, .lookRight, .lookUp, .lookDown, .panLeft, .panRight, .panUp, .panDown:
103+
return []
66104
}
105+
67106
}
107+
}
108+
}
68109

110+
extension Array where Element == String {
111+
func containsIgnoringCase(_ element: Element) -> Bool {
112+
contains { $0.caseInsensitiveCompare(element) == .orderedSame }
69113
}
70114
}

Sources/VimAssistant/Model/VimPrediction.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,48 @@ public struct VimPrediction: Decodable, Equatable {
1616
case tokens = "tokens"
1717
}
1818

19+
enum NerLabel: String, Identifiable {
20+
21+
case person = "PERSON"
22+
case organization = "ORGANIZATION"
23+
case location = "LOCATION"
24+
case date = "DATE"
25+
case time = "TIME"
26+
case event = "EVENT"
27+
case workOfArt = "WORK_OF_ART"
28+
case fac = "FAC"
29+
case gpe = "GPE"
30+
case language = "LANGUAGE"
31+
case law = "LAW"
32+
case norp = "NORP"
33+
case product = "PRODUCT"
34+
case cardinal = "CARDINAL"
35+
case bimCategory = "CON_BIM_CATG"
36+
case bimFamily = "CON_BIM_FAML"
37+
case bimType = "CON_BIM_TYPE"
38+
case bimInstance = "CON_BIM_INST"
39+
case bimLevel = "CON_BIM_LEVL"
40+
case bimView = "CON_BIM_VIEW"
41+
42+
public var id: String {
43+
rawValue
44+
}
45+
}
46+
1947
enum Action: String, Codable, Identifiable {
2048
case isolate = "ISOLATE"
2149
case hide = "HIDE"
2250
case quantify = "QUANTIFY"
51+
case zoomIn = "ZOOM_IN"
52+
case zoomOut = "ZOOM_OUT"
53+
case lookLeft = "LOOK_LEFT"
54+
case lookRight = "LOOK_RIGHT"
55+
case lookUp = "LOOK_UP"
56+
case lookDown = "LOOK_DOWN"
57+
case panLeft = "PAN_LEFT"
58+
case panRight = "PAN_RIGHT"
59+
case panUp = "PAN_UP"
60+
case panDown = "PAN_DOWN"
2361

2462
public var id: String {
2563
rawValue
@@ -76,17 +114,18 @@ public struct VimPrediction: Decodable, Equatable {
76114
case end = "end"
77115
}
78116

79-
var label: String
117+
var label: NerLabel
80118
var value: String = .empty
81119
var range: Range<Int>
82120

83121
public var id: String {
84-
label + "_\(range)"
122+
label.rawValue + "_\(range)"
85123
}
86124

87125
init(from decoder: Decoder) throws {
88126
let values = try decoder.container(keyedBy: CodingKeys.self)
89-
label = try values.decode(String.self, forKey: .label)
127+
let labelString = try values.decode(String.self, forKey: .label)
128+
label = .init(rawValue: labelString)!
90129
let start = try values.decode(Int.self, forKey: .start)
91130
let end = try values.decode(Int.self, forKey: .end)
92131
range = start..<end

Sources/VimAssistant/Views/VimPredictionView.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct VimPredictionView: View {
7171
for entity in prediction.entities {
7272
let entityText = text[entity.range]
7373
var attributedEntityString = AttributedString(entityText)
74-
attributedEntityString.foregroundColor = .orange
74+
attributedEntityString.foregroundColor = .cyan
7575
attributedEntityString.underlineStyle = .single
7676
attributedEntityString.link = URL(string: "/\(entity.label)/\(entity.value)")!
7777
result.replaceSubrange(bounds: entity.range, with: attributedEntityString)
@@ -101,10 +101,11 @@ struct VimPredictionView: View {
101101
HStack {
102102
Text(text[entity.range])
103103
.bold()
104-
Text(entity.label)
105-
.padding(2)
106-
.background(Color.orange)
107-
.cornerRadius(4)
104+
Text(entity.label.rawValue)
105+
.padding(1)
106+
.background(Color.cyan)
107+
.foregroundStyle(Color.black)
108+
.cornerRadius(2)
108109
}
109110
}
110111
if let bestPrediction = prediction.bestPrediction {
@@ -116,7 +117,7 @@ struct VimPredictionView: View {
116117
HStack {
117118
Text(bestPrediction.action.rawValue.lowercased())
118119
.bold()
119-
Text(bestPrediction.confidence.formatted(.percent))
120+
Text(bestPrediction.confidence.formatted(.percent.precision(.fractionLength(2))))
120121
.foregroundStyle(predictionConfidenceColor)
121122
}
122123
}
@@ -128,7 +129,7 @@ struct VimPredictionView: View {
128129

129130
#Preview {
130131

131-
let json = "{\"text\":\"Hide all walls and air terminals \",\"ents\":[{\"start\":9,\"end\":14,\"label\":\"CON-BIM-CATG\"},{\"start\":19,\"end\":32,\"label\":\"CON-BIM-CATG\"}],\"cats\":{\"ISOLATE\":0.0122569752857089,\"HIDE\":0.978784739971161,\"QUANTIFY\":0.00895828753709793}}"
132+
let json = "{\"text\":\"Hide all walls and air terminals \",\"ents\":[{\"start\":9,\"end\":14,\"label\":\"CON_BIM_CATG\"},{\"start\":19,\"end\":32,\"label\":\"CON_BIM_CATG\"}],\"cats\":{\"ISOLATE\":0.0122569752857089,\"HIDE\":0.978784739971161,\"QUANTIFY\":0.00895828753709793}}"
132133
let prediction = try! JSONDecoder().decode(VimPrediction.self, from: json.data(using: .utf8)!)
133-
VimPredictionView(prediction: prediction, explain: .constant(false))
134+
VimPredictionView(prediction: prediction, explain: .constant(true))
134135
}

0 commit comments

Comments
 (0)