Skip to content

Commit 1a97647

Browse files
authored
Merge pull request #2 from rastagan-git/agent/multi-source-aroma-skill
feat: package the crawler as a multi-source aroma data skill
2 parents 9e6dd95 + 2672ab1 commit 1a97647

46 files changed

Lines changed: 4745 additions & 780 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
name: curate-flavor-data
3+
description: Validate, normalize, enrich, and export flavor, odor, aroma, and chemical-sensory datasets with source-level provenance. Use for XLSX, CSV, or TSV workbooks containing CAS numbers, compound names, calculated retention indices, sensory descriptors, odor thresholds, or olfactory-receptor evidence; for selecting among NIST, PubChem, Pyrfume, M2OR, MFFI, and the permission-gated ChemicalBook compatibility connector; and for verifying analysis-ready outputs. Do not use to bypass access controls, relicense source data, or make biomedical or machine-learning claims unsupported by the exported evidence.
4+
---
5+
6+
# Curate Flavor Data
7+
8+
Build traceable compound tables through the repository's `flavor-data` CLI. Keep the skill as an orchestration layer; modify provider behavior in the Python package, not here.
9+
10+
## Workflow
11+
12+
1. Inspect the input without modifying it.
13+
- Run `python .agents/skills/curate-flavor-data/scripts/inspect_workbook.py INPUT` from the repository root.
14+
- Confirm the row count, exact column names, identifier quality, duplicates, and formula-like cells.
15+
2. Choose the smallest provider set that supplies the requested fields.
16+
- Read [references/provider-matrix.md](references/provider-matrix.md) before any network or browser operation.
17+
- Prefer PubChem for canonical identity and sourced odor annotations.
18+
- Use NIST only for the existing retention-index or name-resolution workflows.
19+
- Use Pyrfume only for explicitly selected archives after reviewing each manifest note.
20+
- Use M2OR only when receptor bioassay evidence is relevant; label species and assay scope.
21+
3. Preview the operation.
22+
- State the input, new output path, selected provider, expected columns, approximate request count, cache behavior, and material access caveats.
23+
- Write a sibling output by default. Do not pass `--force` or overwrite the input unless the user explicitly requests that exact replacement.
24+
4. Run one focused command.
25+
- Identity and odor metadata: `flavor-data pubchem INPUT --identifier-column "CAS Number"`
26+
- Retention indices: `flavor-data nist-ri INPUT --cas-column "CAS Number" --calculated-ri-column "Calculated RI"`
27+
- Names to CAS: `flavor-data resolve-cas INPUT --name-column "Name"`
28+
- Curated descriptors: `flavor-data pyrfume INPUT --archives aromadb,superscent`
29+
- Receptor evidence: `flavor-data m2or INPUT --cas-column "CAS Number"`
30+
- Source inventory: `flavor-data sources`
31+
5. Verify the result.
32+
- Re-run the inspection script on the output.
33+
- Confirm identical row order and row count, expected new fields, typed status counts, source URL, retrieval time, version, and license/access fields.
34+
- Treat `http_error`, `network_error`, `parse_error`, `missing_data`, `data_error`, `partial`, `blocked`, and `skipped` separately from `not_found`.
35+
- Consult [references/output-schema.md](references/output-schema.md) when reconciling columns or statuses.
36+
6. Report the output path, provider versions, status counts, partial failures, and any source terms the user must still review.
37+
38+
## Guardrails
39+
40+
- Keep NIST's interval at five seconds or slower and retain caching.
41+
- Never automate CAPTCHA solving. Keep browser sources visible when user intervention may be required.
42+
- Do not run ChemicalBook automation unless the user confirms documented permission; its current robots policy excludes the legacy routes.
43+
- Do not describe the toolkit as AI-powered. Say that structured exports can support downstream statistics, cheminformatics, or machine-learning experiments.
44+
- Do not bundle or republish downloaded Pyrfume or M2OR data in the repository.
45+
- Preserve remote text as literal spreadsheet cells to prevent formula execution.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "Curate Flavor Data"
3+
short_description: "Curate traceable odorant and flavor datasets"
4+
default_prompt: "Use $curate-flavor-data to inspect and enrich this odorant workbook with source provenance."
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Output and validation contract
2+
3+
## Identity
4+
5+
- Preserve the original input identifier.
6+
- Use PubChem CID and InChIKey as canonical joins when available.
7+
- Keep CAS as an external identifier and validate its checksum before exact-match providers.
8+
- Never silently pick an ambiguous name result.
9+
10+
## Status values
11+
12+
- `ok`: a provider returned a parsed record.
13+
- `not_found`: the provider responded successfully but no exact record matched.
14+
- `ambiguous`: more than one defensible identity matched.
15+
- `invalid_input`: the identifier is empty, malformed, or fails validation.
16+
- `network_error`: transport or remote-service failure; safe to retry later.
17+
- `http_error`: the provider returned a non-success HTTP response that was not a clean not-found result.
18+
- `parse_error`: the response arrived but its structure could not be interpreted; investigate a selector/schema change.
19+
- `missing_data`: a required local or cached snapshot is unavailable and downloading is disabled or failed before parsing.
20+
- `data_error`: every selected archive failed to load; inspect the per-archive diagnostic message.
21+
- `partial`: at least one selected source succeeded and at least one failed, or a provider returned usable data with warnings.
22+
- `blocked`: access policy or missing permission prevented the request.
23+
- `skipped`: a user deliberately skipped an interactive record.
24+
25+
Do not merge an access, transport, HTTP, snapshot, parse, or partial failure into `not_found`.
26+
27+
## Provenance
28+
29+
Keep provider status, source URL, retrieval time, cache-hit flag, pinned version or snapshot, license URL, and message columns. For PubChem odor text, also keep contributor source names, URLs, and license URLs.
30+
31+
## Workbook QA
32+
33+
After every run, confirm:
34+
35+
1. Output row count and row order equal the input.
36+
2. Original columns remain present.
37+
3. Requested output columns exist.
38+
4. Every processed row has a typed status.
39+
5. Remote strings beginning with `=`, `+`, `-`, or `@` are stored as literal text.
40+
6. Partial outputs are reported separately if a run is interrupted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Provider matrix
2+
3+
Review these notes before choosing or running a provider. Access policies can change; re-check the linked official page when a live run or redistribution decision matters.
4+
5+
| Provider | Best use | Access behavior | Rights and scientific caveat |
6+
| --- | --- | --- | --- |
7+
| [PubChem PUG REST](https://pubchem.ncbi.nlm.nih.gov/docs/pug-rest) and [PUG-View](https://pubchem.ncbi.nlm.nih.gov/docs/pug-view) | CID, InChIKey, structures, formula, properties, sourced odor annotations | Live API, cached locally, below the official five-requests-per-second ceiling, bounded retries | PubChem aggregates contributor records. Preserve contributor source, URL, and license URL for annotation text. |
8+
| [NIST Chemistry WebBook](https://webbook.nist.gov/chemistry/) | Existing GC retention-index and name-to-CAS workflows | Cached HTML, at least five seconds between uncached requests per [robots.txt](https://webbook.nist.gov/robots.txt) | NIST SRD compilation rights apply. Fetch on demand and cite; do not redistribute a bulk scrape. |
9+
| [Pyrfume Public Data Archive](https://github.com/pyrfume/pyrfume-data) | Curated odor descriptors or collection membership keyed by PubChem CID | Pinned GitHub snapshot; explicit archive allowlist; files cached locally | The repository code is MIT, but manifests record upstream rights and sometimes copyright caveats. Do not treat every archive as MIT-licensed data. |
10+
| [M2OR](https://github.com/chemosim-lab/M2OR) | Molecule-olfactory-receptor pairs, species, responsive assays, and study DOI | Optional pinned CSV download (about 43 MB), cached locally | Dataset repository is Apache-2.0. Results are assay evidence, not human odor perception or clinical prediction. |
11+
| [MFFI](https://mffi.sjtu.edu.cn/database/search) | Chinese/English names, sensory characteristics, and water thresholds | Interactive Selenium browser, conservative pacing | No public API, rate policy, or reuse license was found. Robots allowance is not a data license; use conservatively and cite. |
12+
| [ChemicalBook](https://www.chemicalbook.com/) | Original odor/threshold/type compatibility path only | Disabled until explicit permission confirmation; visible manual browser | Current [robots.txt](https://www.chemicalbook.com/robots.txt) excludes the search and product-property routes. Never bypass CAPTCHA or imply permission. |
13+
14+
Avoid automated extraction from FlavorDB2, Flavornet, Good Scents, VCF, or other attractive-but-undocumented sites unless an official API, bulk download, or written permission clearly covers the intended use.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Inspect a flavor-data workbook without modifying it."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
import sys
8+
from pathlib import Path
9+
10+
REPO_ROOT = Path(__file__).resolve().parents[4]
11+
if str(REPO_ROOT) not in sys.path:
12+
sys.path.insert(0, str(REPO_ROOT))
13+
14+
from flavor_data_crawler.excel_io import FORMULA_PREFIXES, read_table # noqa: E402
15+
from flavor_data_crawler.identifiers import is_valid_cas, normalize_cas # noqa: E402
16+
17+
18+
def inspect(path: Path, cas_column: str | None = None) -> dict[str, object]:
19+
frame = read_table(path)
20+
detected_cas = cas_column or next(
21+
(name for name in ("CAS Number", "CAS", "cas", "cas_number") if name in frame.columns),
22+
None,
23+
)
24+
formula_like = 0
25+
for column in frame.columns:
26+
formula_like += int(
27+
frame[column]
28+
.map(lambda value: isinstance(value, str) and value.startswith(FORMULA_PREFIXES))
29+
.sum()
30+
)
31+
report: dict[str, object] = {
32+
"path": str(path.resolve()),
33+
"rows": len(frame),
34+
"columns": [str(column) for column in frame.columns],
35+
"duplicate_rows": int(frame.duplicated().sum()),
36+
"formula_like_cells": formula_like,
37+
"missing_by_column": {
38+
str(column): int(frame[column].isna().sum() + frame[column].eq("").sum())
39+
for column in frame.columns
40+
},
41+
}
42+
if detected_cas:
43+
normalized = frame[detected_cas].map(normalize_cas)
44+
valid_mask = normalized.map(is_valid_cas)
45+
nonempty_mask = normalized.ne("")
46+
invalid = normalized[nonempty_mask & ~valid_mask]
47+
report["cas"] = {
48+
"column": detected_cas,
49+
"nonempty": int(nonempty_mask.sum()),
50+
"valid": int(valid_mask.sum()),
51+
"invalid": int((nonempty_mask & ~valid_mask).sum()),
52+
"invalid_examples": list(dict.fromkeys(invalid.astype(str)))[:10],
53+
"duplicates": int(normalized[nonempty_mask].duplicated().sum()),
54+
}
55+
else:
56+
report["cas"] = {"column": None, "note": "No common CAS column name detected"}
57+
return report
58+
59+
60+
def main() -> int:
61+
parser = argparse.ArgumentParser(description=__doc__)
62+
parser.add_argument("input", type=Path)
63+
parser.add_argument("--cas-column")
64+
args = parser.parse_args()
65+
print(json.dumps(inspect(args.input, args.cas_column), ensure_ascii=False, indent=2))
66+
return 0
67+
68+
69+
if __name__ == "__main__":
70+
raise SystemExit(main())

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
test:
12+
name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [ubuntu-latest, windows-latest]
18+
python-version: ["3.11", "3.13"]
19+
20+
steps:
21+
- name: Check out repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
cache: pip
29+
30+
- name: Install package and development dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
python -m pip install -e ".[dev]"
34+
35+
- name: Lint
36+
run: python -m ruff check .
37+
38+
- name: Check formatting
39+
run: python -m ruff format --check .
40+
41+
- name: Test
42+
run: python -m pytest

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.venv/
2+
venv/
3+
myenv/
4+
__pycache__/
5+
*.py[cod]
6+
*.egg-info/
7+
.pytest_cache/
8+
.ruff_cache/
9+
.coverage
10+
htmlcov/
11+
build/
12+
dist/
13+
.flavor-data-cache/

0 commit comments

Comments
 (0)