my prefect server setup prefect-metrics.waow.tech
python orchestration
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cache curate flow's LLM call by items content hash

ByItemsContent cache policy hashes the scored items text — when the
top-200 list hasn't changed, the briefing is served from cache and
haiku is never called. 4h expiration. drops the timestamp from the
LLM prompt so input is purely content-dependent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 7390814a 62699b63

+72 -26
+72 -26
flows/curate.py
··· 1 + import hashlib 1 2 import os 2 3 import shutil 3 - from datetime import datetime, timezone 4 + from dataclasses import dataclass 5 + from datetime import datetime, timedelta, timezone 4 6 from pathlib import Path 5 7 6 8 import duckdb ··· 10 12 from pydantic_ai.providers.anthropic import AnthropicProvider 11 13 from prefect import flow, task, get_run_logger 12 14 from prefect.blocks.system import Secret 15 + from prefect.cache_policies import CachePolicy 16 + from prefect.context import TaskRunContext 13 17 14 18 from mps.briefing import Briefing 15 19 20 + @dataclass 21 + class ByItemsContent(CachePolicy): 22 + """Cache briefing by content hash of items + system prompt.""" 23 + 24 + def compute_key( 25 + self, 26 + task_ctx: TaskRunContext, 27 + inputs: dict, 28 + flow_parameters: dict, 29 + **kwargs, 30 + ) -> str | None: 31 + items_text = inputs.get("items_text") 32 + if items_text is None: 33 + return None 34 + h = hashlib.md5((SYSTEM_PROMPT + items_text).encode()).hexdigest()[:12] 35 + return f"briefing/{h}" 36 + 16 37 SYSTEM_PROMPT = """\ 17 - you are a dashboard curator for a software developer's issue tracker. 38 + you are a dashboard curator for a solo developer's issue tracker. 18 39 given a list of scored items from github and tangled.org, produce a briefing 19 - with exactly 4 themed sections. group by actionability, not by source. 40 + with exactly 4 themed sections. group by theme or status, not by source. 20 41 21 - the layout is a 2x2 grid, so always produce exactly 4 sections. 22 - keep each section to 4-6 items max — be selective, not exhaustive. 42 + ## tone 23 43 24 - section titles should be lowercase, short, action-oriented: 25 - "needs review", "going stale", "quick wins", "watching" 44 + be honest and proportionate. most days are normal — say so. don't manufacture 45 + urgency. if nothing is on fire, the headline should reflect that. a calm 46 + "steady week, nothing blocking" is better than "5 critical items demand 47 + attention" when it's really just routine activity. 26 48 27 - each item note should be ~10 words of useful context. 28 - the title should be 2-3 words that capture the vibe: "monday triage", "quiet week", 29 - "bug cluster", "release crunch". lowercase. 49 + reserve words like "critical", "urgent", "demands", "immediate" for genuinely 50 + exceptional situations — a broken deploy, a security issue, a hard deadline 51 + this week. routine PRs and stale issues are not emergencies. 30 52 31 - the headline should be 1-2 sentences about what's going on and what to pay attention to. 53 + the title should be 2-3 lowercase words that honestly capture the vibe: 54 + "quiet week", "steady progress", "a few loose ends", "one thing blocking". 55 + not every day is a "release crunch" or "bug cluster". 56 + 57 + the headline should be 1-2 factual sentences. lead with the most useful 58 + observation, not the most alarming one. 59 + 60 + ## structure 61 + 62 + the layout is a 2x2 grid — always produce exactly 4 sections. 63 + keep each section to 4-6 items max — be selective, not exhaustive. 64 + 65 + section titles should be lowercase, short, descriptive: 66 + "waiting on review", "getting stale", "small fixes", "just tracking" 67 + 68 + each item note should be ~10 words of useful context — what it is, 69 + not why it's supposedly urgent. 32 70 33 71 ## visual styling 34 72 35 73 each section has accent and priority fields to control presentation. 36 74 37 - accent colors — pick the one that matches the section's mood: 38 - - red: urgent, blocked, overdue items 39 - - amber: warnings, going stale, needs attention soon 40 - - emerald: positive signals, quick wins, ready to merge 41 - - sky: informational, watching, low-urgency tracking 42 - - violet: features, enhancements, new ideas 75 + accent colors — use the one that honestly fits: 76 + - red: actually blocked or breaking something right now 77 + - amber: getting old, might need a nudge soon 78 + - emerald: ready to go, easy to close out 79 + - sky: background awareness, no action needed now 80 + - violet: features or ideas, no urgency 81 + 82 + default to sky or emerald. use red sparingly — maybe once a month. 43 83 44 84 priority — all sections should use "normal" for the 2x2 grid layout. 45 85 ··· 91 131 return "\n".join(lines) 92 132 93 133 134 + @task( 135 + cache_policy=ByItemsContent(), 136 + cache_expiration=timedelta(hours=4), 137 + persist_result=True, 138 + result_serializer="json", 139 + ) 140 + async def generate_briefing(items_text: str, api_key: str) -> Briefing: 141 + """Call the LLM to curate items into a briefing. Cached by items content hash.""" 142 + prefect_agent = make_agent(api_key) 143 + result = await prefect_agent.run(f"curate these items:\n\n{items_text}") 144 + return result.output 145 + 146 + 94 147 @task 95 148 def write_briefing(briefing: Briefing, path: str): 96 149 Path(path).write_text(briefing.model_dump_json(indent=2)) ··· 108 161 str(Path(db_path).parent / "briefing.json"), 109 162 ) 110 163 111 - # load API key from Prefect Secret, build agent 112 164 api_key = (await Secret.load("anthropic-api-key")).get() 113 - prefect_agent = make_agent(api_key) 114 165 115 166 items_text = load_items(db_path) 116 167 logger.info(f"loaded {items_text.count(chr(10)) + 1} items for curation") 117 168 118 - now = datetime.now(timezone.utc).isoformat() 119 - 120 - result = await prefect_agent.run( 121 - f"curate these items (current time: {now}):\n\n{items_text}" 122 - ) 123 - briefing = result.output 124 - briefing.generated_at = now 169 + briefing = await generate_briefing(items_text, api_key) 170 + briefing.generated_at = datetime.now(timezone.utc).isoformat() 125 171 126 172 write_briefing(briefing, briefing_path) 127 173 logger.info(f"wrote briefing: {briefing.headline}")