Skip to content

Commit 1cb0dc9

Browse files
authored
Merge branch 'generative-computing:main' into main
2 parents cd51b73 + d92a44f commit 1cb0dc9

27 files changed

+526
-249
lines changed

.github/workflows/quality.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
strategy:
1818
matrix:
1919
python-version: ['3.10', '3.11', '3.12'] # Need to add 3.13 once we resolve outlines issues.
20+
env:
21+
CICD: 1
22+
OLLAMA_HOST: "127.0.0.1:5000"
2023
steps:
2124
- uses: actions/checkout@v4
2225
- name: Install uv and set the python version
@@ -31,9 +34,22 @@ jobs:
3134
path: ~/.cache/pre-commit
3235
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
3336
- name: Install dependencies
34-
run: uv sync --frozen --all-extras
37+
run: uv sync --frozen --all-extras --group dev
3538
- name: Check style and run tests
3639
run: pre-commit run --all-files
37-
- name: Send failure message
40+
- name: Send failure message pre-commit
3841
if: failure() # This step will only run if a previous step failed
3942
run: echo "The quality verification failed. Please run precommit "
43+
- name: Install Ollama
44+
run: curl -fsSL https://ollama.com/install.sh | sh
45+
- name: Start serving ollama
46+
run: nohup ollama serve &
47+
- name: Pull Llama 3.2:1b model
48+
run: ollama pull llama3.2:1b
49+
50+
- name: Run Tests
51+
run: uv run -m pytest -v test
52+
- name: Send failure message tests
53+
if: failure() # This step will only run if a previous step failed
54+
run: echo "Tests failed. Please verify that tests are working locally."
55+

docs/tutorial.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- [Chapter 9: Interoperability with Other Frameworks](#chapter-9-interoperability-with-other-frameworks)
2121
- [Chapter 10: Prompt Engineering for Mellea](#chapter-10-prompt-engineering-for-m)
2222
- [Custom Templates](#custom-templates)
23+
- [Chapter 11: Tool Calling](#chapter-11-tool-calling)
2324
- [Appendix: Contributing to Melles](#appendix-contributing-to-mellea)
2425

2526
## Chapter 1: What Is Generative Programming
@@ -1281,6 +1282,33 @@ To customize the template and template representation of an existing class, simp
12811282

12821283
See [`mellea/docs/examples/mify/rich_document_advanced.py`](./examples/mify/rich_document_advanced.py)
12831284

1285+
## Chapter 11: Tool Calling
1286+
Mellea supports tool calling for providers/models that support it. Most session level functions support setting a tool_calls boolean. Setting this to true allows tools to be called, but there's no guarantee that a model will call them.
1287+
1288+
Tools can be made available for the model to call in a few ways:
1289+
1. Components: components can have a TemplateRepresentation object that contains tools.
1290+
2. Context: depending on the context, the components in that context can be used as sources of additional tools in the exact same way they would if they were the current action.
1291+
3. `ModelOptions.TOOLS`: model options can include a tools parameter. The preferred way of passing these tools is as a list of function objects.
1292+
1293+
Currently, tools are identified by the name of the function. If there are conflicts, the most recent tool with that name will be preferred. This means the tools available to the model will have the same priority listed above:
1294+
1. Tools from the current component will always be included
1295+
2. Tools from the context will be included if there are no name conflicts. A given context can decide what tools to surface, but in most cases, tools from the most recent component in the context will take priority over tools from older requests.
1296+
3. Tools from `ModelOptions.TOOLS` will only be added if they do not conflict with any of the above functions.
1297+
1298+
For examples on adding tools to the template representation of a component, see the `Table` object in [richdocument.py](../mellea/stdlib/docs/richdocument.py).
1299+
1300+
Here's an example of adding a tool through model options. This can be useful when you want to add a tool like web search that should almost always be available:
1301+
```python
1302+
from mellea.backends.types import ModelOption
1303+
1304+
def web_search(query: str) -> str:
1305+
...
1306+
1307+
model_opts = {
1308+
ModelOptions.TOOLS: [web_search]
1309+
}
1310+
```
1311+
12841312
## Appendix: Contributing to Mellea
12851313

12861314
### Contributor Guide: Requirements and Verifiers

mellea/backends/huggingface.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
import datetime
1111
import inspect
1212
import json
13-
import os
1413
from collections.abc import Callable
15-
from typing import TYPE_CHECKING, Any, Optional
14+
from typing import TYPE_CHECKING, Any
1615

1716
import outlines
1817
import outlines_core
@@ -32,8 +31,9 @@
3231
from mellea.backends.formatter import Formatter, FormatterBackend, TemplateFormatter
3332
from mellea.backends.model_ids import ModelIdentifier
3433
from mellea.backends.tools import (
34+
add_tools_from_context_actions,
35+
add_tools_from_model_options,
3536
convert_tools_to_json,
36-
get_tools_from_action,
3737
parse_tools,
3838
)
3939
from mellea.backends.types import ModelOption
@@ -45,7 +45,6 @@
4545
GenerateLog,
4646
ModelOutputThunk,
4747
ModelToolCall,
48-
TemplateRepresentation,
4948
)
5049
from mellea.stdlib.chat import Message
5150
from mellea.stdlib.requirement import ALoraRequirement, LLMaJRequirement, Requirement
@@ -325,28 +324,15 @@ def _generate_from_context_standard(
325324
f"Tool calling typically uses constrained generation, but you have specified a `format` in your generate call. NB: tool calling is superseded by format; we will NOT call tools for your request: {action}"
326325
)
327326
else:
328-
if isinstance(action, Component) and isinstance(
329-
action.format_for_llm(), TemplateRepresentation
330-
):
331-
tools = get_tools_from_action(action)
332-
333-
model_options_tools = model_options.get(ModelOption.TOOLS, None)
334-
if model_options_tools is not None:
335-
assert isinstance(model_options_tools, dict)
336-
for fn_name in model_options_tools:
337-
# invariant re: relationship between the model_options set of tools and the TemplateRepresentation set of tools
338-
assert fn_name not in tools.keys(), (
339-
f"Cannot add tool {fn_name} because that tool was already defined in the TemplateRepresentation for the action."
340-
)
341-
# type checking because ModelOptions is an untyped dict and the calling convention for tools isn't clearly documented at our abstraction boundaries.
342-
assert type(fn_name) is str, (
343-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
344-
)
345-
assert callable(model_options_tools[fn_name]), (
346-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
347-
)
348-
# Add the model_options tool to the existing set of tools.
349-
tools[fn_name] = model_options_tools[fn_name]
327+
add_tools_from_model_options(tools, model_options)
328+
add_tools_from_context_actions(
329+
tools, ctx.actions_for_available_tools()
330+
)
331+
332+
# Add the tools from the action for this generation last so that
333+
# they overwrite conflicting names.
334+
add_tools_from_context_actions(tools, [action])
335+
FancyLogger.get_logger().info(f"Tools for call: {tools.keys()}")
350336

351337
seed = model_options.get(ModelOption.SEED, None)
352338
if seed is not None:

mellea/backends/model_ids.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ class ModelIdentifier:
8989
ollama_name="llama-guard3:1b", hf_model_name="unsloth/Llama-Guard-3-1B"
9090
)
9191

92+
META_LLAMA_3_2_1B = ModelIdentifier(
93+
ollama_name="llama3.2:1b", hf_model_name="unsloth/Llama-3.2-1B"
94+
)
95+
9296
########################
9397
#### Mistral models ####
9498
########################

mellea/backends/ollama.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import asyncio
44
import datetime
5-
import os
65
from collections.abc import Callable
76
from typing import Any
87

@@ -13,7 +12,10 @@
1312
from mellea.backends import BaseModelSubclass
1413
from mellea.backends.formatter import Formatter, FormatterBackend, TemplateFormatter
1514
from mellea.backends.model_ids import ModelIdentifier
16-
from mellea.backends.tools import get_tools_from_action
15+
from mellea.backends.tools import (
16+
add_tools_from_context_actions,
17+
add_tools_from_model_options,
18+
)
1719
from mellea.backends.types import ModelOption
1820
from mellea.helpers.fancy_logger import FancyLogger
1921
from mellea.stdlib.base import (
@@ -295,28 +297,12 @@ def generate_from_chat_context(
295297
f"Tool calling typically uses constrained generation, but you have specified a `format` in your generate call. NB: tool calling is superseded by format; we will NOT call tools for your request: {action}"
296298
)
297299
else:
298-
if isinstance(action, Component) and isinstance(
299-
action.format_for_llm(), TemplateRepresentation
300-
):
301-
tools = get_tools_from_action(action)
302-
303-
model_options_tools = model_opts.get(ModelOption.TOOLS, None)
304-
if model_options_tools is not None:
305-
assert isinstance(model_options_tools, dict)
306-
for fn_name in model_options_tools:
307-
# invariant re: relationship between the model_options set of tools and the TemplateRepresentation set of tools
308-
assert fn_name not in tools.keys(), (
309-
f"Cannot add tool {fn_name} because that tool was already defined in the TemplateRepresentation for the action."
310-
)
311-
# type checking because ModelOptions is an untyped dict and the calling convention for tools isn't clearly documented at our abstraction boundaries.
312-
assert type(fn_name) is str, (
313-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
314-
)
315-
assert callable(model_options_tools[fn_name]), (
316-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
317-
)
318-
# Add the model_options tool to the existing set of tools.
319-
tools[fn_name] = model_options_tools[fn_name]
300+
add_tools_from_model_options(tools, model_opts)
301+
add_tools_from_context_actions(tools, ctx.actions_for_available_tools())
302+
303+
# Add the tools from the action for this generation last so that
304+
# they overwrite conflicting names.
305+
add_tools_from_context_actions(tools, [action])
320306
FancyLogger.get_logger().info(f"Tools for call: {tools.keys()}")
321307

322308
# Generate a chat response from ollama, using the chat messages.

mellea/backends/openai.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
from mellea.backends.aloras import Alora, AloraBackendMixin
2121
from mellea.backends.formatter import Formatter, FormatterBackend, TemplateFormatter
2222
from mellea.backends.model_ids import ModelIdentifier
23-
from mellea.backends.tools import convert_tools_to_json, get_tools_from_action
23+
from mellea.backends.tools import (
24+
add_tools_from_context_actions,
25+
add_tools_from_model_options,
26+
convert_tools_to_json,
27+
)
2428
from mellea.backends.types import ModelOption
2529
from mellea.helpers.fancy_logger import FancyLogger
2630
from mellea.stdlib.base import (
@@ -30,7 +34,6 @@
3034
GenerateLog,
3135
ModelOutputThunk,
3236
ModelToolCall,
33-
TemplateRepresentation,
3437
)
3538
from mellea.stdlib.chat import Message
3639
from mellea.stdlib.requirement import ALoraRequirement, LLMaJRequirement, Requirement
@@ -404,28 +407,13 @@ def _generate_from_chat_context_standard(
404407
f"Tool calling typically uses constrained generation, but you have specified a `format` in your generate call. NB: tool calling is superseded by format; we will NOT call tools for your request: {action}"
405408
)
406409
else:
407-
if isinstance(action, Component) and isinstance(
408-
action.format_for_llm(), TemplateRepresentation
409-
):
410-
tools = get_tools_from_action(action)
411-
412-
model_options_tools = model_opts.get(ModelOption.TOOLS, None)
413-
if model_options_tools is not None:
414-
assert isinstance(model_options_tools, dict)
415-
for fn_name in model_options_tools:
416-
# invariant re: relationship between the model_options set of tools and the TemplateRepresentation set of tools
417-
assert fn_name not in tools.keys(), (
418-
f"Cannot add tool {fn_name} because that tool was already defined in the TemplateRepresentation for the action."
419-
)
420-
# type checking because ModelOptions is an untyped dict and the calling convention for tools isn't clearly documented at our abstraction boundaries.
421-
assert type(fn_name) is str, (
422-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
423-
)
424-
assert callable(model_options_tools[fn_name]), (
425-
"When providing a `ModelOption.TOOLS` parameter to `model_options`, always used the type Dict[str, Callable] where `str` is the function name and the callable is the function."
426-
)
427-
# Add the model_options tool to the existing set of tools.
428-
tools[fn_name] = model_options_tools[fn_name]
410+
add_tools_from_model_options(tools, model_opts)
411+
add_tools_from_context_actions(tools, ctx.actions_for_available_tools())
412+
413+
# Add the tools from the action for this generation last so that
414+
# they overwrite conflicting names.
415+
add_tools_from_context_actions(tools, [action])
416+
FancyLogger.get_logger().info(f"Tools for call: {tools.keys()}")
429417

430418
thinking = model_opts.get(ModelOption.THINKING, None)
431419
if type(thinking) is bool and thinking:

mellea/backends/tools.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,64 @@
11
"""Utilities for dealing with tools."""
22

33
import json
4-
from collections.abc import Callable, Generator, Mapping
4+
from collections.abc import Callable, Generator, Iterable, Mapping
55
from typing import Any
66

77
from ollama._utils import convert_function_to_tool
88

9-
from mellea.stdlib.base import Component, TemplateRepresentation
9+
from mellea.backends.types import ModelOption
10+
from mellea.stdlib.base import CBlock, Component, TemplateRepresentation
1011

1112

12-
def get_tools_from_action(action: Any) -> dict[str, Callable]:
13-
"""If an object is a Component with a TemplateRepresentation, grabs it's tools field.
13+
def add_tools_from_model_options(
14+
tools_dict: dict[str, Callable], model_options: dict[str, Any]
15+
):
16+
"""If model_options has tools, add those tools to the tools_dict."""
17+
model_opts_tools = model_options.get(ModelOption.TOOLS, None)
18+
if model_opts_tools is None:
19+
return
20+
21+
# Mappings are iterable.
22+
assert isinstance(model_opts_tools, Iterable), (
23+
"ModelOption.TOOLS must be a list of Callables or dict[str, Callable]"
24+
)
25+
26+
if isinstance(model_opts_tools, Mapping):
27+
# Handle the dict case.
28+
for func_name, func in model_opts_tools.items():
29+
assert isinstance(func_name, str), (
30+
f"If ModelOption.TOOLS is a dict, it must be a dict of [str, Callable]; found {type(func_name)} as the key instead"
31+
)
32+
assert callable(func), (
33+
f"If ModelOption.TOOLS is a dict, it must be a dict of [str, Callable]; found {type(func)} as the value instead"
34+
)
35+
tools_dict[func_name] = func
36+
else:
37+
# Handle any other iterable / list here.
38+
for func in model_opts_tools:
39+
assert callable(func), (
40+
f"If ModelOption.TOOLS is a list, it must be a list of Callables; found {type(func)}"
41+
)
42+
tools_dict[func.__name__] = func
43+
44+
45+
def add_tools_from_context_actions(
46+
tools_dict: dict[str, Callable], ctx_actions: list[Component | CBlock] | None
47+
):
48+
"""If any of the actions in ctx_actions have tools in their template_representation, add those to the tools_dict."""
49+
if ctx_actions is None:
50+
return
51+
52+
for action in ctx_actions:
53+
if not isinstance(action, Component):
54+
continue # Only components have template representations.
1455

15-
Returns:
16-
dict: mapping function names to callables
17-
"""
18-
if isinstance(action, Component):
1956
tr = action.format_for_llm()
20-
if isinstance(tr, TemplateRepresentation):
21-
if tr.tools:
22-
return tr.tools
57+
if not isinstance(tr, TemplateRepresentation) or tr.tools is None:
58+
continue
2359

24-
return {}
60+
for tool_name, func in tr.tools.items():
61+
tools_dict[tool_name] = func
2562

2663

2764
def convert_tools_to_json(tools: dict[str, Callable]) -> list[dict]:

mellea/backends/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class ModelOption:
1717
"""
1818

1919
TOOLS = "@@@tools@@@"
20+
"""Must be a list of callables or a dict[str, Callable]."""
21+
2022
MAX_NEW_TOKENS = "@@@max_new_tokens@@@"
2123
SYSTEM_PROMPT = "@@@system_prompt@@@"
2224
TEMPERATURE = "temperature"

0 commit comments

Comments
 (0)