Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,4 @@ packages/crow-integration-tests/errors/


# Other
./CLAUDE.md
./CLAUDE.md
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ repos:
rev: v0.24.2
hooks:
- id: toml-sort-fix
exclude: ^uv\.lock$
- repo: https://github.com/crate-ci/typos
rev: v1.30.3
hooks:
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ See our [blog](https://www.futurehouse.org/research-announcements/demonstrating-

- **Python:** Version 3.12 or higher.
- **API Keys:**
- `EDISON_API_KEY`: For accessing Edison platform agents (Crow, Falcon - now called 'Literature'). Obtain from https://platform.edisonscientific.com/profile. You must first create an Edison profile, purchase credits and then create an API key (Account -> Profile -> API Tokens).
- `EDISON_API_KEY`: For accessing Edison platform agents (Crow, Falcon - now called 'Literature'). Obtain from https://platform.edisonscientific.com/profile. You must first create an Edison profile, purchase credits and then create an API key (Account -> Profile -> API Tokens).
- An API key for your chosen LLM provider (e.g., `OPENAI_API_KEY` if using OpenAI models). Robin uses LiteLLM, so it can support various providers.
- The data analysis portion of this repo requires access to the Edison platform. Without access, all the hypothesis and experiment generation code can still be run.

Expand All @@ -19,15 +19,18 @@ Docker is a tool that packages software into a self-contained "container" that r
For a fully self-contained environment that avoids OS-level dependency conflicts, Docker is the recommended approach:

1. **Build the image:**

```bash
docker build -t robin .
```

2. **Set up API keys:**

```bash
cp .env.example .env
# Edit .env and fill in your EDISON_API_KEY and OPENAI_API_KEY
```

Important: do **not** wrap values in quotes (e.g. `OPENAI_API_KEY=sk-abc123`, not `OPENAI_API_KEY="sk-abc123"`). Docker reads the file differently from Python and will include the quotes as part of the key.

3. **Run Jupyter:**
Expand Down Expand Up @@ -159,6 +162,6 @@ These example outputs are provided to help users to understand the depth, format

## Advanced Usage

A full example trajectory of both the initial therapeutic candidate generation and experimental data analysis can be found in the `robin_full.ipynb` notebook. This notebook includes the parameters and agents used in the paper.
A full example trajectory of both the initial therapeutic candidate generation and experimental data analysis can be found in the `robin_full.ipynb` notebook. This notebook includes the parameters and agents used in the paper.

While this guide focuses on the `robin_demo.ipynb` notebook, the `robin` Python module (in the `robin/` directory) can be imported and its functions (`experimental_assay`, `therapeutic_candidates`, `data_analysis`) can be used programmatically in your own Python scripts for more customized workflows.
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ dependencies = [
"aiofiles",
"anthropic",
"choix",
"edison-client>=0.11",
"fhaviary",
"fhlmi",
"edison-client>=0.11",
"openai>=1",
"pandas>=2",
"pydantic>=2",
"openai>=1",
"pandas>=2",
"pydantic>=2",
"python-dotenv",
"tqdm",
]
Expand Down
9 changes: 6 additions & 3 deletions robin/assays.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import re
from pathlib import Path
from typing import cast

Expand Down Expand Up @@ -113,15 +114,17 @@ async def experimental_assay(configuration: RobinConfiguration) -> str | None:

response_text = cast(str, experimental_assay_ideas.text)
if not response_text.strip():
raise ValueError("LLM returned an empty response during assay proposal generation.")
raise ValueError(
"LLM returned an empty response during assay proposal generation."
)
try:
assay_idea_json = json.loads(response_text)
except json.JSONDecodeError:
except json.JSONDecodeError as err:
match = re.search(r"\[.*\]", response_text, re.DOTALL)
if not match:
raise ValueError(
f"LLM response did not contain a JSON array. Response: {response_text[:200]}"
)
) from err
assay_idea_json = json.loads(match.group())
assay_idea_list = format_assay_ideas(assay_idea_json)

Expand Down
9 changes: 5 additions & 4 deletions robin/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import os
import re
from datetime import datetime
from typing import Any

from dotenv import load_dotenv
from edison_client import EdisonClient, JobNames
from lmi import LiteLLMModel
from pydantic import BaseModel, Field, PrivateAttr, model_validator

load_dotenv()

from .prompts import (
ANALYSIS_QUERIES,
ASSAY_HYPOTHESIS_FORMAT,
Expand Down Expand Up @@ -44,6 +43,8 @@
SYNTHESIZE_USER_CONTENT,
)

load_dotenv()

_DEFAULT_LLM_CONFIG_DATA = {
"model_list": [
{
Expand All @@ -58,9 +59,9 @@
}


def get_default_llm_config():
def get_default_llm_config() -> dict[str, Any]:
# Key is read on each instantiation so env vars set after import are picked up.
data = copy.deepcopy(_DEFAULT_LLM_CONFIG_DATA)
data: dict[str, Any] = copy.deepcopy(_DEFAULT_LLM_CONFIG_DATA)
data["model_list"][0]["litellm_params"]["api_key"] = os.getenv(
"OPENAI_API_KEY", "insert_openai_key_here"
)
Expand Down
2 changes: 1 addition & 1 deletion robin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ def parse_custom_tuple_string(s): # noqa: PLR0911
game_scores.append(None)
else:
game_scores.append(None)
processed_ranking_results["Game Score"] = game_scores
processed_ranking_results["Game Score"] = cast(Any, game_scores)

processed_ranking_results = processed_ranking_results.dropna(subset=["Game Score"])

Expand Down
Loading
Loading