| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- //
- // CVTemplateFetchService.swift
- // App for Indeed
- //
- // Fetches CV template definitions from OpenAI (structured JSON) so the gallery
- // is not limited to the built-in static catalog. Falls back are handled by callers.
- //
- import Foundation
- // MARK: - Public API
- final class CVTemplateFetchService {
- static let shared = CVTemplateFetchService()
- private let endpoint = URL(string: "https://api.openai.com/v1/responses")!
- private let session = URLSession(configuration: .ephemeral)
- private init() {}
- private enum CVTemplateCatalogPrompt {
- static let instructions = """
- You are the CV template catalog service for a desktop résumé builder. Invent exactly 30 distinct, plausible template \
- records for job seekers. Each must have a unique `id` (kebab-case ASCII, e.g. "river-stone"), a display `name` (1–3 words), \
- and varied visual parameters so thumbnails differ. Spread entries across all five `family` values (six templates per family). \
- Use creative but professional names; do not copy real commercial template trademarks. \
- Vary `headline`, `accent`, `layoutType`, `sidebarSide`, `sidebarTinted`, and `sectionLabelStyle` across the set — do not repeat \
- the same six-tuple of those fields on consecutive rows. \
- Niche split for the in-app gallery: `modern` and `creative` families are shown under Design-Based (portfolio / UX / visual roles); \
- `minimal`, `professional`, and `executive` appear under Profession-Based (ATS-friendly / corporate / leadership). \
- Output must strictly match the JSON schema — no markdown or extra keys.
- """
- static func userInput(language: AppLanguage) -> String {
- """
- Generate the template catalog now. Exactly six entries per family: professional, modern, creative, minimal, executive. \
- Use both singleColumn and twoColumn layouts across the 30 rows. For twoColumn rows vary leading vs trailing sidebars and tinted true/false. \
- Keep modern and creative entries suitable for design-led résumés; keep minimal, professional, and executive suitable for traditional industries. \
- \(displayNameRule(for: language))
- """
- }
- /// English `name` values double as localization keys in the app; keep them ASCII words.
- static func displayNameRule(for language: AppLanguage) -> String {
- switch language {
- case .english:
- return "Each `name` must be 1–3 English words (ASCII letters and spaces only), e.g. \"Creative Cascade\"."
- case .chineseTraditional:
- return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"Creative Cascade\" — do not use Chinese characters in `name`."
- case .chineseSimplified:
- return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"Design Dynamo\" — do not use Chinese characters in `name`."
- case .arabic:
- return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"UI Sculptor\" — do not use Arabic script in `name`."
- case .french, .frenchCanada:
- return "Each `name` must be 1–3 English words (ASCII letters and spaces only) suitable as localization keys, e.g. \"Creative Cascade\" — do not use French characters in `name`."
- }
- }
- }
- /// Loads templates from OpenAI (one automatic retry on transient network / parse failures).
- func fetchTemplates(
- language: AppLanguage = currentAppLanguage(),
- completion: @escaping (Result<[CVTemplate], Error>) -> Void
- ) {
- let apiKey = OpenAIConfiguration.apiKey
- guard OpenAIConfiguration.hasAPIKey else {
- completion(.failure(Self.missingKeyError))
- return
- }
- var request = URLRequest(url: endpoint)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
- request.timeoutInterval = 60
- do {
- request.httpBody = try Self.encodeCatalogRequestBody(language: language)
- } catch {
- completion(.failure(error))
- return
- }
- session.dataTask(with: request) { data, response, error in
- Self.handleFetchData(
- data,
- response: response,
- error: error,
- language: language,
- attempt: 0,
- completion: completion
- )
- }.resume()
- }
- private static func encodeCatalogRequestBody(language: AppLanguage) throws -> Data {
- let payload = CVTemplateOpenAIRequest.catalogPayload(
- model: "gpt-4o-mini",
- instructions: CVTemplateCatalogPrompt.instructions,
- input: CVTemplateCatalogPrompt.userInput(language: language)
- )
- return try JSONEncoder().encode(payload)
- }
- private static func handleFetchData(
- _ data: Data?,
- response: URLResponse?,
- error: Error?,
- language: AppLanguage,
- attempt: Int,
- completion: @escaping (Result<[CVTemplate], Error>) -> Void
- ) {
- if let error {
- if attempt == 0 {
- DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
- refetchSameRequest(language: language, attempt: 1, completion: completion)
- }
- return
- }
- completion(.failure(error))
- return
- }
- guard let data else {
- if attempt == 0 {
- DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
- refetchSameRequest(language: language, attempt: 1, completion: completion)
- }
- return
- }
- completion(.failure(Self.emptyResponseError))
- return
- }
- if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
- if attempt == 0, (500...599).contains(http.statusCode) || http.statusCode == 429 {
- DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.5) {
- refetchSameRequest(language: language, attempt: 1, completion: completion)
- }
- return
- }
- if let apiError = try? JSONDecoder().decode(OpenAITemplatesAPIErrorResponse.self, from: data) {
- completion(.failure(NSError(
- domain: "CVTemplateFetchService",
- code: http.statusCode,
- userInfo: [NSLocalizedDescriptionKey: apiError.error.message]
- )))
- } else {
- completion(.failure(NSError(
- domain: "CVTemplateFetchService",
- code: http.statusCode,
- userInfo: [NSLocalizedDescriptionKey: "Template request failed with status \(http.statusCode)."]
- )))
- }
- return
- }
- do {
- let modelText = try Self.extractModelTextFromResponsesBody(data)
- let trimmed = modelText.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- throw Self.emptyModelTextError
- }
- let decoded = try Self.decodeCatalog(fromModelText: trimmed)
- let mapped = decoded.compactMap { CVTemplate(aiItem: $0) }
- guard !mapped.isEmpty else {
- throw Self.noValidTemplatesError
- }
- var seen = Set<String>()
- let unique = mapped.filter { seen.insert($0.id).inserted }
- completion(.success(unique))
- } catch {
- if attempt == 0 {
- DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2.0) {
- refetchSameRequest(language: language, attempt: 1, completion: completion)
- }
- return
- }
- completion(.failure(error))
- }
- }
- private static func refetchSameRequest(
- language: AppLanguage,
- attempt: Int,
- completion: @escaping (Result<[CVTemplate], Error>) -> Void
- ) {
- let apiKey = OpenAIConfiguration.apiKey
- guard OpenAIConfiguration.hasAPIKey else {
- completion(.failure(missingKeyError))
- return
- }
- var request = URLRequest(url: URL(string: "https://api.openai.com/v1/responses")!)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
- request.timeoutInterval = 60
- guard let body = try? encodeCatalogRequestBody(language: language) else {
- completion(.failure(NSError(domain: "CVTemplateFetchService", code: 11, userInfo: [NSLocalizedDescriptionKey: "Could not encode request."])))
- return
- }
- request.httpBody = body
- URLSession(configuration: .ephemeral).dataTask(with: request) { data, response, error in
- handleFetchData(
- data,
- response: response,
- error: error,
- language: language,
- attempt: attempt,
- completion: completion
- )
- }.resume()
- }
- // MARK: - Errors
- private static let missingKeyError = NSError(
- domain: "CVTemplateFetchService",
- code: 1,
- userInfo: [NSLocalizedDescriptionKey: "Missing API key. Set OPENAI_API_KEY in the Run scheme environment for local debug."]
- )
- private static let emptyResponseError = NSError(
- domain: "CVTemplateFetchService",
- code: 2,
- userInfo: [NSLocalizedDescriptionKey: "API returned an empty response."]
- )
- private static let emptyModelTextError = NSError(
- domain: "CVTemplateFetchService",
- code: 4,
- userInfo: [NSLocalizedDescriptionKey: "The API returned an empty text payload."]
- )
- private static let noValidTemplatesError = NSError(
- domain: "CVTemplateFetchService",
- code: 10,
- userInfo: [NSLocalizedDescriptionKey: "The assistant reply did not include usable templates."]
- )
- // MARK: - Response parsing (mirrors OpenAI job search handling)
- private static func extractModelTextFromResponsesBody(_ data: Data) throws -> String {
- let rootObject: Any
- do {
- rootObject = try JSONSerialization.jsonObject(with: data, options: [])
- } catch {
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 5,
- userInfo: [NSLocalizedDescriptionKey: "The service returned data that was not valid JSON."]
- )
- }
- guard let root = rootObject as? [String: Any] else {
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 5,
- userInfo: [NSLocalizedDescriptionKey: "Unexpected response shape from the template service."]
- )
- }
- if let status = root["status"] as? String {
- if status == "failed" {
- let message = (root["error"] as? [String: Any])?["message"] as? String ?? "The request failed."
- throw NSError(domain: "CVTemplateFetchService", code: 7, userInfo: [NSLocalizedDescriptionKey: message])
- }
- if status == "incomplete",
- let details = root["incomplete_details"] as? [String: Any],
- let reason = details["reason"] as? String {
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 8,
- userInfo: [NSLocalizedDescriptionKey: "Request stopped early (\(reason)). Try again."]
- )
- }
- }
- if let direct = root["output_text"] as? String {
- let trimmed = direct.trimmingCharacters(in: .whitespacesAndNewlines)
- if !trimmed.isEmpty { return trimmed }
- }
- guard let output = root["output"] as? [Any] else {
- if let fallback = Self.scavengeTemplatesJSONString(fromUTF8Data: data) {
- return fallback
- }
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 9,
- userInfo: [NSLocalizedDescriptionKey: "The service returned no assistant text. Try again."]
- )
- }
- var segments: [String] = []
- for case let item as [String: Any] in output where (item["type"] as? String) == "message" {
- collectOutputTextSegments(fromOutputItem: item, into: &segments)
- }
- if segments.isEmpty {
- for case let item as [String: Any] in output {
- collectOutputTextSegments(fromOutputItem: item, into: &segments)
- }
- }
- let combined = segments.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
- if !combined.isEmpty {
- return combined
- }
- if let fallback = Self.scavengeTemplatesJSONString(fromUTF8Data: data) {
- return fallback
- }
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 9,
- userInfo: [NSLocalizedDescriptionKey: "The model did not return readable text. Try again."]
- )
- }
- /// When `output` is missing, scan the raw body for a JSON object that contains `"templates"`.
- private static func scavengeTemplatesJSONString(fromUTF8Data data: Data) -> String? {
- guard let raw = String(data: data, encoding: .utf8) else { return nil }
- return extractTemplatesJSONObjectString(from: raw)
- }
- private static func collectOutputTextSegments(fromOutputItem item: [String: Any], into segments: inout [String]) {
- guard let content = item["content"] as? [Any] else { return }
- for case let part as [String: Any] in content {
- let type = (part["type"] as? String) ?? ""
- var candidate: String?
- if let s = part["text"] as? String {
- candidate = s
- } else if let nested = part["text"] as? [String: Any], let value = nested["value"] as? String {
- candidate = value
- } else if let j = part["json"] as? String {
- candidate = j
- } else if let jObj = part["json"] as? [String: Any],
- let jData = try? JSONSerialization.data(withJSONObject: jObj, options: []),
- let jStr = String(data: jData, encoding: .utf8) {
- candidate = jStr
- }
- guard let blob = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !blob.isEmpty else { continue }
- let looksLikeCatalog = blob.contains("\"templates\"") && blob.contains("{")
- if type == "output_text" || type == "output_json" || looksLikeCatalog {
- segments.append(blob)
- }
- }
- }
- private static func decodeCatalog(fromModelText text: String) throws -> [CVTemplateAIItem] {
- let stripped = text.trimmingCharacters(in: .whitespacesAndNewlines)
- if stripped.hasPrefix("{"), let directData = stripped.data(using: .utf8) {
- if let payload = try? JSONDecoder().decode(CVTemplateCatalogPayload.self, from: directData) {
- return payload.templates
- }
- if let flex = flexibleTemplates(fromJSONData: directData) {
- return flex
- }
- }
- let jsonString = extractTemplatesJSONObjectString(from: text) ?? extractJSONObject(from: text)
- let jsonData = Data(jsonString.utf8)
- if let payload = try? JSONDecoder().decode(CVTemplateCatalogPayload.self, from: jsonData) {
- return payload.templates
- }
- if let flex = flexibleTemplates(fromJSONData: jsonData) {
- return flex
- }
- throw NSError(
- domain: "CVTemplateFetchService",
- code: 10,
- userInfo: [NSLocalizedDescriptionKey: "The assistant reply was not valid template JSON."]
- )
- }
- private static func flexibleTemplates(fromJSONData data: Data) -> [CVTemplateAIItem]? {
- guard let obj = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
- if let root = obj as? [String: Any] {
- if let items = templatesArray(from: root) {
- let mapped = items.compactMap { CVTemplateAIItem.fromFlexibleDictionary($0) }
- return mapped.isEmpty ? nil : mapped
- }
- }
- return nil
- }
- private static func templatesArray(from root: [String: Any]) -> [[String: Any]]? {
- if let arr = root["templates"] as? [[String: Any]] { return arr }
- for (_, value) in root {
- if let inner = value as? [String: Any], let arr = inner["templates"] as? [[String: Any]] {
- return arr
- }
- }
- return nil
- }
- private static func stripMarkdownCodeFence(_ text: String) -> String {
- var s = text.trimmingCharacters(in: .whitespacesAndNewlines)
- guard s.hasPrefix("```") else { return s }
- s.removeFirst(3)
- if s.lowercased().hasPrefix("json") {
- s.removeFirst(4)
- }
- s = s.trimmingCharacters(in: .whitespacesAndNewlines)
- if let fence = s.range(of: "```", options: .backwards) {
- s = String(s[..<fence.lowerBound])
- }
- return s.trimmingCharacters(in: .whitespacesAndNewlines)
- }
- private static func balancedJSONObject(from openBrace: String.Index, in s: String) -> String? {
- var depth = 0
- var inString = false
- var escaped = false
- var i = openBrace
- while i < s.endIndex {
- let ch = s[i]
- if inString {
- if escaped {
- escaped = false
- } else if ch == "\\" {
- escaped = true
- } else if ch == "\"" {
- inString = false
- }
- } else {
- switch ch {
- case "\"":
- inString = true
- case "{":
- depth += 1
- case "}":
- depth -= 1
- if depth == 0 {
- return String(s[openBrace...i])
- }
- default:
- break
- }
- }
- i = s.index(after: i)
- }
- return nil
- }
- private static func extractTemplatesJSONObjectString(from text: String) -> String? {
- let s = stripMarkdownCodeFence(text)
- guard let keyRange = s.range(of: "\"templates\"", options: .caseInsensitive) else { return nil }
- let head = s[..<keyRange.lowerBound]
- guard let open = head.lastIndex(of: "{") else { return nil }
- return balancedJSONObject(from: open, in: s)
- }
- private static func extractJSONObject(from text: String) -> String {
- if let extracted = extractTemplatesJSONObjectString(from: text) {
- return extracted
- }
- let stripped = stripMarkdownCodeFence(text)
- if let first = stripped.firstIndex(of: "{"), let balanced = balancedJSONObject(from: first, in: stripped) {
- return balanced
- }
- if let range = text.range(of: "\\{[\\s\\S]*\\}", options: .regularExpression) {
- return String(text[range])
- }
- return text
- }
- }
- // MARK: - AI DTO → CVTemplate
- private struct CVTemplateCatalogPayload: Codable {
- let templates: [CVTemplateAIItem]
- }
- private struct CVTemplateAIItem: Codable {
- let id: String
- let name: String
- let family: String
- let headline: String
- let accent: String
- let layoutType: String
- let sidebarSide: String
- let sidebarTinted: Bool
- let sectionLabelStyle: String
- /// Decodes rows when keys differ slightly or booleans arrive as numbers.
- fileprivate static func fromFlexibleDictionary(_ dict: [String: Any]) -> CVTemplateAIItem? {
- func firstString(keys: [String]) -> String? {
- for wanted in keys {
- for (dk, dv) in dict {
- guard dk.caseInsensitiveCompare(wanted) == .orderedSame else { continue }
- if let s = dv as? String { return s }
- }
- }
- return nil
- }
- func firstBool(keys: [String]) -> Bool {
- for wanted in keys {
- for (dk, dv) in dict {
- guard dk.caseInsensitiveCompare(wanted) == .orderedSame else { continue }
- if let b = dv as? Bool { return b }
- if let i = dv as? Int { return i != 0 }
- if let s = dv as? String {
- let t = s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- if ["true", "1", "yes", "y"].contains(t) { return true }
- if ["false", "0", "no", "n"].contains(t) { return false }
- }
- }
- }
- return false
- }
- guard let id = firstString(keys: ["id"]),
- let name = firstString(keys: ["name"]),
- let family = firstString(keys: ["family"]),
- let headline = firstString(keys: ["headline"]),
- let accent = firstString(keys: ["accent"]),
- let layoutType = firstString(keys: ["layoutType", "layout_type"]),
- let sidebarSide = firstString(keys: ["sidebarSide", "sidebar_side"]),
- let sectionLabelStyle = firstString(keys: ["sectionLabelStyle", "section_label_style"])
- else { return nil }
- let tinted = firstBool(keys: ["sidebarTinted", "sidebar_tinted"])
- return CVTemplateAIItem(
- id: id,
- name: name,
- family: family,
- headline: headline,
- accent: accent,
- layoutType: layoutType,
- sidebarSide: sidebarSide,
- sidebarTinted: tinted,
- sectionLabelStyle: sectionLabelStyle
- )
- }
- }
- extension CVTemplate {
- fileprivate init?(aiItem: CVTemplateAIItem) {
- let id = Self.slugifyID(aiItem.id)
- let name = aiItem.name.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !id.isEmpty, !name.isEmpty else { return nil }
- let familyNorm = aiItem.family.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- guard let family = Self.parseDesignFamily(familyNorm) else { return nil }
- let headline = CVTemplate.parseHeadline(aiItem.headline)
- let accent = CVTemplate.parseAccent(aiItem.accent)
- let section = CVTemplate.parseSectionLabelStyle(aiItem.sectionLabelStyle)
- let layoutNorm = Self.normalizedSchemaToken(aiItem.layoutType)
- let layout: Layout
- switch layoutNorm {
- case "singlecolumn":
- layout = .singleColumn
- case "twocolumn":
- let sideKey = Self.normalizedSchemaToken(aiItem.sidebarSide)
- let side: SidebarSide = (sideKey == "trailing") ? .trailing : .leading
- layout = .twoColumn(sidebar: side, tinted: aiItem.sidebarTinted)
- default:
- layout = .singleColumn
- }
- self.init(
- id: id,
- name: name,
- family: family,
- headline: headline,
- accent: accent,
- layout: layout,
- sectionLabelStyle: section
- )
- }
- /// Collapses spaces and underscores so values like "Two Column" still map.
- private static func normalizedSchemaToken(_ raw: String) -> String {
- raw.trimmingCharacters(in: .whitespacesAndNewlines)
- .lowercased()
- .filter { !$0.isWhitespace }
- .replacingOccurrences(of: "_", with: "")
- }
- private static func parseDesignFamily(_ raw: String) -> CVDesignFamily? {
- let key = normalizedSchemaToken(raw)
- if let f = CVDesignFamily(rawValue: key) { return f }
- if key == "minimalist" { return .minimal }
- if key == "exec" { return .executive }
- return nil
- }
- private static func slugifyID(_ raw: String) -> String {
- let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- guard !trimmed.isEmpty else { return "" }
- var out = ""
- var lastWasHyphen = false
- for ch in trimmed {
- if ch.isLetter || ch.isNumber {
- out.append(ch)
- lastWasHyphen = false
- } else if ch == "-" || ch == "_" || ch == " " {
- if !out.isEmpty, !lastWasHyphen {
- out.append("-")
- lastWasHyphen = true
- }
- }
- }
- while out.last == "-" { out.removeLast() }
- return out
- }
- private static func parseHeadline(_ s: String) -> Headline {
- let key = normalizedSchemaToken(s)
- switch key {
- case "centered": return .centered
- case "leftaligned", "left": return .leftAligned
- case "leftwithinitials", "initials": return .leftWithInitials
- case "avatarstacked", "stacked": return .avatarStacked
- default: return .leftAligned
- }
- }
- private static func parseAccent(_ s: String) -> Accent {
- let key = normalizedSchemaToken(s)
- switch key {
- case "none": return .none
- case "redunderline": return .redUnderline
- case "redbar": return .redBar
- case "bluebar": return .blueBar
- default: return .none
- }
- }
- private static func parseSectionLabelStyle(_ s: String) -> SectionLabelStyle {
- let key = normalizedSchemaToken(s)
- switch key {
- case "uppercase", "caps": return .uppercase
- case "slashed", "slash": return .slashed
- case "bracketed", "brackets": return .bracketed
- default: return .uppercase
- }
- }
- }
- // MARK: - OpenAI request types
- private struct OpenAITemplatesAPIErrorResponse: Codable {
- let error: APIErrorPayload
- struct APIErrorPayload: Codable {
- let message: String
- }
- }
- private struct CVTemplateOpenAIRequest: Encodable {
- let model: String
- let instructions: String
- let input: String
- let text: CVTemplateOpenAITextOutputConfig
- static func catalogPayload(model: String, instructions: String, input: String) -> CVTemplateOpenAIRequest {
- let itemProperties = CVTemplateAIItemSchemaProperties(
- id: OpenAIJSONSchemaStringField(type: "string", description: "Unique kebab-case id."),
- name: OpenAIJSONSchemaStringField(type: "string", description: "Short display name."),
- family: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Template style family.",
- enumValues: CVTemplateSchemaEnumValues.families
- ),
- headline: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Header arrangement for the mini preview.",
- enumValues: CVTemplateSchemaEnumValues.headlines
- ),
- accent: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Accent decoration.",
- enumValues: CVTemplateSchemaEnumValues.accents
- ),
- layoutType: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Page layout.",
- enumValues: CVTemplateSchemaEnumValues.layouts
- ),
- sidebarSide: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Sidebar column side when twoColumn.",
- enumValues: CVTemplateSchemaEnumValues.sides
- ),
- sidebarTinted: OpenAIJSONSchemaBooleanField(
- type: "boolean",
- description: "Tint sidebar column when twoColumn."
- ),
- sectionLabelStyle: OpenAIJSONSchemaStringEnum(
- type: "string",
- description: "Section heading style.",
- enumValues: CVTemplateSchemaEnumValues.sectionStyles
- )
- )
- let itemSchema = CVTemplateAIItemSchema(
- type: "object",
- properties: itemProperties,
- required: [
- "id", "name", "family", "headline", "accent",
- "layoutType", "sidebarSide", "sidebarTinted", "sectionLabelStyle"
- ],
- additionalProperties: false
- )
- let templatesProperty = CVTemplateTemplatesArrayProperty(
- type: "array",
- description: "Exactly 30 CV template definitions.",
- items: itemSchema
- )
- let rootProperties = CVTemplateCatalogRootProperties(templates: templatesProperty)
- let rootSchema = CVTemplateCatalogRootSchema(
- type: "object",
- properties: rootProperties,
- required: ["templates"],
- additionalProperties: false
- )
- let format = CVTemplateCatalogResponseJSONSchemaFormat(
- type: "json_schema",
- name: "cv_template_catalog",
- strict: true,
- schema: rootSchema
- )
- return CVTemplateOpenAIRequest(
- model: model,
- instructions: instructions,
- input: input,
- text: CVTemplateOpenAITextOutputConfig(format: format)
- )
- }
- }
- private struct CVTemplateOpenAITextOutputConfig: Encodable {
- let format: CVTemplateCatalogResponseJSONSchemaFormat
- }
- private struct CVTemplateCatalogResponseJSONSchemaFormat: Encodable {
- let type: String
- let name: String
- let strict: Bool
- let schema: CVTemplateCatalogRootSchema
- }
- private struct CVTemplateCatalogRootSchema: Encodable {
- let type: String
- let properties: CVTemplateCatalogRootProperties
- let required: [String]
- let additionalProperties: Bool
- }
- private struct CVTemplateCatalogRootProperties: Encodable {
- let templates: CVTemplateTemplatesArrayProperty
- }
- private enum CVTemplateSchemaEnumValues {
- static let families = ["creative", "executive", "minimal", "modern", "professional"]
- static let headlines = ["avatarStacked", "centered", "leftAligned", "leftWithInitials"]
- static let accents = ["blueBar", "none", "redBar", "redUnderline"]
- static let layouts = ["singleColumn", "twoColumn"]
- static let sides = ["leading", "trailing"]
- static let sectionStyles = ["bracketed", "slashed", "uppercase"]
- }
- private struct CVTemplateTemplatesArrayProperty: Encodable {
- let type: String
- let description: String
- let items: CVTemplateAIItemSchema
- }
- private struct CVTemplateAIItemSchema: Encodable {
- let type: String
- let properties: CVTemplateAIItemSchemaProperties
- let required: [String]
- let additionalProperties: Bool
- }
- private struct CVTemplateAIItemSchemaProperties: Encodable {
- let id: OpenAIJSONSchemaStringField
- let name: OpenAIJSONSchemaStringField
- let family: OpenAIJSONSchemaStringEnum
- let headline: OpenAIJSONSchemaStringEnum
- let accent: OpenAIJSONSchemaStringEnum
- let layoutType: OpenAIJSONSchemaStringEnum
- let sidebarSide: OpenAIJSONSchemaStringEnum
- let sidebarTinted: OpenAIJSONSchemaBooleanField
- let sectionLabelStyle: OpenAIJSONSchemaStringEnum
- }
- private struct OpenAIJSONSchemaStringEnum: Encodable {
- let type: String
- let description: String
- let enumValues: [String]
- enum CodingKeys: String, CodingKey {
- case type, description
- case enumValues = "enum"
- }
- }
- private struct OpenAIJSONSchemaStringField: Encodable {
- let type: String
- let description: String
- }
- private struct OpenAIJSONSchemaBooleanField: Encodable {
- let type: String
- let description: String
- }
|