Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
d742e15
Add LLM optional dependency infrastructure
tobiasploetz Jul 7, 2026
e6d36c6
Add LLMResponseError exception
tobiasploetz Jul 7, 2026
feb82b2
Add LLMRecommender for LLM-based experimental design
tobiasploetz Jul 7, 2026
8d2bb13
Add tests for LLMRecommender
tobiasploetz Jul 7, 2026
d443bbb
Update CHANGELOG
tobiasploetz Jul 7, 2026
b7f01d6
Update LLMRecommender model version to gpt-5.4 in tests
tobiasploetz Jul 7, 2026
831d0a5
Address PR review findings for LLMRecommender
tobiasploetz Jul 8, 2026
8418b20
Address review findings for LLMRecommender
tobiasploetz Jul 8, 2026
ca1d3ff
fix mypy error and exclude problematic litellm versions
tobiasploetz Jul 8, 2026
6ef7974
Derive LLMRecommender from PureRecommender
AVHopp Jul 29, 2026
7019b3e
Remove concept of feasibility check from `LLMRecommender`
AVHopp Jul 29, 2026
de839ed
Remove overflow_experiments from LLMRecommender
AVHopp Jul 29, 2026
4ddef5f
Feed objective into LLMRecommender prompt
AVHopp Jul 29, 2026
6275385
Exclude LLMRecommender from recommender auto-enumeration in tests
AVHopp Jul 30, 2026
e861583
Add parse-error test cases
AVHopp Jul 30, 2026
4b934c1
Add helper for extracting JSON from response
AVHopp Jul 30, 2026
c146340
Remove related_data field
AVHopp Jul 30, 2026
43cb52c
Add serialization test for LLMRecommender
AVHopp Jul 30, 2026
f2fd5be
Remove test for related data
AVHopp Jul 30, 2026
e91b16c
Reorganize subpackage
AVHopp Jul 30, 2026
04fd78a
Bump version of litellm
AVHopp Jul 30, 2026
ef83fa1
Add example
AVHopp Jul 30, 2026
f58d911
Update user guide
AVHopp Jul 30, 2026
5456c64
Add test for JSON parsing
AVHopp Jul 30, 2026
69ea49b
Update CHANGELOG
AVHopp Jul 30, 2026
0ffb204
Reduce number of warmstart experiments
AVHopp Jul 30, 2026
461c3ae
Reduce number of MC iterations to 1
AVHopp Jul 30, 2026
ca0312d
Deactivate parallel simulation for LLMRecommender example
AVHopp Jul 30, 2026
2f435a2
Fix broken link in example
AVHopp Jul 30, 2026
6d43304
Replace frozenset by set
AVHopp Aug 20, 2026
4398ace
Rename helper for autotestable recommenders
AVHopp Aug 20, 2026
1045760
Use `is not None` instead of `or` for checking recovery_litellm_args
AVHopp Aug 20, 2026
c516809
Remove option to provide format constructions
AVHopp Aug 20, 2026
67b7fda
Prevent the usage of preserved key in construction
AVHopp Aug 20, 2026
134e655
Add missing validation
AVHopp Aug 20, 2026
f17c84f
Unify tests for handling protected keys
AVHopp Aug 21, 2026
8120b0a
Add `litellm` to list for `replace-imports-with-any`
AVHopp Aug 21, 2026
6021e35
Raise instead of warn if too few recommendations are returned
AVHopp Aug 21, 2026
ae91603
Add hypothesis strategies and tests for `LLMRecommender`
AVHopp Aug 21, 2026
dc58144
Fix test about raising errors
AVHopp Aug 21, 2026
01030eb
Improve metadata usage in `LLMRecommender`
AVHopp Aug 21, 2026
2624c5b
Pass full dict of metadata to the prompt
AVHopp Aug 21, 2026
ca350ec
Handle constraints for LLM responses
AVHopp Aug 21, 2026
e9adf55
Align index of returned recommendations with search space
AVHopp Aug 21, 2026
81fe399
Re-use existing validation mechanisms for LLM response
AVHopp Sep 3, 2026
eb3f9dc
Match against actual candidates, not the full space
AVHopp Sep 3, 2026
7a729a8
Add `SerialMixin` to `LLMRecommender`
AVHopp Sep 3, 2026
7d5bc9b
Simplify methods for parsing responses and building prompts
AVHopp Sep 3, 2026
c6b5f88
Rename `build_*`-named functions to `make_*`-named functions
AVHopp Sep 3, 2026
ef3cf8a
Replace hardcoded names
AVHopp Sep 3, 2026
af7f06e
Simplify comments and docstring
AVHopp Sep 3, 2026
06a0dfa
Remove unnecessary check
AVHopp Sep 3, 2026
469a3d2
Generalize type
AVHopp Sep 3, 2026
394b02b
Introduce more fine-grained LLM error handling
AVHopp Sep 3, 2026
f142246
Remove user-configurable recovery functionality
AVHopp Sep 3, 2026
8aa9c6b
Use recovery instructions from errors
AVHopp Sep 3, 2026
dfe3b53
Rework LLM recovery into a single error-specific retry
AVHopp Sep 3, 2026
f1d8cb0
Improve type safety of LLM prompt construction and parsing
AVHopp Sep 4, 2026
8d15f8e
Add convenience functions for LLM recommender
AVHopp Sep 7, 2026
4cb94f6
Fix JSON extraction for multi-block LLM responses
AVHopp Sep 7, 2026
031f307
Add type guard
AVHopp Sep 7, 2026
2c27aa6
Add typed converter
AVHopp Sep 7, 2026
578afa4
Fix typing
AVHopp Sep 7, 2026
0a66350
Use DiscreteSelectionConstraint instead of deprecated DiscreteExclude…
AVHopp Sep 7, 2026
39f7ced
Add forbidden configurations into prompt
AVHopp Sep 7, 2026
9d9adcf
Align `make_prompt` signature with the canonical recommender argument…
AVHopp Sep 7, 2026
b1647ca
Validate type of `litellm_args` keys
AVHopp Sep 7, 2026
7b254f2
Enable handling of batch sizes >1
AVHopp Sep 10, 2026
ab29162
Move CHANGELOG entry to Unreleased section
AVHopp Oct 6, 2026
3b35a86
Improve wording and handling of forbidden configurations
AVHopp Oct 6, 2026
41c7145
Improve docstrings
AVHopp Oct 6, 2026
4a455d9
Change type of `recommenders` to `tuple` in `SequentialMetaRecommender`
AVHopp Oct 7, 2026
6af4ea0
Improve and fix parameter representation in prompts
AVHopp Oct 7, 2026
089c36b
Delete `LLMRecommender` tests
AVHopp Oct 7, 2026
bf3f3b8
Bump `litellm` version to 1.97.0
AVHopp Oct 8, 2026
77b4b2e
Rework and improve LLM-related exceptions and error handling
AVHopp Oct 8, 2026
1a3ec1a
Implement `_recommend_hybrid` instead of overriding `recommend`
AVHopp Oct 8, 2026
81484b2
Remove examples and docs on LLMs
AVHopp Oct 8, 2026
eb4768a
Create convenience LLM-based meta recommenders
AVHopp Oct 8, 2026
60afb19
Provide full context in recovery prompt
AVHopp Oct 8, 2026
0d90d46
Improve handling of targets and objectives in prompt
AVHopp Oct 8, 2026
233f862
Simplify the JSON array extraction
AVHopp Oct 9, 2026
a0e0ece
Remove dead checks
AVHopp Oct 9, 2026
a2b78e6
Move model query inside of `try` block
AVHopp Oct 9, 2026
88eaf2d
Improve handling of point matching
AVHopp Oct 9, 2026
75ca275
Force mode to be cyclic in LLMAlternatingRecommender
AVHopp Oct 9, 2026
762fd06
Improve spacing in prompt
AVHopp Oct 9, 2026
a3980df
Add tests for LLM-related code parts
AVHopp Oct 9, 2026
c60cbb8
Fix typing issues in `get_`- and `find-subclass` methods
AVHopp Oct 12, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Comment thread
AVHopp marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `df_apply_permutation_augmentation` has a different interface and now expects
permutation groups instead of column groups
- `ParameterSelectorProtocol.__call__` now declares its input as positional-only
- `SequentialMetaRecommender.recommenders` is now a `tuple` instead of a `list`, making
the attribute immutable. Serialization is unaffected

### Fixed
- `DiscretePermutationInvarianceConstraint` no longer erroneously removes points where
Expand Down Expand Up @@ -48,6 +50,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `DiscreteLinearConstraint` for (optionally weighted) sum constraints on discrete
parameters, supporting `coefficients` and mirroring `ContinuousLinearConstraint`'s
`operator`/`rhs`/`coefficients` interface (replaces `DiscreteSumConstraint`)
- `LLMRecommender` for LLM-based experimental design suggestions via LiteLLM,
available through the optional `llm` dependency group
- `LLMTwoPhaseRecommender` and `LLMAlternatingRecommender` convenience recommenders
built on top of `LLMRecommender`: the former warm-starts a campaign with the LLM and
then switches to another recommender, the latter alternates between the two

### Changed
- `BOTORCH` GP preset now includes `BetaPrior(2.5, 1.5)` for the task covariance
Expand Down
3 changes: 3 additions & 0 deletions baybe/_optional/info.py
Comment thread
Scienfitz marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def exclude_sys_path(path: str, /): # noqa: DOC402
# Individual packages
with exclude_sys_path(os.getcwd()):
FLAKE8_INSTALLED = find_spec("flake8") is not None
JINJA2_INSTALLED = find_spec("jinja2") is not None
LIME_INSTALLED = find_spec("lime") is not None
LITELLM_INSTALLED = find_spec("litellm") is not None
ONNX_INSTALLED = find_spec("onnxruntime") is not None
NGBOOST_INSTALLED = find_spec("ngboost") is not None
POLARS_INSTALLED = find_spec("polars") is not None
Expand All @@ -49,6 +51,7 @@ def exclude_sys_path(path: str, /): # noqa: DOC402
# Information on whether all required packages for certain functionality are available
CHEM_INSTALLED = SKFP_INSTALLED
INSIGHTS_INSTALLED = SHAP_INSTALLED and LIME_INSTALLED
LLM_INSTALLED = JINJA2_INSTALLED and LITELLM_INSTALLED
LINT_INSTALLED = all(
(
FLAKE8_INSTALLED,
Expand Down
20 changes: 20 additions & 0 deletions baybe/_optional/llm.py
Comment thread
AVHopp marked this conversation as resolved.

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.

TODO: Figure out which of the general LLM utilities (e.g. retry) could be extracted (i.e. should not be part of the recommender sub-package), so everything that takes the LLM output and creates a JSON or any other reasonable format.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Optional LLM imports."""

from baybe.exceptions import OptionalImportError

try:
from jinja2 import StrictUndefined, Template
except ModuleNotFoundError as ex:
raise OptionalImportError(name="jinja2", group="llm") from ex

try:
from litellm import AuthenticationError, completion
except ModuleNotFoundError as ex:
raise OptionalImportError(name="litellm", group="llm") from ex

__all__ = [
"AuthenticationError",
"StrictUndefined",
"Template",
"completion",
]
184 changes: 184 additions & 0 deletions baybe/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Custom exceptions and warnings."""

import gc
from collections.abc import Collection
from typing import Any

import pandas as pd
Expand All @@ -15,6 +16,10 @@ class InputDataTypeWarning(UserWarning):
"""An input has unexpected data type."""


class LLMResponseWarning(UserWarning):
"""A language model response did not fully meet the request."""


class UnusedObjectWarning(UserWarning):
"""
A method or function was called with undesired arguments which indicates an
Expand Down Expand Up @@ -187,5 +192,184 @@ class UnsupportedEarlyFilteringError(Exception):
"""A constraint does not support early filtering with the given parameters."""


#### LLM exclusive exceptions ####


class LLMError(Exception):
"""A language model interaction failed."""


class LLMCallError(LLMError):
"""The call to the language model failed before producing a response.

Raised for failures such as network errors, rate limiting, timeouts, or an
unknown model identifier, where no response is returned to process.
"""


class LLMAuthenticationError(LLMCallError):
"""Authentication with the language model provider failed."""


class LLMResponseError(LLMError):
"""An error occurred while processing a language model response."""

@property
def recovery_instruction(self) -> str:
"""Guidance for the language model on how to correct its response."""
return (
"Your previous response could not be used. Please provide a new "
"recommendation that follows the required format and respects the search "
"space."
)


class LLMMalformedResponseError(LLMResponseError):
"""A language model response could not be parsed into the expected structure."""

@property
@override
def recovery_instruction(self) -> str:
return (
"Your previous response was not valid JSON in the required format. Return "
"a JSON array of objects, each with an 'explanation' string and a "
"'parameters' object mapping parameter names to values, and nothing else."
)


class LLMUnknownParameterError(LLMResponseError):
"""A language model response referenced parameters outside the search space."""

def __init__(
self,
*args: Any,
unknown_names: Collection[str],
valid_names: Collection[str],
):
super().__init__(*args)
self.unknown_names = unknown_names
self.valid_names = valid_names

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response used unknown parameter names "
f"{sorted(self.unknown_names)}. Use only the following parameters: "
f"{sorted(self.valid_names)}."
)


class LLMMissingParameterError(LLMResponseError):
"""A language model response omitted required search space parameters."""

def __init__(self, *args: Any, parameters: Collection[str]):
super().__init__(*args)
self.parameters = parameters

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response did not specify values for the required "
f"parameters {sorted(self.parameters)}. Every suggestion must provide a "
f"value for all parameters."
)


class LLMNonNumericParameterError(LLMResponseError):
"""A language model response gave non-numeric values for a numerical parameter."""

def __init__(self, *args: Any, detail: str):
super().__init__(*args)
self.detail = detail

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response provided non-numeric values for a numerical "
f"parameter: {self.detail} Provide numeric values for all numerical "
f"parameters."
)


class LLMInvalidParameterValueError(LLMResponseError):
"""A language model response contained invalid parameter values."""

def __init__(self, *args: Any, detail: str):
super().__init__(*args)
self.detail = detail

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response contained invalid parameter values: {self.detail} "
f"Provide values that lie within the allowed range (for numerical "
f"parameters) or match an allowed choice exactly (for categorical "
f"parameters)."
)


class LLMConstraintViolationError(LLMResponseError):
"""A language model response violated a search space constraint."""

def __init__(
self,
*args: Any,
constraint_name: str,
parameters: Collection[str],
):
super().__init__(*args)
self.constraint_name = constraint_name
self.parameters = parameters

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response violated the '{self.constraint_name}' constraint "
f"on parameters {list(self.parameters)}. Ensure all suggestions satisfy "
f"this constraint."
)


class LLMIneligiblePointsError(LLMResponseError):
"""A language model response proposed points outside the eligible candidate set."""

def __init__(self, *args: Any, n_ineligible: int):
super().__init__(*args)
self.n_ineligible = n_ineligible

@property
@override
def recovery_instruction(self) -> str:
return (
f"{self.n_ineligible} of your proposed configurations are not eligible "
f"candidates (they may already have been measured or recommended, or they "
f"do not exist in the search space). Propose different points from the "
f"available search space."
)


class LLMBatchSizeError(LLMResponseError):
"""A language model response contained the wrong number of recommendations."""

def __init__(self, *args: Any, requested: int, received: int):
super().__init__(*args)
self.requested = requested
self.received = received

@property
@override
def recovery_instruction(self) -> str:
return (
f"Your previous response provided {self.received} valid recommendation(s), "
f"but exactly {self.requested} are required. Provide {self.requested} "
f"distinct, valid recommendations."
)


# Collect leftover original slotted classes processed by `attrs.define`
gc.collect()
8 changes: 8 additions & 0 deletions baybe/recommenders/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""BayBE recommenders."""

from baybe.recommenders.meta.llm import (
LLMAlternatingRecommender,
LLMTwoPhaseRecommender,
)
from baybe.recommenders.meta.sequential import (
SequentialMetaRecommender,
StreamingSequentialMetaRecommender,
TwoPhaseMetaRecommender,
)
from baybe.recommenders.naive import NaiveHybridSpaceRecommender
from baybe.recommenders.pure.bayesian.botorch import BotorchRecommender
from baybe.recommenders.pure.llm import LLMRecommender
from baybe.recommenders.pure.nonpredictive.clustering import (
GaussianMixtureClusteringRecommender,
KMeansClusteringRecommender,
Expand All @@ -22,6 +27,9 @@
"FPSRecommender",
"GaussianMixtureClusteringRecommender",
"KMeansClusteringRecommender",
"LLMAlternatingRecommender",
"LLMRecommender",
"LLMTwoPhaseRecommender",
"PAMClusteringRecommender",
"NaiveHybridSpaceRecommender",
"RandomRecommender",
Expand Down
6 changes: 6 additions & 0 deletions baybe/recommenders/meta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
query.
"""

from baybe.recommenders.meta.llm import (
LLMAlternatingRecommender,
LLMTwoPhaseRecommender,
)
from baybe.recommenders.meta.sequential import (
SequentialMetaRecommender,
StreamingSequentialMetaRecommender,
TwoPhaseMetaRecommender,
)

__all__ = [
"LLMAlternatingRecommender",
"LLMTwoPhaseRecommender",
"TwoPhaseMetaRecommender",
"SequentialMetaRecommender",
"StreamingSequentialMetaRecommender",
Expand Down
Loading
Loading