| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- //
- // JobSearchProtocols.swift
- // indeed_ai_job_search
- //
- import Foundation
- protocol JobDataSource: Sendable {
- func search(criteria: SearchCriteria, query: String) async throws -> [JobListing]
- }
- protocol OpenAISearchService: Sendable {
- func interpret(query: String) async throws -> InterpretedSearch
- }
- protocol JobValidationService: Sendable {
- func validate(listing: JobListing) async -> JobAvailability
- }
- /// End-to-end Phase 4 search: interpret → fetch → filter → validate → rank → dedupe.
- protocol JobSearchPipeline: Sendable {
- func search(_ request: JobSearchPipelineRequest) async throws -> JobSearchPipelineResult
- /// Staged search: may yield a partial page (e.g. web_search) then a final merge with live scrape.
- func searchProgressive(
- _ request: JobSearchPipelineRequest
- ) -> AsyncThrowingStream<JobSearchPipelineProgress, Error>
- }
- extension JobSearchPipeline {
- /// Default: single final emission from `search`.
- func searchProgressive(
- _ request: JobSearchPipelineRequest
- ) -> AsyncThrowingStream<JobSearchPipelineProgress, Error> {
- AsyncThrowingStream { continuation in
- Task {
- do {
- let result = try await search(request)
- continuation.yield(JobSearchPipelineProgress(from: result, phase: .complete))
- continuation.finish()
- } catch {
- continuation.finish(throwing: error)
- }
- }
- }
- }
- }
- struct JobSearchPipelineRequest: Sendable {
- var naturalLanguageQuery: String
- var userCriteria: SearchCriteria
- /// When false, skips OpenAI interpretation and uses `userCriteria` as-is.
- var reinterpret: Bool
- /// When false, skips the live Indeed scrape enrichment pass (faster filter refreshes).
- var includeEnrichment: Bool
- init(
- naturalLanguageQuery: String,
- userCriteria: SearchCriteria,
- reinterpret: Bool = true,
- includeEnrichment: Bool = true
- ) {
- self.naturalLanguageQuery = naturalLanguageQuery
- self.userCriteria = userCriteria
- self.reinterpret = reinterpret
- self.includeEnrichment = includeEnrichment
- }
- }
- struct JobSearchPipelineResult: Sendable {
- var interpreted: InterpretedSearch
- var listings: [JobListing]
- var totalEstimated: Int?
- var usedCache: Bool
- }
- /// One progressive emission from the search pipeline.
- struct JobSearchPipelineProgress: Sendable {
- enum Phase: Sendable, Equatable {
- /// First usable batch (typically OpenAI web_search). More listings may follow.
- case partial
- /// Final merged (or single-source / cache) result.
- case complete
- }
- var interpreted: InterpretedSearch
- var listings: [JobListing]
- var totalEstimated: Int?
- var usedCache: Bool
- var phase: Phase
- var isLoadingMore: Bool { phase == .partial }
- init(
- interpreted: InterpretedSearch,
- listings: [JobListing],
- totalEstimated: Int?,
- usedCache: Bool,
- phase: Phase
- ) {
- self.interpreted = interpreted
- self.listings = listings
- self.totalEstimated = totalEstimated
- self.usedCache = usedCache
- self.phase = phase
- }
- init(from result: JobSearchPipelineResult, phase: Phase) {
- self.init(
- interpreted: result.interpreted,
- listings: result.listings,
- totalEstimated: result.totalEstimated,
- usedCache: result.usedCache,
- phase: phase
- )
- }
- var asResult: JobSearchPipelineResult {
- JobSearchPipelineResult(
- interpreted: interpreted,
- listings: listings,
- totalEstimated: totalEstimated,
- usedCache: usedCache
- )
- }
- }
- protocol SavedJobsStore: AnyObject {
- var savedJobs: [SavedJob] { get }
- func isSaved(_ listing: JobListing) -> Bool
- func toggleSave(_ listing: JobListing)
- func remove(_ listing: JobListing)
- /// Updates the persisted listing snapshot and verification timestamp (revalidation).
- func updateListing(_ listing: JobListing, lastVerifiedAt: Date)
- func clearAll()
- }
- protocol AlertsStore: AnyObject {
- var alerts: [JobAlert] { get }
- var events: [AlertNotificationEvent] { get }
- func upsert(_ alert: JobAlert)
- func delete(_ alert: JobAlert)
- func setEnabled(_ alert: JobAlert, enabled: Bool)
- func markEventsRead()
- }
|