JobSearchProtocols.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. //
  2. // JobSearchProtocols.swift
  3. // indeed_ai_job_search
  4. //
  5. import Foundation
  6. protocol JobDataSource: Sendable {
  7. func search(criteria: SearchCriteria, query: String) async throws -> [JobListing]
  8. }
  9. protocol OpenAISearchService: Sendable {
  10. func interpret(query: String) async throws -> InterpretedSearch
  11. }
  12. protocol JobValidationService: Sendable {
  13. func validate(listing: JobListing) async -> JobAvailability
  14. }
  15. /// End-to-end Phase 4 search: interpret → fetch → filter → validate → rank → dedupe.
  16. protocol JobSearchPipeline: Sendable {
  17. func search(_ request: JobSearchPipelineRequest) async throws -> JobSearchPipelineResult
  18. /// Staged search: may yield a partial page (e.g. web_search) then a final merge with live scrape.
  19. func searchProgressive(
  20. _ request: JobSearchPipelineRequest
  21. ) -> AsyncThrowingStream<JobSearchPipelineProgress, Error>
  22. }
  23. extension JobSearchPipeline {
  24. /// Default: single final emission from `search`.
  25. func searchProgressive(
  26. _ request: JobSearchPipelineRequest
  27. ) -> AsyncThrowingStream<JobSearchPipelineProgress, Error> {
  28. AsyncThrowingStream { continuation in
  29. Task {
  30. do {
  31. let result = try await search(request)
  32. continuation.yield(JobSearchPipelineProgress(from: result, phase: .complete))
  33. continuation.finish()
  34. } catch {
  35. continuation.finish(throwing: error)
  36. }
  37. }
  38. }
  39. }
  40. }
  41. struct JobSearchPipelineRequest: Sendable {
  42. var naturalLanguageQuery: String
  43. var userCriteria: SearchCriteria
  44. /// When false, skips OpenAI interpretation and uses `userCriteria` as-is.
  45. var reinterpret: Bool
  46. /// When false, skips the live Indeed scrape enrichment pass (faster filter refreshes).
  47. var includeEnrichment: Bool
  48. init(
  49. naturalLanguageQuery: String,
  50. userCriteria: SearchCriteria,
  51. reinterpret: Bool = true,
  52. includeEnrichment: Bool = true
  53. ) {
  54. self.naturalLanguageQuery = naturalLanguageQuery
  55. self.userCriteria = userCriteria
  56. self.reinterpret = reinterpret
  57. self.includeEnrichment = includeEnrichment
  58. }
  59. }
  60. struct JobSearchPipelineResult: Sendable {
  61. var interpreted: InterpretedSearch
  62. var listings: [JobListing]
  63. var totalEstimated: Int?
  64. var usedCache: Bool
  65. }
  66. /// One progressive emission from the search pipeline.
  67. struct JobSearchPipelineProgress: Sendable {
  68. enum Phase: Sendable, Equatable {
  69. /// First usable batch (typically OpenAI web_search). More listings may follow.
  70. case partial
  71. /// Final merged (or single-source / cache) result.
  72. case complete
  73. }
  74. var interpreted: InterpretedSearch
  75. var listings: [JobListing]
  76. var totalEstimated: Int?
  77. var usedCache: Bool
  78. var phase: Phase
  79. var isLoadingMore: Bool { phase == .partial }
  80. init(
  81. interpreted: InterpretedSearch,
  82. listings: [JobListing],
  83. totalEstimated: Int?,
  84. usedCache: Bool,
  85. phase: Phase
  86. ) {
  87. self.interpreted = interpreted
  88. self.listings = listings
  89. self.totalEstimated = totalEstimated
  90. self.usedCache = usedCache
  91. self.phase = phase
  92. }
  93. init(from result: JobSearchPipelineResult, phase: Phase) {
  94. self.init(
  95. interpreted: result.interpreted,
  96. listings: result.listings,
  97. totalEstimated: result.totalEstimated,
  98. usedCache: result.usedCache,
  99. phase: phase
  100. )
  101. }
  102. var asResult: JobSearchPipelineResult {
  103. JobSearchPipelineResult(
  104. interpreted: interpreted,
  105. listings: listings,
  106. totalEstimated: totalEstimated,
  107. usedCache: usedCache
  108. )
  109. }
  110. }
  111. protocol SavedJobsStore: AnyObject {
  112. var savedJobs: [SavedJob] { get }
  113. func isSaved(_ listing: JobListing) -> Bool
  114. func toggleSave(_ listing: JobListing)
  115. func remove(_ listing: JobListing)
  116. /// Updates the persisted listing snapshot and verification timestamp (revalidation).
  117. func updateListing(_ listing: JobListing, lastVerifiedAt: Date)
  118. func clearAll()
  119. }
  120. protocol AlertsStore: AnyObject {
  121. var alerts: [JobAlert] { get }
  122. var events: [AlertNotificationEvent] { get }
  123. func upsert(_ alert: JobAlert)
  124. func delete(_ alert: JobAlert)
  125. func setEnabled(_ alert: JobAlert, enabled: Bool)
  126. func markEventsRead()
  127. }