Skip to content

feat: look up compounds by name, SMILES, or InChI (#465) - #817

Open
skearnes wants to merge 1 commit into
mainfrom
feat/465-lookup-identifier-type
Open

feat: look up compounds by name, SMILES, or InChI (#465)#817
skearnes wants to merge 1 commit into
mainfrom
feat/465-lookup-identifier-type

Conversation

@skearnes

@skearnes skearnes commented Jun 23, 2026

Copy link
Copy Markdown
Member

Closes #465.

Problem

"Look up name" hardcoded identifier_type: 'name', so pasting a SMILES or InChI only resolved when an external service happened to accept it as a free-text name — hence the inconsistent "works for some structures, not others."

Changes

Frontend

  • Add a Name / SMILES / InChI selector to the lookup dialog (ComponentsLookup.tsx) and thread the chosen type through addIdentifierByName/resolve-compound.

Backend (utilities.py)

  • A SMILES is canonicalized locally (RDKit) with no remote lookup.
  • Name / InChI are resolved remotely with the type threaded through (so e.g. PubChem searches by InChI, not as a name).
  • Fix a latent 500: when every resolver fails, name_resolve_cached returns None; the old code unpacked it (resolver, smiles = None) → TypeError. Now returns a clean 400.

Tests

  • BE (utilities_test.py): SMILES short-circuit (no remote call), identifier-type passthrough, and 400-on-unresolvable.
  • FE: the thunk test asserts the selected type reaches /resolve-compound.
  • tsc -b, lint, ruff, ty all green.

Note: the live external-resolution behavior for a given InChI depends on PubChem/CACTUS and isn't unit-testable; the routing is what's covered here. A quick manual check against a real InChI is worth doing before merge.

🤖 Generated with Claude Code

Greptile Summary

This PR fixes compound lookup so that SMILES and InChI identifiers are resolved correctly instead of always being treated as names. A new identifier-type dropdown is wired end-to-end from the UI through the Redux action and thunk to the /resolve-compound endpoint, which now short-circuits SMILES to a local RDKit canonicalization and also fixes a latent 500 that occurred when all remote resolvers returned None.

  • Backend: SMILES is canonicalized locally (no external call); name/InChI are forwarded to PubChem/CACTUS/eMolecules with the type threaded through; None from exhausted resolvers now returns a 400 instead of a TypeError.
  • Frontend: ComponentsLookup gains a Name / SMILES / InChI Select; the chosen type travels through the action payload and thunk to the POST body.
  • Tests: New BE file covers the three routing branches; FE thunk test adds the InChI passthrough case.

Confidence Score: 4/5

The core routing logic is correct and well-tested; the only gaps are a missing input constraint in the Pydantic schema and a loose TypeScript type that the frontend dropdown already enforces in practice.

The SMILES short-circuit, the None-guard fix, and the type passthrough all work correctly. The main weakness is that ResolveCompoundInputs.identifier_type is an unconstrained str, so an out-of-band caller sending an unrecognized type (e.g. wrong case) gets a generic "Could not resolve" error rather than a clear validation failure. The UI dropdown prevents this in the happy path, but a Literal constraint would close the gap. Everything else — the thunk change, action typing, and tests — is straightforward.

ord_app/service_api/schemas/utilites.py deserves a second look to add the Literal constraint on identifier_type; ComponentsLookup.tsx has a minor UX inaccuracy in the description text when SMILES is selected.

Important Files Changed

Filename Overview
ord_app/service_api/resources/v1/utilities.py Adds SMILES short-circuit (local RDKit canonicalization) and fixes latent 500 when all resolvers return None; logic is correct but identifier_type is unconstrained in the schema.
ord_app/service_api/schemas/utilites.py ResolveCompoundInputs.identifier_type is a plain str with no enum/Literal constraint, allowing arbitrary values to pass Pydantic validation and reach remote resolvers.
ord_app/service_api/resources/v1/utilities_test.py New test file covering SMILES short-circuit, identifier-type passthrough, and the 400-on-unresolvable fix; all three paths are well-covered.
ui/src/features/reactions/ReactionEntities/entityFormConfiguration/components/CustomIdentifiers/ComponentsLookup/ComponentsLookup.tsx Adds identifier-type selector and wires it through the form; description text claiming external database search is shown even when SMILES (local-only) is selected.
ui/src/store/entities/reactions/reactionsInputs/reactionInputs.actions.ts Adds identifierType to the action payload; typed as string instead of a narrower literal union.
ui/src/store/entities/reactions/reactionsInputs/reactionInputs.thunks.ts Replaces hardcoded 'name' with the user-supplied identifierType in the /resolve-compound POST body; change is minimal and correct.
ui/src/store/entities/reactions/reactionsInputs/reactionInputs.thunks.test.ts Extends existing tests with identifierType and adds a new case verifying InChI type is forwarded to the resolver endpoint.

Comments Outside Diff (1)

  1. ui/src/features/reactions/ReactionEntities/entityFormConfiguration/components/CustomIdentifiers/ComponentsLookup/ComponentsLookup.tsx, line 101-122 (link)

    P2 Description text is inaccurate when SMILES is selected

    When the user picks "SMILES", the text "Searching in PubChem, CACTUS, eMolecules databases" remains visible even though the SMILES path never contacts those services — it is canonicalized locally. Consider hiding or replacing the databases sentence conditionally on form.values.identifierType === 'smiles'.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "feat: look up compounds by name, SMILES,..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

"Look up name" previously hardcoded identifier_type=name, so pasting a SMILES or
InChI only resolved when an external service happened to accept it as a name.

- FE: add a Name/SMILES/InChI selector to the lookup dialog and thread the chosen
  type through addIdentifierByName to /resolve-compound.
- BE: resolve_compound canonicalizes a SMILES locally (no remote lookup) and
  threads the type to the resolvers for name/InChI; also returns a clean 400
  instead of a 500 when every resolver fails (the None-unpack bug).

Tests: BE resolve_compound (SMILES short-circuit, type passthrough, 400 on miss);
FE thunk passes the selected type through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pulumi

pulumi Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🤖 Pulumi Neo didn't review this pull request: no Pulumi preview ran for it. Agentic reviews require a preview of the affected stacks (for example from your CI's pulumi preview).

@sonarqubecloud

Copy link
Copy Markdown

Comment on lines +27 to +28
// Resolver identifier type — 'name' | 'smiles' | 'inchi' (#465).
identifierType: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The identifierType field is typed as string, which is looser than what the backend accepts and what the UI dropdown enforces. Narrowing it to a union literal lets the TypeScript compiler catch any mismatch at the call sites.

Suggested change
// Resolver identifier type — 'name' | 'smiles' | 'inchi' (#465).
identifierType: string;
// Resolver identifier type (#465).
identifierType: 'name' | 'smiles' | 'inchi';

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Look up name" doesn't perform searches in the database using InChI or certain SMILES.

1 participant