name: retrosynthesis_planning description: Search multi-step retrosynthesis routes from a target to stock with AiZynthFinder, then audit and rank route trees. Use for recursive planning, not mapping, forward prediction, conditions, or yield. origin: openai4s category: chemistry capabilities: network: mode: raw_required domains: []
This Skill owns one scientific problem: multi-step search from a target molecule to a declared purchasable or in-house stock. It also provides engineering review functions for the returned route trees. It does not redefine single-step prediction, forward outcome prediction, atom mapping, condition recommendation, or yield estimation as phases of one model.
Use single-step-retrosynthesis to inspect a single disconnection directly,
reaction-forward-prediction for round-trip product recovery,
reaction-atom-mapping for changed bonds on a complete reaction,
reaction-condition-recommendation for condition hypotheses, and
reaction-yield-estimation for a bounded yield-screening signal. The audited
task/model selection is in MODEL_TASKS.md.
The recommended backend is AiZynthFinder running in a separate environment. This skill keeps OpenAI4S core dependency-free: the helper module is pure stdlib and uses RDKit from the optional chemistry environment for transparent-background 2D molecule depictions. On a platform with an RDKit wheel, install both optional environments before generating visual reports:
uv sync --extra science --extra chemistry
For optional RetroChimera single-step proposals, use model_deployment.py to
verify and safely extract a reviewed public checkpoint, then call it through
SyntheseusBackend; never place model weights in the Skill directory. When a
checkpoint must be fetched from a Python cell, use its download_checkpoint
helper, which routes bytes through host.web_download; do not add a raw
urllib, requests, or socket download to the Skill.
RDKit is not required to plan, rank, or review routes. If the chemistry extra is unavailable on the current platform, omit it; the dashboard falls back to transparent local SVG placeholders while the rest of the skill remains usable.
For a frozen target set, stock, route budget, and private reference routes, use
multistep_benchmark.py. Call validate_targets and normalize_stock, run the
planner, then pass its complete output through normalize_planner_outputs and
evaluate_routes. The verifier recomputes solution state using molecule-OR and
reaction-AND semantics; a backend solved=true flag is never trusted. Preserve
search statistics, termination reason, failed routes, duplicates, and budget
violations in the intermediate artifact.
For any of the six Science Queries, use the same basename across
scenarios/queries/, scenarios/gt_codebases/, and
scenarios/openai4s_codebases/. The query is the frozen OpenAI4S input and must
describe every installed public file and field needed to align with the case.
The GT entrypoint is a reviewed evaluator reference; never label it as generated
code. The generated entrypoint must come from an actual OpenAI4S CLI run, whose
status and hashes are frozen in
scenarios/openai4s_codebases/generation_manifest.json.
Install the evaluation example with scenarios/test_cases/install.py. Run each
codebase against only the installed public/ boundary. Freeze
results/intermediate_results.json before the separate
scenarios/test_cases/evaluate.py process reads private_evaluator/. Treat
bundled cases as protocol smoke tests, never as scientific accuracy evidence.
Consult scenarios/test_cases/database_sources.json and fail closed while a
production dataset is marked not_frozen. Missing LLM credentials or failed
generation must remain explicit; copying the GT source into the generated
directory is prohibited.
Treat planner solved=true as stock closure, not reaction feasibility. For a
route proposed for deeper review, freeze one route before validation and apply
these independent checks to every reaction node:
Bounded diagnostics may compare no-condition input with a fixed number of condition beams, but must not search conditions indefinitely until the intended product appears. Record the tested budget and every result. Never report a route as experimentally ready merely because all leaves are in stock. Do not run a quarantined yield model or use its number to rescue a failed route.
Keep public artifacts path-free. Use OPENAI4S_REPOSITORY,
OPENAI4S_RETRO_MODEL_ROOT, OPENAI4S_RETRO_DATA_ROOT, and
OPENAI4S_RETRO_RUN_DIR at execution time; do not serialize usernames, home
directories, mount points, temporary directories, credentials, or environment
prefixes into reports. Model manifests should contain logical artifact IDs,
versions, licenses, sizes, and hashes.
Before sharing a reproducibility ZIP, use reproducibility_bundle.py to scan a
prepared path-free directory and build a deterministic archive. Include source,
configuration, environment specifications, manifests, summaries, and bounded
result JSON. Exclude checkpoints, stocks, caches, credentials, and complete
binary environments unless redistribution is explicitly authorized. A binary
environment archive is not a substitute for an environment specification.
This skill implements multi-step search plus route-review support:
aizynthcli command for a target SMILEShost.llm) for route, molecule, and
reaction annotationsThe knowledge graph is interactive only in a downloaded copy. Artifact
HTML is model-authored, so the daemon serves it with script-src 'none' inside
a sandbox with no allow-scripts, and the Workbench frames previews the same
way — running it on the origin that holds the session cookie is the one thing
that policy exists to prevent. In the Workbench and at /preview/<id> the
dashboard renders its panels, tables and SVG trees but the graph does not draw;
download the file and open it from the filesystem to expand, collapse and
inspect nodes. The preview says so rather than showing an empty canvas.
The dashboard is intended for chemist review and route triage. It does not claim experimental validation. Conditions, yield ranges, route verdicts, and safety notes produced by the LLM must be treated as hypotheses until checked against literature, internal ELN data, vendor availability, and expert review.
target_smiles (required) — target molecule as SMILES.config_path (required for live search) — AiZynthFinder config.yml.workdir (optional) — where to write route JSON, HTML dashboard, and
Markdown report. Defaults to the current workspace.max_routes (optional) — number of ranked routes to visualize; default 10.| Purpose | Tool |
|---|---|
| Route search | host.bash running conda run -n retro aizynthcli ... |
| Molecule lookup | host.web_fetch or host.web_search using PubChem, vendor pages, or literature |
| Visualization | render_route_tree_html(...) from this skill |
| Reporting | build_markdown_report(...) from this skill |
For publication-facing visuals, load figure-style before finalizing the HTML
or figures. This skill's dashboard follows the same principles: data-grounded
labels, limited semantic colors, explicit uncertainty, CVD-safe distinctions,
and a render-then-verify QA pass.
Create the backend once outside OpenAI4S:
MODEL_ROOT="$PWD/models"
uv run python skills/retrosynthesis_planning/reaction_model_deployment.py plan \
aizynthfinder-4.4.1 --root "$MODEL_ROOT"
conda create --prefix "$MODEL_ROOT/envs/aizynthfinder-4.4.1" python=3.11 pip -y
conda run --prefix "$MODEL_ROOT/envs/aizynthfinder-4.4.1" \
python -m pip install "aizynthfinder[all]==4.4.1"
conda run --prefix "$MODEL_ROOT/envs/aizynthfinder-4.4.1" \
download_public_data "$MODEL_ROOT/artifacts/aizynthfinder-4.4.1"
The public-data command writes a config.yml file. Snapshot the whole policy,
template, filter, stock, and config tree with reaction_model_deployment.py
snapshot, and verify that manifest before each benchmark. The registry pins
AiZynthFinder 4.4.1, its release commit, and the PyPI wheel SHA-256. Public-data
files remain separately versioned artifacts whose terms and hashes must be
reviewed after download. Keep environments, caches, models, and stock out of git.
The isolated JSON backend now executes the search directly, so Scenario 2 does not need to scrape the CLI. Supply a manifest whose SHA-256 identifies the whole reviewed public-data snapshot:
from retrosynthesis_planning.reaction_model_backends import ReactionModelBackend
planner = ReactionModelBackend(
"aizynthfinder",
manifest="/models/aizynthfinder/model-manifest.json",
python_command=("/models/aizynthfinder/env/bin/python",),
timeout_seconds=1800,
)
search = planner.plan_routes(
[{"target_id": "target-01", "target_smiles": "CC(=O)Oc1ccccc1C(=O)O"}],
config_path="/models/aizynthfinder/config.yml",
max_routes=10,
)
The result already uses the Scenario 2 boundary: one record per target with
routes, termination_reason, and search_stats. An individual target failure
returns termination_reason="backend_error" and a scrubbed diagnostic while
the remaining batch continues. The evaluator must still recompute solved state
with reaction-AND/molecule-OR semantics; it must not trust backend metadata.
from retrosynthesis_planning.kernel import (
annotate_routes_with_llm,
build_aizynth_command,
build_host_web_fetch_doi_verifier,
build_llm_annotation_prompt,
build_markdown_report,
build_molecule_structure_src,
build_pubchem_query_url,
canonicalize_smiles,
collect_molecule_briefs,
collect_reaction_evidence,
collect_reaction_briefs,
command_to_shell,
load_aizynth_routes,
normalize_routes,
OpenAI4SLLMReactionEvidenceProvider,
rank_routes,
render_route_tree_html,
)
Normalize the target and run AiZynthFinder. Set MPLCONFIGDIR to a writable
temporary directory when running through the web app so Matplotlib does not
pause on cache creation.
target = canonicalize_smiles("CC(=O)Oc1ccccc1C(=O)O") # aspirin
cmd = build_aizynth_command(
target,
config_path="~/Documents/Openai4S/retro_data/config.yml",
output_path="aspirin_routes.json",
conda_env="retro",
)
host.bash("MPLCONFIGDIR=/private/tmp/aizynth_mpl " + command_to_shell(cmd))
routes = load_aizynth_routes("aspirin_routes.json")
ranked = rank_routes(normalize_routes(routes))
for route in ranked[:5]:
print(route["rank"], route["solved"], route["score"], route["steps"])
Every report must explain each target/intermediate/terminal molecule that
appears in the displayed routes. Use collect_molecule_briefs(...) first, then
query the key molecules. At minimum, check the target and all top-route terminal
precursors in PubChem; for industrial deployment, also check vendors and
literature precedent.
briefs = collect_molecule_briefs(ranked[:10], target_smiles=target)
for brief in briefs:
print(brief["role"], brief["smiles"], brief["stock_status"])
print("query:", brief["pubchem_url"])
print("structure:", build_molecule_structure_src(brief["smiles"]))
Use host.web_fetch(brief["pubchem_url"]) or host.web_search(...) for the
important molecules, then summarize:
For a concise chemistry narrative, always ask the configured conversation LLM to annotate the displayed molecules and reactions before rendering the dashboard:
annotations = annotate_routes_with_llm(ranked[:8], llm=host.llm, target_smiles=target)
annotate_routes_with_llm(...) tolerates the usual conversation-model reply
shapes: bare JSON, a fenced block, or JSON wrapped in prose. If no JSON can be
recovered it warns and returns {}, and the dashboard falls back to its own
"Route Planning Readout" instead of failing the render. Check the warning if a
Route X card shows the readout rather than "LLM Route Analysis".
The LLM must return a human-readable reaction_type, detailed reaction
description, mechanistic rationale, bond changes, plausible conditions,
expected yield range, yield rationale, selectivity risks, safety notes, and a
validation plan for each reaction key. It must also return route-level
annotations for each displayed route: route_strategy, key_disconnections,
reaction_sequence, conditions_strategy, yield_outlook, route_risks,
recommended_next_steps, and chemist_verdict. render_route_tree_html(...,
llm=host.llm) calls the configured conversation LLM before rendering and embeds
these annotations directly into each Route X card and the interactive graph
detail panel. Treat conditions and yields as hypotheses unless the route export
or literature lookup provides experimental evidence. If AiZynthFinder reports a
backend class such as 0.0 Unrecognized, do not repeat it as the final reaction
type. Use the SMARTS/mapped reaction, policy probability, and LLM/literature
annotation to explain the disconnection.
For an industrial decision workflow, attach source-backed evidence separately from LLM annotations. Retrieve the stable keys first, then pass only evidence records that identify their source and verification state:
reaction_briefs = collect_reaction_briefs(ranked[:10])
for reaction in reaction_briefs:
print(reaction["reaction_key"], reaction["template"])
reaction_evidence = {
"reactions": {
reaction_briefs[0]["reaction_key"]: [
{
"source_type": "literature", # literature, patent, internal_eln, reaction_database, vendor
"title": "Verified source title",
"identifier": "DOI, patent, ELN run, or database record",
"url": "https://example.org/record",
"match_level": "exact_substrate", # exact_substrate, close_analog, reaction_class
"verified": True,
"conditions": {"solvent": "...", "temperature": "..."},
"yield_range": "82-88%",
"risk_flags": ["exothermic quench"],
"notes": "Reviewer-confirmed record.",
"retrieved_at": "2026-07-14",
}
]
}
}
Pass reaction_evidence=reaction_evidence to render_route_tree_html(...).
Each route then includes a Step Evidence card, and the same evidence appears in
the selected reaction node of the knowledge graph. The displayed coverage score
is a retrieval-completeness heuristic, not a probability of experimental
success. LLM-generated conditions and yields never become evidence records
automatically.
Use OpenAI4SLLMReactionEvidenceProvider when a live evidence sweep is
appropriate. It explicitly composes the configured conversation model with the
existing host.web_search and optional host.web_fetch skills: the LLM drafts
reaction-aware search queries, OpenAI4S retrieves the pages, and a second LLM
pass can select only returned source IDs. This is intentionally not an
implicit tool-call: host.llm is a text-completion API, so the provider keeps
all network activity observable and auditable.
Use build_host_web_fetch_doi_verifier(...) when DOI resolution checks are
appropriate. The adapter routes requests through the auditable
host.web_fetch capability instead of opening raw worker-network connections.
A resolving DOI does not prove that the paper supports the proposed substrate
scope. The output is therefore marked as a retrieved source candidate,
receives capped coverage, and never becomes verified exact-substrate precedent
without reviewer or ELN confirmation.
doi_verifier = build_host_web_fetch_doi_verifier(host.web_fetch)
provider = OpenAI4SLLMReactionEvidenceProvider(
llm=host.llm,
search=host.web_search,
fetch=host.web_fetch,
doi_verifier=doi_verifier, # optional; omit when DOI checks are not needed
max_reactions=10,
max_queries_per_reaction=2,
results_per_query=5,
)
reaction_evidence = collect_reaction_evidence(ranked[:10], [provider])
The provider never accepts an LLM-supplied URL or title: every card is built
from a retrieved host.web_search result. It also never auto-populates
conditions, yields, or verified=True. A chemist should promote a reviewed
candidate by adding a source record with explicit scope, conditions, yield, and
verification status before using it in execution scoring.
The HTML artifact is a self-contained dashboard: KPI summary, ranked route table, a retrosynthesis knowledge graph, molecule briefs, color-coded SVG route trees with molecule structure thumbnails, stock precursor chips, and a text outline for audit/debugging. The knowledge graph merges identical molecule nodes across displayed routes, preserves AND-OR route semantics, and supports pan, zoom, collapse/expand, node selection, neighbor highlighting, molecule structure display, LLM reaction-type display, backend class audit, policy probability, template details, and rich reaction interpretation with proposed conditions, yield caveats, risk notes, and validation steps.
AiZynthFinder's normal JSON export contains solved/top route trees, not necessarily every internal MCTS visit. The dashboard therefore visualizes the exported route hypotheses as a merged knowledge graph. If the user asks for the complete internal search tree, export an AiZynthFinder checkpoint/search graph from the backend and state which graph source was used.
Molecule structures are rendered with RDKit SVG when RDKit is installed in the
kernel; otherwise the dashboard uses transparent local SVG placeholders. Do not
use PubChem PNGs as in-dashboard molecule images; PubChem remains a query link
for lookup only. (build_pubchem_structure_image_url(...) is still exported for
external reports, but must not be embedded in the dashboard.) Reaction conditions are not predicted by AiZynthFinder route
planning; label them as not predicted unless an external condition-prediction,
literature lookup, or LLM hypothesis with explicit uncertainty provides
evidence.
html = render_route_tree_html(
ranked,
target_smiles=target,
max_routes=10,
llm=host.llm,
reaction_evidence=reaction_evidence,
)
report = build_markdown_report(ranked, target_smiles=target)
host.write_file("aspirin_retrosynthesis.html", html)
host.write_file("aspirin_retrosynthesis_report.md", report)
This skill includes an example dashboard generated from an aspirin route export:
skills/retrosynthesis_planning/examples/aspirin_retrosynthesis.html
It is regenerated from committed source data rather than hand-edited:
uv run python skills/retrosynthesis_planning/examples/build_example.py
aspirin_routes.json holds the route trees and aspirin_annotations.json the
deterministic demonstration annotations. The committed dashboard is rendered
with RDKit depictions. To regenerate it with the same molecule rendering, first
install the science and chemistry extras and then run the build command above.
Open it directly in a browser, or serve the skill directory locally:
python3 -m http.server 9876 --bind 127.0.0.1 -d skills/retrosynthesis_planning
Then visit:
http://127.0.0.1:9876/examples/aspirin_retrosynthesis.html
The aspirin example demonstrates:
The example annotations are deterministic demonstration text, not experimental evidence for aspirin manufacturing conditions.
routes = load_aizynth_routes("routes.json")
ranked = rank_routes(normalize_routes(routes))
target = (
ranked[0]["tree"].get("smiles")
if ranked and isinstance(ranked[0].get("tree"), dict)
else None
)
briefs = collect_molecule_briefs(ranked[:10], target_smiles=target)
for route in ranked[:5]:
print(route["rank"], route["solved"], route["score"], route["steps"])
print("starting materials:", ", ".join(route["starting_materials"]))
for brief in briefs:
print(brief["role"], brief["smiles"], brief["pubchem_url"])
host.write_file(
"routes.html",
render_route_tree_html(ranked, max_routes=10, target_smiles=target, llm=host.llm),
)
host.write_file("routes_report.md", build_markdown_report(ranked))
<workdir>/
├── <target>_routes.json # raw AiZynthFinder output
├── <target>_retrosynthesis.html # visual dashboard
└── <target>_retrosynthesis_report.md # route rationale + molecule briefs
For each recommended route, explicitly discuss:
Do not claim that a predicted route is experimentally validated unless the data source explicitly contains experimental evidence.
Before submitting the final answer: