Skip to content

Commit 0ed6d48

Browse files
committed
fix(macos): resolve embedding and Heimdall model loading failures
Critical hotfixes for 1.0.3 macOS menu bar app: - Fix double .gguf extension bug causing model not found errors - pkg/heimdall/scheduler.go: Check HasSuffix before adding .gguf - pkg/embed/local_gguf.go: Same fix for embedding models - Models like 'bge-m3.gguf' were becoming 'bge-m3.gguf.gguf' - Add missing environment variables to LaunchAgent plist - NORNICDB_MODELS_DIR=/usr/local/var/nornicdb/models - NORNICDB_HEIMDALL_MODEL=qwen2.5-0.5b-instruct.gguf - NORNICDB_EMBEDDING_MODEL=bge-m3.gguf - macos/MenuBarApp/NornicDBMenuBar.swift: Both plist generators - Add macOS default models path to resolution candidates - /usr/local/var/nornicdb/models now checked first - Make regenerate embeddings endpoint non-blocking - POST /nornicdb/embed/trigger?regenerate=true returns 202 Accepted - Clearing and regeneration happens in background goroutine - pkg/server/server.go: Async processing with logging - Add UI confirmation dialog for destructive regenerate operation - ui/src/pages/Browser.tsx: Modal with warning and count - Create Swift YAML parser unit test - macos/MenuBarApp/ConfigParserTest.swift: Verify config loading Verified working: - Auto-embedding: New nodes embedded within 3s - Heimdall: Active and responding to queries - K-Means: Clustering 40K+ embeddings successfully
1 parent 8892212 commit 0ed6d48

File tree

10 files changed

+639
-75
lines changed

10 files changed

+639
-75
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env swift
2+
3+
import Foundation
4+
5+
// Helper function for separator lines
6+
func separator() -> String {
7+
return String(repeating: "=", count: 60)
8+
}
9+
10+
// Copy of parsing functions from NornicDBMenuBar.swift for testing
11+
func extractYAMLSection(named sectionName: String, from content: String) -> String? {
12+
let lines = content.components(separatedBy: .newlines)
13+
var inSection = false
14+
var sectionLines: [String] = []
15+
16+
for line in lines {
17+
// Check if this is the start of our target section (no leading whitespace)
18+
if line.hasPrefix("\(sectionName):") && !line.hasPrefix(" ") && !line.hasPrefix("\t") {
19+
inSection = true
20+
continue
21+
}
22+
23+
// If we're in the section, check if we've hit another top-level key
24+
if inSection {
25+
// A line that starts with a non-whitespace character and contains ":" is a new section
26+
let trimmed = line.trimmingCharacters(in: .whitespaces)
27+
if !line.isEmpty && !line.hasPrefix(" ") && !line.hasPrefix("\t") && !line.hasPrefix("#") && trimmed.contains(":") {
28+
// We've hit a new top-level section, stop
29+
break
30+
}
31+
sectionLines.append(line)
32+
}
33+
}
34+
35+
return sectionLines.isEmpty ? nil : sectionLines.joined(separator: "\n")
36+
}
37+
38+
func getYAMLBool(key: String, from section: String, default defaultValue: Bool = false) -> Bool {
39+
for line in section.components(separatedBy: .newlines) {
40+
let trimmed = line.trimmingCharacters(in: .whitespaces)
41+
if trimmed.hasPrefix("\(key):") {
42+
if trimmed.contains("true") { return true }
43+
if trimmed.contains("false") { return false }
44+
}
45+
}
46+
return defaultValue
47+
}
48+
49+
func getYAMLString(key: String, from section: String) -> String? {
50+
for line in section.components(separatedBy: .newlines) {
51+
let trimmed = line.trimmingCharacters(in: .whitespaces)
52+
if trimmed.hasPrefix("\(key):") {
53+
let value = trimmed.dropFirst("\(key):".count).trimmingCharacters(in: .whitespaces)
54+
// Remove quotes if present
55+
if value.hasPrefix("\"") && value.hasSuffix("\"") {
56+
return String(value.dropFirst().dropLast())
57+
}
58+
if value.hasPrefix("'") && value.hasSuffix("'") {
59+
return String(value.dropFirst().dropLast())
60+
}
61+
return value.isEmpty ? nil : value
62+
}
63+
}
64+
return nil
65+
}
66+
67+
// Test with ACTUAL config file content
68+
let testConfig = """
69+
# NornicDB Configuration (Full Edition)
70+
# Edit via Settings app (⌘,) or manually
71+
72+
server:
73+
port: 7687
74+
host: localhost
75+
bolt_port: 7687
76+
http_port: 7474
77+
78+
storage:
79+
path: "/usr/local/var/nornicdb/data"
80+
81+
database:
82+
encryption_enabled: false
83+
encryption_password: ""
84+
85+
embedding:
86+
enabled: true
87+
model: bge-m3
88+
provider: "local"
89+
90+
kmeans:
91+
enabled: true
92+
93+
heimdall:
94+
enabled: true
95+
model: qwen2.5-0.5b-instruct.gguf
96+
97+
auth:
98+
username: admin
99+
password: password
100+
jwt_secret: "[stored-in-keychain]"
101+
102+
auto_tlp:
103+
enabled: true
104+
"""
105+
106+
print(separator())
107+
print("YAML PARSING TEST")
108+
print(separator())
109+
110+
// Test embedding section
111+
print("\n📦 Testing EMBEDDING section:")
112+
if let embeddingSection = extractYAMLSection(named: "embedding", from: testConfig) {
113+
print(" ✅ Section found:")
114+
print(" ---")
115+
for line in embeddingSection.components(separatedBy: .newlines) {
116+
print(" \(line)")
117+
}
118+
print(" ---")
119+
120+
let enabled = getYAMLBool(key: "enabled", from: embeddingSection)
121+
print(" enabled: \(enabled) \(enabled == true ? "" : "❌ EXPECTED: true")")
122+
123+
if let model = getYAMLString(key: "model", from: embeddingSection) {
124+
print(" model: '\(model)' \(model == "bge-m3" ? "" : "❌ EXPECTED: bge-m3")")
125+
} else {
126+
print(" model: nil ❌ EXPECTED: bge-m3")
127+
}
128+
129+
if let provider = getYAMLString(key: "provider", from: embeddingSection) {
130+
print(" provider: '\(provider)' \(provider == "local" ? "" : "❌ EXPECTED: local")")
131+
} else {
132+
print(" provider: nil ❌ EXPECTED: local")
133+
}
134+
} else {
135+
print(" ❌ FAILED: Could not extract embedding section!")
136+
}
137+
138+
// Test heimdall section
139+
print("\n🔮 Testing HEIMDALL section:")
140+
if let heimdallSection = extractYAMLSection(named: "heimdall", from: testConfig) {
141+
print(" ✅ Section found:")
142+
print(" ---")
143+
for line in heimdallSection.components(separatedBy: .newlines) {
144+
print(" \(line)")
145+
}
146+
print(" ---")
147+
148+
let enabled = getYAMLBool(key: "enabled", from: heimdallSection)
149+
print(" enabled: \(enabled) \(enabled == true ? "" : "❌ EXPECTED: true")")
150+
151+
if let model = getYAMLString(key: "model", from: heimdallSection) {
152+
print(" model: '\(model)' \(model == "qwen2.5-0.5b-instruct.gguf" ? "" : "❌ EXPECTED: qwen2.5-0.5b-instruct.gguf")")
153+
} else {
154+
print(" model: nil ❌ EXPECTED: qwen2.5-0.5b-instruct.gguf")
155+
}
156+
} else {
157+
print(" ❌ FAILED: Could not extract heimdall section!")
158+
}
159+
160+
// Test server section
161+
print("\n🖥️ Testing SERVER section:")
162+
if let serverSection = extractYAMLSection(named: "server", from: testConfig) {
163+
print(" ✅ Section found")
164+
165+
if let boltPort = getYAMLString(key: "bolt_port", from: serverSection) {
166+
print(" bolt_port: '\(boltPort)' \(boltPort == "7687" ? "" : "")")
167+
}
168+
if let httpPort = getYAMLString(key: "http_port", from: serverSection) {
169+
print(" http_port: '\(httpPort)' \(httpPort == "7474" ? "" : "")")
170+
}
171+
} else {
172+
print(" ❌ FAILED: Could not extract server section!")
173+
}
174+
175+
// Test auth section
176+
print("\n🔐 Testing AUTH section:")
177+
if let authSection = extractYAMLSection(named: "auth", from: testConfig) {
178+
print(" ✅ Section found")
179+
180+
if let jwtSecret = getYAMLString(key: "jwt_secret", from: authSection) {
181+
print(" jwt_secret: '\(jwtSecret)' ✅")
182+
} else {
183+
print(" jwt_secret: nil ❌")
184+
}
185+
} else {
186+
print(" ❌ FAILED: Could not extract auth section!")
187+
}
188+
189+
// Now test with ACTUAL file
190+
print("\n" + separator())
191+
print("TESTING WITH ACTUAL CONFIG FILE")
192+
print(separator())
193+
194+
let configPath = NSString(string: "~/.nornicdb/config.yaml").expandingTildeInPath
195+
if let actualConfig = try? String(contentsOfFile: configPath, encoding: .utf8) {
196+
print("📄 Loaded config from: \(configPath)")
197+
print(" File size: \(actualConfig.count) chars")
198+
199+
print("\n📦 EMBEDDING from actual file:")
200+
if let embeddingSection = extractYAMLSection(named: "embedding", from: actualConfig) {
201+
print(" Section content:")
202+
for line in embeddingSection.components(separatedBy: .newlines) {
203+
print(" '\(line)'")
204+
}
205+
206+
let enabled = getYAMLBool(key: "enabled", from: embeddingSection)
207+
let model = getYAMLString(key: "model", from: embeddingSection)
208+
let provider = getYAMLString(key: "provider", from: embeddingSection)
209+
210+
print(" Parsed values:")
211+
print(" enabled: \(enabled)")
212+
print(" model: \(model ?? "nil")")
213+
print(" provider: \(provider ?? "nil")")
214+
} else {
215+
print(" ❌ Could not extract embedding section!")
216+
print(" Raw file content around 'embedding':")
217+
if let range = actualConfig.range(of: "embedding") {
218+
let start = actualConfig.index(range.lowerBound, offsetBy: -20, limitedBy: actualConfig.startIndex) ?? actualConfig.startIndex
219+
let end = actualConfig.index(range.upperBound, offsetBy: 100, limitedBy: actualConfig.endIndex) ?? actualConfig.endIndex
220+
print(" ...\(actualConfig[start..<end])...")
221+
}
222+
}
223+
224+
print("\n🔮 HEIMDALL from actual file:")
225+
if let heimdallSection = extractYAMLSection(named: "heimdall", from: actualConfig) {
226+
print(" Section content:")
227+
for line in heimdallSection.components(separatedBy: .newlines) {
228+
print(" '\(line)'")
229+
}
230+
231+
let enabled = getYAMLBool(key: "enabled", from: heimdallSection)
232+
let model = getYAMLString(key: "model", from: heimdallSection)
233+
234+
print(" Parsed values:")
235+
print(" enabled: \(enabled)")
236+
print(" model: \(model ?? "nil")")
237+
} else {
238+
print(" ❌ Could not extract heimdall section!")
239+
}
240+
} else {
241+
print("❌ Could not read config file at: \(configPath)")
242+
}
243+
244+
print("\n" + separator())
245+
print("TEST COMPLETE")
246+
print(separator())

macos/MenuBarApp/NornicDBMenuBar.swift

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,8 +1061,55 @@ class ConfigManager: ObservableObject {
10611061
}
10621062

10631063
func saveConfig() -> Bool {
1064-
guard var content = try? String(contentsOfFile: configPath, encoding: .utf8) else {
1065-
return false
1064+
// Try to read existing config, or create a new base template
1065+
var content: String
1066+
if let existingContent = try? String(contentsOfFile: configPath, encoding: .utf8) {
1067+
content = existingContent
1068+
} else {
1069+
// Create directory if needed
1070+
let configDir = NSString(string: "~/.nornicdb").expandingTildeInPath
1071+
try? FileManager.default.createDirectory(atPath: configDir, withIntermediateDirectories: true)
1072+
1073+
// Start with COMPLETE template including all sections
1074+
content = """
1075+
# NornicDB Configuration (Full Edition)
1076+
# Edit via Settings app (⌘,) or manually
1077+
1078+
server:
1079+
port: 7687
1080+
host: localhost
1081+
bolt_port: 7687
1082+
http_port: 7474
1083+
1084+
storage:
1085+
path: "/usr/local/var/nornicdb/data"
1086+
1087+
database:
1088+
encryption_enabled: false
1089+
encryption_password: ""
1090+
1091+
embedding:
1092+
enabled: false
1093+
model: ""
1094+
provider: "local"
1095+
1096+
kmeans:
1097+
enabled: false
1098+
1099+
heimdall:
1100+
enabled: false
1101+
model: ""
1102+
1103+
auth:
1104+
username: "admin"
1105+
password: "password"
1106+
jwt_secret: ""
1107+
1108+
auto_tlp:
1109+
enabled: false
1110+
1111+
"""
1112+
print("📝 Creating new config file at: \(configPath)")
10661113
}
10671114

10681115
// Ensure required sections exist
@@ -1081,6 +1128,41 @@ class ConfigManager: ObservableObject {
10811128
encryption_password: ""
10821129
""")
10831130

1131+
content = ensureSectionExists(in: content, section: "embedding", defaultContent: """
1132+
1133+
embedding:
1134+
enabled: false
1135+
model: ""
1136+
provider: "local"
1137+
""")
1138+
1139+
content = ensureSectionExists(in: content, section: "kmeans", defaultContent: """
1140+
1141+
kmeans:
1142+
enabled: false
1143+
""")
1144+
1145+
content = ensureSectionExists(in: content, section: "heimdall", defaultContent: """
1146+
1147+
heimdall:
1148+
enabled: false
1149+
model: ""
1150+
""")
1151+
1152+
content = ensureSectionExists(in: content, section: "auto_tlp", defaultContent: """
1153+
1154+
auto_tlp:
1155+
enabled: false
1156+
""")
1157+
1158+
content = ensureSectionExists(in: content, section: "server", defaultContent: """
1159+
1160+
server:
1161+
bolt_port: 7687
1162+
http_port: 7474
1163+
host: localhost
1164+
""")
1165+
10841166
// Update each feature setting
10851167
content = updateYAMLValue(in: content, section: "embedding", key: "enabled", value: embeddingsEnabled)
10861168
content = updateYAMLValue(in: content, section: "kmeans", key: "enabled", value: kmeansEnabled)
@@ -1186,7 +1268,8 @@ class ConfigManager: ObservableObject {
11861268
}
11871269

11881270
private func ensureSectionExists(in content: String, section: String, defaultContent: String) -> String {
1189-
// Check if section exists
1271+
// Check if section exists with proper YAML format (section name at start of line, followed by colon)
1272+
// Note: We look for the section at the start of a line (with optional leading whitespace for robustness)
11901273
let pattern = "^\(section):"
11911274
if let regex = try? NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines]) {
11921275
let range = NSRange(content.startIndex..., in: content)
@@ -1444,6 +1527,13 @@ struct SettingsView: View {
14441527
let launchAgentPath = NSString(string: "~/Library/LaunchAgents/com.nornicdb.server.plist").expandingTildeInPath
14451528
let homeDir = NSString(string: "~").expandingTildeInPath
14461529

1530+
// DEBUG: Print model values being used
1531+
print("🔧 updateServerPlist called with:")
1532+
print(" heimdallModel: '\(config.heimdallModel)'")
1533+
print(" embeddingModel: '\(config.embeddingModel)'")
1534+
print(" heimdallEnabled: \(config.heimdallEnabled)")
1535+
print(" embeddingsEnabled: \(config.embeddingsEnabled)")
1536+
14471537
// Get secrets from Keychain for environment variables
14481538
let jwtSecretEnv = KeychainHelper.shared.getJWTSecret() ?? config.jwtSecret
14491539
let encryptionPasswordEnv = config.encryptionEnabled ? (KeychainHelper.shared.getEncryptionPassword() ?? config.encryptionPassword) : ""
@@ -1469,6 +1559,12 @@ struct SettingsView: View {
14691559
<string>\(config.autoTLPEnabled ? "true" : "false")</string>
14701560
<key>NORNICDB_HEIMDALL_ENABLED</key>
14711561
<string>\(config.heimdallEnabled ? "true" : "false")</string>
1562+
<key>NORNICDB_HEIMDALL_MODEL</key>
1563+
<string>\(config.heimdallModel)</string>
1564+
<key>NORNICDB_EMBEDDING_MODEL</key>
1565+
<string>\(config.embeddingModel)</string>
1566+
<key>NORNICDB_MODELS_DIR</key>
1567+
<string>/usr/local/var/nornicdb/models</string>
14721568
<key>NORNICDB_PLUGINS_DIR</key>
14731569
<string>/usr/local/share/nornicdb/plugins</string>
14741570
<key>NORNICDB_HEIMDALL_PLUGINS_DIR</key>
@@ -2284,6 +2380,12 @@ struct FirstRunWizard: View {
22842380
<string>\(config.autoTLPEnabled ? "true" : "false")</string>
22852381
<key>NORNICDB_HEIMDALL_ENABLED</key>
22862382
<string>\(config.heimdallEnabled ? "true" : "false")</string>
2383+
<key>NORNICDB_HEIMDALL_MODEL</key>
2384+
<string>\(config.heimdallModel)</string>
2385+
<key>NORNICDB_EMBEDDING_MODEL</key>
2386+
<string>\(config.embeddingModel)</string>
2387+
<key>NORNICDB_MODELS_DIR</key>
2388+
<string>/usr/local/var/nornicdb/models</string>
22872389
<key>NORNICDB_PLUGINS_DIR</key>
22882390
<string>/usr/local/share/nornicdb/plugins</string>
22892391
<key>NORNICDB_HEIMDALL_PLUGINS_DIR</key>

0 commit comments

Comments
 (0)