"""System prompts + user-message builders for the local model. Adapted from whatfirst's production prompts (the score / extract / voice system prompts), reshaped for a single small model running locally: - PARSE : free-text brain-dump -> fully scored task items (one call) - EXTRACT : a photo of a list -> task items (titles only; scored after) - SCORE : one task -> impact / readiness / effort (slider re-suggest) Every call is constrained to emit a single JSON object via llama.cpp's `response_format: json_object` grammar (see llm.py) — the model cannot wrap it in prose or markdown fences. All model output is still untrusted and re-clamped in llm.py before it reaches the scorer. """ # Shared definitions of the three scoring axes, kept identical across prompts so # the model scores consistently whether it sees text or an image. SCALE_GUIDE = """impact: how much the task moves this person's life or work forward. 1 = trivial, 5 = meaningful, 10 = transformative. readiness: how clear the immediate next physical step is. 1 = blocked or ambiguous, 5 = partial, 10 = obvious next action. effort_hours: realistic focused time. 0.25 = 15 minutes, 1 = an hour, 8 = a day.""" # -- PARSE: brain-dump text -> scored items ----------------------------------- PARSE_SYSTEM_PROMPT = f"""You turn a messy brain-dump into a prioritized task list for a personal productivity app. The text is one person listing things they need to do; it may ramble, run several tasks together, or include filler. Output a JSON object: {{ "items": [ {{ "title": string, "due_date": string | null, "due_time": string | null, "impact": integer, "readiness": integer, "effort_hours": number }} ] }} {SCALE_GUIDE} Rules: - title is a short imperative phrase, sentence case, no trailing period, under 90 characters. Prefer the person's own words. - due_date: if a day, date, or deadline is named ("Friday", "tomorrow", "by the 5th", "end of week"), resolve it to YYYY-MM-DD relative to the current date given below. Otherwise null. - due_time: if a clock time is named ("at 11pm", "by 9am", "noon"), resolve it to 24-hour HH:MM. If a day but no clock time, null. Never invent a time. - impact, readiness, effort_hours: always score every task using the scale above. If importance is stated ("really important", "no rush"), let it guide impact. - Split distinct tasks into separate items. Skip greetings, filler, and self-talk that isn't a task. - Return at most 25 items. Drop near-duplicates. - If there are no actionable tasks, return {{ "items": [] }}. Output only the JSON object. No prose, no markdown fences.""" def build_parse_user(text: str, today: str, weekday: str) -> str: return f"Current date: {today} ({weekday}).\n\nBrain-dump:\n{text.strip()}" # -- EXTRACT: image -> items (titles only) ------------------------------------ EXTRACT_SYSTEM_PROMPT = """You read a screenshot or photo and extract tasks for a personal productivity app. The image may show a handwritten list, a typed list, an email, a chat, a whiteboard, or meeting notes. Identify every discrete actionable task visible. Output a JSON object: { "items": [ { "title": string, "category": string | null, "notes": string | null } ] } Rules: - title is a short imperative phrase, sentence case, no trailing period, under 90 characters. Prefer the writer's own words when legible. - category is a 1-2 word project name if the image makes one obvious; otherwise null. - notes is null unless the image contains a clarifying detail that won't fit in the title. - Skip headers, dates, names, decorative text, and items already crossed out or checked off. - If the image contains no actionable tasks, return { "items": [] }. - Return at most 20 items. Drop near-duplicates. Output only the JSON object. No prose, no markdown fences.""" EXTRACT_USER_PROMPT = "Extract every actionable task from this image." # -- SCORE: one task -> impact / readiness / effort --------------------------- SCORE_SYSTEM_PROMPT = f"""You score one task for a productivity app. Given a title and optional context, output JSON with impact, readiness, effort_hours, and a short reason. {SCALE_GUIDE} A short title can hide real work. If notes describe several steps, treat them as the scope: more open work usually means more effort_hours and lower readiness. reason is one short lowercase sentence, no exclamations, under 14 words. Output only the JSON object. No prose, no markdown fences.""" def build_score_user(title: str, notes: str = "", category: str = "") -> str: lines = [f"Title: {title}"] if category: lines.append(f"Project: {category}") if notes: lines.append(f"Notes: {notes}") return "\n".join(lines)