Edit on GitHub

starplast.chat

An assistant pane that can see the map.

The same shape as the AI console in spaCR: shell out to whichever vendor coding-agent CLI the user already has logged in (claude, codex, gemini) rather than asking for an API key. Those CLIs authenticate against a chat subscription, so there is no key to store, leak, or forget to revoke, and the one in this repository stays a viewer rather than becoming a thing that bills people.

What makes it worth having here rather than in a browser tab is context. The panel builds a briefing from the actual state -- the selected gene and its evidence, the coloring, how many genes are filtered out, and the standing caveats about what this map does and does not recover -- so a question about "this gene" has a referent. An assistant that cannot see the screen answers about Toxoplasma in general, which the user can already get anywhere.

The caveats are not decoration. The held-out search says localization is recovered worse than study effort, so an assistant left to free-associate about a gene's neighbours would produce exactly the overclaim the rest of the project is built to prevent.

GROUNDING = 'You are answering inside starplast, a gene-evidence browser for Toxoplasma and Plasmodium.\nUse the organism, source measurements and analysis supplied with this question.\n\nThe packaged maps use a saved balanced feature recipe with numeric biological measurements,\nmedian imputation and robust scaling. Categorical compartment labels are not one-hot encoded;\nnumeric localization confidence can still be an input. No literature column is an input, but\nmeasurement coverage and assay selection can still shape the map. The archive records the\nactual features, algorithm, backend and ordered gene IDs. An interactive recipe can differ.\n\nThe display is exploratory, not an independently validated function prediction. Do not infer\nlocalization, interaction or mechanism from proximity alone. Guided predictions use separate\ngroup-held-out evaluation, target-derived input exclusions and calibration where labels permit.\nUse the actual exported run for performance claims; do not reuse historical cluster-recovery\nfigures as if they measured a newly fitted model. Distinguish measured evidence, transferred\nannotations and model hypotheses. Check performance against a shuffled-label negative control.\nUnsupported genes receive no call; grey means unknown.\n\nDescribe the evidence and its source. Say when a requested inference is unsupported. Prefer\n"these genes are near each other in this embedding" over "these genes are related". Be brief.'
class Provider:

A vendor CLI that can answer a prompt non-interactively.

Provider(name: str, label: str, cli: str, hint: str, login: str)
def available(self) -> bool:

Whether this provider's CLI is on PATH.

def argv(self, prompt: str, system: str) -> list[str]:

The command line that asks this CLI one question non-interactively.

PROVIDERS = [<Provider object>, <Provider object>, <Provider object>]
def available_providers() -> list[Provider]:

Providers whose CLI is actually on PATH, in preference order.

def stream( provider: Provider, prompt: str, system: str, timeout: int = 180, register=None) -> Iterator[str]:

Yield stdout chunks from the CLI. Errors are yielded as text, never raised.

This runs on a worker thread feeding a widget, and an exception there is either a silent dead panel or a crash. A message the user can read is strictly better than both.

Reading happens on a helper thread feeding a queue, rather than by iterating readline() here. That looks like the long way round and is the only version where timeout means anything: a CLI that hangs without printing never returns from readline, so a timeout applied to the wait() afterwards is never reached and the panel waits forever on a process that will never speak.

register receives the Popen as soon as it exists, so a caller can kill it. Without that, "stop" can only set a flag that is checked between chunks -- and a thread blocked on a silent process never reaches the check, so closing the window would leave a live thread and abort the process.

class ChatPanel(PyQt6.QtWidgets.QWidget):

The assistant pane. context_provider is a callable returning a string about the map.

ChatPanel(context_provider=None, parent=None)
context_provider
provider_box
send_btn
log
input
def refresh_providers(self):

Repopulate the provider list from what is actually installed.

def current_provider(self) -> Optional[Provider]:

The selected provider, or None when nothing is installed.

def system_prompt(self) -> str:

The standing caveats plus a briefing on what is currently on screen.

def send(self):

Send the question in the input box, streaming the reply.

def stop(self):

Stop a reply in progress, killing the child process so it actually stops.

def closeEvent(self, ev):

closeEvent(self, a0: QCloseEvent|None)