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
2 changes: 1 addition & 1 deletion docs/docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ A decorator that converts a typed Python function into an AI-powered function.
`@generative` uses the function's name, docstring, parameters, and return type
annotation to instruct the LLM. The output is constrained to match the return type.
Write the function in idiomatic Python — the more natural the signature and
docstring, the better the model understands and imitates it.
docstring, the better the model understands and performs it.

```python
from mellea import generative, start_session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,18 @@ def classify_sentiment(text: str) -> Literal["positive", "negative", "unknown"]:
)
# Prompt to the model looked like:
# ```
# Your task is to imitate the output of the following function for the given arguments.
# Reply Nothing else but the output of the function.

# Function:
# def classify_sentiment(text: str) -> Literal['positive', 'negative', 'unknown']:
# """Classify the sentiment of the text.

# Postconditions:
# - avoid classifying the sentiment as unknown
# """

# Arguments:
# - text: "I love this!" (type: <class 'str'>)
# Task: classify_sentiment
#
# Classify the sentiment of the text.
#
# Inputs:
# - text: "I love this!" (type: str)
#
# Requirements:
# - avoid classifying the sentiment as unknown
#
# Reply in JSON formatted as {"result": <value of type Literal['positive', 'negative', 'unknown']>}, matching this shape:
# {"result": "positive" | "negative" | "unknown"}
# ```

print("\nOutput sentiment is:", sentiment_component)
Expand Down
15 changes: 12 additions & 3 deletions mellea/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1567,9 +1567,18 @@ async def astream(self) -> str:
await self._gen.post_process(self)

self._set_parsed_repr()
assert self.parsed_repr is not None, (
"enforce constraint that a computed ModelOutputThunk has a non-None parsed_repr"
)
if self.parsed_repr is None:
# A `@generative` stub whose return type admits `None` (e.g.
# `-> str | None`) legitimately parses `{"result": null}` to `None`.
# Every other action is still held to a non-`None` parsed repr, so a
# component whose `_parse` returns `None` by mistake still fails here.
# Deferred import: `genstub` imports `mellea.core`, so a top-level
# import here would be circular.
from ..stdlib.components.genstub import GenerativeStub

assert isinstance(self._call.action, GenerativeStub), (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Optional / genuine question, not a blocker: this bakes a specific stdlib subclass (GenerativeStub) into a core invariant purely to exempt it. Unlike the Message._parse dispatch just below, which does real parsing work, this one only exists to special-case one component type. Would a Component-level predicate (the component declaring it permits a None parsed_repr) read better here, or is naming GenerativeStub directly the deliberate call? Fine to leave as-is either way.

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.

Agreed; this was a bit code-smelly when I added it. I've opened #1805 to fix the actual issue which is that we can't distinguish not parsed from parsed nothing.

"enforce constraint that a computed ModelOutputThunk has a non-None parsed_repr"
)

# --- generation_post_call hook ---
if has_plugins(HookType.GENERATION_POST_CALL):
Expand Down
15 changes: 9 additions & 6 deletions mellea/formatters/template_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,13 +368,16 @@ def _get_template(self, root_path: str, template_name: str) -> str:
)

# Only traverse file paths that are in the model id or are prompts/default.
for dir in dirs:
if dir.lower() == "prompts" or dir.lower() == "default":
continue

# Prune in place via slice assignment: `os.walk` reads this same list, and
# removing items while iterating over it skips the entry after each removal,
# which let a non-matching sibling family directory survive and be walked.
dirs[:] = [
dir
for dir in dirs
if dir.lower() in ("prompts", "default")
# Simplify the directory as well for matching names.
if _simplify_model_string(dir.lower()) not in simplified_model_id:
dirs.remove(dir)
or _simplify_model_string(dir.lower()) in simplified_model_id
]

return candidate_template

Expand Down
Loading
Loading