# Project export: BASKR - Research Radar

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Challenge your research with today's new lab papers
- Devpost: https://devpost.com/software/calhack-ai-26
- GitHub: https://github.com/tobygodat/calhackathon2026
- Demo: https://calhackathon2026.vercel.app/
- Video: https://www.youtube.com/embed/nX6t2JRQA9E?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Claude Opus 4.8 (45 commits), Toby Godat (32 commits), will-hamlin (30 commits), Drew Hawley (9 commits), Cursor (1 commits)

## Devpost submission (written by the team)

### Inspiration

~4,000–5,000 new papers hit PubMed every day. No researcher reads them all — they rely on keyword alerts, citation tracking, and luck. The critical failure mode isn't slow reading. It's missing the paper that matters. Labs spend quarters and $40k on experiments that were quietly answered last month. Working assumptions go unchallenged because nobody caught the contradicting paper. Every tool in this space — Elicit, Consensus, Semantic Scholar, Research Rabbit, Google Scholar Alerts — is reactive. You ask, it searches, it answers, it forgets you. None of them maintain a persistent model of your lab's specific context. They're search boxes, not watchdogs.

### What it does

Baskr is a proactive research-monitoring agent for labs. You describe your lab's open questions and working assumptions, and it reads the daily PubMed firehose — flagging the handful of new papers that actually matter to you, before you think to go looking. The name comes from basking sharks: they float with their mouths open, filtering everything in their path and collecting only what matters. Same idea. Baskr holds a structured memory of what your lab cares about, then monitors incoming papers against it — without being asked. When something relevant lands, it fires an alert with a plain-language reason: why this paper matters to your specific work. The core object no competitor has is the lab profile: a structured, persistent record of your open questions, upcoming experiments, working assumptions, and findings. Every paper that enters the system is reasoned against it. The profile is the product — everything else is two views into it.

### How we built it

Baskr uses Redis as its central data layer. All the memory, search, caching, and event routing runs through it — not as a cache bolted on top, but as the actual architecture. Redis Iris is a set of managed AI-focused services on Redis Cloud. Baskr uses three of them live and one is planned for later. RedisVL + Vector Search: Finds semantically similar papers from the corpus using an HNSW index over OpenAI text-embedding-3-small embeddings (1536 dimensions, cosine similarity). When a new paper arrives, the agent searches this index for the five most relevant prior works before reasoning about it. Agent Memory: Stores the lab profile — assumptions, findings, open questions — as long-term memories that persist across restarts and grow as new papers arrive. The agent queries this before every classification, so Claude sees the new paper in the context of what your lab already knows. Redis Hashes + Strings: Stores individual papers as metadata hashes (baskr:paper:{pmid}) and frozen daily digests as strings. The digest feed makes Baskr feel like a product that's been running all week, not a demo that just started. LangCache: Caches Claude responses keyed on a normalized query hash to avoid redundant API calls on semantically similar searches. Hit/miss counters surface in the UI. The agent loop itself is seven steps: receive a paper from the Redis Stream → embed the abstract → vector search prior work → query Agent Memory for relevant lab context → send all three to Claude → broadcast an SSE alert if relevant → write a new long-term memory if the paper updates lab knowledge. Steps 3 and 4 are what make the reasoning good. Claude doesn't just see the new paper — it sees it against what the lab already knows. The AI reasoning layer uses Claude (claude-sonnet-4-6) with forced tool use to enforce a strict JSON output contract: { label, reason, matched_item_id, confidence }. No freeform prose, no parsing fragility.

### Challenges we ran into

The local Redis server didn't ship with RediSearch. The standard redis-server (7.0.15) doesn't include the RediSearch module, and Docker image pulls were 403-blocked in our build environment. We couldn't run RedisVL's HNSW index against the local server at all. Our solution was a transparent degradation layer: ensure_papers_index probes FT._LIST at startup and falls back to a pure-Python brute-force cosine scan over all baskr:paper:* hashes when no search module is present. Same function signatures, same result ordering. The RedisVL/HNSW path auto-engages the moment a redis-stack server is reachable — no code change required. The demo runs against redis/redis-stack-server to exercise real HNSW. No API keys in the build environment. We had no OPENAI_API_KEY, ANTHROPIC_API_KEY, or NCBI_API_KEY during most of the build. Rather than hard-requiring keys and blocking all progress, we built a degraded-mode layer: embeddings fall back to deterministic hashed vectors, the LLM falls back to a rule-based canned classifier, and the data pipeline falls back to staged sample papers. Live mode engages automatically when keys appear. /status reports these connections as unknown rather than down in degraded mode, so the health endpoint is honest without being alarming. The critical demo data separation problem. If "new" papers get loaded into the vector index alongside the historical corpus, the agent finds itself when searching for prior work and the alert moment dies. We enforced a strict split: corpus papers go through the full chunk → embed → index pipeline; demo papers go to the Redis Stream only, with no vector index entry. This required careful orchestration of two separate data paths and a dedup check on Stream consumption. Import path drift. Mid-build, a branch merge renamed the data pipeline module from implementations/data_pipeline to system_pieces/data_pipeline. Several files were importing the old path. We resolved it with a path shim in app/__init__.py that inserts the repo root onto sys.path, keeping DataPipeline imports local-in-function and resolving regardless of launch directory.

### Accomplishments we're proud of

Redis is the architecture, not the cache. Every piece of meaningful state in Baskr lives in Redis: the lab profile in Agent Memory, the paper corpus in the vector index, the event feed in Streams, the digest snapshots in Hashes/Strings, and the response cache in LangCache. We didn't reach for Postgres or SQLite for anything. The data model is Redis-native from the start. The degradation story actually works end-to-end. Eighteen tests pass green — including nearest-neighbor search and live /status checks — against a real Redis server, with no external API keys. The same code that runs in degraded mode is identical to what runs in production; there's no test-specific branch. This is the correct way to build for environments you don't control. Forced tool use for JSON enforcement. Getting structured output from Claude reliably is harder than it looks. We use a single-tool forcing pattern — Claude is given exactly one tool (record_classification) with a strict input schema — which means the JSON contract is enforced at the API level, not via regex parsing of free text. Classification results are never malformed. The lab profile as a persistent, compounding object. This is the thing no competitor has. Lab context grows over time as new papers are classified and written back as long-term memories. A lab that's been using Baskr for six months has a richer context than one that started yesterday, and a new competitor can't cold-start that for your customers.

### What we learned

Proactive versus reactive is an architecture problem, not a feature. Building a system that monitors on your behalf — rather than answering when asked — requires a completely different data model. You need persistent context, an event loop that runs without user input, and a way to separate "what the system already knows" from "what just arrived." None of that is a UI decision; it's a data design decision. Redis Iris is genuinely shaped for this problem. Agent Memory for persistent lab context, Vector Search for similarity retrieval, Streams for the event bus, LangCache for response deduplication — these aren't forced fits. The architecture fell out naturally from the problem, and Redis Iris happened to have a managed service for each piece. That's either good product design from the Redis team or a well-chosen problem on our part. Probably both.

## README (from the GitHub repository)

# calhackathon2026

## Detected evidence (automated analysis)

Indexed codebase: 131 recognized source files, 676 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 179)

```
.dockerignore
.gitignore
baskr/.env.example
baskr/.gitignore
baskr/backend/app/__init__.py
baskr/backend/app/agent_memory.py
baskr/backend/app/batches.py
baskr/backend/app/chemrxiv_cloudflare.py
baskr/backend/app/config.py
baskr/backend/app/connections.py
baskr/backend/app/consumer.py
baskr/backend/app/embeddings.py
baskr/backend/app/engine.py
baskr/backend/app/ingest.py
baskr/backend/app/langcache.py
baskr/backend/app/llm.py
baskr/backend/app/main.py
baskr/backend/app/memory.py
baskr/backend/app/models.py
baskr/backend/app/monitoring.py
baskr/backend/app/pipeline_state.py
baskr/backend/app/producer.py
baskr/backend/app/prompts.py
baskr/backend/app/redis_client.py
baskr/backend/app/seed_profile.py
baskr/backend/app/status.py
baskr/backend/app/streams.py
baskr/backend/app/thumbnails.py
baskr/backend/requirements-dev.txt
baskr/backend/requirements.txt
baskr/backend/tests/__init__.py
baskr/backend/tests/conftest.py
baskr/backend/tests/test_api.py
baskr/backend/tests/test_consumer.py
baskr/backend/tests/test_e2e_integration.py
baskr/backend/tests/test_embeddings.py
baskr/backend/tests/test_engine.py
baskr/backend/tests/test_ingest_integration.py
baskr/backend/tests/test_llm.py
baskr/backend/tests/test_memory_semantic.py
baskr/backend/tests/test_monitoring.py
baskr/backend/tests/test_prompts.py
baskr/backend/tests/test_redis_client.py
baskr/backend/tests/test_redis_integration.py
baskr/backend/tests/test_redis_unit.py
baskr/backend/tests/test_routes.py
baskr/backend/tests/test_seed.py
baskr/backend/tests/test_smoke.py
baskr/BUILD_STATUS.md
baskr/data/digest_frozen/.gitkeep
baskr/data/digest_frozen/2026-06-18.json
baskr/data/digest_frozen/2026-06-19.json
baskr/data/digest_frozen/2026-06-20.json
baskr/data/digest_frozen/2026-06-21.json
baskr/data/profile_seed_blueprint.json
baskr/data/profile_seed.json
baskr/data/sample_papers.json
baskr/data/synthetic_intake/paper_01_answers_oq1.json
baskr/data/synthetic_intake/paper_02_answers_oq2.json
baskr/data/synthetic_intake/paper_03_answers_oq3.json
baskr/data/synthetic_intake/paper_04_answers_oq1.json
baskr/data/synthetic_intake/paper_05_answers_oq2.json
baskr/data/synthetic_intake/paper_06_contradicts_asm1.json
baskr/data/synthetic_intake/paper_07_contradicts_asm2.json
baskr/data/synthetic_intake/paper_08_contradicts_fnd2.json
baskr/data/synthetic_intake/paper_09_contradicts_fnd1.json
baskr/data/synthetic_intake/paper_10_contradicts_asm1.json
baskr/data/synthetic_intake/paper_11_extends_reinforce_fnd2.json
baskr/data/synthetic_intake/paper_12_extends_reinforce_fnd1.json
baskr/data/synthetic_intake/paper_13_extends_reinforce_fnd2.json
baskr/data/synthetic_intake/paper_14_extends_gap_fnd1.json
baskr/data/synthetic_intake/paper_15_extends_gap_asm2.json
baskr/data/synthetic_intake/paper_16_extends_gap_oq3.json
baskr/data/synthetic_intake/paper_17_notrel_vent.json
baskr/data/synthetic_intake/paper_18_notrel_parkinsons.json
baskr/data/synthetic_intake/paper_19_notrel_histopath.json
baskr/data/synthetic_intake/paper_20_notrel_soil.json
baskr/data/synthetic_intake/README.md
baskr/data/synthetic_intake/testset.json
baskr/design-explorations/card-actions-explorations.html
baskr/design-explorations/dashboard-explorations.html
baskr/design-explorations/popout-explorations.html
baskr/frontend/.gitignore
baskr/frontend/index.html
baskr/frontend/package.json
baskr/frontend/postcss.config.js
baskr/frontend/src/api.ts
baskr/frontend/src/App.tsx
baskr/frontend/src/components/ActiveSearchPanel.tsx
baskr/frontend/src/components/DigestHistoryPanel.tsx
baskr/frontend/src/components/flagged/FlaggedPapersPage.tsx
baskr/frontend/src/components/labcontext/LabContextPage.tsx
baskr/frontend/src/components/LabelBadge.tsx
baskr/frontend/src/components/LabProfilePanel.tsx
baskr/frontend/src/components/PaperCard.tsx
baskr/frontend/src/components/welcome/DocThumbnail.tsx
baskr/frontend/src/components/welcome/PaperCardGrid.tsx
baskr/frontend/src/components/welcome/PaperExpandModal.tsx
baskr/frontend/src/components/welcome/PaperThumbnail.tsx
baskr/frontend/src/components/welcome/SearchResultsPage.tsx
baskr/frontend/src/components/welcome/TopNav.tsx
baskr/frontend/src/components/welcome/usePaperActions.ts
baskr/frontend/src/components/welcome/WelcomeDashboard.tsx
baskr/frontend/src/index.css
baskr/frontend/src/labelStyles.ts
baskr/frontend/src/main.tsx
baskr/frontend/src/types.ts
baskr/frontend/tailwind.config.js
baskr/frontend/tsconfig.json
baskr/frontend/vercel.json
baskr/frontend/vite.config.ts
baskr/README.md
baskr/scripts/demo_stream.py
baskr/scripts/freeze_digest.py
baskr/scripts/gen_synthetic_intake.py
baskr/scripts/live_stream.py
baskr/scripts/seed_profiles.py
BUILD_LOOP.md
dev-ui/.gitignore
dev-ui/index.html
[59 more files omitted for size]
```

### Dependencies

- baskr/backend/requirements.txt: anthropic, fastapi, pydantic@>=2, pymupdf, python-multipart, redis, redisvl, requests, uvicorn[standard]
- baskr/frontend/package.json: @types/react@^18.3.3, @types/react-dom@^18.3.0, @vitejs/plugin-react@^4.3.1, autoprefixer@^10.4.19, postcss@^8.4.39, react@^18.3.1, react-dom@^18.3.1, tailwindcss@^3.4.4, typescript@^5.5.3, vite@^5.3.4
- dev-ui/package.json: @tailwindcss/vite@^4.1.8, @types/react@^19.1.6, @types/react-dom@^19.1.5, @vitejs/plugin-react@^4.5.2, react@^19.1.0, react-dom@^19.1.0, tailwindcss@^4.1.8, typescript@~5.8.3, vite@^6.3.5
- system_pieces/data_pipeline/requirements.txt: requests@>=2.31, sentry-sdk@>=2.0

### Recent commits (newest first)

- Merge pull request #21 from tobygodat/frontend-to-main
- Refine welcome dashboard components and add shark logo asset
- Merge pull request #20 from tobygodat/dev-will
- Merge branch 'main' into dev-will
- Add Docker + Render deploy setup and compat tests
- Add Iris Agent Memory LTM + OpenAI-embedding vector gate threshold
- Replace baskr frontend with main version
- Verify test suites and update backend/frontend config
- Merge pull request #19 from tobygodat/dev
- Merge remote-tracking branch 'origin/dev' into dev-toby
- Redesign research dashboard: cover-stack cards, pop-out, save/dismiss
- Bound classification concurrency + cheap pre-filter + rate-limit backoff
- Merge branch 'dev-experimental' into dev-will
- Add new data sources, dev-ui panels, and monitoring/consumer updates
- Merge pull request #18 from tobygodat/dev
- Merge pull request #17 from tobygodat/dev-loop-labmemory
- feat(memory): write back long-term memories after classification
- Merge remote-tracking branch 'origin/dev-will' into dev-will
- Merge remote-tracking branch 'origin/dev' into dev-will
- Point Vercel deploy at baskr/frontend

## Key source files (fetched from GitHub, selected and truncated for size)

### SPEC.md

```markdown
# Baskr — Product Spec

> **Purpose:** This document is the source of truth for scaffolding the Baskr implementation repo. It defines what to build, the data contracts, the API surface, and the target file structure. This repo (claude-chats-hack) is a planning hub — no code lives here.
>
> Cal Hacks AI 2026 · tracks: Ddoski's Lab + Anthropic + Redis.

---

## 1. Product

**Baskr** is a research radar for a lab. It holds a persistent **lab context profile** — the lab's open questions, working assumptions, and prior findings — in Redis agent memory, then classifies new PubMed papers against that profile and surfaces the ones that matter, each with a one-sentence plain-language reason.

**Two surfaces, one engine:**

| Surface | Trigger | Purpose |
|---|---|---|
| **Active Search** | User submits an open question | Live, deterministic query against today's PubMed papers |
| **Daily Digest** | Scheduled / pre-generated | Surfaces relevant papers from the day's full feed with no query |

Both surfaces call the same classification engine and reason against the same profile.

---

## 2. Locked decisions

| Axis | Decision |
|---|---|
| **Name** | Baskr |
| **Niche** | Gut microbiome (~400–600 PubMed papers/day) |
| **Relationship labels** | `ANSWERS` · `CONTRADICTS` · `EXTENDS` · `NOT_RELEVANT` |
| **Stretch label** | `SCOOP` (paper pre-empts a planned experiment) — only if `planned_experiment` items exist in the profile |
| **Demo data** | Active search hits live PubMed (last 1–7 days). Digest is pre-generated and frozen. |
| **Embeddings** | OpenAI `text-embedding-3-small` (1536 dims) |
| **Reasoning LLM** | Anthropic Claude — confirm recommended model at build time |
| **Backend** | FastAPI (Python, async) |
| **Redis** | Agent Memory (profile) · RedisVL HNSW index (papers) · LangCache (query cache) |
| **Frontend** | React + Vite + TypeScript + Tailwind |

---

## 3. Open decision

**Lab context profile source** — the content of `data/profile_seed.json`:

| Option | Trade-off |
|---|---|
| Real gut-microbiome researcher (one open question + one assumption) | Highest demo authenticity; requires external lead time |
| NIH Reporter grant abstract (2024–25 Project Summary) | Real and citable; no waiting |
| Hardcoded placeholder | Fastest; weakest demo impact |

The schema (§5.1) is identical regardless of source. Scaffold against the placeholder; swap content before the demo.

---

## 4. Fact-checks (confirm before build)

- **Redis credit code + branding** — two codes appear in planning notes (`CALHACKER2026` vs. "25k from the Live Site"). Confirm from the live sponsor page.
- **Anthropic prize rule** — confirm whether it requires Claude Code specifically (not just the API) and that usage qualifies.
- **LLM model ID** — confirm current recommended `claude-*` model at build time; do not hardcode without checking.

---

## 5. Data models

### 5.1 Lab Context Profile

Stored in Redis Agent Memory (long-term), one memory per item. Mirrored in `data/profil
[truncated — 7266 more characters]
```

### BUILD_LOOP.md

```markdown
# Baskr Build Loop — Subagent Director

> **You are the Subagent Director.** You do not write production code yourself.
> Your job is to **spin up, prompt, and manage subagents** that fill in the Baskr
> scaffolding until the project is complete, working, and visible in the dev UI.
> This file is your standing instruction set; it is re-run on an interval. Each
> run, you resume from the ledger, advance as far as the checks allow, and stop.

---

## 0. Operating contract

- **Role.** Director of subagents. Use the `Agent` tool (`subagent_type: general-purpose`
  for build/test work, `Explore` for read-only lookups) to dispatch concrete,
  self-contained tasks. Reserve your own context for planning, dispatching,
  checking, and recording state.
- **Resume, don't restart.** On every run, first read `baskr/BUILD_STATUS.md`
  (the ledger). Find the lowest phase that is not `DONE` and work it. If the
  ledger does not exist, create it from the phase list in §3.
- **Do / Check discipline.** Every phase has a **Do** set (work you dispatch to
  agents) and a **Check** set (a hard gate). A phase only becomes `DONE` when
  **every** Check passes. Never start phase _N+1_ while phase _N_ has a failing
  Check.
- **Tests are part of Do, not optional.** Every phase's Do includes writing unit
  and/or integration tests for the code it touches. The matching Check **runs**
  those tests; a failing or un-runnable test fails the Check and the phase stays
  open.
- **Everything wires to the dev UI.** There is no end-user UI yet. The dev UI
  (`dev-ui/`, Vite on :5174, proxying `/api/*` → FastAPI on :8000) is the single
  surface of truth. If a capability exists but the dev UI cannot show it, the
  phase is **not** done — extend the dev UI (build on top of it; add panels where
  a view is missing) until the capability is visible.
- **Bias to action.** Attempt as much as possible per run. Make reasonable
  decisions and proceed. Only use `AskUserQuestion` when genuinely blocked by
  something irreversible, ambiguous, or external (e.g. a missing paid credential
  with no fallback, or a contradiction between two specs you cannot resolve from
  context).
- **Document decisions.** Record every impactful architectural decision in
  `claude-chats-hack/ARCHITECTURE_DECISIONS.md` (create it if absent): one dated
  entry per decision — _context, decision, alternatives, consequence_. Examples
  that MUST be logged: the data-pipeline import-path fix (§2), the pinned Claude
  model, mock-vs-live fallback behavior, any deviation from `SPEC.md`.
- **Stop condition.** When all phases (including the Final gate in §4) are `DONE`,
  do not spin further: write a `COMPLETE` marker at the top of the ledger,
  summarize what is working / not working, and end the run. Subsequent ticks
  should no-op after confirming nothing regressed.

---

## 1. Source-of-truth documents (read before dispatching)

| Doc | Use |
|---|---|
| `SPEC.md` (repo root) | Data models, API surface (§8), Redis key map
[truncated — 11865 more characters]
```

### docker-compose.yml

```yaml
# Local dev stack: redis-stack (RediSearch/RedisVL) + the Baskr backend.
#
#   docker compose up --build
#   -> backend on http://localhost:8002  (health: /api/health)
#   -> redis-stack on localhost:6379, RedisInsight on localhost:8001
#
# Put ANTHROPIC_API_KEY / OPENAI_API_KEY (and any Iris AGENT_MEMORY_* keys) in
# baskr/.env — they are loaded via env_file below.
#
# NOTE: app/config.py rebuilds REDIS_URL from REDIS_PUBLIC_ENDPOINT + REDIS_PASSWORD
# when both are present. If your baskr/.env has those Redis Cloud vars set, comment
# them out for the local stack so the backend talks to the `redis` service here.

services:
  redis:
    image: redis/redis-stack-server:latest   # plain redis has no RediSearch -> RedisVL fails
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  backend:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8002:8002"
    environment:
      PORT: "8002"
      REDIS_URL: "redis://redis:6379"   # points at the redis service above
    env_file:
      - path: ./baskr/.env
        required: false                 # ok if the file is missing (e.g. CI)
    depends_on:
      redis:
        condition: service_healthy

volumes:
  redis-data:

```

### Dockerfile

```
# Baskr backend image.
#
# Build context MUST be this directory (calhackathon2026/) because the app imports
# BOTH `app.*` (from baskr/backend) and `system_pieces.data_pipeline` (from here).
#
#   docker build -t baskr-backend .
#
# Runs the FastAPI app + the background two-stage consumer (started via the
# FastAPI lifespan in app/main.py) in a single process.

FROM python:3.12-slim AS base

# PyMuPDF ships self-contained manylinux wheels (MuPDF statically linked), so no
# system libGL/glib is needed; everything else is pure-Python or wheels too. That
# keeps the image slim and the build free of an apt mirror round-trip.
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Install deps first for layer caching. Three requirement sets:
#   - baskr backend runtime
#   - data_pipeline (requests, sentry-sdk)
#   - redis-agent-memory: optional Iris LTM SDK, imported lazily by app/agent_memory.py
COPY baskr/backend/requirements.txt /tmp/req-backend.txt
COPY system_pieces/data_pipeline/requirements.txt /tmp/req-pipeline.txt
RUN pip install --upgrade pip \
    && pip install -r /tmp/req-backend.txt -r /tmp/req-pipeline.txt \
    && pip install redis-agent-memory

# App code. Copy the two import roots into /app:
#   /app/baskr/backend/app   -> imported as `app.*`     (cwd = baskr/backend)
#   /app/system_pieces       -> imported as `system_pieces.*` (PYTHONPATH = /app)
COPY baskr/ /app/baskr/
COPY system_pieces/ /app/system_pieces/

# `app.*` resolves from cwd; `system_pieces.*` resolves from /app on PYTHONPATH.
ENV PYTHONPATH=/app
WORKDIR /app/baskr/backend

# Render (and most PaaS) inject $PORT; default to 8002 for local `docker run`.
ENV PORT=8002
EXPOSE 8002

# Exec form via sh -c so ${PORT} still expands at runtime (Render injects PORT).
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]

```

### dev-ui/package.json

```
{
  "name": "baskr-dev-ui",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.1.8",
    "@types/react": "^19.1.6",
    "@types/react-dom": "^19.1.5",
    "@vitejs/plugin-react": "^4.5.2",
    "tailwindcss": "^4.1.8",
    "typescript": "~5.8.3",
    "vite": "^6.3.5"
  }
}

```

### system_pieces/data_pipeline/requirements.txt

```
# Data pipeline dependencies. Stdlib handles XML parsing; requests handles HTTP.
requests>=2.31

# Error monitoring — captures unhandled exceptions from the pipeline / CLI.
sentry-sdk>=2.0

```

### baskr/backend/requirements.txt

```
fastapi
python-multipart
uvicorn[standard]
pydantic>=2
redis
redisvl
anthropic
# Paper fetching is provided by implementations/data_pipeline (stdlib + requests).
requests
# First-page thumbnail rendering for /api/thumbnail (self-contained wheels).
pymupdf

```

### baskr/frontend/package.json

```
{
  "name": "baskr-frontend",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.39",
    "tailwindcss": "^3.4.4",
    "typescript": "^5.5.3",
    "vite": "^5.3.4"
  }
}

```

### dev-ui/src/main.tsx

```typescript
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

```

### system_pieces/data_pipeline/cli.py

```python
"""Command-line entry point for the data pipeline.

Examples:
    python -m implementations.data_pipeline.cli "gut microbiome immunotherapy"
    python -m implementations.data_pipeline.cli "amyloid clearance" --days 3 --sources pubmed,biorxiv
    python -m implementations.data_pipeline.cli --check        # show API-key readiness
    python -m implementations.data_pipeline.cli "tau" --json out.json
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import sys

import sentry_sdk

from .config import CONFIG
from .pipeline import DataPipeline
from .sources import SOURCE_REGISTRY


def _print_status() -> None:
    print("Data pipeline configuration / API-key readiness:\n")
    for key, val in CONFIG.status().items():
        print(f"  {key:24} {val}")
    print(f"\n  Available sources: {', '.join(sorted(SOURCE_REGISTRY))}")


def main(argv: list[str] | None = None) -> int:
    # Initialize error monitoring as early as possible so any failure during a
    # pipeline run is reported. Enabled only when SENTRY_DSN is set (see
    # .env.example); unset is a clean no-op for local/dev runs.
    sentry_dsn = os.environ.get("SENTRY_DSN")
    if sentry_dsn:
        sentry_sdk.init(
            dsn=sentry_dsn,
            # Add data like request headers and IP for users; see
            # https://docs.sentry.io/platforms/python/data-management/data-collected/
            send_default_pii=True,
        )

    parser = argparse.ArgumentParser(prog="baskr-pipeline", description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("query", nargs="?", help="search query / lab open question")
    parser.add_argument("--days", type=int, default=CONFIG.default_lookback_days,
                        help=f"lookback window in days (default {CONFIG.default_lookback_days})")
    parser.add_argument("--max", type=int, default=CONFIG.default_max_per_source,
                        dest="max_per_source", help="max papers per source")
    parser.add_argument("--sources", default=None,
                        help=f"comma-separated subset of: {','.join(SOURCE_REGISTRY)}")
    parser.add_argument("--json", dest="json_out", metavar="PATH",
                        help="write full results to a JSON file")
    parser.add_argument("--check", action="store_true", help="print key readiness and exit")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args(argv)

    logging.basicConfig(level=logging.INFO if args.verbose else logging.WARNING,
                        format="%(levelname)s %(name)s: %(message)s")

    if args.check:
        _print_status()
        return 0

    if not args.query:
        parser.error("a query is required (or use --check)")

    sources = args.sources.split(",") if args.sources else None
    pipe = DataPipeline(sources=sources)
    result = pipe.fetch(args.query, days=args.days, max_per_source=args.max_per_source)

    # console summary
    print(f"\nQuery: {args.query!r}   window: last {args.days} days")
    print(f"Sources: {result.counts}")
    if result.errors:
        print(f"Errors:  {result.errors}")
    print(f"Unique papers after dedupe: {len(result.papers)}\n")

    for i, p in enumerate(result.papers[:25], 1):
        flag = "" if p.has_abstract else "  [no abstract]"
        print(f"{i:>3}. [{p.source}] {p.citation()}{flag}")

    if args.json_out:
        payload = {
            "query": args.query,
            "days": args.days,
            "counts": result.counts,
            "errors": result.errors,
            "papers": [p.to_dict() for p in result.papers],
        }
        with open(args.json_out, "w") as fh:
            json.dump(payload, fh, indent=2, ensure_ascii=False)
        print(f"\nWrote {len(result.papers)} papers -> {args.json_out}")

    return 0


if __name__ == "__main__":
    sys.exit(main())

```

[119 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]