Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 23 additions & 2 deletions src/outlines/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,38 @@ def is_callable(value: Any) -> bool:
# Type conversion


def _unique_str_keys(values) -> dict:
"""Build a name -> value mapping for an Enum's functional API, keyed by str(value)
as before, but disambiguating with the value's type name whenever two values would
otherwise collide on the same str() (e.g. the string "1" and the int 1 both stringify
to "1"), which previously caused the dict comprehension to silently drop all but the
last colliding value.
"""
by_str: dict = {}
for value in values:
by_str.setdefault(str(value), []).append(value)

result = {}
for key, group in by_str.items():
if len(group) == 1:
result[key] = group[0]
else:
for value in group:
result[f"{type(value).__name__}_{value}"] = value
return result

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.

This fallback key can still collide when two values have the same type name and string form. For example, Choice accepts two enum members from classes both named State with a member named READY; both become State_State.READY, so one is still dropped. Could the disambiguator use an index rather than another value-derived key?



def get_enum_from_literal(value) -> Enum:
return Enum(
value.__name__,
{str(arg): arg for arg in get_args(value)}
_unique_str_keys(get_args(value))
)


def get_enum_from_choice(value) -> Enum:
return Enum(
'Choice',
{str(item): item for item in value.items}
_unique_str_keys(value.items)
)


Expand Down
20 changes: 20 additions & 0 deletions tests/types/test_types_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,26 @@ def test_get_enum_from_literal(sample_enum):
assert getattr(complex_enum, "SampleEnum.A").value == sample_enum.A


def test_get_enum_from_literal_str_int_collision_preserves_both_members():
"""str(1) == str("1") == "1" previously made the naive `{str(x): x for x in ...}`
dict comprehension silently drop one of the two values -- whichever came first was
overwritten by the other. Both must survive as distinct enum members."""
enum = get_enum_from_literal(Literal["1", 1])
assert is_enum(enum)
values = {member.value for member in enum}
assert values == {"1", 1}
assert len(list(enum)) == 2


def test_get_enum_from_choice_str_int_collision_preserves_both_members():
choice = Choice(["1", 1])
enum = get_enum_from_choice(choice)
assert is_enum(enum)
values = {member.value for member in enum}
assert values == {"1", 1}
assert len(list(enum)) == 2


def test_get_schema_from_signature(sample_function, sample_function_missing_type):
result = get_schema_from_signature(sample_function)
assert result["type"] == "object"
Expand Down
Loading