|
|
@@ -0,0 +1,139 @@
|
|
|
+//
|
|
|
+// SearchCriteriaInterpretationDTO.swift
|
|
|
+// indeed_ai_job_search
|
|
|
+//
|
|
|
+// Schema-validated JSON shape returned by OpenAI query interpretation.
|
|
|
+//
|
|
|
+
|
|
|
+import Foundation
|
|
|
+
|
|
|
+struct SearchCriteriaInterpretationDTO: Codable, Equatable, Sendable {
|
|
|
+ var jobTitle: String? = nil
|
|
|
+ var keywords: String? = nil
|
|
|
+ var exactPhrase: Bool? = nil
|
|
|
+ var excludedKeywords: String? = nil
|
|
|
+ var country: String? = nil
|
|
|
+ var city: String? = nil
|
|
|
+ var remoteTypes: [String]? = nil
|
|
|
+ var employmentTypes: [String]? = nil
|
|
|
+ var experienceLevels: [String]? = nil
|
|
|
+ var salaryFloor: Double? = nil
|
|
|
+ var dateFreshnessDays: Int? = nil
|
|
|
+ var confidenceNote: String? = nil
|
|
|
+
|
|
|
+ func toSearchCriteria(defaults: SearchCriteria = SearchCriteria()) -> SearchCriteria {
|
|
|
+ var criteria = defaults
|
|
|
+
|
|
|
+ if let jobTitle, !jobTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
+ criteria.jobTitle = jobTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ }
|
|
|
+ if let keywords, !keywords.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
+ criteria.keywords = keywords.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ }
|
|
|
+ if let exactPhrase {
|
|
|
+ criteria.exactPhrase = exactPhrase
|
|
|
+ }
|
|
|
+ if let excludedKeywords {
|
|
|
+ criteria.excludedKeywords = excludedKeywords.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ }
|
|
|
+ if let country, !country.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
+ criteria.country = country.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ }
|
|
|
+ if let city {
|
|
|
+ criteria.city = city.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ }
|
|
|
+ if let remoteTypes {
|
|
|
+ criteria.remoteTypes = Set(remoteTypes.compactMap(Self.mapRemote))
|
|
|
+ }
|
|
|
+ if let employmentTypes {
|
|
|
+ criteria.employmentTypes = Set(employmentTypes.compactMap(Self.mapEmployment))
|
|
|
+ }
|
|
|
+ if let experienceLevels {
|
|
|
+ criteria.experienceLevels = Set(experienceLevels.compactMap(Self.mapExperience))
|
|
|
+ }
|
|
|
+ if let salaryFloor, salaryFloor > 0 {
|
|
|
+ criteria.salaryFloor = salaryFloor
|
|
|
+ }
|
|
|
+ if let dateFreshnessDays, dateFreshnessDays > 0 {
|
|
|
+ criteria.dateFreshnessDays = dateFreshnessDays
|
|
|
+ }
|
|
|
+
|
|
|
+ return criteria
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func mapRemote(_ raw: String) -> RemoteType? {
|
|
|
+ switch raw.lowercased().replacingOccurrences(of: "-", with: "").replacingOccurrences(of: "_", with: "") {
|
|
|
+ case "remote": return .remote
|
|
|
+ case "hybrid": return .hybrid
|
|
|
+ case "onsite", "onsiteonly": return .onsite
|
|
|
+ case "unknown", "any": return .unknown
|
|
|
+ default: return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func mapEmployment(_ raw: String) -> EmploymentType? {
|
|
|
+ switch raw.lowercased().replacingOccurrences(of: "-", with: "").replacingOccurrences(of: "_", with: "") {
|
|
|
+ case "fulltime": return .fullTime
|
|
|
+ case "parttime": return .partTime
|
|
|
+ case "contract": return .contract
|
|
|
+ case "temporary", "temp": return .temporary
|
|
|
+ case "internship", "intern": return .internship
|
|
|
+ default: return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func mapExperience(_ raw: String) -> ExperienceLevel? {
|
|
|
+ switch raw.lowercased().replacingOccurrences(of: "-", with: "").replacingOccurrences(of: "_", with: "") {
|
|
|
+ case "entry", "entrylevel": return .entry
|
|
|
+ case "junior": return .junior
|
|
|
+ case "mid", "midlevel": return .mid
|
|
|
+ case "senior": return .senior
|
|
|
+ default: return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+enum SearchCriteriaInterpretationSchema {
|
|
|
+ /// JSON Schema fragment sent to OpenAI `response_format.json_schema`.
|
|
|
+ static let jsonSchemaObject: [String: Any] = [
|
|
|
+ "type": "object",
|
|
|
+ "additionalProperties": false,
|
|
|
+ "properties": [
|
|
|
+ "jobTitle": ["type": ["string", "null"]],
|
|
|
+ "keywords": ["type": ["string", "null"]],
|
|
|
+ "exactPhrase": ["type": ["boolean", "null"]],
|
|
|
+ "excludedKeywords": ["type": ["string", "null"]],
|
|
|
+ "country": ["type": ["string", "null"]],
|
|
|
+ "city": ["type": ["string", "null"]],
|
|
|
+ "remoteTypes": [
|
|
|
+ "type": ["array", "null"],
|
|
|
+ "items": ["type": "string", "enum": ["remote", "hybrid", "onsite", "unknown"]]
|
|
|
+ ],
|
|
|
+ "employmentTypes": [
|
|
|
+ "type": ["array", "null"],
|
|
|
+ "items": ["type": "string", "enum": ["fullTime", "partTime", "contract", "temporary", "internship"]]
|
|
|
+ ],
|
|
|
+ "experienceLevels": [
|
|
|
+ "type": ["array", "null"],
|
|
|
+ "items": ["type": "string", "enum": ["entry", "junior", "mid", "senior"]]
|
|
|
+ ],
|
|
|
+ "salaryFloor": ["type": ["number", "null"]],
|
|
|
+ "dateFreshnessDays": ["type": ["integer", "null"]],
|
|
|
+ "confidenceNote": ["type": ["string", "null"]]
|
|
|
+ ],
|
|
|
+ "required": [
|
|
|
+ "jobTitle",
|
|
|
+ "keywords",
|
|
|
+ "exactPhrase",
|
|
|
+ "excludedKeywords",
|
|
|
+ "country",
|
|
|
+ "city",
|
|
|
+ "remoteTypes",
|
|
|
+ "employmentTypes",
|
|
|
+ "experienceLevels",
|
|
|
+ "salaryFloor",
|
|
|
+ "dateFreshnessDays",
|
|
|
+ "confidenceNote"
|
|
|
+ ]
|
|
|
+ ]
|
|
|
+}
|