Download backend/astroparse_api/annotate.py from kiyer/beacon: direct link, hf CLI and curl.
- Browser
- Download file 7.83 kB
-
https://huggingface.co/spaces/kiyer/beacon/resolve/main/backend/astroparse_api/annotate.py
- Command line
-
hf download hf://spaces/kiyer/beacon/backend/astroparse_api/annotate.py
-
curl -L -o annotate.py https://huggingface.co/spaces/kiyer/beacon/resolve/main/backend/astroparse_api/annotate.py
7.83 kB
| import hashlib | |
| import json | |
| import sqlite3 | |
| import time | |
| MODE_INSTRUCTIONS = { | |
| "history": "Provide HISTORICAL CONTEXT: how this claim or approach emerged in the field, which earlier works set it up, and how thinking shifted over time.", | |
| "referee": ( | |
| "Act as a RIGOROUS REFEREE grounded in the provided PAPER CONTEXT and RETRIEVED LITERATURE. " | |
| "You MUST: (a) ground your critique in a SPECIFIC retrieved work [n] (comparing numbers where possible) " | |
| "OR a specific internal inconsistency with the provided paper context; " | |
| "(b) name EXACTLY ONE concrete check the authors should run or report; " | |
| "(c) NOT restate any caveat already present in the paragraph or its neighbors — " | |
| "if the paragraph survives scrutiny, say what evidence would strengthen it instead." | |
| ), | |
| "literature": "Offer OBSERVATIONS FROM THE RELATED LITERATURE: how the retrieved works support, contradict, or extend what this paragraph claims. Compare numbers where possible.", | |
| "explainer": "Write a PLAIN-LANGUAGE EXPLAINER for a beginning graduate student: unpack jargon and explain why this paragraph matters, without dumbing down.", | |
| "methods": "Provide a METHODS COMPARISON: contrast the technique in this paragraph with the approaches used in the retrieved works, noting trade-offs.", | |
| } | |
| _PREAMBLE = ( | |
| "You are an erudite astronomical annotator writing a marginal note beside a paragraph of a research paper, " | |
| "in the tradition of an annotated encyclopedia. Scholarly, precise, a little wry. " | |
| "Write 55–85 words, a single paragraph, no markdown, no preamble. " | |
| "Refer to the retrieved works by bracketed number, e.g. [1], where relevant.\n\n" | |
| ) | |
| def build_prompt( | |
| paragraph: str, | |
| section: str, | |
| mode: str, | |
| lit: list[dict], | |
| paper_context: dict | None = None, | |
| ) -> str: | |
| prompt = _PREAMBLE + MODE_INSTRUCTIONS[mode] + "\n\n" | |
| # Inject PAPER CONTEXT block for referee mode only | |
| if mode == "referee" and paper_context: | |
| outline = " → ".join(paper_context.get("sectionOutline") or []) | |
| prev = paper_context.get("prevParagraph", "") | |
| nxt = paper_context.get("nextParagraph", "") | |
| prompt += ( | |
| f"PAPER CONTEXT:\n" | |
| f"Title: {paper_context.get('title', '')}\n" | |
| f"Section outline: {outline}\n" | |
| f"Opening paragraph: {paper_context.get('opening', '')}\n" | |
| ) | |
| if prev: | |
| prompt += f"Previous paragraph: {prev}\n" | |
| if nxt: | |
| prompt += f"Next paragraph: {nxt}\n" | |
| prompt += "\n" | |
| prompt += f'PARAGRAPH (from section "{section}"):\n{paragraph}\n\n' | |
| if lit: | |
| block = "\n".join( | |
| f'[{i + 1}] {l["short"]} — "{l["title"]}" ({l["journal"]}). {l["abstract"]}' | |
| for i, l in enumerate(lit) | |
| ) | |
| prompt += f"RETRIEVED LITERATURE (via Pathfinder):\n{block}\n\n" | |
| return prompt + "Marginal note:" | |
| def cache_key( | |
| paragraph: str, | |
| mode: str, | |
| lit_ids: list[str], | |
| model: str, | |
| paper_context: dict | None = None, | |
| ) -> str: | |
| if mode == "referee" and paper_context: | |
| ctx_hash = hashlib.sha256( | |
| json.dumps(paper_context, sort_keys=True).encode() | |
| ).hexdigest()[:16] | |
| payload = json.dumps([paragraph, mode, sorted(lit_ids), model, ctx_hash]) | |
| else: | |
| payload = json.dumps([paragraph, mode, sorted(lit_ids), model]) | |
| return hashlib.sha256(payload.encode()).hexdigest() | |
| class CompletionCache: | |
| def __init__(self, path): | |
| self.conn = sqlite3.connect(str(path), check_same_thread=False) | |
| self.conn.execute( | |
| "CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, text TEXT, created REAL)" | |
| ) | |
| def get(self, key: str) -> str | None: | |
| row = self.conn.execute("SELECT text FROM cache WHERE key = ?", (key,)).fetchone() | |
| return row[0] if row else None | |
| def put(self, key: str, text: str) -> None: | |
| self.conn.execute( | |
| "INSERT OR REPLACE INTO cache VALUES (?, ?, ?)", (key, text, time.time()) | |
| ) | |
| self.conn.commit() | |
| import httpx | |
| class ProviderError(Exception): | |
| pass | |
| def _parse_or_none(data: str): | |
| try: | |
| return json.loads(data) | |
| except json.JSONDecodeError: | |
| return None | |
| _client: httpx.AsyncClient | None = None | |
| def _http() -> httpx.AsyncClient: | |
| global _client | |
| if _client is None: | |
| _client = httpx.AsyncClient(timeout=httpx.Timeout(60.0, connect=10.0)) | |
| return _client | |
| async def _raise_provider_error(response: httpx.Response): | |
| body = (await response.aread()).decode(errors="replace") | |
| raise ProviderError(f"{response.status_code}: {body}"[:300]) | |
| def _sse_data_lines(response): | |
| async def gen(): | |
| async for line in response.aiter_lines(): | |
| if line.startswith("data: "): | |
| yield line[6:] | |
| return gen() | |
| async def stream_completion(provider: str, model: str, prompt: str, key: str): | |
| """Yields text chunks. Raises ProviderError on non-200 (message truncated to 300 chars). | |
| The user's key is used for the request and nothing else — never log it. | |
| """ | |
| if provider == "anthropic": | |
| req = _http().stream( | |
| "POST", "https://api.anthropic.com/v1/messages", | |
| headers={"x-api-key": key, "anthropic-version": "2023-06-01"}, | |
| json={"model": model, "max_tokens": 400, "stream": True, | |
| "messages": [{"role": "user", "content": prompt}]}, | |
| ) | |
| async with req as r: | |
| if r.status_code != 200: | |
| await _raise_provider_error(r) | |
| async for data in _sse_data_lines(r): | |
| ev = _parse_or_none(data) | |
| if ev is None: | |
| continue | |
| if ev.get("type") == "error": | |
| raise ProviderError(json.dumps(ev.get("error", {}))[:300]) | |
| if ev.get("type") == "content_block_delta": | |
| yield ev["delta"].get("text", "") | |
| elif provider == "openai": | |
| req = _http().stream( | |
| "POST", "https://api.openai.com/v1/chat/completions", | |
| headers={"Authorization": f"Bearer {key}"}, | |
| json={"model": model, "max_tokens": 400, "stream": True, | |
| "messages": [{"role": "user", "content": prompt}]}, | |
| ) | |
| async with req as r: | |
| if r.status_code != 200: | |
| await _raise_provider_error(r) | |
| async for data in _sse_data_lines(r): | |
| if data.strip() == "[DONE]": | |
| break | |
| ev = _parse_or_none(data) | |
| if ev is None: | |
| continue | |
| if "error" in ev: | |
| raise ProviderError(json.dumps(ev["error"])[:300]) | |
| choices = ev.get("choices") or [{}] | |
| delta = choices[0].get("delta", {}) | |
| if delta.get("content"): | |
| yield delta["content"] | |
| elif provider == "gemini": | |
| url = (f"https://generativelanguage.googleapis.com/v1beta/models/" | |
| f"{model}:streamGenerateContent?alt=sse&key={key}") | |
| req = _http().stream( | |
| "POST", url, | |
| json={"contents": [{"parts": [{"text": prompt}]}]}, | |
| ) | |
| async with req as r: | |
| if r.status_code != 200: | |
| await _raise_provider_error(r) | |
| async for data in _sse_data_lines(r): | |
| ev = _parse_or_none(data) | |
| if ev is None: | |
| continue | |
| for cand in ev.get("candidates", []): | |
| for part in cand.get("content", {}).get("parts", []): | |
| if part.get("text"): | |
| yield part["text"] | |
| else: | |
| raise ProviderError(f"unknown provider: {provider}") | |