diff --git a/notetaker/Models/Settings.swift b/notetaker/Models/Settings.swift index 5f00418..3cc637b 100644 --- a/notetaker/Models/Settings.swift +++ b/notetaker/Models/Settings.swift @@ -6,7 +6,23 @@ struct Settings: Codable { var userBlurb: String var systemPrompt: String - static let defaultSystemPrompt = "Generate concise meeting notes from the transcript and any additional notes provided. Focus on key points, action items, and decisions made." + static let defaultSystemPrompt: String = { + guard let url = Bundle.main.url(forResource: "DefaultSystemPrompt", withExtension: "txt"), + let content = try? String(contentsOf: url) else { + assertionFailure("DefaultSystemPrompt.txt missing from bundle") + return "" + } + return content + }() + + // Required variables for the template + static let requiredVariables: Set = [ + "meeting_title", + "meeting_date", + "transcript", + "user_blurb", + "user_notes" + ] init(deepgramKey: String = "", openAIKey: String = "", @@ -17,4 +33,40 @@ struct Settings: Codable { self.userBlurb = userBlurb self.systemPrompt = systemPrompt } + + // MARK: - Template Methods + + /// Extracts all template variables from a system prompt string + static func extractTemplateVariables(from template: String) -> Set { + let pattern = #"\{\{(\w+)\}\}"# + let regex = try! NSRegularExpression(pattern: pattern, options: []) + let matches = regex.matches(in: template, options: [], range: NSRange(location: 0, length: template.count)) + + var variables: Set = [] + for match in matches { + if let range = Range(match.range(at: 1), in: template) { + variables.insert(String(template[range])) + } + } + return variables + } + + /// Validates that all required variables are present in the system prompt + func validateSystemPrompt() -> (isValid: Bool, missingVariables: Set) { + let presentVariables = Settings.extractTemplateVariables(from: systemPrompt) + let missingVariables = Settings.requiredVariables.subtracting(presentVariables) + return (isValid: missingVariables.isEmpty, missingVariables: missingVariables) + } + + /// Replaces template variables in the system prompt with actual values + static func processTemplate(_ template: String, with variables: [String: String]) -> String { + var result = template + + for (key, value) in variables { + let placeholder = "{{\(key)}}" + result = result.replacingOccurrences(of: placeholder, with: value) + } + + return result + } } \ No newline at end of file diff --git a/notetaker/Resources/DefaultSystemPrompt.txt b/notetaker/Resources/DefaultSystemPrompt.txt new file mode 100644 index 0000000..799b7ed --- /dev/null +++ b/notetaker/Resources/DefaultSystemPrompt.txt @@ -0,0 +1,84 @@ +Your job is to generate enhanced meeting notes for the following meeting: + +Title: {{meeting_title}} +Date & Time: {{meeting_date}} + +You will be provided with: +- a transcript of the meeting +- information the user provided as additional context +- any additional notes the user manually noted down + +Your enhanced meeting notes should include: +- the key discussion points +- action items +- decisions made +- any deadlines or follow-up required + +Create the meeting notes in markdown formatting, and only include these sections. The title, attendees, date, and other metadata must not be included as this information is already displayed elsewhere in the UI. Your job is the notes themselves only. + +Here are explanations of how to handle the different information you will be given: + + +Transcripts use specific labels to identify speakers: +- "Me": The user using this meeting notes application. Technically speaking, these utterances were picked up by the user's microphone. +- "Them": The other person or other people in the meeting. Technically speaking, these utterances were picked up through system audio. + +Audio transcription often mislabels speakers. Use context clues to infer the correct speaker. + +The transcript of the meeting is provided in tags below. + + + +Any additional context the user provided in their global settings will be attached. +- This information is always included regardless of the meeting, so it may or may not be relevant. +- Use your best judgement to determine if the information is relevant, as it may not be. +- It could include personal details, preferences, spellings of companies/people/products, etc. + +This information is provided in tags below. + + + +The user may manually add notes to the meeting notes. If so, these notes are very important. + +Here are some examples of things the user may note down, how to recognize them, and how to handle them: + + +The user may note down key details they want to include in the meeting notes. + +How to recognize: the user writes things down that are related to content you can see in the transcript. + +How to handle: ensure they are included and prioritized in the enhanced meeting notes. + + + +The user may note down thoughts that they don't say out loud (ex. thoughts on a candidate they're interviewing). + +How to recognize: the user writes things down that are not related to content you can see in the transcript. + +How to handle: include in its own section, or tagged onto other points throughout where relevant. + + + +The user may note down formatting requests they want to include in the meeting notes. + +How to recognize: the user provides a meeting notes structure without filling in the content (ex. they use hashtags to create markdown headers but don't fill in the content). + +How to handle: structure the enhanced meeting notes according to the user's notes. + + +In general, the user's notes are very important. These are things that the user has specifically manually written down. + +These notes are provided in tags below. + + + +{{transcript}} + + + +{{user_blurb}} + + + +{{user_notes}} + \ No newline at end of file diff --git a/notetaker/Services/NotesGenerator.swift b/notetaker/Services/NotesGenerator.swift index 7999275..d482cf7 100644 --- a/notetaker/Services/NotesGenerator.swift +++ b/notetaker/Services/NotesGenerator.swift @@ -10,15 +10,13 @@ class NotesGenerator { private init() {} - /// Generates meeting notes from transcript and user notes + /// Generates meeting notes from meeting data using template-based system prompt /// - Parameters: - /// - transcript: The meeting transcript - /// - userNotes: Additional notes from the user + /// - meeting: The meeting object containing all necessary data /// - userBlurb: Information about the user for context - /// - systemPrompt: The system prompt for AI generation + /// - systemPrompt: The system prompt template with placeholders /// - Returns: Generated meeting notes - func generateNotes(transcript: String, - userNotes: String, + func generateNotes(meeting: Meeting, userBlurb: String, systemPrompt: String) async throws -> String { @@ -28,15 +26,27 @@ class NotesGenerator { let openAI = OpenAI(apiToken: apiKey) - // Compose system prompt with user blurb wrapped in XML tags for clarity - let systemContent = "\(systemPrompt)\n\n\n\(userBlurb)\n" + // Create date formatter for meeting date + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .full + dateFormatter.timeStyle = .short + + // Prepare template variables + let templateVariables: [String: String] = [ + "meeting_title": meeting.title.isEmpty ? "Untitled Meeting" : meeting.title, + "meeting_date": dateFormatter.string(from: meeting.date), + "transcript": meeting.formattedTranscript, + "user_blurb": userBlurb, + "user_notes": meeting.userNotes + ] + + // Process the system prompt template + let systemContent = Settings.processTemplate(systemPrompt, with: templateVariables) let systemMessage = ChatQuery.ChatCompletionMessageParam(role: .system, content: systemContent)! - - // Provide the model with structured XML-tagged input variables for improved parsing - let userContent = "Generate concise and well-structured meeting notes using the information below.\n\n\n\(transcript)\n\n\n\n\(userNotes)\n" - let userMessage = ChatQuery.ChatCompletionMessageParam(role: .user, content: userContent)! - - let query = ChatQuery(messages: [systemMessage, userMessage], model: .gpt4_1_mini) + + print(systemContent) + + let query = ChatQuery(messages: [systemMessage], model: .gpt4_1) let result = try await openAI.chats(query: query) diff --git a/notetaker/ViewModels/MeetingViewModel.swift b/notetaker/ViewModels/MeetingViewModel.swift index d4fa867..7651e6e 100644 --- a/notetaker/ViewModels/MeetingViewModel.swift +++ b/notetaker/ViewModels/MeetingViewModel.swift @@ -118,10 +118,8 @@ class MeetingViewModel: ObservableObject { let userBlurb = KeychainHelper.shared.get(forKey: "userBlurb") ?? "" let systemPrompt = KeychainHelper.shared.get(forKey: "systemPrompt") ?? Settings.defaultSystemPrompt - // Generate notes meeting.generatedNotes = try await NotesGenerator.shared.generateNotes( - transcript: meeting.transcript, - userNotes: meeting.userNotes, + meeting: meeting, userBlurb: userBlurb, systemPrompt: systemPrompt ) diff --git a/notetaker/ViewModels/SettingsViewModel.swift b/notetaker/ViewModels/SettingsViewModel.swift index d68e71b..ee7ce40 100644 --- a/notetaker/ViewModels/SettingsViewModel.swift +++ b/notetaker/ViewModels/SettingsViewModel.swift @@ -26,6 +26,15 @@ class SettingsViewModel: ObservableObject { errorMessage = nil saveSuccessful = false + // Validate system prompt template before saving + let validation = settings.validateSystemPrompt() + if !validation.isValid { + let missingVars = validation.missingVariables.joined(separator: ", ") + errorMessage = "System prompt is missing required variables: \(missingVars). Please include these variables in your template using double curly braces (e.g., {{transcript}})." + isSaving = false + return + } + // Save to Keychain let deepgramSaved = KeychainHelper.shared.save(settings.deepgramKey, forKey: "deepgramKey") let openAISaved = KeychainHelper.shared.save(settings.openAIKey, forKey: "openAIKey")