SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
6.3 KB · 152 lines swift
Raw Blame History
1//2//  SelfTest.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Headless end-to-end check of the Phase 3 gate: load a real page in an9//  offscreen WKWebView, extract a PageContext, and stream a grounded summary10//  with a live Cloud model. Proves ContentExtractor + Summarizer + AIService +11//  the ported provider client work together. Invoked via12//  `ZyquoAtlas --selftest-summarize <url> [--provider <id>] [--model <id>]`;13//  the API key comes from the environment (never logged).14//1516import Foundation17import WebKit18import AppKit1920@MainActor21enum SelfTest {22    private static let envKeys: [ProviderID: String] = [23        .openai: "OPENAI_API_KEY", .anthropic: "ANTHROPIC_API_KEY", .xai: "XAI_API_KEY",24        .mistral: "MISTRAL_API_KEY", .gemini: "GEMINI_API_KEY", .qwen: "DASHSCOPE_API_KEY",25        .deepseek: "DEEPSEEK_API_KEY", .kimi: "MOONSHOT_API_KEY", .perplexity: "PERPLEXITY_API_KEY",26        .together: "TOGETHER_API_KEY", .deepinfra: "DEEPINFRA_API_KEY", .cerebras: "CEREBRAS_API_KEY",27    ]2829    static func summarize(arguments: [String]) async -> Int32 {30        guard let urlString = value(after: "--selftest-summarize", in: arguments),31              let url = URL(string: urlString) else {32            printErr("usage: --selftest-summarize <url> [--provider <id>] [--model <id>]")33            return 234        }3536        let catalog = ModelCatalog()37        let providerArg = value(after: "--provider", in: arguments).flatMap { ProviderID(rawValue: $0) }38        let modelArg = value(after: "--model", in: arguments)3940        let model: AIModel41        if let id = modelArg, let provider = providerArg, let m = catalog.model(id: id, provider: provider) {42            model = m43        } else if let provider = providerArg, let m = catalog.cheapestModel(for: provider) {44            model = m45        } else if let m = catalog.defaultModel {46            model = m47        } else {48            printErr("no model available in catalog"); return 149        }5051        guard let keyName = envKeys[model.provider],52              let apiKey = ProcessInfo.processInfo.environment[keyName], !apiKey.isEmpty else {53            printErr("no API key in env for \(model.provider.rawValue) (expected \(envKeys[model.provider] ?? "?"))")54            return 155        }5657        print("→ loading \(url.absoluteString)")58        let config = ProfileStore.shared.makeConfiguration(for: .defaultProfile)59        let loader = PageLoader(configuration: config)60        do {61            try await loader.load(url)62        } catch {63            printErr("page load failed: \(error.localizedDescription)"); return 164        }65        // Let late/lazy content settle before extracting.66        try? await Task.sleep(nanoseconds: 1_500_000_000)6768        let context: PageContext69        do {70            context = try await ContentExtractor.extract(from: loader.webView)71        } catch {72            printErr("extraction failed: \(error.localizedDescription)"); return 173        }7475        print("✓ extracted: \"\(context.title)\" — quality=\(context.quality.rawValue), "76              + "words=\(context.wordCount), ~\(context.estimatedTokens) tokens, "77              + "headings=\(context.headings.count), truncated=\(context.truncated)")78        let plan = Summarizer.plan(for: context, model: model)79        print("✓ model=\(model.id) (\(model.provider.rawValue)), plan=\(plan)")80        print("---- extracted markdown (first 400 chars) ----")81        print(String(context.markdown.prefix(400)))82        print("---- streaming summary ----")8384        let service = AIService()85        service.run(.summarize, on: context, model: model, apiKey: apiKey)8687        // Drain the stream, printing new text as it arrives.88        var printed = 089        while service.isStreaming || printed < service.output.count {90            let current = service.output91            if current.count > printed {92                let start = current.index(current.startIndex, offsetBy: printed)93                FileHandle.standardOutput.write(Data(String(current[start...]).utf8))94                printed = current.count95            }96            if let err = service.errorText { printErr("\n\nAI error: \(err)"); return 1 }97            if !service.isStreaming && printed >= service.output.count { break }98            try? await Task.sleep(nanoseconds: 80_000_000)99        }100        print("\n---- end ----")101        print("✓ summary chars=\(service.output.count)")102        return service.output.isEmpty ? 1 : 0103    }104105    private static func value(after flag: String, in args: [String]) -> String? {106        guard let i = args.firstIndex(of: flag), i + 1 < args.count else { return nil }107        return args[i + 1]108    }109110    private static func printErr(_ s: String) {111        FileHandle.standardError.write(Data((s + "\n").utf8))112    }113}114115/// Loads a URL in an offscreen, windowed WKWebView (a window ensures WebKit116/// runs layout/timers/JS the same as onscreen) and resolves when navigation117/// finishes.118@MainActor119final class PageLoader: NSObject, WKNavigationDelegate {120    let webView: WKWebView121    private let window: NSWindow122    private var continuation: CheckedContinuation<Void, Error>?123124    init(configuration: WKWebViewConfiguration) {125        webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 1200, height: 900),126                            configuration: configuration)127        window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 1200, height: 900),128                          styleMask: [.borderless], backing: .buffered, defer: false)129        super.init()130        window.contentView = webView131        window.orderOut(nil)132        webView.navigationDelegate = self133    }134135    func load(_ url: URL) async throws {136        try await withCheckedThrowingContinuation { (c: CheckedContinuation<Void, Error>) in137            continuation = c138            webView.load(URLRequest(url: url))139        }140    }141142    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {143        continuation?.resume(); continuation = nil144    }145    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {146        continuation?.resume(throwing: error); continuation = nil147    }148    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {149        continuation?.resume(throwing: error); continuation = nil150    }151}152