From 60f217815000d55f1714a7c00b31c9ffe66714c7 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Thu, 11 Dec 2025 16:11:36 -0500 Subject: [PATCH 01/14] Adds a SimpleComponent. --- mellea/stdlib/span/__init__.py | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 mellea/stdlib/span/__init__.py diff --git a/mellea/stdlib/span/__init__.py b/mellea/stdlib/span/__init__.py new file mode 100644 index 00000000..30cdd45b --- /dev/null +++ b/mellea/stdlib/span/__init__.py @@ -0,0 +1,52 @@ +"""Conceptual Spans.""" + +from mellea.stdlib.base import ( + CBlock, + Component, + ModelOutputThunk, + TemplateRepresentation, +) + +Span = CBlock | Component | ModelOutputThunk + + +class SimpleComponent(Component): + """A Component that is make up of named spans.""" + + def __init__(self, **kwargs): + """Initialized a simple component of the constructor's kwargs.""" + for key in kwargs.keys(): + if type(kwargs[key]) is str: + kwargs[key] = CBlock(value=kwargs[key]) + self._kwargs_type_check(kwargs) + self._kwargs = kwargs + + def parts(self): + """Returns the values of the kwargs.""" + return self._kwargs.values() + + def _kwargs_type_check(self, kwargs): + for key in kwargs.keys(): + value = kwargs[key] + assert issubclass(type(value), Component) or issubclass( + type(value), CBlock + ), f"Expected span but found {type(value)}" + assert type(key) is str + return True + + @staticmethod + def make_simple_string(kwargs): + """Uses <|key|>value to represent a simple component.""" + return "\n".join( + [f"<|{key}|>{value}" for (key, value) in kwargs.items()] + ) + + def format_for_llm(self): + """Uses a string rep.""" + return SimpleComponent.make_simple_string(self._kwargs) + # """ Uses a simple tagging structure that needs to be changed in the future. """ + # return TemplateRepresentation( + # obj=self, + # args=self._kwargs, + # template=SimpleComponent.make_simple_template(self._kwargs), + # ) From ef6daf6d3ba3feb491b96b109bfefb4a1e11ff25 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Thu, 11 Dec 2025 16:12:16 -0500 Subject: [PATCH 02/14] Adds a simple lazy example. --- docs/examples/melp/lazy.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/examples/melp/lazy.py diff --git a/docs/examples/melp/lazy.py b/docs/examples/melp/lazy.py new file mode 100644 index 00000000..992888e6 --- /dev/null +++ b/docs/examples/melp/lazy.py @@ -0,0 +1,29 @@ +import asyncio +from mellea.stdlib.span import Span, SimpleComponent +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.backends import Backend +from mellea.backends.ollama import OllamaModelBackend + +backend = OllamaModelBackend("granite4:latest") + + +async def main(backend: Backend, ctx: Context): + s1 = CBlock("What is 1+1? Respond with the number only.") + s1_out, _ = await backend.generate_from_context(action=s1, ctx=SimpleContext()) + + s2 = CBlock("What is 2+2? Respond with the number only.") + s2_out, _ = await backend.generate_from_context(action=s2, ctx=SimpleContext()) + + sc1 = SimpleComponent( + instruction="What is x+y? Respond with the number only", x=s1_out, y=s2_out + ) + + print(await s1_out.avalue()) + print(await s2_out.avalue()) + + sc1_out, _ = await backend.generate_from_context(action=sc1, ctx=SimpleContext()) + + print(await sc1_out.avalue()) + + +asyncio.run(main(backend, SimpleContext())) From 2fa6967682ec652f673e8aeee168049935c13863 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Thu, 11 Dec 2025 22:27:40 -0500 Subject: [PATCH 03/14] ollama generate walk. TODO-nrf: we need to add generate walks to every generation call. --- docs/examples/melp/lazy.py | 33 +++++++++------ docs/examples/melp/lazy_fib.py | 38 +++++++++++++++++ docs/examples/melp/lazy_fib_sample.py | 61 +++++++++++++++++++++++++++ docs/examples/melp/states.py | 46 ++++++++++++++++++++ mellea/backends/_utils.py | 21 ++++++++- mellea/backends/ollama.py | 14 +++++- mellea/stdlib/span/__init__.py | 20 +++++++-- 7 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 docs/examples/melp/lazy_fib.py create mode 100644 docs/examples/melp/lazy_fib_sample.py create mode 100644 docs/examples/melp/states.py diff --git a/docs/examples/melp/lazy.py b/docs/examples/melp/lazy.py index 992888e6..cb674914 100644 --- a/docs/examples/melp/lazy.py +++ b/docs/examples/melp/lazy.py @@ -7,23 +7,28 @@ backend = OllamaModelBackend("granite4:latest") -async def main(backend: Backend, ctx: Context): - s1 = CBlock("What is 1+1? Respond with the number only.") - s1_out, _ = await backend.generate_from_context(action=s1, ctx=SimpleContext()) - - s2 = CBlock("What is 2+2? Respond with the number only.") - s2_out, _ = await backend.generate_from_context(action=s2, ctx=SimpleContext()) - - sc1 = SimpleComponent( - instruction="What is x+y? Respond with the number only", x=s1_out, y=s2_out +async def fib(backend: Backend, ctx: Context, x: CBlock, y: CBlock) -> ModelOutputThunk: + sc = SimpleComponent( + instruction="What is x+y? Respond with the number only.", x=x, y=y ) + mot, _ = await backend.generate_from_context(action=sc, ctx=SimpleContext()) + return mot - print(await s1_out.avalue()) - print(await s2_out.avalue()) - sc1_out, _ = await backend.generate_from_context(action=sc1, ctx=SimpleContext()) - - print(await sc1_out.avalue()) +async def main(backend: Backend, ctx: Context): + fibs = [] + for i in range(100): + if i == 0 or i == 1: + fibs.append(CBlock(f"{i + 1}")) + else: + fibs.append(await fib(backend, ctx, fibs[i - 1], fibs[i - 2])) + + for x in fibs: + match x: + case ModelOutputThunk(): + print(await x.avalue()) + case CBlock(): + print(x.value) asyncio.run(main(backend, SimpleContext())) diff --git a/docs/examples/melp/lazy_fib.py b/docs/examples/melp/lazy_fib.py new file mode 100644 index 00000000..c711012c --- /dev/null +++ b/docs/examples/melp/lazy_fib.py @@ -0,0 +1,38 @@ +import asyncio +from mellea.stdlib.span import Span, SimpleComponent +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.stdlib.requirement import Requirement +from mellea.backends import Backend +from mellea.backends.ollama import OllamaModelBackend +from typing import Tuple + +backend = OllamaModelBackend("granite4:latest") + + +async def fib(backend: Backend, ctx: Context, x: CBlock, y: CBlock) -> ModelOutputThunk: + sc = SimpleComponent( + instruction="What is x+y? Respond with the number only.", x=x, y=y + ) + mot, _ = await backend.generate_from_context(action=sc, ctx=SimpleContext()) + return mot + + +async def fib_main(backend: Backend, ctx: Context): + fibs = [] + for i in range(20): + if i == 0 or i == 1: + fibs.append(CBlock(f"{i}")) + else: + mot = await fib(backend, ctx, fibs[i - 1], fibs[i - 2]) + fibs.append(mot) + + for x in enumerate(fibs): + match x: + case ModelOutputThunk(): + n = await x.avalue() + print(n) + case CBlock(): + print(x.value) + + +asyncio.run(fib_main(backend, SimpleContext())) diff --git a/docs/examples/melp/lazy_fib_sample.py b/docs/examples/melp/lazy_fib_sample.py new file mode 100644 index 00000000..bed771a3 --- /dev/null +++ b/docs/examples/melp/lazy_fib_sample.py @@ -0,0 +1,61 @@ +import asyncio +from mellea.stdlib.span import Span, SimpleComponent +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.stdlib.requirement import Requirement +from mellea.backends import Backend +from mellea.backends.ollama import OllamaModelBackend +from typing import Tuple + +backend = OllamaModelBackend("granite4:latest") + + +async def _fib_sample( + backend: Backend, ctx: Context, x: CBlock, y: CBlock +) -> ModelOutputThunk | None: + sc = SimpleComponent( + instruction="What is x+y? Respond with the number only.", x=x, y=y + ) + answer_mot, _ = await backend.generate_from_context(action=sc, ctx=SimpleContext()) + + # This is a fundamental thing: it means computation must occur. + # We need to be able to read this off at c.g. construction time. + value = await answer_mot.avalue() + + try: + int(value) + return answer_mot + except: + return None + + +async def fib_sampling_version( + backend: Backend, ctx: Context, x: CBlock, y: CBlock +) -> ModelOutputThunk | None: + for i in range(5): + sample = await _fib_sample(backend, ctx, x, y) + if sample is not None: + return sample + else: + continue + return None + + +async def fib_sampling_version_main(backend: Backend, ctx: Context): + fibs = [] + for i in range(20): + if i == 0 or i == 1: + fibs.append(CBlock(f"{i}")) + else: + mot = await fib_sampling_version(backend, ctx, fibs[i - 1], fibs[i - 2]) + fibs.append(mot) + + for x_i, x in enumerate(fibs): + match x: + case ModelOutputThunk(): + n = await x.avalue() + print(n) + case CBlock(): + print(x.value) + + +asyncio.run(fib_sampling_version_main(backend, SimpleContext())) diff --git a/docs/examples/melp/states.py b/docs/examples/melp/states.py new file mode 100644 index 00000000..9c7f15be --- /dev/null +++ b/docs/examples/melp/states.py @@ -0,0 +1,46 @@ +import mellea +from mellea.stdlib.base import CBlock, Context, SimpleContext +from mellea.stdlib.span import Span, SimpleComponent +from mellea.backends import Backend +from mellea.backends.ollama import OllamaModelBackend +import asyncio + + +async def main(backend: Backend, ctx: Context): + a_states = "Alaska,Arizona,Arkansas".split(",") + m_states = "Missouri", "Minnesota", "Montana", "Massachusetts" + + a_state_pops = dict() + for state in a_states: + a_state_pops[state], _ = await backend.generate_from_context( + CBlock(f"What is the population of {state}? Respond with an integer only."), + SimpleContext(), + ) + a_total_pop = SimpleComponent( + instruction=CBlock( + "What is the total population of these states? Respond with an integer only." + ), + **a_state_pops, + ) + a_state_total, _ = await backend.generate_from_context(a_total_pop, SimpleContext()) + + m_state_pops = dict() + for state in m_states: + m_state_pops[state], _ = await backend.generate_from_context( + CBlock(f"What is the population of {state}? Respond with an integer only."), + SimpleContext(), + ) + m_total_pop = SimpleComponent( + instruction=CBlock( + "What is the total population of these states? Respond with an integer only." + ), + **m_state_pops, + ) + m_state_total, _ = await backend.generate_from_context(m_total_pop, SimpleContext()) + + print(await a_state_total.avalue()) + print(await m_state_total.avalue()) + + +backend = OllamaModelBackend(model_id="granite4:latest") +asyncio.run(main(backend, SimpleContext())) diff --git a/mellea/backends/_utils.py b/mellea/backends/_utils.py index 08720bc0..694edbbb 100644 --- a/mellea/backends/_utils.py +++ b/mellea/backends/_utils.py @@ -1,13 +1,20 @@ from __future__ import annotations import inspect +import itertools from collections.abc import Callable from typing import Any, Literal from mellea.backends.formatter import Formatter from mellea.backends.tools import parse_tools from mellea.helpers.fancy_logger import FancyLogger -from mellea.stdlib.base import CBlock, Component, Context, ModelToolCall +from mellea.stdlib.base import ( + CBlock, + Component, + Context, + ModelOutputThunk, + ModelToolCall, +) from mellea.stdlib.chat import Message from mellea.stdlib.requirement import ALoraRequirement, LLMaJRequirement, Requirement @@ -80,3 +87,15 @@ def to_tool_calls( if len(model_tool_calls) > 0: return model_tool_calls return None + + +def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: + """Returns the generation walk ordering for a Span.""" + match c: + case ModelOutputThunk() if not c.is_computed(): + return [c] + case CBlock(): + return [] + case Component(): + parts_walk = [generate_walk(p) for p in c.parts()] + return itertools.chain.from_iterable(parts_walk) # aka flatten diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index 713acdd7..d75219c3 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -11,6 +11,7 @@ import mellea.backends.model_ids as model_ids from mellea.backends import BaseModelSubclass +from mellea.backends._utils import generate_walk from mellea.backends.formatter import Formatter, FormatterBackend, TemplateFormatter from mellea.backends.model_ids import ModelIdentifier from mellea.backends.tools import ( @@ -294,6 +295,12 @@ async def generate_from_chat_context( Raises: RuntimeError: If not called from a thread with a running event loop. """ + # Start by awaiting any necessary computation. + _computed = [await todo.avalue() for todo in generate_walk(action)] + FancyLogger.get_logger().info( + f"generate_from_chat_context awaited on {len(_computed)} uncomputed mots." + ) + model_opts = self._simplify_and_merge(model_options) linearized_context = ctx.view_for_generation() @@ -408,9 +415,14 @@ async def generate_from_raw( model_opts = self._simplify_and_merge(model_options) + for act in actions: + for todo in generate_walk(act): + await todo.avalue() + + prompts = [self.formatter.print(action) for action in actions] + # Ollama doesn't support "batching". There's some ability for concurrency. Use that here. # See https://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests. - prompts = [self.formatter.print(action) for action in actions] # Run async so that we can make use of Ollama's concurrency. coroutines: list[Coroutine[Any, Any, ollama.GenerateResponse]] = [] diff --git a/mellea/stdlib/span/__init__.py b/mellea/stdlib/span/__init__.py index 30cdd45b..4067ee93 100644 --- a/mellea/stdlib/span/__init__.py +++ b/mellea/stdlib/span/__init__.py @@ -23,14 +23,14 @@ def __init__(self, **kwargs): def parts(self): """Returns the values of the kwargs.""" - return self._kwargs.values() + return list(self._kwargs.values()) def _kwargs_type_check(self, kwargs): for key in kwargs.keys(): value = kwargs[key] assert issubclass(type(value), Component) or issubclass( type(value), CBlock - ), f"Expected span but found {type(value)}" + ), f"Expected span but found {type(value)} of value: {value}" assert type(key) is str return True @@ -41,9 +41,23 @@ def make_simple_string(kwargs): [f"<|{key}|>{value}" for (key, value) in kwargs.items()] ) + @staticmethod + def make_json_string(kwargs): + """Uses json.""" + str_args = dict() + for key in kwargs.keys(): + match kwargs[key]: + case ModelOutputThunk() | CBlock(): + str_args[key] = kwargs[key].value + case Component(): + str_args[key] = kwargs[key].format_for_llm() + import json + + return json.dumps(str_args) + def format_for_llm(self): """Uses a string rep.""" - return SimpleComponent.make_simple_string(self._kwargs) + return SimpleComponent.make_json_string(self._kwargs) # """ Uses a simple tagging structure that needs to be changed in the future. """ # return TemplateRepresentation( # obj=self, From a9be6f6a048e69e006198f51209ec35fa6772197 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Fri, 12 Dec 2025 10:39:42 -0500 Subject: [PATCH 04/14] Does gather() instead of awaiting on each thunk separately. --- mellea/backends/ollama.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index d75219c3..4850afb6 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -296,9 +296,10 @@ async def generate_from_chat_context( RuntimeError: If not called from a thread with a running event loop. """ # Start by awaiting any necessary computation. - _computed = [await todo.avalue() for todo in generate_walk(action)] + _to_compute = generate_walk(action) + await asyncio.gather([x.avalue() for x in _to_compute]) FancyLogger.get_logger().info( - f"generate_from_chat_context awaited on {len(_computed)} uncomputed mots." + f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." ) model_opts = self._simplify_and_merge(model_options) @@ -415,9 +416,10 @@ async def generate_from_raw( model_opts = self._simplify_and_merge(model_options) + _to_compute = [] for act in actions: - for todo in generate_walk(act): - await todo.avalue() + _to_compute.extend(generate_walk(act)) + await asyncio.gather([x.avalue() for x in _to_compute]) prompts = [self.formatter.print(action) for action in actions] From 5ea53129faa12bd41a0fc94488773ad7982f20c1 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Fri, 12 Dec 2025 11:23:39 -0500 Subject: [PATCH 05/14] Refactor and bug fixes. Deletes the stdlib.span package and moves simplecomponent into base. Fixes a big in call to gather (should be *list not list) --- docs/examples/melp/lazy.py | 3 +- docs/examples/melp/lazy_fib.py | 3 +- docs/examples/melp/lazy_fib_sample.py | 3 +- docs/examples/melp/states.py | 4 +- mellea/backends/_utils.py | 2 +- mellea/backends/ollama.py | 8 ++-- mellea/stdlib/base.py | 50 ++++++++++++++++++++ mellea/stdlib/span/__init__.py | 66 --------------------------- 8 files changed, 60 insertions(+), 79 deletions(-) delete mode 100644 mellea/stdlib/span/__init__.py diff --git a/docs/examples/melp/lazy.py b/docs/examples/melp/lazy.py index cb674914..45635701 100644 --- a/docs/examples/melp/lazy.py +++ b/docs/examples/melp/lazy.py @@ -1,6 +1,5 @@ import asyncio -from mellea.stdlib.span import Span, SimpleComponent -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend diff --git a/docs/examples/melp/lazy_fib.py b/docs/examples/melp/lazy_fib.py index c711012c..95f36821 100644 --- a/docs/examples/melp/lazy_fib.py +++ b/docs/examples/melp/lazy_fib.py @@ -1,6 +1,5 @@ import asyncio -from mellea.stdlib.span import Span, SimpleComponent -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent from mellea.stdlib.requirement import Requirement from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend diff --git a/docs/examples/melp/lazy_fib_sample.py b/docs/examples/melp/lazy_fib_sample.py index bed771a3..b11a55ef 100644 --- a/docs/examples/melp/lazy_fib_sample.py +++ b/docs/examples/melp/lazy_fib_sample.py @@ -1,6 +1,5 @@ import asyncio -from mellea.stdlib.span import Span, SimpleComponent -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk +from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent from mellea.stdlib.requirement import Requirement from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend diff --git a/docs/examples/melp/states.py b/docs/examples/melp/states.py index 9c7f15be..2383bf4a 100644 --- a/docs/examples/melp/states.py +++ b/docs/examples/melp/states.py @@ -1,6 +1,4 @@ -import mellea -from mellea.stdlib.base import CBlock, Context, SimpleContext -from mellea.stdlib.span import Span, SimpleComponent +from mellea.stdlib.base import SimpleContext, Context, CBlock, SimpleComponent from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend import asyncio diff --git a/mellea/backends/_utils.py b/mellea/backends/_utils.py index 694edbbb..3966762e 100644 --- a/mellea/backends/_utils.py +++ b/mellea/backends/_utils.py @@ -98,4 +98,4 @@ def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputT return [] case Component(): parts_walk = [generate_walk(p) for p in c.parts()] - return itertools.chain.from_iterable(parts_walk) # aka flatten + return list(itertools.chain.from_iterable(parts_walk)) # aka flatten diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index 4850afb6..f53cf6f7 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -296,8 +296,9 @@ async def generate_from_chat_context( RuntimeError: If not called from a thread with a running event loop. """ # Start by awaiting any necessary computation. - _to_compute = generate_walk(action) - await asyncio.gather([x.avalue() for x in _to_compute]) + _to_compute = list(generate_walk(action)) + coroutines = [x.avalue() for x in _to_compute] + await asyncio.gather(*coroutines) FancyLogger.get_logger().info( f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." ) @@ -419,7 +420,8 @@ async def generate_from_raw( _to_compute = [] for act in actions: _to_compute.extend(generate_walk(act)) - await asyncio.gather([x.avalue() for x in _to_compute]) + coroutines = [x.avalue() for x in _to_compute] + await asyncio.gather(*coroutines) prompts = [self.formatter.print(action) for action in actions] diff --git a/mellea/stdlib/base.py b/mellea/stdlib/base.py index 111d44f6..362ce237 100644 --- a/mellea/stdlib/base.py +++ b/mellea/stdlib/base.py @@ -656,3 +656,53 @@ class ModelToolCall: def call_func(self) -> Any: """A helper function for calling the function/tool represented by this object.""" return self.func(**self.args) + + +class SimpleComponent(Component): + """A Component that is make up of named spans.""" + + def __init__(self, **kwargs): + """Initialized a simple component of the constructor's kwargs.""" + for key in kwargs.keys(): + if type(kwargs[key]) is str: + kwargs[key] = CBlock(value=kwargs[key]) + self._kwargs_type_check(kwargs) + self._kwargs = kwargs + + def parts(self): + """Returns the values of the kwargs.""" + return list(self._kwargs.values()) + + def _kwargs_type_check(self, kwargs): + for key in kwargs.keys(): + value = kwargs[key] + assert issubclass(type(value), Component) or issubclass( + type(value), CBlock + ), f"Expected span but found {type(value)} of value: {value}" + assert type(key) is str + return True + + @staticmethod + def make_simple_string(kwargs): + """Uses <|key|>value to represent a simple component.""" + return "\n".join( + [f"<|{key}|>{value}" for (key, value) in kwargs.items()] + ) + + @staticmethod + def make_json_string(kwargs): + """Uses json.""" + str_args = dict() + for key in kwargs.keys(): + match kwargs[key]: + case ModelOutputThunk() | CBlock(): + str_args[key] = kwargs[key].value + case Component(): + str_args[key] = kwargs[key].format_for_llm() + import json + + return json.dumps(str_args) + + def format_for_llm(self): + """Uses a string rep.""" + return SimpleComponent.make_json_string(self._kwargs) \ No newline at end of file diff --git a/mellea/stdlib/span/__init__.py b/mellea/stdlib/span/__init__.py deleted file mode 100644 index 4067ee93..00000000 --- a/mellea/stdlib/span/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Conceptual Spans.""" - -from mellea.stdlib.base import ( - CBlock, - Component, - ModelOutputThunk, - TemplateRepresentation, -) - -Span = CBlock | Component | ModelOutputThunk - - -class SimpleComponent(Component): - """A Component that is make up of named spans.""" - - def __init__(self, **kwargs): - """Initialized a simple component of the constructor's kwargs.""" - for key in kwargs.keys(): - if type(kwargs[key]) is str: - kwargs[key] = CBlock(value=kwargs[key]) - self._kwargs_type_check(kwargs) - self._kwargs = kwargs - - def parts(self): - """Returns the values of the kwargs.""" - return list(self._kwargs.values()) - - def _kwargs_type_check(self, kwargs): - for key in kwargs.keys(): - value = kwargs[key] - assert issubclass(type(value), Component) or issubclass( - type(value), CBlock - ), f"Expected span but found {type(value)} of value: {value}" - assert type(key) is str - return True - - @staticmethod - def make_simple_string(kwargs): - """Uses <|key|>value to represent a simple component.""" - return "\n".join( - [f"<|{key}|>{value}" for (key, value) in kwargs.items()] - ) - - @staticmethod - def make_json_string(kwargs): - """Uses json.""" - str_args = dict() - for key in kwargs.keys(): - match kwargs[key]: - case ModelOutputThunk() | CBlock(): - str_args[key] = kwargs[key].value - case Component(): - str_args[key] = kwargs[key].format_for_llm() - import json - - return json.dumps(str_args) - - def format_for_llm(self): - """Uses a string rep.""" - return SimpleComponent.make_json_string(self._kwargs) - # """ Uses a simple tagging structure that needs to be changed in the future. """ - # return TemplateRepresentation( - # obj=self, - # args=self._kwargs, - # template=SimpleComponent.make_simple_template(self._kwargs), - # ) From 22ac0dbb57753b25a9051e221914296c6195b28c Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Fri, 12 Dec 2025 16:43:01 -0500 Subject: [PATCH 06/14] backend walks. --- mellea/backends/__init__.py | 39 ++++++++++++++++++++++++++++++++++ mellea/backends/_utils.py | 12 ----------- mellea/backends/huggingface.py | 4 ++++ mellea/backends/litellm.py | 3 +++ mellea/backends/ollama.py | 10 ++------- mellea/backends/openai.py | 4 ++++ mellea/backends/vllm.py | 4 ++++ mellea/backends/watsonx.py | 4 ++++ 8 files changed, 60 insertions(+), 20 deletions(-) diff --git a/mellea/backends/__init__.py b/mellea/backends/__init__.py index 4a56665a..c621e102 100644 --- a/mellea/backends/__init__.py +++ b/mellea/backends/__init__.py @@ -3,12 +3,15 @@ from __future__ import annotations import abc +import asyncio +import itertools from typing import TypeVar import pydantic from mellea.backends.model_ids import ModelIdentifier from mellea.backends.types import ModelOption +from mellea.helpers.fancy_logger import FancyLogger from mellea.stdlib.base import CBlock, Component, Context, GenerateLog, ModelOutputThunk BaseModelSubclass = TypeVar( @@ -76,3 +79,39 @@ async def generate_from_raw( model_options: Any model options to upsert into the defaults for this call. tool_calls: Always set to false unless supported by backend. """ + + async def do_generate_walk( + self, action: CBlock | Component | ModelOutputThunk + ) -> None: + """Does the generation walk.""" + _to_compute = list(generate_walk(action)) + coroutines = [x.avalue() for x in _to_compute] + await asyncio.gather(*coroutines) + FancyLogger.get_logger().info( + f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." + ) + + async def do_generate_walks( + self, actions: list[CBlock | Component | ModelOutputThunk] + ) -> None: + """Does the generation walk.""" + _to_compute = [] + for action in actions: + _to_compute.extend(list(generate_walk(action))) + coroutines = [x.avalue() for x in _to_compute] + await asyncio.gather(*coroutines) + FancyLogger.get_logger().info( + f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." + ) + + +def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: + """Returns the generation walk ordering for a Span.""" + match c: + case ModelOutputThunk() if not c.is_computed(): + return [c] + case CBlock(): + return [] + case Component(): + parts_walk = [generate_walk(p) for p in c.parts()] + return list(itertools.chain.from_iterable(parts_walk)) # aka flatten diff --git a/mellea/backends/_utils.py b/mellea/backends/_utils.py index 3966762e..28dc6d5f 100644 --- a/mellea/backends/_utils.py +++ b/mellea/backends/_utils.py @@ -87,15 +87,3 @@ def to_tool_calls( if len(model_tool_calls) > 0: return model_tool_calls return None - - -def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: - """Returns the generation walk ordering for a Span.""" - match c: - case ModelOutputThunk() if not c.is_computed(): - return [c] - case CBlock(): - return [] - case Component(): - parts_walk = [generate_walk(p) for p in c.parts()] - return list(itertools.chain.from_iterable(parts_walk)) # aka flatten diff --git a/mellea/backends/huggingface.py b/mellea/backends/huggingface.py index 79d719af..4f9d647f 100644 --- a/mellea/backends/huggingface.py +++ b/mellea/backends/huggingface.py @@ -196,6 +196,8 @@ async def generate_from_context( tool_calls: bool = False, ): """Generate using the huggingface model.""" + await self.do_generate_walk(action) + # Upsert model options. model_opts = self._simplify_and_merge(model_options) @@ -677,6 +679,8 @@ async def generate_from_raw( tool_calls: bool = False, ) -> list[ModelOutputThunk]: """Generate using the completions api. Gives the input provided to the model without templating.""" + await self.do_generate_walks(actions) + if tool_calls: FancyLogger.get_logger().warning( "The raw endpoint does not support tool calling at the moment." diff --git a/mellea/backends/litellm.py b/mellea/backends/litellm.py index 555431c5..80adbc8b 100644 --- a/mellea/backends/litellm.py +++ b/mellea/backends/litellm.py @@ -241,6 +241,8 @@ async def _generate_from_chat_context_standard( model_options: dict | None = None, tool_calls: bool = False, ) -> ModelOutputThunk: + await self.do_generate_walk(action) + model_opts = self._simplify_and_merge(model_options) linearized_context = ctx.view_for_generation() assert linearized_context is not None, ( @@ -484,6 +486,7 @@ async def generate_from_raw( tool_calls: bool = False, ) -> list[ModelOutputThunk]: """Generate using the completions api. Gives the input provided to the model without templating.""" + await self.do_generate_walks(actions) extra_body = {} if format is not None: FancyLogger.get_logger().warning( diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index f53cf6f7..ca11d0a0 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -10,8 +10,7 @@ from tqdm import tqdm import mellea.backends.model_ids as model_ids -from mellea.backends import BaseModelSubclass -from mellea.backends._utils import generate_walk +from mellea.backends import BaseModelSubclass, generate_walk from mellea.backends.formatter import Formatter, FormatterBackend, TemplateFormatter from mellea.backends.model_ids import ModelIdentifier from mellea.backends.tools import ( @@ -296,12 +295,7 @@ async def generate_from_chat_context( RuntimeError: If not called from a thread with a running event loop. """ # Start by awaiting any necessary computation. - _to_compute = list(generate_walk(action)) - coroutines = [x.avalue() for x in _to_compute] - await asyncio.gather(*coroutines) - FancyLogger.get_logger().info( - f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." - ) + await self.do_generate_walk(action) model_opts = self._simplify_and_merge(model_options) diff --git a/mellea/backends/openai.py b/mellea/backends/openai.py index ba825753..c9a7299a 100644 --- a/mellea/backends/openai.py +++ b/mellea/backends/openai.py @@ -316,6 +316,8 @@ async def generate_from_chat_context( tool_calls: bool = False, ) -> tuple[ModelOutputThunk, Context]: """Generates a new completion from the provided Context using this backend's `Formatter`.""" + await self.do_generate_walk(action) + # Requirements can be automatically rerouted to a requirement adapter. if isinstance(action, Requirement): # See docs/dev/requirement_aLoRA_rerouting.md @@ -786,6 +788,8 @@ async def generate_from_raw( tool_calls: bool = False, ) -> list[ModelOutputThunk]: """Generate using the completions api. Gives the input provided to the model without templating.""" + await self.do_generate_walks(actions) + extra_body = {} if format is not None: FancyLogger.get_logger().warning( diff --git a/mellea/backends/vllm.py b/mellea/backends/vllm.py index f9d6a753..07f483ee 100644 --- a/mellea/backends/vllm.py +++ b/mellea/backends/vllm.py @@ -248,6 +248,8 @@ async def generate_from_context( tool_calls: bool = False, ) -> tuple[ModelOutputThunk, Context]: """Generate using the huggingface model.""" + await self.do_generate_walk(action) + # Upsert model options. model_options = self._simplify_and_merge(model_options) @@ -437,6 +439,8 @@ async def generate_from_raw( tool_calls: bool = False, ) -> list[ModelOutputThunk]: """Generate using the completions api. Gives the input provided to the model without templating.""" + await self.do_generate_walks(actions) + if tool_calls: FancyLogger.get_logger().warning( "The completion endpoint does not support tool calling at the moment." diff --git a/mellea/backends/watsonx.py b/mellea/backends/watsonx.py index 5821b446..721e4f05 100644 --- a/mellea/backends/watsonx.py +++ b/mellea/backends/watsonx.py @@ -269,6 +269,8 @@ async def generate_from_chat_context( tool_calls: bool = False, ) -> ModelOutputThunk: """Generates a new completion from the provided Context using this backend's `Formatter`.""" + await self.do_generate_walk(action) + model_opts = self._simplify_and_merge( model_options, is_chat_context=ctx.is_chat_context ) @@ -490,6 +492,8 @@ async def generate_from_raw( tool_calls: bool = False, ) -> list[ModelOutputThunk]: """Generates a completion text. Gives the input provided to the model without templating.""" + await self.do_generate_walks(actions) + if format is not None: FancyLogger.get_logger().warning( "WatsonxAI completion api does not accept response format, ignoring it for this request." From 7187941d7e30848720139c1195f50ef475b31e26 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 10:28:09 -0500 Subject: [PATCH 07/14] Adds heapcomponents. --- mellea/stdlib/base.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/mellea/stdlib/base.py b/mellea/stdlib/base.py index 362ce237..cfd790fc 100644 --- a/mellea/stdlib/base.py +++ b/mellea/stdlib/base.py @@ -705,4 +705,39 @@ def make_json_string(kwargs): def format_for_llm(self): """Uses a string rep.""" - return SimpleComponent.make_json_string(self._kwargs) \ No newline at end of file + return SimpleComponent.make_json_string(self._kwargs) + + +class HeapContext(Context): + """A HeapContext is a context that is constructed by reading off all of the locals() and globals() whose values are CBlock | Component | MoTs.""" + def __init__(self): + """Heap at construction-time. Should this be at the use site?""" + self._heap = dict() + + for key, value in globals().items(): + match value: + case ModelOutputThunk() | Component() | CBlock(): + self._heap[key] = value + case _: + continue + + for key, value in locals().items(): + match value: + case ModelOutputThunk() | Component() | CBlock(): + self._heap[key] = value + case _: + continue + + def is_chat_context(self): + """Heap contexts are not chat contexts.""" + return False + + def add(self, c: Component | CBlock) -> Context: + """Returns a new context obtained by adding `c` to this context as the "last item", using _ to denote the last expression.""" + new_context = HeapContext() + new_context["_"] = c + return new_context + + def view_for_generation(self) -> list[Component | CBlock] | None: + """Provides a linear list of context components to use for generation, or None if that is not possible to construct.""" + return [SimpleComponent(**self._heap)] From 6ea6d4615b7174dcfaa7075d9be1f513fc00fd1e Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 11:02:56 -0500 Subject: [PATCH 08/14] Make uncomputed mots logging less noisy. --- mellea/backends/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mellea/backends/__init__.py b/mellea/backends/__init__.py index c621e102..88e3aec5 100644 --- a/mellea/backends/__init__.py +++ b/mellea/backends/__init__.py @@ -86,10 +86,12 @@ async def do_generate_walk( """Does the generation walk.""" _to_compute = list(generate_walk(action)) coroutines = [x.avalue() for x in _to_compute] + # The following log message might get noisy. Feel free to remove if so. + if len(_to_compute) > 0: + FancyLogger.get_logger().info( + f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." + ) await asyncio.gather(*coroutines) - FancyLogger.get_logger().info( - f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." - ) async def do_generate_walks( self, actions: list[CBlock | Component | ModelOutputThunk] @@ -99,10 +101,12 @@ async def do_generate_walks( for action in actions: _to_compute.extend(list(generate_walk(action))) coroutines = [x.avalue() for x in _to_compute] + # The following log message might get noisy. Feel free to remove if so. + if len(_to_compute) > 0: + FancyLogger.get_logger().info( + f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." + ) await asyncio.gather(*coroutines) - FancyLogger.get_logger().info( - f"generate_from_chat_context awaited on {len(_to_compute)} uncomputed mots." - ) def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: From 4f37d96f583ad470ba1dc5706d0bc52dd2c3a38f Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 11:04:16 -0500 Subject: [PATCH 09/14] adds a simple example. --- docs/examples/melp/simple_example.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/examples/melp/simple_example.py diff --git a/docs/examples/melp/simple_example.py b/docs/examples/melp/simple_example.py new file mode 100644 index 00000000..3aec05f9 --- /dev/null +++ b/docs/examples/melp/simple_example.py @@ -0,0 +1,38 @@ +import asyncio +from mellea.stdlib.base import Context, CBlock, SimpleContext, ModelOutputThunk +from mellea.backends import Backend +from mellea.backends.ollama import OllamaModelBackend + + +async def main(backend: Backend, ctx: Context): + """ + In this example, we show how executing multiple MOTs in parallel should work. + """ + m_states = "Missouri", "Minnesota", "Montana", "Massachusetts" + + poem_thunks = [] + for state_name in m_states: + mot, ctx = await backend.generate_from_context( + CBlock(f"Write a poem about {state_name}"), + ctx + ) + poem_thunks.append(mot) + + # Notice that what we have now is a list of ModelOutputThunks, none of which are computed. + for poem_thunk in poem_thunks: + assert type(poem_thunk) == ModelOutputThunk + print(f"Computed: {poem_thunk.is_computed()}") + + # Let's run all of these in parallel. + await asyncio.gather(*[c.avalue() for c in poem_thunks]) + + # Print out the final results, which are now computed. + for poem_thunk in poem_thunks: + print(f"Computed: {poem_thunk.is_computed()}") + + # And let's print out the final results. + for poem_thunk in poem_thunks: + print(poem_thunk.value) + +backend = OllamaModelBackend(model_id="granite4:latest") +asyncio.run(main(backend, SimpleContext())) \ No newline at end of file From 152ede9773fd0a0a3b228648a8b796d9dbac0c35 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 11:09:32 -0500 Subject: [PATCH 10/14] Cleans up fib example. --- docs/examples/melp/lazy_fib.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/examples/melp/lazy_fib.py b/docs/examples/melp/lazy_fib.py index 95f36821..feec18e0 100644 --- a/docs/examples/melp/lazy_fib.py +++ b/docs/examples/melp/lazy_fib.py @@ -1,5 +1,11 @@ import asyncio -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent +from mellea.stdlib.base import ( + SimpleContext, + Context, + CBlock, + ModelOutputThunk, + SimpleComponent, +) from mellea.stdlib.requirement import Requirement from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend @@ -25,13 +31,14 @@ async def fib_main(backend: Backend, ctx: Context): mot = await fib(backend, ctx, fibs[i - 1], fibs[i - 2]) fibs.append(mot) - for x in enumerate(fibs): - match x: - case ModelOutputThunk(): - n = await x.avalue() - print(n) - case CBlock(): - print(x.value) + print(await fibs[-1].avalue()) + # for x in fibs: + # match x: + # case ModelOutputThunk(): + # n = await x.avalue() + # print(n) + # case CBlock(): + # print(x.value) asyncio.run(fib_main(backend, SimpleContext())) From 477275d30397b22bc28f8590d7ccab86098bd3e5 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 11:22:08 -0500 Subject: [PATCH 11/14] Adds parts() for instruction and genslot components. --- mellea/stdlib/genslot.py | 10 +++++++--- mellea/stdlib/instruction.py | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/mellea/stdlib/genslot.py b/mellea/stdlib/genslot.py index a020d4e8..99001d6d 100644 --- a/mellea/stdlib/genslot.py +++ b/mellea/stdlib/genslot.py @@ -140,10 +140,10 @@ def __init__( class Function: - """A Function Component.""" + """A Function.""" def __init__(self, func: Callable): - """A Function Component.""" + """A Function.""" self._func: Callable = func self._function_dict: FunctionDict = describe_function(func) @@ -382,7 +382,11 @@ def _context_backend_extract_args_and_kwargs( def parts(self): """Not implemented.""" - raise NotImplementedError + cs: list = self._arguments + cs.extend(self.requirements) + cs.extend(self._function) + return cs + def format_for_llm(self) -> TemplateRepresentation: """Formats the instruction for Formatter use.""" diff --git a/mellea/stdlib/instruction.py b/mellea/stdlib/instruction.py index f8d07efb..7c8f2c27 100644 --- a/mellea/stdlib/instruction.py +++ b/mellea/stdlib/instruction.py @@ -121,9 +121,11 @@ def __init__( def parts(self): """Returns all of the constituent parts of an Instruction.""" - raise Exception( - "Disallowing use of `parts` until we figure out exactly what it's supposed to be for" - ) + cs = [self._description, self._grounding_context, self._prefix, self._output_prefix] + cs.extend(self._requirements) + cs.extend(self._icl_examples) + cs = list(filter(lambda x: x is not None, cs)) + return cs def format_for_llm(self) -> TemplateRepresentation: """Formats the instruction for Formatter use.""" From 976ac0685fa6e1ac8408ba770723acdc236166dd Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 11:23:07 -0500 Subject: [PATCH 12/14] Don't call things components which are not components. --- mellea/stdlib/genslot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mellea/stdlib/genslot.py b/mellea/stdlib/genslot.py index 99001d6d..037367c0 100644 --- a/mellea/stdlib/genslot.py +++ b/mellea/stdlib/genslot.py @@ -74,7 +74,7 @@ class ArgumentDict(TypedDict): class Argument: - """An Argument Component.""" + """An Argument.""" def __init__( self, @@ -82,7 +82,7 @@ def __init__( name: str | None = None, value: str | None = None, ): - """An Argument Component.""" + """An Argument.""" self._argument_dict: ArgumentDict = { "name": name, "annotation": annotation, From 1797be494d84e370e75c69b4a2312420e8d29792 Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 12:26:23 -0500 Subject: [PATCH 13/14] ruff. --- docs/examples/melp/lazy.py | 8 +++++++- docs/examples/melp/lazy_fib_sample.py | 8 +++++++- docs/examples/melp/simple_example.py | 10 +++++----- mellea/stdlib/base.py | 1 + mellea/stdlib/genslot.py | 1 - mellea/stdlib/instruction.py | 7 ++++++- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/examples/melp/lazy.py b/docs/examples/melp/lazy.py index 45635701..cbdb33c0 100644 --- a/docs/examples/melp/lazy.py +++ b/docs/examples/melp/lazy.py @@ -1,5 +1,11 @@ import asyncio -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent +from mellea.stdlib.base import ( + SimpleContext, + Context, + CBlock, + ModelOutputThunk, + SimpleComponent, +) from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend diff --git a/docs/examples/melp/lazy_fib_sample.py b/docs/examples/melp/lazy_fib_sample.py index b11a55ef..0bec2907 100644 --- a/docs/examples/melp/lazy_fib_sample.py +++ b/docs/examples/melp/lazy_fib_sample.py @@ -1,5 +1,11 @@ import asyncio -from mellea.stdlib.base import SimpleContext, Context, CBlock, ModelOutputThunk, SimpleComponent +from mellea.stdlib.base import ( + SimpleContext, + Context, + CBlock, + ModelOutputThunk, + SimpleComponent, +) from mellea.stdlib.requirement import Requirement from mellea.backends import Backend from mellea.backends.ollama import OllamaModelBackend diff --git a/docs/examples/melp/simple_example.py b/docs/examples/melp/simple_example.py index 3aec05f9..7ac1059b 100644 --- a/docs/examples/melp/simple_example.py +++ b/docs/examples/melp/simple_example.py @@ -6,18 +6,17 @@ async def main(backend: Backend, ctx: Context): """ - In this example, we show how executing multiple MOTs in parallel should work. + In this example, we show how executing multiple MOTs in parallel should work. """ m_states = "Missouri", "Minnesota", "Montana", "Massachusetts" poem_thunks = [] for state_name in m_states: mot, ctx = await backend.generate_from_context( - CBlock(f"Write a poem about {state_name}"), - ctx + CBlock(f"Write a poem about {state_name}"), ctx ) poem_thunks.append(mot) - + # Notice that what we have now is a list of ModelOutputThunks, none of which are computed. for poem_thunk in poem_thunks: assert type(poem_thunk) == ModelOutputThunk @@ -34,5 +33,6 @@ async def main(backend: Backend, ctx: Context): for poem_thunk in poem_thunks: print(poem_thunk.value) + backend = OllamaModelBackend(model_id="granite4:latest") -asyncio.run(main(backend, SimpleContext())) \ No newline at end of file +asyncio.run(main(backend, SimpleContext())) diff --git a/mellea/stdlib/base.py b/mellea/stdlib/base.py index cfd790fc..a76ff83d 100644 --- a/mellea/stdlib/base.py +++ b/mellea/stdlib/base.py @@ -710,6 +710,7 @@ def format_for_llm(self): class HeapContext(Context): """A HeapContext is a context that is constructed by reading off all of the locals() and globals() whose values are CBlock | Component | MoTs.""" + def __init__(self): """Heap at construction-time. Should this be at the use site?""" self._heap = dict() diff --git a/mellea/stdlib/genslot.py b/mellea/stdlib/genslot.py index 037367c0..048a917c 100644 --- a/mellea/stdlib/genslot.py +++ b/mellea/stdlib/genslot.py @@ -387,7 +387,6 @@ def parts(self): cs.extend(self._function) return cs - def format_for_llm(self) -> TemplateRepresentation: """Formats the instruction for Formatter use.""" return TemplateRepresentation( diff --git a/mellea/stdlib/instruction.py b/mellea/stdlib/instruction.py index 7c8f2c27..132f4686 100644 --- a/mellea/stdlib/instruction.py +++ b/mellea/stdlib/instruction.py @@ -121,7 +121,12 @@ def __init__( def parts(self): """Returns all of the constituent parts of an Instruction.""" - cs = [self._description, self._grounding_context, self._prefix, self._output_prefix] + cs = [ + self._description, + self._grounding_context, + self._prefix, + self._output_prefix, + ] cs.extend(self._requirements) cs.extend(self._icl_examples) cs = list(filter(lambda x: x is not None, cs)) From 24de7610b71050d1c1018e2d2061c1ba6f9bc46a Mon Sep 17 00:00:00 2001 From: Nathan Fulton Date: Wed, 17 Dec 2025 13:37:29 -0500 Subject: [PATCH 14/14] Starts adding some examples for a deepdive on sessions. --- docs/rewrite/session_deepdive/1.py | 9 +++++++++ docs/rewrite/session_deepdive/2.py | 9 +++++++++ docs/rewrite/session_deepdive/3.py | 15 +++++++++++++++ docs/rewrite/session_deepdive/4.py | 19 +++++++++++++++++++ docs/rewrite/session_deepdive/5.py | 25 +++++++++++++++++++++++++ mellea/stdlib/chat.py | 9 ++++++--- 6 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 docs/rewrite/session_deepdive/1.py create mode 100644 docs/rewrite/session_deepdive/2.py create mode 100644 docs/rewrite/session_deepdive/3.py create mode 100644 docs/rewrite/session_deepdive/4.py create mode 100644 docs/rewrite/session_deepdive/5.py diff --git a/docs/rewrite/session_deepdive/1.py b/docs/rewrite/session_deepdive/1.py new file mode 100644 index 00000000..97044e85 --- /dev/null +++ b/docs/rewrite/session_deepdive/1.py @@ -0,0 +1,9 @@ +import mellea.stdlib.functional as mfuncs +from mellea.stdlib.base import SimpleContext +from mellea.backends.ollama import OllamaModelBackend + +response, next_context = mfuncs.chat("What is 1+1?", + context=SimpleContext(), + backend=OllamaModelBackend("granite4:latest")) + +print(response.content) \ No newline at end of file diff --git a/docs/rewrite/session_deepdive/2.py b/docs/rewrite/session_deepdive/2.py new file mode 100644 index 00000000..700d6076 --- /dev/null +++ b/docs/rewrite/session_deepdive/2.py @@ -0,0 +1,9 @@ +import mellea.stdlib.functional as mfuncs +from mellea.stdlib.base import SimpleContext, CBlock +from mellea.backends.ollama import OllamaModelBackend + +response, next_context = mfuncs.act(CBlock("What is 1+1?"), + context=SimpleContext(), + backend=OllamaModelBackend("granite4:latest")) + +print(response.value) \ No newline at end of file diff --git a/docs/rewrite/session_deepdive/3.py b/docs/rewrite/session_deepdive/3.py new file mode 100644 index 00000000..09065bac --- /dev/null +++ b/docs/rewrite/session_deepdive/3.py @@ -0,0 +1,15 @@ +import mellea.stdlib.functional as mfuncs +from mellea.stdlib.base import SimpleContext, CBlock, Context +from mellea.backends.ollama import OllamaModelBackend +from mellea.backends import Backend +import asyncio + + +async def main(backend: Backend, ctx: Context): + response, next_context = await mfuncs.aact(CBlock("What is 1+1?"), + context=ctx, + backend=backend) + + print(response.value) + +asyncio.run(main(OllamaModelBackend("granite4:latest"), SimpleContext())) \ No newline at end of file diff --git a/docs/rewrite/session_deepdive/4.py b/docs/rewrite/session_deepdive/4.py new file mode 100644 index 00000000..79dce652 --- /dev/null +++ b/docs/rewrite/session_deepdive/4.py @@ -0,0 +1,19 @@ +import mellea.stdlib.functional as mfuncs +from mellea.stdlib.base import SimpleContext, CBlock, Context +from mellea.backends.ollama import OllamaModelBackend +from mellea.backends import Backend +import asyncio + + +async def main(backend: Backend, ctx: Context): + response, next_context = await backend.generate_from_context( + CBlock("What is 1+1?"), + ctx=ctx # TODO we should rationalize ctx and context acress mfuncs and base/backend. + ) + + print(f"Currently computed: {response.is_computed()}") + print(await response.avalue()) + print(f"Currently computed: {response.is_computed()}") + + +asyncio.run(main(OllamaModelBackend("granite4:latest"), SimpleContext())) \ No newline at end of file diff --git a/docs/rewrite/session_deepdive/5.py b/docs/rewrite/session_deepdive/5.py new file mode 100644 index 00000000..6503fc61 --- /dev/null +++ b/docs/rewrite/session_deepdive/5.py @@ -0,0 +1,25 @@ +import mellea.stdlib.functional as mfuncs +from mellea.stdlib.base import SimpleContext, CBlock, Context, SimpleComponent +from mellea.backends.ollama import OllamaModelBackend +from mellea.backends import Backend +import asyncio + + +async def main(backend: Backend, ctx: Context): + x, _ = await backend.generate_from_context(CBlock("What is 1+1?"), ctx=ctx) + + y, _ = await backend.generate_from_context(CBlock("What is 2+2?"), ctx=ctx) + + response, _ = await backend.generate_from_context( + SimpleComponent(instruction="What is x+y?", x=x, y=y), + ctx=ctx # TODO we should rationalize ctx and context acress mfuncs and base/backend. + ) + + print(f"x currently computed: {x.is_computed()}") + print(f"y currently computed: {y.is_computed()}") + print(f"response currently computed: {response.is_computed()}") + print(await response.avalue()) + print(f"response currently computed: {response.is_computed()}") + + +asyncio.run(main(OllamaModelBackend("granite4:latest"), SimpleContext())) \ No newline at end of file diff --git a/mellea/stdlib/chat.py b/mellea/stdlib/chat.py index 574e6fa6..4719f5d7 100644 --- a/mellea/stdlib/chat.py +++ b/mellea/stdlib/chat.py @@ -51,9 +51,12 @@ def images(self) -> None | list[str]: def parts(self): """Returns all of the constituent parts of an Instruction.""" - raise Exception( - "Disallowing use of `parts` until we figure out exactly what it's supposed to be for" - ) + assert self._images is None, "TODO: images are not handled correctly in the mellea core." + parts = [] + if self._docs is not None: + parts.extend(self._docs) + return parts + def format_for_llm(self) -> TemplateRepresentation: """Formats the content for a Language Model.