Skip to content

Commit 4631e12

Browse files
committed
Gate synthetic lexical-reader generation behind LexicalReaders handler
Per eb8680: needing to coach the LLM around noisy lexical-reader tools in `test_handlers_llm_tool_calling_poem.py` was a smell. Make the generation opt-in instead. - New `expose_lexical_readers()` Operation in `completions.py`, default return `False`. `_collect_tools` gates the reader branch on it. - New `LexicalReaders` ObjectInterpretation overrides the Operation to return `True`; users install it for the call sites where the LLM should see closure state. - Revert the poem prompt-hack: the docstring no longer has to ask the LLM to ignore read-only lexical readers, because they are now off by default. - Test A (`test_template_synthesis_uses_lexical_reader`) and the reader-integration test (`test_llm_reads_lexical_value`) install `LexicalReaders` in their handler stack. Both fixtures re-recorded against gpt-4o-mini. - Template tests that exercise reader generation install the handler. New `test_lexical_readers_off_by_default` and `test_lexical_readers_handler_enables_collection` pin both sides of the gate. - `test_template_method` / `test_template_method_nested_class`: drop the side-note `"local_variable" in tools` assertions; those pinned implicit reader generation. The core method-template tool collection (random, reverse, etc.) is the actual point and still passes. - `test_template_exposes_lexical_classes` now pins both off (default) and on (handler installed) — the #497 motivating example with explicit gate semantics.
1 parent d44cc6c commit 4631e12

12 files changed

Lines changed: 135 additions & 88 deletions

effectful/handlers/llm/completions.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,40 @@ def tool_fn():
219219
return super().define(tool_fn)
220220

221221

222+
@Operation.define
223+
def expose_lexical_readers() -> bool:
224+
"""Effect controlling whether `_collect_tools` builds synthetic
225+
read-only Tools for non-Tool/Template values in a Template's
226+
lexical scope.
227+
228+
Default behaviour is *off*: only real Tools/Templates/Agents reach
229+
the LLM, and the lexical context is invisible. Install
230+
`LexicalReaders` to flip it on for the call-site where the LLM
231+
should be able to inspect closure state.
232+
"""
233+
return False
234+
235+
236+
class LexicalReaders(ObjectInterpretation):
237+
"""Handler that enables synthetic lexical-reader generation in
238+
`_collect_tools`. Each plain value in a Template's lexical context
239+
becomes a zero-argument Tool that returns the captured value.
240+
"""
241+
242+
@implements(expose_lexical_readers)
243+
def _enabled(self) -> bool:
244+
return True
245+
246+
222247
def _collect_tools(
223248
env: collections.abc.Mapping[str, typing.Any],
224249
) -> collections.abc.Mapping[str, Tool]:
225-
"""Operations and Templates available as tools, plus synthetic
226-
readers for other lexical symbols. Auto-captured from lexical context."""
250+
"""Operations and Templates available as tools. When
251+
`expose_lexical_readers` is on (see :class:`LexicalReaders`),
252+
plain values in the lexical context are also wrapped as synthetic
253+
read-only tools."""
227254
result: dict[str, Tool] = {}
255+
readers_on = expose_lexical_readers()
228256

229257
for name, obj in env.items():
230258
if isinstance(obj, Tool | Template):
@@ -234,7 +262,7 @@ def _collect_tools(
234262
for attr_name in vars(cls):
235263
if isinstance(getattr(obj, attr_name), Tool):
236264
result[f"{name}__{attr_name}"] = getattr(obj, attr_name)
237-
elif name.isidentifier():
265+
elif readers_on and name.isidentifier():
238266
try:
239267
result[name] = _LexicalVariableTool.define(obj, name=name)
240268
# `TypeError` joins the three Pydantic errors because the

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k0fEKWeTJpZDy3rcqAKPKE46Cu",
3-
"created": 1780108168,
2+
"id": "chatcmpl-DoaWCNbxu6WP3Lx3l3Dmkmr5CBxwg",
3+
"created": 1780949148,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "tool_calls",
@@ -17,7 +17,7 @@
1717
"arguments": "{}",
1818
"name": "_known_data"
1919
},
20-
"id": "call_DoVBVCnKiU5k7FTzWRo5nnzm",
20+
"id": "call_nEkf11zJbFZRFcBclPCAvTao",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 11,
35-
"prompt_tokens": 6043,
36-
"total_tokens": 6054,
35+
"prompt_tokens": 3753,
36+
"total_tokens": 3764,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,
@@ -45,7 +45,7 @@
4545
},
4646
"prompt_tokens_details": {
4747
"audio_tokens": 0,
48-
"cached_tokens": 0,
48+
"cached_tokens": 2816,
4949
"text_tokens": null,
5050
"image_tokens": null,
5151
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value_1.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k2MUxG3A1S3HrmgwMXFmDNScCt",
3-
"created": 1780108170,
2+
"id": "chatcmpl-DoaWFqScxo1oUrffPxQIyJYv8ENNr",
3+
"created": 1780949151,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "tool_calls",
@@ -17,7 +17,7 @@
1717
"arguments": "{\"a\":10,\"b\":20}",
1818
"name": "add_numbers"
1919
},
20-
"id": "call_vypFiLTjH5MEpMKSL3VLoD4z",
20+
"id": "call_TABbctvXDmYyjourZxIgLFo1",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 18,
35-
"prompt_tokens": 6077,
36-
"total_tokens": 6095,
35+
"prompt_tokens": 3787,
36+
"total_tokens": 3805,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,
@@ -45,7 +45,7 @@
4545
},
4646
"prompt_tokens_details": {
4747
"audio_tokens": 0,
48-
"cached_tokens": 6016,
48+
"cached_tokens": 3712,
4949
"text_tokens": null,
5050
"image_tokens": null,
5151
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value_2.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k5bfkN00lZh8L5D82Cr9YhOJsA",
3-
"created": 1780108173,
2+
"id": "chatcmpl-DoaWHTP5RK0QyZdDhiHbaUNkv3IfS",
3+
"created": 1780949153,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "tool_calls",
@@ -17,7 +17,7 @@
1717
"arguments": "{\"a\":30,\"b\":30}",
1818
"name": "add_numbers"
1919
},
20-
"id": "call_EmO3tVtqZBqdL6K4dUqLmfgn",
20+
"id": "call_SpMwdgqjznHPUWgE19dxgSnc",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 18,
35-
"prompt_tokens": 6104,
36-
"total_tokens": 6122,
35+
"prompt_tokens": 3814,
36+
"total_tokens": 3832,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,
@@ -45,7 +45,7 @@
4545
},
4646
"prompt_tokens_details": {
4747
"audio_tokens": 0,
48-
"cached_tokens": 6016,
48+
"cached_tokens": 3712,
4949
"text_tokens": null,
5050
"image_tokens": null,
5151
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value_3.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k7gKpQZx4an1oAXAhdYViG1NjR",
3-
"created": 1780108175,
2+
"id": "chatcmpl-DoaWJQAi5ihtJlJL7algRwDhzcgcx",
3+
"created": 1780949155,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "tool_calls",
@@ -17,7 +17,7 @@
1717
"arguments": "{\"a\":60,\"b\":40}",
1818
"name": "add_numbers"
1919
},
20-
"id": "call_WhTflV4rMdUJUJOY2qqOHv7x",
20+
"id": "call_IZ66N2KVq8wyYt3mAOIqfxW0",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 18,
35-
"prompt_tokens": 6131,
36-
"total_tokens": 6149,
35+
"prompt_tokens": 3841,
36+
"total_tokens": 3859,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,
@@ -45,7 +45,7 @@
4545
},
4646
"prompt_tokens_details": {
4747
"audio_tokens": 0,
48-
"cached_tokens": 6016,
48+
"cached_tokens": 3712,
4949
"text_tokens": null,
5050
"image_tokens": null,
5151
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value_4.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k86CG3iPs5P7110XgQ3Eso2CfZ",
3-
"created": 1780108176,
2+
"id": "chatcmpl-DoaWLlSqQM5WHmGa4dcjSRypJ0EXu",
3+
"created": 1780949157,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "tool_calls",
@@ -17,7 +17,7 @@
1717
"arguments": "{\"a\":100,\"b\":50}",
1818
"name": "add_numbers"
1919
},
20-
"id": "call_LTy1WHCfWTFnwZ5GSAUElfDA",
20+
"id": "call_0unsPGaZPBHhTLObCfc5zAgf",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 18,
35-
"prompt_tokens": 6158,
36-
"total_tokens": 6176,
35+
"prompt_tokens": 3868,
36+
"total_tokens": 3886,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,
@@ -45,7 +45,7 @@
4545
},
4646
"prompt_tokens_details": {
4747
"audio_tokens": 0,
48-
"cached_tokens": 6016,
48+
"cached_tokens": 3840,
4949
"text_tokens": null,
5050
"image_tokens": null,
5151
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_llm_reads_lexical_value_5.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"id": "chatcmpl-Dl3k9lFSuyqAQbfM8J1H3BQmgYopC",
3-
"created": 1780108177,
2+
"id": "chatcmpl-DoaWNFKpBO8KO9IuPrwpMf2aDayBQ",
3+
"created": 1780949159,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
6-
"system_fingerprint": "fp_df8c8d3b43",
6+
"system_fingerprint": "fp_6c2953e649",
77
"choices": [
88
{
99
"finish_reason": "stop",
@@ -23,8 +23,8 @@
2323
],
2424
"usage": {
2525
"completion_tokens": 11,
26-
"prompt_tokens": 6185,
27-
"total_tokens": 6196,
26+
"prompt_tokens": 3895,
27+
"total_tokens": 3906,
2828
"completion_tokens_details": {
2929
"accepted_prediction_tokens": 0,
3030
"audio_tokens": 0,
@@ -36,7 +36,7 @@
3636
},
3737
"prompt_tokens_details": {
3838
"audio_tokens": 0,
39-
"cached_tokens": 6144,
39+
"cached_tokens": 3840,
4040
"text_tokens": null,
4141
"image_tokens": null,
4242
"video_tokens": null

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_template_synthesis_uses_lexical_reader.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"id": "chatcmpl-DoXmRMT0faR6nMFTGqu3wIgzXKSki",
3-
"created": 1780938623,
2+
"id": "chatcmpl-DoaVmdWxIVHC9IAfb8gM7TohRDtBb",
3+
"created": 1780949122,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
66
"system_fingerprint": "fp_6c2953e649",
@@ -17,7 +17,7 @@
1717
"arguments": "{}",
1818
"name": "threshold"
1919
},
20-
"id": "call_ypeSxtaJp5QNiymevzksgMSO",
20+
"id": "call_LZbU6rs5AtcMphaKxdwuERBs",
2121
"type": "function"
2222
}
2323
],
@@ -32,8 +32,8 @@
3232
],
3333
"usage": {
3434
"completion_tokens": 9,
35-
"prompt_tokens": 3897,
36-
"total_tokens": 3906,
35+
"prompt_tokens": 3937,
36+
"total_tokens": 3946,
3737
"completion_tokens_details": {
3838
"accepted_prediction_tokens": 0,
3939
"audio_tokens": 0,

tests/fixtures/tests_test_handlers_llm_provider.py__TestSyntheticReaderIntegration__test_template_synthesis_uses_lexical_reader_1.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"id": "chatcmpl-DoXmTBNxEKjwPzBRAJJUzwj3sKj5r",
3-
"created": 1780938625,
2+
"id": "chatcmpl-DoaVqPg5WV0Vjsp6Mklax4LPZSAD1",
3+
"created": 1780949126,
44
"model": "gpt-4o-mini-2024-07-18",
55
"object": "chat.completion",
66
"system_fingerprint": "fp_6c2953e649",
@@ -23,8 +23,8 @@
2323
],
2424
"usage": {
2525
"completion_tokens": 31,
26-
"prompt_tokens": 3916,
27-
"total_tokens": 3947,
26+
"prompt_tokens": 3956,
27+
"total_tokens": 3987,
2828
"completion_tokens_details": {
2929
"accepted_prediction_tokens": 0,
3030
"audio_tokens": 0,
@@ -36,7 +36,7 @@
3636
},
3737
"prompt_tokens_details": {
3838
"audio_tokens": 0,
39-
"cached_tokens": 3840,
39+
"cached_tokens": 0,
4040
"text_tokens": null,
4141
"image_tokens": null,
4242
"video_tokens": null

0 commit comments

Comments
 (0)