(
options: { engine: ResolvedEngine; all?: boolean; onBatch?: (done: number, total: number) => void },
)
| 283 | } |
| 284 | |
| 285 | export async function classifyDomainsWithLlm( |
| 286 | options: { engine: ResolvedEngine; all?: boolean; onBatch?: (done: number, total: number) => void }, |
| 287 | ): Promise<LlmClassifyResult> { |
| 288 | const { engine } = options; |
| 289 | |
| 290 | const dbPath = twitterBookmarksIndexPath(); |
| 291 | const db = await openDb(dbPath); |
| 292 | |
| 293 | // Ensure domain columns exist (migration from schema v2) |
| 294 | try { db.run('ALTER TABLE bookmarks ADD COLUMN domains TEXT'); } catch { /* already exists */ } |
| 295 | try { db.run('ALTER TABLE bookmarks ADD COLUMN primary_domain TEXT'); } catch { /* already exists */ } |
| 296 | |
| 297 | try { |
| 298 | const where = options.all |
| 299 | ? '1=1' |
| 300 | : 'primary_domain IS NULL'; |
| 301 | const rows = db.exec( |
| 302 | `SELECT id, text, author_handle, categories FROM bookmarks |
| 303 | WHERE ${where} ORDER BY RANDOM()` |
| 304 | ); |
| 305 | |
| 306 | if (!rows.length || !rows[0].values.length) { |
| 307 | return { engine: engine.name, totalUnclassified: 0, classified: 0, failed: 0, batches: 0 }; |
| 308 | } |
| 309 | |
| 310 | const bookmarks: DomainBookmark[] = rows[0].values.map(r => ({ |
| 311 | id: r[0] as string, |
| 312 | text: r[1] as string, |
| 313 | authorHandle: r[2] as string | null, |
| 314 | categories: r[3] as string | null, |
| 315 | })); |
| 316 | |
| 317 | const total = bookmarks.length; |
| 318 | let classified = 0; |
| 319 | let failed = 0; |
| 320 | let batchCount = 0; |
| 321 | |
| 322 | for (let i = 0; i < bookmarks.length; i += BATCH_SIZE) { |
| 323 | const batch = bookmarks.slice(i, i + BATCH_SIZE); |
| 324 | const batchIds = new Set(batch.map(b => b.id)); |
| 325 | batchCount++; |
| 326 | |
| 327 | options.onBatch?.(i, total); |
| 328 | |
| 329 | try { |
| 330 | const prompt = buildDomainPrompt(batch); |
| 331 | const raw = invokeEngine(engine, prompt); |
| 332 | // Reuse the same parse logic — structure is identical |
| 333 | const results = parseResponse(raw, batchIds); |
| 334 | |
| 335 | const stmt = db.prepare( |
| 336 | `UPDATE bookmarks SET domains = ?, primary_domain = ? WHERE id = ?` |
| 337 | ); |
| 338 | for (const r of results) { |
| 339 | stmt.run([r.categories.join(','), r.primary, r.id]); |
| 340 | } |
| 341 | stmt.free(); |
| 342 |
no test coverage detected