Skip to content

Commit 5eb4a06

Browse files
filchyfilip.chytil
andauthored
added new vector selector middleware & new message & small ref (#75)
Co-authored-by: filip.chytil <filip.chytil@firma.seznam.cz>
1 parent c8ab268 commit 5eb4a06

24 files changed

Lines changed: 875 additions & 114 deletions

File tree

docs/api-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ class MyMiddleware(TinyBaseMiddleware):
401401

402402
```python
403403
from tinygent.core.datamodels.messages import (
404+
TinyUserMessage,
404405
TinyHumanMessage,
405406
TinyChatMessage,
406407
TinySystemMessage,

docs/concepts/memory.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,10 @@ Tinygent supports multiple message types:
286286

287287
```python
288288
from tinygent.core.datamodels.messages import (
289-
TinyHumanMessage, # User messages
289+
TinyHumanMessage, # Human messages
290290
TinyChatMessage, # AI responses
291291
TinySystemMessage, # System prompts
292+
TinyUserMessage, # User prompts
292293
TinyPlanMessage, # Planning messages
293294
TinyToolMessage, # Tool results
294295
)

docs/concepts/middleware.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,126 @@ agent = TinyMultiStepAgent(
801801

802802
---
803803

804+
### TinyVectorToolSelectorMiddleware
805+
806+
Selects the most relevant tools for each LLM call using semantic similarity between the user query and tool descriptions. No secondary LLM call required — selection is done purely via vector embeddings and cosine similarity.
807+
808+
**Features:**
809+
- Uses an embedder to compute cosine similarity between the query and each tool description
810+
- Ranks tools by similarity and selects the top candidates
811+
- Supports always-include list for critical tools
812+
- Configurable maximum tools limit and minimum similarity threshold
813+
- Customizable query and tool transform functions for fine-grained embedding control
814+
815+
**How It Works:**
816+
1. Before each LLM call, the last `TinyHumanMessage` is embedded as the query
817+
2. Each tool's name and description is embedded
818+
3. Cosine similarity is computed between the query and every tool embedding
819+
4. Tools are ranked by similarity; only those above `similarity_threshold` (up to `max_tools`) are passed to the main agent
820+
821+
**Basic Usage:**
822+
823+
```python
824+
from tinygent.agents.middleware import TinyVectorToolSelectorMiddleware
825+
from tinygent.agents import TinyMultiStepAgent
826+
from tinygent.core.factory import build_embedder, build_llm
827+
828+
selector = TinyVectorToolSelectorMiddleware(
829+
embedder=build_embedder('openai:text-embedding-3-small'),
830+
similarity_threshold=0.5,
831+
max_tools=5,
832+
)
833+
834+
agent = TinyMultiStepAgent(
835+
llm=build_llm('openai:gpt-4o'),
836+
tools=[search, calculator, weather, database, email, calendar, notes],
837+
middleware=[selector],
838+
)
839+
```
840+
841+
**Always Include Critical Tools:**
842+
843+
```python
844+
selector = TinyVectorToolSelectorMiddleware(
845+
embedder=build_embedder('openai:text-embedding-3-small'),
846+
similarity_threshold=0.4,
847+
max_tools=5,
848+
always_include=[search],
849+
)
850+
```
851+
852+
**Custom Transform Functions:**
853+
854+
```python
855+
from tinygent.core.datamodels.tool import AbstractTool
856+
from tinygent.core.types.io.llm_io_input import TinyLLMInput
857+
858+
def query_transform(llm_input: TinyLLMInput) -> str:
859+
# Embed the last 3 messages combined for richer context
860+
recent = llm_input.messages[-3:]
861+
return ' '.join(m.content for m in recent if hasattr(m, 'content'))
862+
863+
def tool_transform(tool: AbstractTool) -> str:
864+
# Repeat name to increase its weight in the embedding
865+
return f'{tool.info.name} {tool.info.name}: {tool.info.description}'
866+
867+
selector = TinyVectorToolSelectorMiddleware(
868+
embedder=build_embedder('openai:text-embedding-3-small'),
869+
similarity_threshold=0.45,
870+
max_tools=4,
871+
query_transform_fn=query_transform,
872+
tool_transform_fn=tool_transform,
873+
)
874+
```
875+
876+
**Using Config Factory:**
877+
878+
Transform functions and the similarity threshold can also be set directly on the config:
879+
880+
```python
881+
from tinygent.agents.middleware import TinyVectorToolSelectorMiddlewareConfig
882+
883+
config = TinyVectorToolSelectorMiddlewareConfig(
884+
embedder='openai:text-embedding-3-small',
885+
similarity_threshold=0.5,
886+
max_tools=5,
887+
always_include=['search'],
888+
query_transform_fn=query_transform,
889+
tool_transform_fn=tool_transform,
890+
)
891+
892+
selector = config.build()
893+
```
894+
895+
**Factory Configuration Options:**
896+
897+
| Field | Type | Default | Description |
898+
|-------|------|---------|-------------|
899+
| `type` | `Literal['vector_tool_classifier']` | `'vector_tool_classifier'` | Type identifier (frozen) |
900+
| `embedder` | `AbstractEmbedderConfig \| AbstractEmbedder` | Required | Embedder used to compute similarity. Can be a string like `'openai:text-embedding-3-small'` or an embedder instance |
901+
| `similarity_threshold` | `float \| None` | `None` | Minimum cosine similarity score for a tool to be selected. `None` = no threshold |
902+
| `max_tools` | `int \| None` | `None` | Maximum number of tools to select. `None` = no limit |
903+
| `always_include` | `list[str] \| None` | `None` | List of tool names to always include regardless of similarity score |
904+
| `query_transform_fn` | `Callable[[TinyLLMInput], str] \| None` | `None` | Custom function to extract the query string from the LLM input. Defaults to last `TinyHumanMessage` found |
905+
| `tool_transform_fn` | `Callable[[AbstractTool], str] \| None` | `None` | Custom function to produce the text embedded for each tool. Defaults to `"name - description"` |
906+
907+
**LLM vs. Vector Tool Selector:**
908+
909+
| | `TinyLLMToolSelectorMiddleware` | `TinyVectorToolSelectorMiddleware` |
910+
|---|---|---|
911+
| Selection method | Secondary LLM call | Cosine similarity |
912+
| Extra API cost | Yes (LLM tokens) | Yes (embeddings, cheaper) |
913+
| Latency | Higher | Lower |
914+
| Accuracy | Higher (understands context) | Good (semantic similarity) |
915+
| Custom logic | Via prompt template | Via transform functions |
916+
917+
**When to Use:**
918+
- You have 10+ tools and want lower latency/cost than the LLM selector
919+
- Tool descriptions are semantically distinct
920+
- You want deterministic, reproducible selection behavior
921+
922+
---
923+
804924
## Next Steps
805925

806926
- **[Agents](agents.md)**: Use middleware with agents

docs/examples.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,66 @@ Three custom middleware examples:
323323
uv run examples/agents/middleware/main.py
324324
```
325325

326+
---
327+
328+
#### 6. LLM Tool Selector Middleware
329+
330+
**Location**: `examples/agents/middleware/llm_tool_selector_example.py`
331+
332+
Demonstrates intelligent tool selection using a secondary LLM before each agent call.
333+
334+
**Run:**
335+
336+
```bash
337+
uv run examples/agents/middleware/llm_tool_selector_example.py
338+
```
339+
340+
---
341+
342+
#### 7. Vector Tool Selector Middleware
343+
344+
**Location**: `examples/agents/middleware/vector_tool_selector_example.py`
345+
346+
Demonstrates tool selection using semantic similarity (embeddings + cosine similarity) — no secondary LLM call needed.
347+
348+
**Run:**
349+
350+
```bash
351+
uv run examples/agents/middleware/vector_tool_selector_example.py
352+
```
353+
354+
**Highlights:**
355+
356+
```python
357+
from tinygent.agents.middleware import TinyVectorToolSelectorMiddlewareConfig
358+
from tinygent.core.factory import build_embedder
359+
360+
# Basic: embed query, rank tools by cosine similarity
361+
selector = TinyVectorToolSelectorMiddlewareConfig(
362+
embedder=build_embedder('openai:text-embedding-3-small'),
363+
max_tools=4,
364+
)
365+
366+
# Always include a critical tool regardless of similarity
367+
selector = TinyVectorToolSelectorMiddlewareConfig(
368+
embedder=build_embedder('openai:text-embedding-3-small'),
369+
max_tools=4,
370+
always_include=[greet],
371+
)
372+
373+
# Custom transform functions for fine-grained embedding control
374+
from tinygent.agents.middleware.vector_tool_selector import TinyVectorToolSelectorMiddleware
375+
376+
selector = TinyVectorToolSelectorMiddleware(
377+
embedder=build_embedder('openai:text-embedding-3-small'),
378+
max_tools=4,
379+
query_transform_fn=lambda llm_input: ' '.join(
380+
m.content for m in llm_input.messages[-3:] if hasattr(m, 'content')
381+
),
382+
tool_transform_fn=lambda tool: f'{tool.info.name}: {tool.info.description}',
383+
)
384+
```
385+
326386
**Highlights:**
327387

328388
```python

examples/agents/middleware/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This example demonstrates how to use **middleware** in TinyGent agents. Middlewa
88
uv sync --extra openai
99

1010
uv run examples/agents/middleware/main.py
11+
uv run examples/agents/middleware/llm_tool_selector_example.py
12+
uv run examples/agents/middleware/vector_tool_selector_example.py
1113
```
1214

1315
## Concept

examples/agents/middleware/llm_tool_selector_example.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def example_1_basic_selection() -> None:
108108
middleware=[selector],
109109
)
110110

111-
result = agent.run('Greet Alice and then add 5 and 7')
111+
result = agent.run(
112+
'Greet Alice and then tell her what is weather like in San Francisco'
113+
)
112114
print(f'Result: {result}\n')
113115

114116

@@ -151,7 +153,7 @@ def example_3_always_include() -> None:
151153
selector = build_middleware(
152154
'llm_tool_selector',
153155
llm=build_llm('openai:gpt-4o-mini'),
154-
always_include=['greet'],
156+
always_include=[greet],
155157
)
156158

157159
agent = build_agent(
@@ -182,7 +184,7 @@ def example_4_combined_constraints() -> None:
182184
selector = TinyLLMToolSelectorMiddlewareConfig(
183185
llm=build_llm('openai:gpt-4o-mini'),
184186
max_tools=4,
185-
always_include=['greet'],
187+
always_include=[greet],
186188
)
187189

188190
agent = build_agent(

0 commit comments

Comments
 (0)