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.

add agent-curated briefing to hub dashboard

hourly curate flow reads hub_action_items, calls Claude Haiku via
PrefectAgent to produce a themed briefing, writes briefing.json to the
shared analytics volume. hub frontend renders the briefing above the
existing filter/table when data is available.

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

+370 -1
+104
flows/curate.py
··· 1 + import os 2 + from datetime import datetime, timezone 3 + from pathlib import Path 4 + 5 + import duckdb 6 + from pydantic_ai import Agent 7 + from pydantic_ai.durable_exec.prefect import PrefectAgent, TaskConfig 8 + from prefect import flow, task, get_run_logger 9 + from prefect.blocks.system import Secret 10 + 11 + from mps.briefing import Briefing 12 + 13 + SYSTEM_PROMPT = """\ 14 + you are a dashboard curator for a software developer's issue tracker. 15 + given a list of scored items from github and tangled.org, produce a briefing 16 + with 2-5 themed sections. group by actionability, not by source. 17 + 18 + section titles should be lowercase, short, action-oriented: 19 + "needs review", "going stale", "quick wins", "watching" 20 + 21 + each item note should be ~10 words of useful context. 22 + the headline should be a single sentence summary. 23 + """ 24 + 25 + agent = Agent( 26 + "anthropic:claude-haiku-4-5", 27 + output_type=Briefing, 28 + system_prompt=SYSTEM_PROMPT, 29 + name="hub-curator", 30 + ) 31 + 32 + prefect_agent = PrefectAgent( 33 + agent, 34 + model_task_config=TaskConfig( 35 + retries=2, 36 + retry_delay_seconds=[2.0, 5.0], 37 + ), 38 + ) 39 + 40 + 41 + @task 42 + def load_items(db_path: str) -> str: 43 + """Read scored items from hub_action_items, format as text for the LLM.""" 44 + db = duckdb.connect(db_path, read_only=True) 45 + rows = db.execute( 46 + "SELECT source, repo, identifier, kind, title, url, " 47 + "author, labels, importance_score, updated " 48 + "FROM hub_action_items ORDER BY importance_score DESC LIMIT 200" 49 + ).fetchall() 50 + db.close() 51 + 52 + lines = [] 53 + for r in rows: 54 + source, repo, ident, kind, title, url, author, labels, score, updated = r 55 + item_id = f"{source}:{repo}#{ident}" 56 + label_str = ", ".join(labels) if labels else "" 57 + lines.append( 58 + f"- [{item_id}] {kind}: {title} " 59 + f"(repo={repo}, author={author}, score={score:.2f}, " 60 + f"updated={updated}, labels=[{label_str}])" 61 + ) 62 + return "\n".join(lines) 63 + 64 + 65 + @task 66 + def write_briefing(briefing: Briefing, path: str): 67 + Path(path).write_text(briefing.model_dump_json(indent=2)) 68 + 69 + 70 + @flow(name="curate", log_prints=True) 71 + async def curate(): 72 + logger = get_run_logger() 73 + db_path = os.environ.get( 74 + "ANALYTICS_DB_PATH", 75 + os.environ.get("PREFECT_LOCAL_STORAGE_PATH", "/tmp") + "/analytics.duckdb", 76 + ) 77 + briefing_path = os.environ.get( 78 + "BRIEFING_PATH", 79 + str(Path(db_path).parent / "briefing.json"), 80 + ) 81 + 82 + # set API key from Prefect Secret 83 + api_key = await Secret.load("anthropic-api-key") 84 + os.environ["ANTHROPIC_API_KEY"] = api_key.get() 85 + 86 + items_text = load_items(db_path) 87 + logger.info(f"loaded {items_text.count(chr(10)) + 1} items for curation") 88 + 89 + now = datetime.now(timezone.utc).isoformat() 90 + 91 + result = await prefect_agent.run( 92 + f"curate these items (current time: {now}):\n\n{items_text}" 93 + ) 94 + briefing = result.output 95 + briefing.generated_at = now 96 + 97 + write_briefing(briefing, briefing_path) 98 + logger.info(f"wrote briefing: {briefing.headline}") 99 + 100 + 101 + if __name__ == "__main__": 102 + import asyncio 103 + 104 + asyncio.run(curate())
+1
packages/mps/pyproject.toml
··· 7 7 "httpx>=0.27", 8 8 "duckdb>=1.0", 9 9 "pydantic>=2", 10 + "pydantic-ai-slim[anthropic,prefect]", 10 11 ] 11 12 12 13 [build-system]
+24
packages/mps/src/mps/briefing.py
··· 1 + from pydantic import BaseModel 2 + 3 + 4 + class BriefingItem(BaseModel): 5 + """A reference to a hub item with agent commentary.""" 6 + 7 + item_id: str # matches Card.id: "github:prefecthq/prefect#1234" 8 + note: str # 1-line context: "stale 2 weeks, might be blocked" 9 + 10 + 11 + class BriefingSection(BaseModel): 12 + """A themed group of items.""" 13 + 14 + title: str # e.g. "needs review", "quick wins", "going stale" 15 + summary: str # 1-2 sentence section summary 16 + items: list[BriefingItem] 17 + 18 + 19 + class Briefing(BaseModel): 20 + """The agent's full dashboard briefing.""" 21 + 22 + headline: str # e.g. "3 items need attention today" 23 + sections: list[BriefingSection] 24 + generated_at: str # ISO 8601
+6
prefect.yaml
··· 63 63 batch_size: 100 64 64 rate_limit_delay: 0.5 65 65 dry_run: false 66 + 67 + - name: curate 68 + entrypoint: flows/curate.py:curate 69 + work_pool: *k8s 70 + schedules: 71 + - cron: "10 * * * *" # after enrich refreshes the mart
+144
uv.lock
··· 77 77 ] 78 78 79 79 [[package]] 80 + name = "anthropic" 81 + version = "0.86.0" 82 + source = { registry = "https://pypi.org/simple" } 83 + dependencies = [ 84 + { name = "anyio" }, 85 + { name = "distro" }, 86 + { name = "docstring-parser" }, 87 + { name = "httpx" }, 88 + { name = "jiter" }, 89 + { name = "pydantic" }, 90 + { name = "sniffio" }, 91 + { name = "typing-extensions" }, 92 + ] 93 + sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" } 94 + wheels = [ 95 + { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" }, 96 + ] 97 + 98 + [[package]] 80 99 name = "anyio" 81 100 version = "4.12.1" 82 101 source = { registry = "https://pypi.org/simple" } ··· 594 613 ] 595 614 596 615 [[package]] 616 + name = "distro" 617 + version = "1.9.0" 618 + source = { registry = "https://pypi.org/simple" } 619 + sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } 620 + wheels = [ 621 + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, 622 + ] 623 + 624 + [[package]] 597 625 name = "docker" 598 626 version = "7.1.0" 599 627 source = { registry = "https://pypi.org/simple" } ··· 697 725 sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } 698 726 wheels = [ 699 727 { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, 728 + ] 729 + 730 + [[package]] 731 + name = "genai-prices" 732 + version = "0.0.56" 733 + source = { registry = "https://pypi.org/simple" } 734 + dependencies = [ 735 + { name = "httpx" }, 736 + { name = "pydantic" }, 737 + ] 738 + sdist = { url = "https://files.pythonhosted.org/packages/44/6b/94b3018a672c7775edfb485f0fed8f6068fba75e49b067e8a1ac5eb96764/genai_prices-0.0.56.tar.gz", hash = "sha256:ac24b16a84d0ab97539bfa48dfa4649689de8e3ce71c12ebacef29efb1998045", size = 65872, upload-time = "2026-03-20T20:33:00.732Z" } 739 + wheels = [ 740 + { url = "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl", hash = "sha256:dbe86be8f3f556bed1b72209ed36851fec8b01793b3b220f42921a4e7da945f6", size = 68966, upload-time = "2026-03-20T20:33:02.555Z" }, 700 741 ] 701 742 702 743 [[package]] ··· 921 962 ] 922 963 923 964 [[package]] 965 + name = "jiter" 966 + version = "0.13.0" 967 + source = { registry = "https://pypi.org/simple" } 968 + sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } 969 + wheels = [ 970 + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, 971 + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, 972 + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, 973 + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, 974 + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, 975 + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, 976 + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, 977 + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, 978 + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, 979 + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, 980 + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, 981 + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, 982 + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, 983 + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, 984 + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, 985 + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, 986 + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, 987 + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, 988 + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, 989 + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, 990 + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, 991 + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, 992 + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, 993 + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, 994 + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, 995 + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, 996 + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, 997 + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, 998 + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, 999 + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, 1000 + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, 1001 + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, 1002 + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, 1003 + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, 1004 + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, 1005 + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, 1006 + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, 1007 + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, 1008 + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, 1009 + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, 1010 + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, 1011 + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, 1012 + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, 1013 + ] 1014 + 1015 + [[package]] 924 1016 name = "jsonpatch" 925 1017 version = "1.33" 926 1018 source = { registry = "https://pypi.org/simple" } ··· 975 1067 sdist = { url = "https://files.pythonhosted.org/packages/9e/09/849cf129d7eae1e42f873f2dbd60323267c738390b686a7384fb3fb289ad/leather-0.4.1.tar.gz", hash = "sha256:67119c2aee93be821f077193bd8534e296c05b38bd174d9c5a80c4aa31d1a4d3", size = 44072, upload-time = "2025-12-15T19:01:42.224Z" } 976 1068 wheels = [ 977 1069 { url = "https://files.pythonhosted.org/packages/1a/d4/c4dcb02ed11f8884e169b3350fc40aa4c08edf8bed77a8f0f267542e6452/leather-0.4.1-py3-none-any.whl", hash = "sha256:ec61cba1ca3ccb96ed90e38b116fc58757d97d352171006b3288c47ce3fbd183", size = 30340, upload-time = "2025-12-15T19:01:40.823Z" }, 1070 + ] 1071 + 1072 + [[package]] 1073 + name = "logfire-api" 1074 + version = "4.29.0" 1075 + source = { registry = "https://pypi.org/simple" } 1076 + sdist = { url = "https://files.pythonhosted.org/packages/16/a4/ed2d823b4ad9a4c9dad1959c3399705c90ed3d96e6faaea5b897deb0f17c/logfire_api-4.29.0.tar.gz", hash = "sha256:55430c554cf198dcbddee390eca259a10a26d5f7e3527d51f859ddc31a83c840", size = 76407, upload-time = "2026-03-13T15:30:25.611Z" } 1077 + wheels = [ 1078 + { url = "https://files.pythonhosted.org/packages/e0/cc/62df4abc3e4650c25b81a8e39a1d498d3246c43f3aa4bfab7a73689317b4/logfire_api-4.29.0-py3-none-any.whl", hash = "sha256:48a1361b818357f5a37c71f9683f97e626e5df6c17f35212bfc1f19dddc6771c", size = 121457, upload-time = "2026-03-13T15:30:22.652Z" }, 978 1079 ] 979 1080 980 1081 [[package]] ··· 1147 1248 { name = "httpx" }, 1148 1249 { name = "prefect" }, 1149 1250 { name = "pydantic" }, 1251 + { name = "pydantic-ai-slim", extra = ["anthropic", "prefect"] }, 1150 1252 ] 1151 1253 1152 1254 [package.metadata] ··· 1155 1257 { name = "httpx", specifier = ">=0.27" }, 1156 1258 { name = "prefect", specifier = ">=3.0" }, 1157 1259 { name = "pydantic", specifier = ">=2" }, 1260 + { name = "pydantic-ai-slim", extras = ["anthropic", "prefect"] }, 1158 1261 ] 1159 1262 1160 1263 [[package]] ··· 1494 1597 ] 1495 1598 1496 1599 [[package]] 1600 + name = "pydantic-ai-slim" 1601 + version = "1.70.0" 1602 + source = { registry = "https://pypi.org/simple" } 1603 + dependencies = [ 1604 + { name = "genai-prices" }, 1605 + { name = "griffelib" }, 1606 + { name = "httpx" }, 1607 + { name = "opentelemetry-api" }, 1608 + { name = "pydantic" }, 1609 + { name = "pydantic-graph" }, 1610 + { name = "typing-inspection" }, 1611 + ] 1612 + sdist = { url = "https://files.pythonhosted.org/packages/ac/97/d57ee44976c349658ea7c645c5c2e1a26830e4b60fdeeee2669d4aaef6eb/pydantic_ai_slim-1.70.0.tar.gz", hash = "sha256:3df0c0e92f72c35e546d24795bce1f4d38f81da2d10addd2e9f255b2d2c83c91", size = 445474, upload-time = "2026-03-18T04:24:34.393Z" } 1613 + wheels = [ 1614 + { url = "https://files.pythonhosted.org/packages/da/8c/8545d28d0b3a9957aa21393cfdab8280bb854362360b296cd486ed1713ec/pydantic_ai_slim-1.70.0-py3-none-any.whl", hash = "sha256:162907092a562b3160d9ef0418d317ec941c5c0e6dd6e0aa0dbb53b5a5cd3450", size = 576244, upload-time = "2026-03-18T04:24:27.301Z" }, 1615 + ] 1616 + 1617 + [package.optional-dependencies] 1618 + anthropic = [ 1619 + { name = "anthropic" }, 1620 + ] 1621 + prefect = [ 1622 + { name = "prefect" }, 1623 + ] 1624 + 1625 + [[package]] 1497 1626 name = "pydantic-core" 1498 1627 version = "2.41.5" 1499 1628 source = { registry = "https://pypi.org/simple" } ··· 1557 1686 sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } 1558 1687 wheels = [ 1559 1688 { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, 1689 + ] 1690 + 1691 + [[package]] 1692 + name = "pydantic-graph" 1693 + version = "1.70.0" 1694 + source = { registry = "https://pypi.org/simple" } 1695 + dependencies = [ 1696 + { name = "httpx" }, 1697 + { name = "logfire-api" }, 1698 + { name = "pydantic" }, 1699 + { name = "typing-inspection" }, 1700 + ] 1701 + sdist = { url = "https://files.pythonhosted.org/packages/07/27/f7a71ca2a3705e7c24fd777959cf5515646cc5f23b5b16c886a2ed373340/pydantic_graph-1.70.0.tar.gz", hash = "sha256:3f76d9137369ef8748b0e8a6df1a08262118af20a32bc139d23e5c0509c6b711", size = 58578, upload-time = "2026-03-18T04:24:37.007Z" } 1702 + wheels = [ 1703 + { url = "https://files.pythonhosted.org/packages/38/fd/19c42b60c37dfdbbf5b76c7b218e8309b43dac501f7aaf2025527ca05023/pydantic_graph-1.70.0-py3-none-any.whl", hash = "sha256:6083c1503a2587990ee1b8a15915106e3ddabc8f3f11fbc4a108a7d7496af4a5", size = 72351, upload-time = "2026-03-18T04:24:30.291Z" }, 1560 1704 ] 1561 1705 1562 1706 [[package]]
+54
web/src/lib/components/Briefing.svelte
··· 1 + <script lang="ts"> 2 + import type { Briefing, BriefingItem } from '$lib/server/briefing'; 3 + import type { Card } from '$lib/types'; 4 + import { timeAgo } from '$lib/format'; 5 + 6 + let { briefing, cards }: { briefing: Briefing | null; cards: Card[] } = $props(); 7 + 8 + let cardMap = $derived( 9 + new Map(cards.map((c) => [c.id, c])) 10 + ); 11 + 12 + function urlFor(item: BriefingItem): string | null { 13 + return cardMap.get(item.item_id)?.url ?? null; 14 + } 15 + </script> 16 + 17 + {#if briefing} 18 + <div class="space-y-4"> 19 + <h2 class="text-xl font-semibold text-gray-100">{briefing.headline}</h2> 20 + 21 + <div class="grid gap-4 sm:grid-cols-2"> 22 + {#each briefing.sections as section (section.title)} 23 + <div class="bg-gray-800 rounded-lg px-5 py-4 space-y-3"> 24 + <h3 class="text-sm font-medium text-gray-300 lowercase">{section.title}</h3> 25 + <p class="text-sm text-gray-400">{section.summary}</p> 26 + 27 + {#if section.items.length > 0} 28 + <ul class="space-y-1.5"> 29 + {#each section.items as item (item.item_id)} 30 + {@const url = urlFor(item)} 31 + <li class="text-sm text-gray-300"> 32 + {#if url} 33 + <a 34 + href={url} 35 + target="_blank" 36 + rel="noopener noreferrer" 37 + class="underline decoration-gray-600 hover:decoration-gray-400 transition-colors" 38 + > 39 + {item.note} 40 + </a> 41 + {:else} 42 + {item.note} 43 + {/if} 44 + </li> 45 + {/each} 46 + </ul> 47 + {/if} 48 + </div> 49 + {/each} 50 + </div> 51 + 52 + <p class="text-xs text-gray-500">updated {timeAgo(briefing.generated_at)}</p> 53 + </div> 54 + {/if}
+25
web/src/lib/server/briefing.ts
··· 1 + import { readFile } from 'fs/promises'; 2 + 3 + export interface BriefingItem { 4 + item_id: string; 5 + note: string; 6 + } 7 + export interface BriefingSection { 8 + title: string; 9 + summary: string; 10 + items: BriefingItem[]; 11 + } 12 + export interface Briefing { 13 + headline: string; 14 + sections: BriefingSection[]; 15 + generated_at: string; 16 + } 17 + 18 + export async function loadBriefing(): Promise<Briefing | null> { 19 + try { 20 + const raw = await readFile('/analytics/briefing.json', 'utf-8'); 21 + return JSON.parse(raw); 22 + } catch { 23 + return null; 24 + } 25 + }
+4 -1
web/src/routes/+page.server.ts
··· 1 1 import { query } from '$lib/server/db'; 2 + import { loadBriefing } from '$lib/server/briefing'; 2 3 import type { Card, DashboardStats } from '$lib/types'; 3 4 4 5 interface ActionRow { ··· 48 49 } 49 50 })); 50 51 51 - return { stats, cards }; 52 + const briefing = await loadBriefing(); 53 + 54 + return { stats, cards, briefing }; 52 55 }
+8
web/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 + import Briefing from '$lib/components/Briefing.svelte'; 2 3 import StatBar from '$lib/components/StatBar.svelte'; 3 4 import FilterBar from '$lib/components/FilterBar.svelte'; 4 5 import CardTable from '$lib/components/CardTable.svelte'; ··· 7 8 8 9 const stats = $derived(data.stats); 9 10 const cards = $derived(data.cards); 11 + const briefing = $derived(data.briefing); 10 12 11 13 let search = $state(''); 12 14 let sourceFilter = $state(''); ··· 43 45 { value: stats.repos, label: 'repos' } 44 46 ]} 45 47 /> 48 + 49 + {#if briefing} 50 + <div class="mt-8"> 51 + <Briefing {briefing} {cards} /> 52 + </div> 53 + {/if} 46 54 47 55 <div class="mt-10"> 48 56 <FilterBar