Skip to content

Add LLMRecommender for LLM-based experimental design - #855

Open
tobiasploetz wants to merge 44 commits into
mainfrom
feature/llm-recommender
Open

Add LLMRecommender for LLM-based experimental design#855
tobiasploetz wants to merge 44 commits into
mainfrom
feature/llm-recommender

Conversation

@tobiasploetz

@tobiasploetz tobiasploetz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Add LLMRecommender for LLM-based experimental design

Summary

Adds an LLMRecommender that queries a large language model (via LiteLLM) to propose experiments. It is primarily intended for warm-starting a campaign: before enough data exists to train a surrogate, the model can use parameter semantics and domain/literature priors to suggest more sensible points than random sampling. It slots into the recommender hierarchy like any other pure recommender and is typically combined with a Bayesian recommender via a TwoPhaseMetaRecommender.

Ported from #561 and substantially reworked for the current codebase.

What it does

  • Builds a natural-language prompt from the search space (parameter names, types, ranges/allowed values, and any metadata descriptions/units), the textual experiment_description / objective_description, the structured objective, and any collected measurements / pending_experiments.
  • Sends the prompt to the configured model through LiteLLM, parses the returned JSON into candidate experiments, and validates them against the search space.
  • Recovers from malformed responses via a one-shot correction prompt, and tolerates models that wrap JSON in Markdown fences or surrounding prose.

Key design decisions

  • Inherits directly from PureRecommender. It legitimately consumes measurements/pending_experiments for the prompt but is neither non-predictive nor Bayesian. recommend() is fully overridden and builds the result directly from the model response, so the recommend* hooks are intentionally not implemented.
  • Credentials are not a field. LiteLLM reads provider keys from environment variables (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY) selected by the model prefix — so there is deliberately no API-key attribute.
  • Scope kept tight. There were some fields that were not really necessary for the first version like related_data or a feasibility check. These have been removed.
  • Module layout. Prompt construction (_prompts.py) and response parsing/validation (_parsing.py) live in dedicated submodules; llm.py holds the recommender class. litellm/jinja2 are lazy-imported so import baybe stays light.

Testing

  • Unit tests are fully mocked — no real LLM calls in CI (@pytest.mark.skipif(not LLM_INSTALLED)); they skip cleanly under coretest.
  • Covers success paths, prompt inclusion of measurements/pending/objective, the full set of parse-error branches, code-fence/prose stripping, error wrapping, and a serialization roundtrip.
  • LLMRecommender is excluded from the automatic get_subclasses test enumerations (it has required constructor args) via an explicit, documented helper in conftest.py.

Docs & example

  • New "LLM Recommenders" section in docs/components/recommenders.md.
  • Runnable example (examples/LLM/llm_recommender.py) comparing an LLM warm-start (then Bayesian) against BayBE's default recommender and a random baseline on the direct-arylation dataset; it mocks the LLM when no credentials are present, and the committed comparison images are pre-generated (the docs build is mocked).

@tobiasploetz tobiasploetz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Converting discussion comments into review threads for easier inline discussion.

Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread tests/test_llm_recommender.py
Comment thread tests/test_llm_recommender.py
Comment thread tests/serialization/test_naive_hybrid_serialization.py Outdated
@AVHopp

AVHopp commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@Scienfitz @AdrianSosic @kalama-ai I will do the first round of reviews/feedback, feel free to ignore until we move it out of draft :)

@AVHopp AVHopp added the enhancement Expand / change existing functionality label Jul 8, 2026

@AVHopp AVHopp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

First round of comments. I hope that impementing those will solve the seralization issues as well as the most important design choice such that we can then see the CI in action. Note that I did not yet investigate the tests or the inner workings of the class in detail, but I think that doing small iterations like this is better than me providing too many comments :)

Comment thread baybe/_optional/info.py
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread tests/serialization/test_naive_hybrid_serialization.py Outdated
Comment thread pyproject.toml Outdated
Comment thread tests/test_llm_recommender.py
Comment thread baybe/_optional/llm.py
Comment thread baybe/recommenders/pure/llm/llm.py
@AVHopp AVHopp self-assigned this Jul 29, 2026
@AVHopp
AVHopp force-pushed the feature/llm-recommender branch from e419a9d to f62c14b Compare July 29, 2026 10:52
@AVHopp
AVHopp force-pushed the feature/llm-recommender branch from 8184fa5 to 84ed98c Compare August 11, 2026 14:49
@AVHopp
AVHopp marked this pull request as ready for review August 11, 2026 15:08
@AVHopp
AVHopp requested a review from AdrianSosic as a code owner August 11, 2026 15:08
Copilot AI lite review requested due to automatic review settings August 11, 2026 15:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an LLMRecommender to BayBE’s pure recommender stack, enabling LLM-driven experiment suggestions (via LiteLLM) for warm-starting campaigns before surrogate-based Bayesian optimization becomes effective.

Changes:

  • Add LLMRecommender implementation with prompt construction and robust JSON response parsing (including one-shot recovery).
  • Add optional dependency group llm (Jinja2 + LiteLLM) plus optional-import plumbing and mypy config.
  • Add tests, documentation, and an end-to-end example (with pre-generated comparison plots) and adjust iteration tests to avoid auto-instantiating non-default-constructible recommenders.

Reviewed changes

Copilot reviewed 19 out of 22 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
baybe/recommenders/pure/llm/llm.py Implements LLMRecommender with LiteLLM call, recovery flow, and prompt/parse integration.
baybe/recommenders/pure/llm/_prompts.py Builds Jinja2 prompts from search space + descriptions + optional measurements/objective.
baybe/recommenders/pure/llm/_parsing.py Extracts/validates JSON suggestions into a DataFrame.
baybe/recommenders/pure/llm/__init__.py Exposes LLMRecommender from the llm subpackage.
baybe/recommenders/pure/__init__.py Re-exports LLMRecommender in pure recommenders.
baybe/recommenders/__init__.py Re-exports LLMRecommender at the recommender package level.
baybe/_optional/llm.py Adds optional imports for Jinja2 Template + LiteLLM completion.
baybe/_optional/info.py Adds LLM_INSTALLED check based on Jinja2 + LiteLLM presence.
baybe/exceptions.py Adds LLMResponseError and LLMResponseWarning.
pyproject.toml Adds baybe[llm] extra with jinja2 + litellm.
mypy.ini Adds mypy ignore section for litellm.*.
tests/test_llm_recommender.py Adds mocked unit tests for prompting, parsing branches, recovery, and warnings.
tests/serialization/test_llm_recommender_serialization.py Adds serialization roundtrip test via meta recommender wrapper.
tests/conftest.py Adds helper to exclude non-default-constructible recommenders from subclass enumeration tests.
tests/test_iterations.py Switches subclass enumeration to the new default-constructible helper.
docs/components/recommenders.md Documents the new LLM recommender and the baybe[llm] extra.
examples/LLM/llm_recommender.py Adds runnable warm-start example with mock fallback for CI/offline runs.
examples/LLM/LLM_Header.md Adds header page for the LLM examples section.
examples/LLM/llm_recommender_light.svg Adds pre-generated plot asset (light theme).
examples/LLM/llm_recommender_dark.svg Adds pre-generated plot asset (dark theme).
CHANGELOG.md Adds changelog entry for LLMRecommender.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread baybe/recommenders/pure/llm/llm.py
Comment thread baybe/recommenders/pure/llm/_parsing.py

@Scienfitz Scienfitz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

currently does not use all available metadata

Comment thread pyproject.toml
Comment thread tests/conftest.py Outdated
Comment thread tests/conftest.py Outdated
serialized through an enclosing (meta) recommender, which is what we exercise here.
Serialization does not touch LiteLLM, so no optional dependencies are required.
"""
recommender = TwoPhaseMetaRecommender(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

any particular reason why this si going via TwoPhaseMetaRecommender and not hjust smply via LLMRecommender?

normally Id expect a hypothesis strat and a serialziaiton test absed on that, why was it don different here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes - none of our recommenders can be serialized directly as none of them uses the SerialMixin. Not sure if intentional, and until this has been made clear, I'll just wrap it inside the meta recommender, but already added a hypothesis strategy for the LLMRecommender.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hmmm im clearly missing something here but if the pure reocmmenders cant be serialzied how can the meta-recommender usign them be serialized?

the fact that pure recommenders cannot be serialized is proably

  • a bug or forgotten thing
  • on purpose due to something only @AdrianSosic remembers

Comment thread baybe/recommenders/pure/llm/_prompts.py
Comment thread baybe/recommenders/pure/llm/_parsing.py
Comment thread baybe/recommenders/pure/llm/llm.py
Comment thread baybe/recommenders/pure/llm/_prompts.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py Outdated
Comment thread baybe/recommenders/pure/llm/llm.py
tobiasploetz and others added 15 commits August 21, 2026 09:44
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements a recommender that uses LiteLLM to query language models for
experimental design suggestions. Leverages the metadata system to
auto-extract parameter descriptions from the search space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Inherit from NonPredictiveRecommender instead of RecommenderProtocol
- Add field validators for required string fields
- Improve error handling in _attempt_recovery (split broad except)
- Raise IncompatibilityError for unsupported parameter types
- Add batch_size count validation with warning
- Improve error messages with invalid/allowed values detail
- Add Raises section to recommend() docstring
- Fix dict type annotations to dict[str, Any]
- Add LLMRecommender to pure/__init__.py for consistent hierarchy
- Refactor tests: parametrize error cases, use batch_size=3,
  add NumericalDiscreteParameter, test related_data/recovery_model/
  format_instructions, test feasibility edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add pending_experiments rejection and objective warning in recommend()
  to match NonPredictiveRecommender contract
- Guard test_naive_hybrid_serialization against classes with required args
- Split optional imports to give accurate error for jinja2 vs litellm
- Add overflow_experiments ge(0) validator
- Add litellm_args defensive copy and reserved-key validation
- Add empty suggestions guard in _parse_llm_response
- Add math.isfinite check for continuous parameter values
- Add discrete parameter canonical type coercion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NonPredictiveRecommender rejects pending experiments and ignores
measurements, but the LLMRecommender feeds both into the prompt. Inherit
from PureRecommender instead and fully override recommend accordingly.
The field only existed to over-generate suggestions for feasibility
filtering, which no longer exists. Request exactly `batch_size` suggestions.
The recommender has required constructor arguments and cannot be
default-constructed, so route the PureRecommender enumerations through a
dedicated exclusion helper
@AVHopp
AVHopp force-pushed the feature/llm-recommender branch from 560855c to 80ff257 Compare August 21, 2026 07:45
AVHopp added 7 commits August 21, 2026 10:33
Necessary since there is a `# type: ignore` inside of
the `__init__.py` file of `litellm`. This has been
fixed in `litellm` version 1.97.0, so this can be
removed once we bump to that version.
Since none of our current recommenders currently uses
our `SerialMixin`, the recommender is wrapped in a
meta recommender.
The recommender now uses the description of the objective
itself insted of exponsing an additional field for it.
Furthermore, the targets as well as their metadata is now
being used within the recommender.
As a consequence, the prompt template was also extended
such that it contains whatever is in the `misc` field
of the metadata dict.
For discrete constraints, we check for validity
using the existing mechanisms and raise an
`LLMResponseError` if violated. Continuous constraints
can currently not be validated, so an `LLMResponseWarning`
is issued if any such constraint is present.
@AVHopp
AVHopp force-pushed the feature/llm-recommender branch from 6720e6a to 54a9150 Compare August 21, 2026 14:04
Inspired by how the same issue is handled in the
hybrid recommender.
@AVHopp
AVHopp force-pushed the feature/llm-recommender branch from 6e2022a to b1c8cb4 Compare August 21, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Expand / change existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants