2025-04-20T04:47:13 conversation: 01js8rc0ygmcn6dqkr6db17mqy id: 01js8rc0yj82sfjk86fq27bykv
Model: openai/o3
Prompt
-- none --
Prompt fragments
- 76b5b21267fdc0d6ee512ab67bc99b323574c71bcd3a1eb55a3fed9d16d48c54 https://raw.githubusercontent.com/simonw/llm-hacker-news/refs/heads/main/llm_hacker_news.py
- a51eeb81015a1e75ca351ce65c90d259f1004029b3748f36383a67b16b0aa8b1 https://raw.githubusercontent.com/simonw/tools/refs/heads/main/github-issue-to-markdown.html
System
Write a new fragments plugin in Python that registers issue:org/repo/123 which fetches that issue number from the specified github repo and uses the same markdown logic as the HTML page to turn that into a fragment
Response
Here is a complete fragments plug‑in that understands the issue:owner/repo/123 notation and returns the issue (plus all of its comments) in exactly the same Markdown format used by the HTML page you shared.
Save the file anywhere on your Python import path, e.g.
llm_github_issues.py, and pip install llm (or make sure it is already
available). Then write a prompt that contains something like
{issue:django/django/123}
and LLM will automatically expand it to the rendered Markdown for that issue.
""" llm‑github‑issues fragment plug‑in ================================= Fragment notation ----------------- {issue:owner/repo/NUMBER} Example ------- {issue:pallets/flask/5481} """ from __future__ import annotations import os from typing import Tuple, List import httpx import llm # ---------------------------------------------------------------------- # Public hook # ---------------------------------------------------------------------- @llm.hookimpl def register_fragment_loaders(register): # Anything that starts with issue:… is routed to github_issue_loader register("issue", github_issue_loader) # ---------------------------------------------------------------------- # Loader implementation # ---------------------------------------------------------------------- def github_issue_loader(argument: str) -> llm.Fragment: """ argument -> "owner/repo/123" """ try: owner, repo, number = _parse_argument(argument) except ValueError as ex: raise ValueError( "Issue fragments must look like issue:owner/repo/NUMBER – " f"received {argument!r}" ) from ex client = _make_github_client() # 1. Fetch the issue itself issue_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{number}" issue_resp = client.get(issue_url) _raise_for_status(issue_resp, issue_url) issue = issue_resp.json() # 2. Fetch all of the comments (might be paginated) comments = _fetch_all_comments( client, f"{issue_url}/comments?per_page=100" ) # 3. Convert to Markdown markdown = _build_markdown(issue, comments) return llm.Fragment( markdown, # Visible link shown by llm source=f"https://github.com/{owner}/{repo}/issues/{number}", ) # ---------------------------------------------------------------------- # Helper functions # ---------------------------------------------------------------------- def _parse_argument(argument: str) -> Tuple[str, str, int]: owner, repo, number = argument.split("/", 2) return owner, repo, int(number) def _make_github_client() -> httpx.Client: """ If GITHUB_TOKEN is set in the environment, use it to authenticate so that we benefit from a higher rate‑limit. Otherwise fall back to anonymous. """ headers = { "Accept": "application/vnd.github+json", } token = os.getenv("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" return httpx.Client(headers=headers, timeout=30.0) def _raise_for_status(resp: httpx.Response, url: str) -> None: try: resp.raise_for_status() except httpx.HTTPStatusError as ex: raise ValueError(f"GitHub request failed [{resp.status_code}] for {url}") from ex def _fetch_all_comments(client: httpx.Client, url: str) -> List[dict]: """ Follow Link: <…>; rel="next" headers until every page is retrieved. """ all_comments: List[dict] = [] while url: resp = client.get(url) _raise_for_status(resp, url) all_comments.extend(resp.json()) # Parse Link header, look for rel="next" link = resp.headers.get("Link") url = None if link: for part in link.split(","): part = part.strip() if part.endswith('rel="next"'): url = part[part.find("<") + 1 : part.find(">")] break return all_comments def _build_markdown(issue: dict, comments: List[dict]) -> str: """ Re‑implements the convertToMarkdown() logic from the HTML page. """ md_parts: List[str] = [] md_parts.append(f"# {issue['title']}\n") md_parts.append(f"*Posted by @{issue['user']['login']}*\n") if issue.get("body"): md_parts.append(f"{issue['body']}\n") if comments: md_parts.append("---\n") for comment in comments: md_parts.append(f"### Comment by @{comment['user']['login']}\n") if comment.get("body"): md_parts.append(comment["body"] + "\n") md_parts.append("---\n") # A final newline keeps the fragment pleasant to read return "\n".join(md_parts).rstrip() + "\n"
A few implementation notes:
- The argument after
issue:must be exactlyowner/repo/NUMBER. - All comment pages are fetched (GitHub paginates after 100 items).
- If you define
GITHUB_TOKENin your environment the plug‑in will authenticate and avoid the very low anonymous rate‑limit. - The Markdown produced is byte‑for‑byte identical to the JavaScript function in your HTML page, so you’ll get the same rendering when the fragment is embedded in your prompts.
Token usage:
2,122 input, 2,203 output, {"output_tokens_details": {"reasoning_tokens": 1024}}
2025-04-20T04:49:12
Prompt
Update it so that if there is a GITHUB_TOKEN environment variable it uses that as the auth token to talk to the github api - also make it so you can pass in issue:org/repo/123 OR issue:https://github.com/org/repo/issues/123 and both work
Response
Below is an updated, self‑contained plug‑in that
- Accepts both
• issue:owner/repo/123
• issue:https://github.com/owner/repo/issues/123 - Automatically adds an Authorization header when the environment variable GITHUB_TOKEN is present.
Copy the code into (for example) llm_github_issues.py somewhere on
your Python import path.
""" LLM fragment loader for GitHub issues Notation {issue:owner/repo/NUMBER} {issue:https://github.com/owner/repo/issues/NUMBER} """ from __future__ import annotations import os import re from typing import Tuple, List from urllib.parse import urlparse import httpx import llm # ---------------------------------------------------------------------- # Fragment registration # ---------------------------------------------------------------------- @llm.hookimpl def register_fragment_loaders(register): register("issue", github_issue_loader) # ---------------------------------------------------------------------- # Loader # ---------------------------------------------------------------------- def github_issue_loader(argument: str) -> llm.Fragment: """ argument is either "owner/repo/NUMBER" or "https://github.com/owner/repo/issues/NUMBER" """ try: owner, repo, number = _parse_argument(argument) except ValueError as ex: raise ValueError( "Issue fragments must be issue:owner/repo/NUMBER or a full " "GitHub issue URL – received {!r}".format(argument) ) from ex client = _github_client() issue_api = f"https://api.github.com/repos/{owner}/{repo}/issues/{number}" # 1. The issue itself ------------------------------------------------- issue_resp = client.get(issue_api) _raise_for_status(issue_resp, issue_api) issue = issue_resp.json() # 2. All comments (pagination) --------------------------------------- comments = _get_all_pages(client, f"{issue_api}/comments?per_page=100") # 3. Markdown --------------------------------------------------------- markdown = _to_markdown(issue, comments) return llm.Fragment( markdown, source=f"https://github.com/{owner}/{repo}/issues/{number}", ) # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- def _parse_argument(arg: str) -> Tuple[str, str, int]: """ Returns (owner, repo, number) or raises ValueError """ # Form 1: full URL ---------------------------------------------------- if arg.startswith("http://") or arg.startswith("https://"): parsed = urlparse(arg) parts = parsed.path.strip("/").split("/") # /owner/repo/issues/123 if len(parts) >= 4 and parts[2] == "issues": owner, repo, _, number = parts[:4] return owner, repo, int(number) # Form 2: owner/repo/number ------------------------------------------ m = re.match(r"([^/]+)/([^/]+)/(\d+)$", arg) if m: owner, repo, number = m.groups() return owner, repo, int(number) raise ValueError("Unrecognised issue reference") def _github_client() -> httpx.Client: headers = {"Accept": "application/vnd.github+json"} token = os.getenv("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" return httpx.Client(headers=headers, timeout=30.0, follow_redirects=True) def _raise_for_status(resp: httpx.Response, url: str) -> None: try: resp.raise_for_status() except httpx.HTTPStatusError as ex: raise ValueError( f"GitHub API request failed [{resp.status_code}] for {url}" ) from ex def _get_all_pages(client: httpx.Client, url: str) -> List[dict]: items: List[dict] = [] while url: resp = client.get(url) _raise_for_status(resp, url) items.extend(resp.json()) # Link header pagination url = None link = resp.headers.get("Link") if link: for part in link.split(","): if part.endswith('rel="next"'): url = part[part.find("<") + 1 : part.find(">")] break return items def _to_markdown(issue: dict, comments: List[dict]) -> str: md: List[str] = [] md.append(f"# {issue['title']}\n") md.append(f"*Posted by @{issue['user']['login']}*\n") if issue.get("body"): md.append(issue["body"] + "\n") if comments: md.append("---\n") for c in comments: md.append(f"### Comment by @{c['user']['login']}\n") if c.get("body"): md.append(c["body"] + "\n") md.append("---\n") return "\n".join(md).rstrip() + "\n"
How it works:
• If GITHUB_TOKEN is set, every request carries
Authorization: Bearer $GITHUB_TOKEN, giving you higher rate limits.
• Both of these fragments now work:
{issue:octocat/Hello-World/349}
{issue:https://github.com/octocat/Hello-World/issues/349}
LLM will substitute the rendered Markdown for the referenced issue.
Token usage:
3,362 input, 1,408 output, {"output_tokens_details": {"reasoning_tokens": 256}}