Kaynağa Gözat

Restrict job search results to Indeed listings

Tighten OpenAI job-search prompts and JSON schema copy so discovery
targets Indeed domains only. Filter parsed listings to drop URLs
that are not Indeed (or empty), and add host checks for regional
Indeed sites.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 hafta önce
ebeveyn
işleme
4f5160a9f3
1 değiştirilmiş dosya ile 28 ekleme ve 9 silme
  1. 28 9
      App for Indeed/Views/DashboardView.swift

+ 28 - 9
App for Indeed/Views/DashboardView.swift

@@ -2264,15 +2264,17 @@ private final class OpenAIJobSearchService {
2264
         }
2264
         }
2265
 
2265
 
2266
         let developerInstructions = """
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
         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.
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
         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.
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
         let userInput = """
2280
         let userInput = """
@@ -2283,7 +2285,7 @@ private final class OpenAIJobSearchService {
2283
 
2285
 
2284
         Latest user query: "\(query)"
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
         let payload = OpenAIJobSearchAPIRequest.jobSearchPayload(
2291
         let payload = OpenAIJobSearchAPIRequest.jobSearchPayload(
@@ -2340,7 +2342,9 @@ private final class OpenAIJobSearchService {
2340
                         userInfo: [NSLocalizedDescriptionKey: "The API returned an empty text payload."]
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
                 completion(.success(JobSearchOutput(jobs: jobs)))
2348
                 completion(.success(JobSearchOutput(jobs: jobs)))
2345
             } catch {
2349
             } catch {
2346
                 completion(.failure(error))
2350
                 completion(.failure(error))
@@ -2433,6 +2437,21 @@ private final class OpenAIJobSearchService {
2433
         return JobListing(title: job.title, description: job.description, url: trimmedURL)
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
     private static func parseJobListings(fromModelText text: String) throws -> [JobListing] {
2455
     private static func parseJobListings(fromModelText text: String) throws -> [JobListing] {
2437
         let stripped = text.trimmingCharacters(in: .whitespacesAndNewlines)
2456
         let stripped = text.trimmingCharacters(in: .whitespacesAndNewlines)
2438
         if stripped.hasPrefix("{"), let directData = stripped.data(using: .utf8),
2457
         if stripped.hasPrefix("{"), let directData = stripped.data(using: .utf8),
@@ -2588,9 +2607,9 @@ private struct OpenAIJobSearchAPIRequest: Encodable {
2588
         jobLimit: Int
2607
         jobLimit: Int
2589
     ) -> OpenAIJobSearchAPIRequest {
2608
     ) -> OpenAIJobSearchAPIRequest {
2590
         let itemProperties = OpenAIJobSearchJobItemProperties(
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
         let itemSchema = OpenAIJobSearchJobItemSchema(
2614
         let itemSchema = OpenAIJobSearchJobItemSchema(
2596
             type: "object",
2615
             type: "object",
@@ -2600,7 +2619,7 @@ private struct OpenAIJobSearchAPIRequest: Encodable {
2600
         )
2619
         )
2601
         let jobsProperty = OpenAIJobSearchJobsArrayProperty(
2620
         let jobsProperty = OpenAIJobSearchJobsArrayProperty(
2602
             type: "array",
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
             items: itemSchema
2623
             items: itemSchema
2605
         )
2624
         )
2606
         let rootProperties = OpenAIJobSearchRootProperties(jobs: jobsProperty)
2625
         let rootProperties = OpenAIJobSearchRootProperties(jobs: jobsProperty)