Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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