Skip to content

Commit 88a4d75

Browse files
authored
feat: rename generative slots -> generative stubs (#801)
* feat: rename generative slots -> generative stubs * feat: add backwards compat with deprecation warning; add genslot fixer cli
1 parent 839eead commit 88a4d75

47 files changed

Lines changed: 1977 additions & 1256 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli/fix/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class _FixMode(StrEnum):
1414
ADD_STREAM_LOOP = "add-stream-loop"
1515

1616

17-
from cli.fix.commands import fix_async # noqa: E402
17+
from cli.fix.commands import fix_async, fix_genslots # noqa: E402
1818

1919
fix_app.command("async")(fix_async)
20+
fix_app.command("genslots")(fix_genslots)
File renamed without changes.

cli/fix/commands.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""CLI command for `m fix async`."""
1+
"""CLI commands for `m fix async` and `m fix genslots`."""
22

33
from pathlib import Path
44

@@ -54,7 +54,7 @@ def fix_async(
5454
`await r.astream()`, or a `while not r.is_computed()` loop are
5555
automatically skipped, even when nested inside if/try/for blocks.
5656
"""
57-
from cli.fix.fixer import fix_path
57+
from cli.fix.async_fixer import fix_path
5858

5959
target = Path(path)
6060
if not target.exists():
@@ -75,3 +75,51 @@ def fix_async(
7575
typer.echo(
7676
f" {loc.filepath}:{loc.line} - {loc.function_name}() [{loc.call_style}]"
7777
)
78+
79+
80+
def fix_genslots(
81+
path: str = typer.Argument(..., help="File or directory to scan"),
82+
dry_run: bool = typer.Option(
83+
False, "--dry-run", help="Report locations without modifying files"
84+
),
85+
):
86+
"""Rewrite old genslot imports and class names to genstub equivalents.
87+
88+
Args:
89+
path: File or directory to scan.
90+
dry_run: If ``True``, report locations without modifying files.
91+
92+
Raises:
93+
typer.Exit: If *path* does not exist.
94+
95+
\b
96+
Rewrites:
97+
- mellea.stdlib.components.genslot → mellea.stdlib.components.genstub
98+
- GenerativeSlot → GenerativeStub
99+
- SyncGenerativeSlot → SyncGenerativeStub
100+
- AsyncGenerativeSlot → AsyncGenerativeStub
101+
102+
\b
103+
Best practices:
104+
- Run with --dry-run first to review what will be changed.
105+
- The tool is idempotent — running it twice on the same file is safe.
106+
"""
107+
from cli.fix.genstub_fixer import fix_genslot_path
108+
109+
target = Path(path)
110+
if not target.exists():
111+
typer.echo(f"Error: {path} does not exist", err=True)
112+
raise typer.Exit(code=1)
113+
114+
result = fix_genslot_path(target, dry_run=dry_run)
115+
116+
if result.total_fixes == 0:
117+
typer.echo("No genslot references found.")
118+
return
119+
120+
action = "Found" if dry_run else "Fixed"
121+
typer.echo(
122+
f"{action} {result.total_fixes} reference(s) in {result.files_affected} file(s):"
123+
)
124+
for loc in result.locations:
125+
typer.echo(f" {loc.filepath}:{loc.line} - {loc.description}")

cli/fix/genstub_fixer.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Line-based detection and rewriting of old genslot imports and class names.
2+
3+
Targets:
4+
- ``from mellea.stdlib.components.genslot import ...`` → ``genstub``
5+
- ``import mellea.stdlib.components.genslot [as ...]`` → ``genstub``
6+
- ``from mellea.stdlib.components import genslot [as ...]`` → ``genstub``
7+
- ``from .genslot import ...`` (relative imports) → ``genstub``
8+
- ``GenerativeSlot`` → ``GenerativeStub`` (and Sync/Async variants)
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import re
14+
from dataclasses import dataclass
15+
from pathlib import Path
16+
17+
# Directories to skip during traversal.
18+
SKIP_DIRS = {"__pycache__", ".git", ".venv", "node_modules"}
19+
20+
# Ordered longest-first so ``SyncGenerativeSlot`` is replaced before ``GenerativeSlot``.
21+
_CLASS_RENAMES: list[tuple[str, str]] = [
22+
("AsyncGenerativeSlot", "AsyncGenerativeStub"),
23+
("SyncGenerativeSlot", "SyncGenerativeStub"),
24+
("GenerativeSlot", "GenerativeStub"),
25+
]
26+
27+
# --- Module-path patterns ---
28+
29+
# Fully-qualified module path (handles both `from … import` and `import …`).
30+
_MODULE_OLD = "mellea.stdlib.components.genslot"
31+
_MODULE_NEW = "mellea.stdlib.components.genstub"
32+
_MODULE_RE = re.compile(re.escape(_MODULE_OLD))
33+
34+
# `from mellea.stdlib.components import genslot` (with optional ` as …`).
35+
_FROM_PARENT_RE = re.compile(
36+
r"(\bfrom\s+mellea\.stdlib\.components\s+import\s+)" # prefix
37+
r"(\bgenslot\b)" # the name to replace
38+
)
39+
40+
# Relative imports: `from .genslot import …` or `from ..components.genslot import …`
41+
# Matches any leading dots followed by an optional dotted path ending in `.genslot`.
42+
_RELATIVE_RE = re.compile(
43+
r"(\bfrom\s+\.[\w.]*?)" # `from .` or `from ..foo.bar`
44+
r"(\bgenslot\b)" # the segment to replace
45+
)
46+
47+
# Patterns for old class names — word-boundary aware to avoid false positives.
48+
_CLASS_RES: list[tuple[re.Pattern[str], str]] = [
49+
(re.compile(rf"\b{old}\b"), new) for old, new in _CLASS_RENAMES
50+
]
51+
52+
53+
@dataclass
54+
class GenStubFixLocation:
55+
"""A single replacement within a file.
56+
57+
Args:
58+
filepath: Path to the source file.
59+
line: One-based line number.
60+
description: Human-readable description of the replacement.
61+
"""
62+
63+
filepath: Path
64+
line: int
65+
description: str
66+
67+
68+
@dataclass
69+
class GenStubFixResult:
70+
"""Aggregated results across all scanned files.
71+
72+
Args:
73+
locations: Individual fix locations.
74+
total_fixes: Total replacements made (or found in dry-run).
75+
files_affected: Number of distinct files modified.
76+
"""
77+
78+
locations: list[GenStubFixLocation]
79+
total_fixes: int
80+
files_affected: int
81+
82+
83+
def _fix_line(line: str) -> tuple[str, list[str]]:
84+
"""Apply all genslot→genstub replacements to a single line.
85+
86+
Returns:
87+
A (new_line, descriptions) tuple. *descriptions* is empty when the
88+
line was not changed.
89+
"""
90+
descriptions: list[str] = []
91+
92+
# Fully-qualified module path.
93+
if _MODULE_RE.search(line):
94+
line = _MODULE_RE.sub(_MODULE_NEW, line)
95+
descriptions.append(f"{_MODULE_OLD}{_MODULE_NEW}")
96+
97+
# `from mellea.stdlib.components import genslot`
98+
if _FROM_PARENT_RE.search(line):
99+
line = _FROM_PARENT_RE.sub(r"\1genstub", line)
100+
descriptions.append("import genslot → import genstub")
101+
102+
# Relative imports: `from .genslot import …`
103+
if _RELATIVE_RE.search(line):
104+
line = _RELATIVE_RE.sub(r"\1genstub", line)
105+
descriptions.append(".genslot → .genstub")
106+
107+
for pattern, replacement in _CLASS_RES:
108+
if pattern.search(line):
109+
line = pattern.sub(replacement, line)
110+
old = pattern.pattern.replace(r"\b", "")
111+
descriptions.append(f"{old}{replacement}")
112+
113+
return line, descriptions
114+
115+
116+
def find_genslot_refs(source: str, filepath: Path) -> list[GenStubFixLocation]:
117+
"""Scan *source* for old genslot references and return their locations.
118+
119+
Args:
120+
source: Python source text.
121+
filepath: Used for the ``filepath`` field in returned locations.
122+
123+
Returns:
124+
List of locations that would be changed.
125+
"""
126+
locations: list[GenStubFixLocation] = []
127+
for lineno, line in enumerate(source.splitlines(), start=1):
128+
_, descriptions = _fix_line(line)
129+
for desc in descriptions:
130+
locations.append(
131+
GenStubFixLocation(filepath=filepath, line=lineno, description=desc)
132+
)
133+
return locations
134+
135+
136+
def fix_genslot_file(
137+
filepath: Path, *, dry_run: bool = False
138+
) -> list[GenStubFixLocation]:
139+
"""Fix a single file.
140+
141+
Args:
142+
filepath: Path to the Python file to fix.
143+
dry_run: If ``True``, return locations without modifying the file.
144+
145+
Returns:
146+
List of locations found (and optionally fixed).
147+
"""
148+
source = filepath.read_text()
149+
locations = find_genslot_refs(source, filepath)
150+
151+
if not locations or dry_run:
152+
return locations
153+
154+
new_lines: list[str] = []
155+
for line in source.splitlines(keepends=True):
156+
fixed, _ = _fix_line(line)
157+
new_lines.append(fixed)
158+
159+
filepath.write_text("".join(new_lines))
160+
return locations
161+
162+
163+
def fix_genslot_path(path: Path, *, dry_run: bool = False) -> GenStubFixResult:
164+
"""Fix a file or directory recursively.
165+
166+
Args:
167+
path: File or directory to process.
168+
dry_run: If ``True``, report locations without modifying files.
169+
170+
Returns:
171+
Aggregated result with all fix locations and summary counts.
172+
"""
173+
all_locations: list[GenStubFixLocation] = []
174+
files_affected = 0
175+
176+
if path.is_file():
177+
files = [path]
178+
else:
179+
files = sorted(path.rglob("*.py"))
180+
181+
for f in files:
182+
parts = f.relative_to(path).parts if path.is_dir() else ()
183+
if any(part in SKIP_DIRS for part in parts):
184+
continue
185+
186+
locs = fix_genslot_file(f, dry_run=dry_run)
187+
if locs:
188+
all_locations.extend(locs)
189+
files_affected += 1
190+
191+
return GenStubFixResult(
192+
locations=all_locations,
193+
total_fixes=len(all_locations),
194+
files_affected=files_affected,
195+
)

docs/docs/concepts/requirements-system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ from typing import Literal
162162

163163
from mellea import generative, start_session
164164
from mellea.core import Requirement
165-
from mellea.stdlib.components.genslot import PreconditionException
165+
from mellea.stdlib.components.genstub import PreconditionException
166166
from mellea.stdlib.requirements import simple_validate
167167
from mellea.stdlib.sampling import RejectionSamplingStrategy
168168

docs/docs/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"pages": [
3434
"tutorials/01-your-first-generative-program",
3535
"tutorials/02-streaming-and-async",
36-
"tutorials/03-using-generative-slots",
36+
"tutorials/03-using-generative-stubs",
3737
"tutorials/04-making-agents-reliable",
3838
"tutorials/05-mifying-legacy-code"
3939
]

docs/docs/examples/data-extraction-pipeline.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This example shows the most direct path from raw text to typed, structured
88
output in Mellea: a `@generative` function whose return annotation tells the
99
runtime exactly what shape the result must have.
1010

11-
**Source file:** `docs/examples/information_extraction/101_with_gen_slots.py`
11+
**Source file:** `docs/examples/information_extraction/101_with_gen_stubs.py`
1212

1313
## Concepts covered
1414

@@ -46,7 +46,7 @@ def extract_all_person_names(doc: str) -> list[str]:
4646
```
4747

4848
The `@generative` decorator converts a bare function stub into a generative
49-
slot. Three things drive the extraction:
49+
stub. Three things drive the extraction:
5050

5151
- **Parameter names** (`doc`) become the named inputs the model receives.
5252
- **Return annotation** (`list[str]`) tells the runtime to parse and validate
@@ -79,7 +79,7 @@ extracted, type-validated data — not a raw string or a thunk.
7979
```python
8080
# pytest: ollama, llm
8181

82-
"""Simple Example of information extraction with Mellea using generative slots."""
82+
"""Simple Example of information extraction with Mellea using generative stubs."""
8383

8484
from mellea import generative, start_session
8585
from mellea.backends import model_ids

docs/docs/examples/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ to run.
2828
| Category | What it shows |
2929
| -------- | ------------- |
3030
| `instruct_validate_repair/` | The IVR loop end-to-end: basic generation, adding requirements, automatic repair on failure, custom validators |
31-
| `generative_slots/` | `@generative` functions with typed returns, pipeline composition, `ChatContext` persona injection, pre/postcondition checks |
31+
| `generative_stubs/` | `@generative` functions with typed returns, pipeline composition, `ChatContext` persona injection, pre/postcondition checks |
3232
| `context/` | Context inspection, sampling with context trees, parallel context branches |
3333
| `sessions/` | Custom session types and backend selection |
3434
| `async/` | How to utilize basic async capabilities |
@@ -103,7 +103,7 @@ to run.
103103
| Category | What it shows |
104104
| -------- | ------------- |
105105
| `hello_world.py` | Minimal single-file starting point |
106-
| `tutorial/` | Python script versions of the tutorials: email generation, IVR, generative slots, contexts, MObjects, model options, and more |
106+
| `tutorial/` | Python script versions of the tutorials: email generation, IVR, generative stubs, contexts, MObjects, model options, and more |
107107
| `notebooks/` | Jupyter notebook versions of the same tutorials for interactive, cell-by-cell exploration |
108108

109109
---

docs/docs/guide/generative-functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ is never called:
100100
```python
101101
from mellea import generative, start_session
102102
from mellea.core import Requirement
103-
from mellea.stdlib.components.genslot import PreconditionException
103+
from mellea.stdlib.components.genstub import PreconditionException
104104
from mellea.stdlib.requirements import simple_validate
105105
from typing import Literal
106106

docs/docs/guide/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ fails — i.e., before the LLM call is made. Catch it to handle pre-call validat
528528
failures gracefully.
529529

530530
```python
531-
from mellea.stdlib.components.genslot import PreconditionException
531+
from mellea.stdlib.components.genstub import PreconditionException
532532

533533
try:
534534
result = my_generative_fn(m, ...)

0 commit comments

Comments
 (0)