|
|
@@ -2264,15 +2264,17 @@ private final class OpenAIJobSearchService {
|
|
2264
|
2264
|
}
|
|
2265
|
2265
|
|
|
2266
|
2266
|
let developerInstructions = """
|
|
2267
|
|
- You are the job-search backend for a desktop app. Always use web search to find currently posted jobs that match the user's request.
|
|
|
2267
|
+ You are the job-search backend for an Indeed-focused desktop app. Always use web search, but only to discover roles that are listed on Indeed (indeed.com and regional Indeed sites such as indeed.co.uk, ca.indeed.com, etc.).
|
|
|
2268
|
+
|
|
|
2269
|
+ Do not include jobs sourced only from LinkedIn, Glassdoor, company career pages (unless the same opening is clearly on Indeed with an Indeed URL), Google Jobs aggregates, or other job boards.
|
|
2268
|
2270
|
|
|
2269
|
2271
|
Your final assistant message must be JSON that strictly matches the configured response schema (one object with a "jobs" array). Do not add markdown, code fences, or conversational prose outside that JSON.
|
|
2270
|
2272
|
|
|
2271
|
|
- Each job entry needs a title, a single-sentence description, and a "url" string. Use a real listing or apply URL when available; use an empty string for "url" when none is known.
|
|
|
2273
|
+ Each job entry needs a title, a single-sentence description, and a "url" string. The "url" must be a direct Indeed job link (viewjob, pagead, or equivalent on an Indeed domain) when web search finds one; use an empty string for "url" only when you cannot find any Indeed URL for that role (omit the listing if it is not on Indeed).
|
|
2272
|
2274
|
|
|
2273
|
2275
|
Return at most \(jobLimit) distinct listings. If the conversation context already lists jobs, do not repeat the same titles or URLs when the user asks for more—it is fine to return fewer than \(jobLimit) new results.
|
|
2274
|
2276
|
|
|
2275
|
|
- Full sentences such as "looking for an AI developer job" are still job queries: always populate "jobs" from web search rather than answering with chatty text alone.
|
|
|
2277
|
+ Full sentences such as "looking for an AI developer job" are still job queries: always populate "jobs" from Indeed-oriented web search rather than answering with chatty text alone.
|
|
2276
|
2278
|
"""
|
|
2277
|
2279
|
|
|
2278
|
2280
|
let userInput = """
|
|
|
@@ -2283,7 +2285,7 @@ private final class OpenAIJobSearchService {
|
|
2283
|
2285
|
|
|
2284
|
2286
|
Latest user query: "\(query)"
|
|
2285
|
2287
|
|
|
2286
|
|
- If the user refines pay, seniority, work location, or similar, run a new search that applies those constraints to the same career topic as in the context.
|
|
|
2288
|
+ If the user refines pay, seniority, work location, or similar, run a new Indeed-focused search that applies those constraints to the same career topic as in the context.
|
|
2287
|
2289
|
"""
|
|
2288
|
2290
|
|
|
2289
|
2291
|
let payload = OpenAIJobSearchAPIRequest.jobSearchPayload(
|
|
|
@@ -2340,7 +2342,9 @@ private final class OpenAIJobSearchService {
|
|
2340
|
2342
|
userInfo: [NSLocalizedDescriptionKey: "The API returned an empty text payload."]
|
|
2341
|
2343
|
)
|
|
2342
|
2344
|
}
|
|
2343
|
|
- let jobs = try Self.parseJobListings(fromModelText: trimmed).map(Self.normalizedJobListing)
|
|
|
2345
|
+ let jobs = try Self.parseJobListings(fromModelText: trimmed)
|
|
|
2346
|
+ .filter(Self.jobListingUsesIndeedOrEmptyURL)
|
|
|
2347
|
+ .map(Self.normalizedJobListing)
|
|
2344
|
2348
|
completion(.success(JobSearchOutput(jobs: jobs)))
|
|
2345
|
2349
|
} catch {
|
|
2346
|
2350
|
completion(.failure(error))
|
|
|
@@ -2433,6 +2437,21 @@ private final class OpenAIJobSearchService {
|
|
2433
|
2437
|
return JobListing(title: job.title, description: job.description, url: trimmedURL)
|
|
2434
|
2438
|
}
|
|
2435
|
2439
|
|
|
|
2440
|
+ /// Drops listings whose `url` points at non-Indeed sites (e.g. LinkedIn) when the model ignores instructions.
|
|
|
2441
|
+ private static func jobListingUsesIndeedOrEmptyURL(_ job: JobListing) -> Bool {
|
|
|
2442
|
+ let trimmed = job.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
2443
|
+ if trimmed.isEmpty { return true }
|
|
|
2444
|
+ return isIndeedJobURL(trimmed)
|
|
|
2445
|
+ }
|
|
|
2446
|
+
|
|
|
2447
|
+ /// Host looks like an official Indeed property (`indeed.com`, `www.indeed.co.uk`, `ca.indeed.com`, …), not `notindeed.com`.
|
|
|
2448
|
+ private static func isIndeedJobURL(_ string: String) -> Bool {
|
|
|
2449
|
+ guard let host = URL(string: string)?.host?.lowercased() else { return false }
|
|
|
2450
|
+ if host == "indeed.com" { return true }
|
|
|
2451
|
+ if host.hasPrefix("indeed.") { return true }
|
|
|
2452
|
+ return host.contains(".indeed.")
|
|
|
2453
|
+ }
|
|
|
2454
|
+
|
|
2436
|
2455
|
private static func parseJobListings(fromModelText text: String) throws -> [JobListing] {
|
|
2437
|
2456
|
let stripped = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2438
|
2457
|
if stripped.hasPrefix("{"), let directData = stripped.data(using: .utf8),
|
|
|
@@ -2588,9 +2607,9 @@ private struct OpenAIJobSearchAPIRequest: Encodable {
|
|
2588
|
2607
|
jobLimit: Int
|
|
2589
|
2608
|
) -> OpenAIJobSearchAPIRequest {
|
|
2590
|
2609
|
let itemProperties = OpenAIJobSearchJobItemProperties(
|
|
2591
|
|
- title: OpenAIJSONSchemaStringField(type: "string", description: "Job title as shown on the listing."),
|
|
2592
|
|
- description: OpenAIJSONSchemaStringField(type: "string", description: "One concise sentence summarizing the role."),
|
|
2593
|
|
- url: OpenAIJSONSchemaStringField(type: "string", description: "Direct listing or apply URL; use an empty string when unknown.")
|
|
|
2610
|
+ title: OpenAIJSONSchemaStringField(type: "string", description: "Job title as shown on the Indeed listing."),
|
|
|
2611
|
+ description: OpenAIJSONSchemaStringField(type: "string", description: "One concise sentence summarizing the role from the Indeed posting."),
|
|
|
2612
|
+ url: OpenAIJSONSchemaStringField(type: "string", description: "Direct Indeed job URL (https on indeed.com or a regional Indeed domain such as indeed.co.uk); empty string only if no Indeed URL exists—never use LinkedIn, Glassdoor, or other boards.")
|
|
2594
|
2613
|
)
|
|
2595
|
2614
|
let itemSchema = OpenAIJobSearchJobItemSchema(
|
|
2596
|
2615
|
type: "object",
|
|
|
@@ -2600,7 +2619,7 @@ private struct OpenAIJobSearchAPIRequest: Encodable {
|
|
2600
|
2619
|
)
|
|
2601
|
2620
|
let jobsProperty = OpenAIJobSearchJobsArrayProperty(
|
|
2602
|
2621
|
type: "array",
|
|
2603
|
|
- description: "Up to \(jobLimit) jobs from live web search; use an empty array if none are found.",
|
|
|
2622
|
+ description: "Up to \(jobLimit) jobs found on Indeed via web search; use an empty array if none are found. Do not include listings that only exist off Indeed.",
|
|
2604
|
2623
|
items: itemSchema
|
|
2605
|
2624
|
)
|
|
2606
|
2625
|
let rootProperties = OpenAIJobSearchRootProperties(jobs: jobsProperty)
|