feat: add Strands Agents SDK integration with Hindsight memory tools (#659)
* feat: add Strands Agents SDK integration with Hindsight memory tools * fix: add strands docs to versioned docs so build link check passes * fix(strands): run hindsight client calls in thread pool to avoid event loop conflict with Strands
This commit is contained in:
parent
58e68f3e4a
commit
7fe773c0ee
18 changed files with 3706 additions and 1 deletions
155
hindsight-docs/docs/sdks/integrations/strands.md
Normal file
155
hindsight-docs/docs/sdks/integrations/strands.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
sidebar_position: 13
|
||||
---
|
||||
|
||||
# Strands Agents
|
||||
|
||||
Persistent memory tools for [Strands Agents SDK](https://github.com/strands-agents/sdk-python) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Strands' native `@tool` pattern.
|
||||
|
||||
## Features
|
||||
|
||||
- **Native `@tool` Functions** - Tools are plain Python functions, compatible with `Agent(tools=[...])`
|
||||
- **Memory Instructions** - Pre-recall memories for injection into agent system prompt
|
||||
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
|
||||
- **Simple Configuration** - Configure once globally, or pass a client directly
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hindsight-strands
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from strands import Agent
|
||||
from hindsight_strands import create_hindsight_tools
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(tools=tools)
|
||||
agent("Remember that I prefer dark mode")
|
||||
agent("What are my preferences?")
|
||||
```
|
||||
|
||||
The agent now has three tools it can call:
|
||||
|
||||
- **`hindsight_retain`** — Store information to long-term memory
|
||||
- **`hindsight_recall`** — Search long-term memory for relevant facts
|
||||
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
|
||||
|
||||
## With Memory Instructions
|
||||
|
||||
Pre-recall relevant memories and inject them into the system prompt:
|
||||
|
||||
```python
|
||||
from hindsight_strands import create_hindsight_tools, memory_instructions
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
memories = memory_instructions(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
tools=tools,
|
||||
system_prompt=f"You are a helpful assistant.\n\n{memories}",
|
||||
)
|
||||
```
|
||||
|
||||
## Selecting Tools
|
||||
|
||||
Include only the tools you need:
|
||||
|
||||
```python
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
enable_retain=True,
|
||||
enable_recall=True,
|
||||
enable_reflect=False, # Omit reflect
|
||||
)
|
||||
```
|
||||
|
||||
## Global Configuration
|
||||
|
||||
Instead of passing connection details to every call, configure once:
|
||||
|
||||
```python
|
||||
from hindsight_strands import configure, create_hindsight_tools
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
|
||||
budget="mid", # Recall budget: low/mid/high
|
||||
max_tokens=4096, # Max tokens for recall results
|
||||
tags=["env:prod"], # Tags for stored memories
|
||||
recall_tags=["scope:global"], # Tags to filter recall
|
||||
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
|
||||
)
|
||||
|
||||
# Now create tools without passing connection details
|
||||
tools = create_hindsight_tools(bank_id="user-123")
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### `create_hindsight_tools()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `tags` | `None` | Tags applied when storing memories |
|
||||
| `recall_tags` | `None` | Tags to filter when searching |
|
||||
| `recall_tags_match` | `"any"` | Tag matching mode |
|
||||
| `enable_retain` | `True` | Include the retain (store) tool |
|
||||
| `enable_recall` | `True` | Include the recall (search) tool |
|
||||
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
|
||||
|
||||
### `memory_instructions()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `query` | `"relevant context about the user"` | Recall query for memory injection |
|
||||
| `budget` | `"low"` | Recall budget level |
|
||||
| `max_results` | `5` | Maximum memories to inject |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
|
||||
| `tags` | `None` | Tags to filter recall results |
|
||||
| `tags_match` | `"any"` | Tag matching mode |
|
||||
|
||||
### `configure()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `hindsight_api_url` | Production API | Hindsight API URL |
|
||||
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
|
||||
| `budget` | `"mid"` | Default recall budget level |
|
||||
| `max_tokens` | `4096` | Default max tokens for recall |
|
||||
| `tags` | `None` | Default tags for retain operations |
|
||||
| `recall_tags` | `None` | Default tags to filter recall |
|
||||
| `recall_tags_match` | `"any"` | Default tag matching mode |
|
||||
| `verbose` | `False` | Enable verbose logging |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- strands-agents
|
||||
- hindsight-client >= 0.4.0
|
||||
- A running Hindsight API server
|
||||
|
|
@ -244,6 +244,12 @@ const sidebars: SidebarsConfig = {
|
|||
label: 'NemoClaw',
|
||||
customProps: { icon: '/img/icons/nemoclaw.png' },
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/integrations/strands',
|
||||
label: 'Strands Agents',
|
||||
customProps: { icon: '/img/icons/strands.png' },
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/integrations/skills',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ function sdkIcon(sdk: string): string | null {
|
|||
if (sdk.includes('agno')) {
|
||||
return '/img/icons/agno.png';
|
||||
}
|
||||
if (sdk.includes('smolagents')) {
|
||||
return '/img/icons/smolagents.png';
|
||||
}
|
||||
if (sdk.includes('strands')) {
|
||||
return '/img/icons/strands.png';
|
||||
}
|
||||
if (sdk.includes('hindsight-client') || sdk.includes('hindsight-api') || sdk.includes('litellm') || sdk.includes('pydantic') || sdk.includes('crewai')) {
|
||||
return '/img/icons/python.svg';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,16 @@
|
|||
"link": "/sdks/integrations/nemoclaw",
|
||||
"icon": "/img/icons/nemoclaw.png"
|
||||
},
|
||||
{
|
||||
"id": "strands",
|
||||
"name": "Strands Agents",
|
||||
"description": "Give Strands agents persistent long-term memory with Hindsight retain, recall, and reflect tools.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "framework",
|
||||
"link": "/sdks/integrations/strands",
|
||||
"icon": "/img/icons/strands.png"
|
||||
},
|
||||
{
|
||||
"id": "hindclaw",
|
||||
"name": "HindClaw",
|
||||
|
|
|
|||
BIN
hindsight-docs/static/img/icons/strands.png
Normal file
BIN
hindsight-docs/static/img/icons/strands.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
sidebar_position: 13
|
||||
---
|
||||
|
||||
# Strands Agents
|
||||
|
||||
Persistent memory tools for [Strands Agents SDK](https://github.com/strands-agents/sdk-python) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Strands' native `@tool` pattern.
|
||||
|
||||
## Features
|
||||
|
||||
- **Native `@tool` Functions** - Tools are plain Python functions, compatible with `Agent(tools=[...])`
|
||||
- **Memory Instructions** - Pre-recall memories for injection into agent system prompt
|
||||
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
|
||||
- **Simple Configuration** - Configure once globally, or pass a client directly
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hindsight-strands
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from strands import Agent
|
||||
from hindsight_strands import create_hindsight_tools
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(tools=tools)
|
||||
agent("Remember that I prefer dark mode")
|
||||
agent("What are my preferences?")
|
||||
```
|
||||
|
||||
The agent now has three tools it can call:
|
||||
|
||||
- **`hindsight_retain`** — Store information to long-term memory
|
||||
- **`hindsight_recall`** — Search long-term memory for relevant facts
|
||||
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
|
||||
|
||||
## With Memory Instructions
|
||||
|
||||
Pre-recall relevant memories and inject them into the system prompt:
|
||||
|
||||
```python
|
||||
from hindsight_strands import create_hindsight_tools, memory_instructions
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
memories = memory_instructions(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
tools=tools,
|
||||
system_prompt=f"You are a helpful assistant.\n\n{memories}",
|
||||
)
|
||||
```
|
||||
|
||||
## Selecting Tools
|
||||
|
||||
Include only the tools you need:
|
||||
|
||||
```python
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
enable_retain=True,
|
||||
enable_recall=True,
|
||||
enable_reflect=False, # Omit reflect
|
||||
)
|
||||
```
|
||||
|
||||
## Global Configuration
|
||||
|
||||
Instead of passing connection details to every call, configure once:
|
||||
|
||||
```python
|
||||
from hindsight_strands import configure, create_hindsight_tools
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
|
||||
budget="mid", # Recall budget: low/mid/high
|
||||
max_tokens=4096, # Max tokens for recall results
|
||||
tags=["env:prod"], # Tags for stored memories
|
||||
recall_tags=["scope:global"], # Tags to filter recall
|
||||
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
|
||||
)
|
||||
|
||||
# Now create tools without passing connection details
|
||||
tools = create_hindsight_tools(bank_id="user-123")
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### `create_hindsight_tools()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `tags` | `None` | Tags applied when storing memories |
|
||||
| `recall_tags` | `None` | Tags to filter when searching |
|
||||
| `recall_tags_match` | `"any"` | Tag matching mode |
|
||||
| `enable_retain` | `True` | Include the retain (store) tool |
|
||||
| `enable_recall` | `True` | Include the recall (search) tool |
|
||||
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
|
||||
|
||||
### `memory_instructions()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `query` | `"relevant context about the user"` | Recall query for memory injection |
|
||||
| `budget` | `"low"` | Recall budget level |
|
||||
| `max_results` | `5` | Maximum memories to inject |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
|
||||
| `tags` | `None` | Tags to filter recall results |
|
||||
| `tags_match` | `"any"` | Tag matching mode |
|
||||
|
||||
### `configure()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `hindsight_api_url` | Production API | Hindsight API URL |
|
||||
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
|
||||
| `budget` | `"mid"` | Default recall budget level |
|
||||
| `max_tokens` | `4096` | Default max tokens for recall |
|
||||
| `tags` | `None` | Default tags for retain operations |
|
||||
| `recall_tags` | `None` | Default tags to filter recall |
|
||||
| `recall_tags_match` | `"any"` | Default tag matching mode |
|
||||
| `verbose` | `False` | Enable verbose logging |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- strands-agents
|
||||
- hindsight-client >= 0.4.0
|
||||
- A running Hindsight API server
|
||||
|
|
@ -315,6 +315,14 @@
|
|||
"icon": "/img/icons/nemoclaw.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "doc",
|
||||
"id": "sdks/integrations/strands",
|
||||
"label": "Strands Agents",
|
||||
"customProps": {
|
||||
"icon": "/img/icons/strands.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "doc",
|
||||
"id": "sdks/integrations/skills",
|
||||
|
|
|
|||
155
hindsight-integrations/strands/README.md
Normal file
155
hindsight-integrations/strands/README.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# hindsight-strands
|
||||
|
||||
Persistent memory tools for [Strands Agents SDK](https://github.com/strands-agents/sdk-python) agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Strands' native `@tool` pattern.
|
||||
|
||||
## Features
|
||||
|
||||
- **Native `@tool` Functions** - Tools are plain Python functions, compatible with `Agent(tools=[...])`
|
||||
- **Memory Instructions** - Pre-recall memories for injection into agent system prompt
|
||||
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
|
||||
- **Simple Configuration** - Configure once globally, or pass a client directly
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hindsight-strands
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from strands import Agent
|
||||
from hindsight_strands import create_hindsight_tools
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(tools=tools)
|
||||
agent("Remember that I prefer dark mode")
|
||||
agent("What are my preferences?")
|
||||
```
|
||||
|
||||
The agent now has three tools it can call:
|
||||
|
||||
- **`hindsight_retain`** — Store information to long-term memory
|
||||
- **`hindsight_recall`** — Search long-term memory for relevant facts
|
||||
- **`hindsight_reflect`** — Synthesize a reasoned answer from memories
|
||||
|
||||
## With Memory Instructions
|
||||
|
||||
Pre-recall relevant memories and inject them into the system prompt:
|
||||
|
||||
```python
|
||||
from hindsight_strands import create_hindsight_tools, memory_instructions
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
memories = memory_instructions(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
tools=tools,
|
||||
system_prompt=f"You are a helpful assistant.\n\n{memories}",
|
||||
)
|
||||
```
|
||||
|
||||
## Selecting Tools
|
||||
|
||||
Include only the tools you need:
|
||||
|
||||
```python
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
enable_retain=True,
|
||||
enable_recall=True,
|
||||
enable_reflect=False, # Omit reflect
|
||||
)
|
||||
```
|
||||
|
||||
## Global Configuration
|
||||
|
||||
Instead of passing connection details to every call, configure once:
|
||||
|
||||
```python
|
||||
from hindsight_strands import configure, create_hindsight_tools
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
|
||||
budget="mid", # Recall budget: low/mid/high
|
||||
max_tokens=4096, # Max tokens for recall results
|
||||
tags=["env:prod"], # Tags for stored memories
|
||||
recall_tags=["scope:global"], # Tags to filter recall
|
||||
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
|
||||
)
|
||||
|
||||
# Now create tools without passing connection details
|
||||
tools = create_hindsight_tools(bank_id="user-123")
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### `create_hindsight_tools()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `tags` | `None` | Tags applied when storing memories |
|
||||
| `recall_tags` | `None` | Tags to filter when searching |
|
||||
| `recall_tags_match` | `"any"` | Tag matching mode |
|
||||
| `enable_retain` | `True` | Include the retain (store) tool |
|
||||
| `enable_recall` | `True` | Include the recall (search) tool |
|
||||
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
|
||||
|
||||
### `memory_instructions()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `bank_id` | *required* | Hindsight memory bank ID |
|
||||
| `client` | `None` | Pre-configured Hindsight client |
|
||||
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
|
||||
| `api_key` | `None` | API key (used if no client provided) |
|
||||
| `query` | `"relevant context about the user"` | Recall query for memory injection |
|
||||
| `budget` | `"low"` | Recall budget level |
|
||||
| `max_results` | `5` | Maximum memories to inject |
|
||||
| `max_tokens` | `4096` | Maximum tokens for recall results |
|
||||
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
|
||||
| `tags` | `None` | Tags to filter recall results |
|
||||
| `tags_match` | `"any"` | Tag matching mode |
|
||||
|
||||
### `configure()`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `hindsight_api_url` | Production API | Hindsight API URL |
|
||||
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
|
||||
| `budget` | `"mid"` | Default recall budget level |
|
||||
| `max_tokens` | `4096` | Default max tokens for recall |
|
||||
| `tags` | `None` | Default tags for retain operations |
|
||||
| `recall_tags` | `None` | Default tags to filter recall |
|
||||
| `recall_tags_match` | `"any"` | Default tag matching mode |
|
||||
| `verbose` | `False` | Enable verbose logging |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- strands-agents
|
||||
- hindsight-client >= 0.4.0
|
||||
- A running Hindsight API server
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
39
hindsight-integrations/strands/hindsight_strands/__init__.py
Normal file
39
hindsight-integrations/strands/hindsight_strands/__init__.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"""Hindsight-Strands: Persistent memory tools for AI agents.
|
||||
|
||||
Provides Hindsight-backed tool functions for Strands agents,
|
||||
giving them long-term memory via retain, recall, and reflect tools.
|
||||
|
||||
Basic usage::
|
||||
|
||||
from strands import Agent
|
||||
from hindsight_strands import create_hindsight_tools
|
||||
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
agent = Agent(tools=tools)
|
||||
agent("Remember that I prefer dark mode")
|
||||
"""
|
||||
|
||||
from .config import (
|
||||
HindsightStrandsConfig,
|
||||
configure,
|
||||
get_config,
|
||||
reset_config,
|
||||
)
|
||||
from .errors import HindsightError
|
||||
from .tools import create_hindsight_tools, memory_instructions
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
"get_config",
|
||||
"reset_config",
|
||||
"HindsightStrandsConfig",
|
||||
"HindsightError",
|
||||
"create_hindsight_tools",
|
||||
"memory_instructions",
|
||||
]
|
||||
92
hindsight-integrations/strands/hindsight_strands/config.py
Normal file
92
hindsight-integrations/strands/hindsight_strands/config.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Global configuration for Hindsight-Strands integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
|
||||
HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
|
||||
|
||||
|
||||
@dataclass
|
||||
class HindsightStrandsConfig:
|
||||
"""Connection and default settings for the Strands integration.
|
||||
|
||||
Attributes:
|
||||
hindsight_api_url: URL of the Hindsight API server.
|
||||
api_key: API key for Hindsight authentication.
|
||||
budget: Default recall budget level (low/mid/high).
|
||||
max_tokens: Default maximum tokens for recall results.
|
||||
tags: Default tags applied when storing memories.
|
||||
recall_tags: Default tags to filter when searching memories.
|
||||
recall_tags_match: Tag matching mode (any/all/any_strict/all_strict).
|
||||
verbose: Enable verbose logging.
|
||||
"""
|
||||
|
||||
hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
|
||||
api_key: str | None = None
|
||||
budget: str = "mid"
|
||||
max_tokens: int = 4096
|
||||
tags: list[str] | None = None
|
||||
recall_tags: list[str] | None = None
|
||||
recall_tags_match: str = "any"
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
_global_config: HindsightStrandsConfig | None = None
|
||||
|
||||
|
||||
def configure(
|
||||
hindsight_api_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
budget: str = "mid",
|
||||
max_tokens: int = 4096,
|
||||
tags: list[str] | None = None,
|
||||
recall_tags: list[str] | None = None,
|
||||
recall_tags_match: str = "any",
|
||||
verbose: bool = False,
|
||||
) -> HindsightStrandsConfig:
|
||||
"""Configure Hindsight connection and default settings.
|
||||
|
||||
Args:
|
||||
hindsight_api_url: Hindsight API URL (default: production).
|
||||
api_key: API key. Falls back to HINDSIGHT_API_KEY env var.
|
||||
budget: Default recall budget (low/mid/high).
|
||||
max_tokens: Default max tokens for recall.
|
||||
tags: Default tags for retain operations.
|
||||
recall_tags: Default tags to filter recall/search.
|
||||
recall_tags_match: Tag matching mode.
|
||||
verbose: Enable verbose logging.
|
||||
|
||||
Returns:
|
||||
The configured HindsightStrandsConfig.
|
||||
"""
|
||||
global _global_config
|
||||
|
||||
resolved_url = hindsight_api_url or DEFAULT_HINDSIGHT_API_URL
|
||||
resolved_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV)
|
||||
|
||||
_global_config = HindsightStrandsConfig(
|
||||
hindsight_api_url=resolved_url,
|
||||
api_key=resolved_key,
|
||||
budget=budget,
|
||||
max_tokens=max_tokens,
|
||||
tags=tags,
|
||||
recall_tags=recall_tags,
|
||||
recall_tags_match=recall_tags_match,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
return _global_config
|
||||
|
||||
|
||||
def get_config() -> HindsightStrandsConfig | None:
|
||||
"""Get the current global configuration."""
|
||||
return _global_config
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset global configuration to None."""
|
||||
global _global_config
|
||||
_global_config = None
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""Hindsight-Strands error types."""
|
||||
|
||||
|
||||
class HindsightError(Exception):
|
||||
"""Exception raised when a Hindsight memory operation fails."""
|
||||
|
||||
pass
|
||||
278
hindsight-integrations/strands/hindsight_strands/tools.py
Normal file
278
hindsight-integrations/strands/hindsight_strands/tools.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
"""Strands Agents tool factory for Hindsight memory operations.
|
||||
|
||||
Provides a factory function that creates Strands-compatible tool functions
|
||||
backed by Hindsight's retain/recall/reflect APIs. Tools are plain Python
|
||||
functions decorated with ``@tool`` — bank_id and client are captured in
|
||||
the closure at construction time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
|
||||
def _run_in_thread(fn: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Run a callable in a dedicated thread with a clean event loop.
|
||||
|
||||
Strands runs tools inside its own asyncio event loop. The hindsight client
|
||||
uses asyncio internally (including asyncio.timeout), which conflicts with
|
||||
an already-running loop. Running in a separate thread gives a fresh loop.
|
||||
"""
|
||||
return _executor.submit(fn, *args, **kwargs).result()
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
from strands import tool
|
||||
|
||||
from .config import get_config
|
||||
from .errors import HindsightError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_client(
|
||||
client: Hindsight | None,
|
||||
hindsight_api_url: str | None,
|
||||
api_key: str | None,
|
||||
) -> Hindsight:
|
||||
"""Resolve a Hindsight client from explicit args or global config."""
|
||||
if client is not None:
|
||||
return client
|
||||
|
||||
config = get_config()
|
||||
url = hindsight_api_url or (config.hindsight_api_url if config else None)
|
||||
key = api_key or (config.api_key if config else None)
|
||||
|
||||
if url is None:
|
||||
raise HindsightError(
|
||||
"No Hindsight API URL configured. "
|
||||
"Pass client= or hindsight_api_url=, or call configure() first."
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {"base_url": url, "timeout": 30.0}
|
||||
if key:
|
||||
kwargs["api_key"] = key
|
||||
return Hindsight(**kwargs)
|
||||
|
||||
|
||||
def create_hindsight_tools(
|
||||
*,
|
||||
bank_id: str,
|
||||
client: Hindsight | None = None,
|
||||
hindsight_api_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
budget: str = "mid",
|
||||
max_tokens: int = 4096,
|
||||
tags: list[str] | None = None,
|
||||
recall_tags: list[str] | None = None,
|
||||
recall_tags_match: str = "any",
|
||||
enable_retain: bool = True,
|
||||
enable_recall: bool = True,
|
||||
enable_reflect: bool = True,
|
||||
) -> list:
|
||||
"""Create Hindsight memory tools for a Strands agent.
|
||||
|
||||
Returns a list of ``@tool``-decorated functions that can be passed
|
||||
directly to ``Agent(tools=...)``. Each function captures ``bank_id``
|
||||
and the Hindsight client in its closure — no context modification needed.
|
||||
|
||||
Args:
|
||||
bank_id: The Hindsight memory bank to operate on.
|
||||
client: Pre-configured Hindsight client (preferred).
|
||||
hindsight_api_url: API URL (used if no client provided).
|
||||
api_key: API key (used if no client provided).
|
||||
budget: Recall/reflect budget level (low/mid/high).
|
||||
max_tokens: Maximum tokens for recall results.
|
||||
tags: Tags applied when storing memories via retain.
|
||||
recall_tags: Tags to filter when searching memories.
|
||||
recall_tags_match: Tag matching mode (any/all/any_strict/all_strict).
|
||||
enable_retain: Include the retain (store) tool.
|
||||
enable_recall: Include the recall (search) tool.
|
||||
enable_reflect: Include the reflect (synthesize) tool.
|
||||
|
||||
Returns:
|
||||
A list of Strands tool functions.
|
||||
|
||||
Raises:
|
||||
HindsightError: If no client or API URL can be resolved.
|
||||
"""
|
||||
resolved_client = _resolve_client(client, hindsight_api_url, api_key)
|
||||
|
||||
# Resolve defaults from global config
|
||||
config = get_config()
|
||||
effective_tags = tags if tags is not None else (config.tags if config else None)
|
||||
effective_recall_tags = (
|
||||
recall_tags
|
||||
if recall_tags is not None
|
||||
else (config.recall_tags if config else None)
|
||||
)
|
||||
effective_recall_tags_match = recall_tags_match or (
|
||||
config.recall_tags_match if config else "any"
|
||||
)
|
||||
effective_budget = budget or (config.budget if config else "mid")
|
||||
effective_max_tokens = max_tokens or (config.max_tokens if config else 4096)
|
||||
|
||||
created_banks: set[str] = set()
|
||||
|
||||
def _ensure_bank(bid: str) -> None:
|
||||
if bid in created_banks:
|
||||
return
|
||||
try:
|
||||
resolved_client.create_bank(bank_id=bid, name=bid)
|
||||
created_banks.add(bid)
|
||||
except Exception:
|
||||
created_banks.add(bid)
|
||||
|
||||
tools = []
|
||||
|
||||
if enable_retain:
|
||||
|
||||
@tool
|
||||
def hindsight_retain(content: str) -> str:
|
||||
"""Store information to long-term memory for later retrieval.
|
||||
|
||||
Use this to save important facts, user preferences, decisions,
|
||||
or any information that should be remembered across conversations.
|
||||
"""
|
||||
try:
|
||||
_ensure_bank(bank_id)
|
||||
retain_kwargs: dict[str, Any] = {"bank_id": bank_id, "content": content}
|
||||
if effective_tags:
|
||||
retain_kwargs["tags"] = effective_tags
|
||||
_run_in_thread(resolved_client.retain, **retain_kwargs)
|
||||
return "Memory stored successfully."
|
||||
except HindsightError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Retain failed: {e}")
|
||||
raise HindsightError(f"Retain failed: {e}") from e
|
||||
|
||||
tools.append(hindsight_retain)
|
||||
|
||||
if enable_recall:
|
||||
|
||||
@tool
|
||||
def hindsight_recall(query: str) -> str:
|
||||
"""Search long-term memory for relevant information.
|
||||
|
||||
Use this to find previously stored facts, preferences, or context.
|
||||
Returns a numbered list of matching memories.
|
||||
"""
|
||||
try:
|
||||
recall_kwargs: dict[str, Any] = {
|
||||
"bank_id": bank_id,
|
||||
"query": query,
|
||||
"budget": effective_budget,
|
||||
"max_tokens": effective_max_tokens,
|
||||
}
|
||||
if effective_recall_tags:
|
||||
recall_kwargs["tags"] = effective_recall_tags
|
||||
recall_kwargs["tags_match"] = effective_recall_tags_match
|
||||
response = _run_in_thread(resolved_client.recall, **recall_kwargs)
|
||||
if not response.results:
|
||||
return "No relevant memories found."
|
||||
lines = []
|
||||
for i, result in enumerate(response.results, 1):
|
||||
lines.append(f"{i}. {result.text}")
|
||||
return "\n".join(lines)
|
||||
except HindsightError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Recall failed: {e}")
|
||||
raise HindsightError(f"Recall failed: {e}") from e
|
||||
|
||||
tools.append(hindsight_recall)
|
||||
|
||||
if enable_reflect:
|
||||
|
||||
@tool
|
||||
def hindsight_reflect(query: str) -> str:
|
||||
"""Synthesize a thoughtful answer from long-term memories.
|
||||
|
||||
Use this when you need a coherent summary or reasoned response
|
||||
about what you know, rather than raw memory facts.
|
||||
"""
|
||||
try:
|
||||
reflect_kwargs: dict[str, Any] = {
|
||||
"bank_id": bank_id,
|
||||
"query": query,
|
||||
"budget": effective_budget,
|
||||
}
|
||||
response = _run_in_thread(resolved_client.reflect, **reflect_kwargs)
|
||||
return response.text or "No relevant memories found."
|
||||
except HindsightError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Reflect failed: {e}")
|
||||
raise HindsightError(f"Reflect failed: {e}") from e
|
||||
|
||||
tools.append(hindsight_reflect)
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def memory_instructions(
|
||||
*,
|
||||
bank_id: str,
|
||||
client: Hindsight | None = None,
|
||||
hindsight_api_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
query: str = "relevant context about the user",
|
||||
budget: str = "low",
|
||||
max_results: int = 5,
|
||||
max_tokens: int = 4096,
|
||||
prefix: str = "Relevant memories:\n",
|
||||
tags: list[str] | None = None,
|
||||
tags_match: str = "any",
|
||||
) -> str:
|
||||
"""Pre-recall memories for injection into agent system prompt.
|
||||
|
||||
Performs a sync recall and returns a formatted string of memories.
|
||||
Pass the result to ``Agent(system_prompt=...)`` or prepend it to
|
||||
your system prompt string.
|
||||
|
||||
Args:
|
||||
bank_id: The Hindsight memory bank to recall from.
|
||||
client: Pre-configured Hindsight client (preferred).
|
||||
hindsight_api_url: API URL (used if no client provided).
|
||||
api_key: API key (used if no client provided).
|
||||
query: The recall query to find relevant memories.
|
||||
budget: Recall budget level (low/mid/high).
|
||||
max_results: Maximum number of memories to include.
|
||||
max_tokens: Maximum tokens for recall results.
|
||||
prefix: Text prepended before the memory list.
|
||||
tags: Tags to filter recall results.
|
||||
tags_match: Tag matching mode (any/all/any_strict/all_strict).
|
||||
|
||||
Returns:
|
||||
A formatted string of memories, or empty string if none found.
|
||||
|
||||
Raises:
|
||||
HindsightError: If no client or API URL can be resolved.
|
||||
"""
|
||||
resolved_client = _resolve_client(client, hindsight_api_url, api_key)
|
||||
|
||||
try:
|
||||
recall_kwargs: dict[str, Any] = {
|
||||
"bank_id": bank_id,
|
||||
"query": query,
|
||||
"budget": budget,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
if tags:
|
||||
recall_kwargs["tags"] = tags
|
||||
recall_kwargs["tags_match"] = tags_match
|
||||
response = _run_in_thread(resolved_client.recall, **recall_kwargs)
|
||||
results = response.results[:max_results] if response.results else []
|
||||
if not results:
|
||||
return ""
|
||||
lines = [prefix]
|
||||
for i, result in enumerate(results, 1):
|
||||
lines.append(f"{i}. {result.text}")
|
||||
return "\n".join(lines)
|
||||
except Exception:
|
||||
# Silently return empty — instructions failures shouldn't block the agent
|
||||
return ""
|
||||
53
hindsight-integrations/strands/pyproject.toml
Normal file
53
hindsight-integrations/strands/pyproject.toml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[project]
|
||||
name = "hindsight-strands"
|
||||
version = "0.1.0"
|
||||
description = "Strands Agents SDK integration for Hindsight - persistent memory tools for AI agents"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "MIT" }
|
||||
authors = [
|
||||
{ name = "Vectorize", email = "support@vectorize.io" }
|
||||
]
|
||||
keywords = [
|
||||
"ai",
|
||||
"memory",
|
||||
"strands",
|
||||
"agents",
|
||||
"hindsight",
|
||||
"aws",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"strands-agents",
|
||||
"hindsight-client>=0.4.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/vectorize-io/hindsight"
|
||||
Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/strands"
|
||||
Repository = "https://github.com/vectorize-io/hindsight"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_strands"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.2",
|
||||
"ruff>=0.8.0",
|
||||
]
|
||||
1
hindsight-integrations/strands/tests/__init__.py
Normal file
1
hindsight-integrations/strands/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
169
hindsight-integrations/strands/tests/test_config.py
Normal file
169
hindsight-integrations/strands/tests/test_config.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""Unit tests for hindsight_strands configuration."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from hindsight_strands import configure, get_config, reset_config
|
||||
from hindsight_strands.config import (
|
||||
DEFAULT_HINDSIGHT_API_URL,
|
||||
HINDSIGHT_API_KEY_ENV,
|
||||
HindsightStrandsConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestDefaults:
|
||||
def test_default_api_url(self):
|
||||
assert DEFAULT_HINDSIGHT_API_URL == "https://api.hindsight.vectorize.io"
|
||||
|
||||
def test_env_var_name(self):
|
||||
assert HINDSIGHT_API_KEY_ENV == "HINDSIGHT_API_KEY"
|
||||
|
||||
|
||||
class TestHindsightStrandsConfigDataclass:
|
||||
def test_default_values(self):
|
||||
config = HindsightStrandsConfig()
|
||||
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
assert config.api_key is None
|
||||
assert config.budget == "mid"
|
||||
assert config.max_tokens == 4096
|
||||
assert config.tags is None
|
||||
assert config.recall_tags is None
|
||||
assert config.recall_tags_match == "any"
|
||||
assert config.verbose is False
|
||||
|
||||
def test_custom_values(self):
|
||||
config = HindsightStrandsConfig(
|
||||
hindsight_api_url="http://custom:9999",
|
||||
api_key="key-123",
|
||||
budget="high",
|
||||
max_tokens=2048,
|
||||
tags=["t1"],
|
||||
recall_tags=["r1"],
|
||||
recall_tags_match="all",
|
||||
verbose=True,
|
||||
)
|
||||
assert config.hindsight_api_url == "http://custom:9999"
|
||||
assert config.api_key == "key-123"
|
||||
assert config.budget == "high"
|
||||
assert config.max_tokens == 2048
|
||||
assert config.tags == ["t1"]
|
||||
assert config.recall_tags == ["r1"]
|
||||
assert config.recall_tags_match == "all"
|
||||
assert config.verbose is True
|
||||
|
||||
def test_is_mutable_dataclass(self):
|
||||
config = HindsightStrandsConfig()
|
||||
config.budget = "low"
|
||||
assert config.budget == "low"
|
||||
|
||||
|
||||
class TestConfigure:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_configure_with_no_arguments(self):
|
||||
config = configure()
|
||||
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
assert config.api_key is None
|
||||
assert config.budget == "mid"
|
||||
assert config.max_tokens == 4096
|
||||
assert config.tags is None
|
||||
assert config.recall_tags is None
|
||||
assert config.recall_tags_match == "any"
|
||||
assert config.verbose is False
|
||||
|
||||
def test_configure_reads_api_key_from_env(self):
|
||||
with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "test-key"}):
|
||||
config = configure()
|
||||
assert config.api_key == "test-key"
|
||||
|
||||
def test_configure_explicit_overrides_env(self):
|
||||
with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "env-key"}):
|
||||
config = configure(api_key="explicit-key")
|
||||
assert config.api_key == "explicit-key"
|
||||
|
||||
def test_configure_api_key_none_without_env(self):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
config = configure()
|
||||
assert config.api_key is None
|
||||
|
||||
def test_configure_all_options(self):
|
||||
config = configure(
|
||||
hindsight_api_url="http://custom:8888",
|
||||
api_key="my-key",
|
||||
budget="high",
|
||||
max_tokens=2048,
|
||||
tags=["env:test"],
|
||||
recall_tags=["scope:global"],
|
||||
recall_tags_match="all",
|
||||
verbose=True,
|
||||
)
|
||||
assert config.hindsight_api_url == "http://custom:8888"
|
||||
assert config.api_key == "my-key"
|
||||
assert config.budget == "high"
|
||||
assert config.max_tokens == 2048
|
||||
assert config.tags == ["env:test"]
|
||||
assert config.recall_tags == ["scope:global"]
|
||||
assert config.recall_tags_match == "all"
|
||||
assert config.verbose is True
|
||||
|
||||
def test_configure_returns_config_instance(self):
|
||||
config = configure()
|
||||
assert isinstance(config, HindsightStrandsConfig)
|
||||
|
||||
def test_configure_replaces_previous_config(self):
|
||||
configure(budget="low")
|
||||
config1 = get_config()
|
||||
assert config1.budget == "low"
|
||||
|
||||
configure(budget="high")
|
||||
config2 = get_config()
|
||||
assert config2.budget == "high"
|
||||
assert config1 is not config2
|
||||
|
||||
def test_configure_url_defaults_when_none(self):
|
||||
config = configure(hindsight_api_url=None)
|
||||
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
|
||||
|
||||
class TestGetConfig:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_returns_none_without_configure(self):
|
||||
assert get_config() is None
|
||||
|
||||
def test_returns_config_after_configure(self):
|
||||
configure()
|
||||
config = get_config()
|
||||
assert config is not None
|
||||
assert isinstance(config, HindsightStrandsConfig)
|
||||
|
||||
def test_returns_same_instance(self):
|
||||
configure()
|
||||
assert get_config() is get_config()
|
||||
|
||||
|
||||
class TestResetConfig:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_reset_config(self):
|
||||
configure()
|
||||
assert get_config() is not None
|
||||
reset_config()
|
||||
assert get_config() is None
|
||||
|
||||
def test_reset_is_idempotent(self):
|
||||
reset_config()
|
||||
reset_config()
|
||||
assert get_config() is None
|
||||
616
hindsight-integrations/strands/tests/test_tools.py
Normal file
616
hindsight-integrations/strands/tests/test_tools.py
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
"""Unit tests for Hindsight Strands tools."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_strands import (
|
||||
configure,
|
||||
create_hindsight_tools,
|
||||
memory_instructions,
|
||||
reset_config,
|
||||
)
|
||||
from hindsight_strands.errors import HindsightError
|
||||
from hindsight_strands.tools import _resolve_client
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _mock_client():
|
||||
"""Create a mock Hindsight client."""
|
||||
client = MagicMock()
|
||||
client.retain = MagicMock()
|
||||
client.recall = MagicMock()
|
||||
client.reflect = MagicMock()
|
||||
client.create_bank = MagicMock()
|
||||
return client
|
||||
|
||||
|
||||
def _mock_recall_response(texts: list[str]):
|
||||
"""Create a mock RecallResponse with results."""
|
||||
response = MagicMock()
|
||||
results = []
|
||||
for t in texts:
|
||||
r = MagicMock()
|
||||
r.text = t
|
||||
results.append(r)
|
||||
response.results = results
|
||||
return response
|
||||
|
||||
|
||||
def _mock_reflect_response(text: str):
|
||||
"""Create a mock ReflectResponse."""
|
||||
response = MagicMock()
|
||||
response.text = text
|
||||
return response
|
||||
|
||||
|
||||
def _call_tool(tool_fn, **kwargs):
|
||||
"""Call a Strands @tool decorated function directly, bypassing the decorator."""
|
||||
# Strands @tool stores the original function as __wrapped__ or we can call it directly
|
||||
# since the decorator preserves the callable interface
|
||||
return tool_fn(**kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveClient:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_returns_explicit_client(self):
|
||||
client = _mock_client()
|
||||
assert _resolve_client(client, None, None) is client
|
||||
|
||||
def test_explicit_client_ignores_url_and_key(self):
|
||||
client = _mock_client()
|
||||
result = _resolve_client(client, "http://ignored", "ignored-key")
|
||||
assert result is client
|
||||
|
||||
def test_creates_client_from_url(self):
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, "http://localhost:8888", None)
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://localhost:8888", timeout=30.0
|
||||
)
|
||||
|
||||
def test_creates_client_with_api_key(self):
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, "http://localhost:8888", "my-key")
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://localhost:8888", timeout=30.0, api_key="my-key"
|
||||
)
|
||||
|
||||
def test_falls_back_to_global_config_url(self):
|
||||
configure(hindsight_api_url="http://config:8888")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, None, None)
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://config:8888", timeout=30.0
|
||||
)
|
||||
|
||||
def test_falls_back_to_global_config_api_key(self):
|
||||
configure(hindsight_api_url="http://config:8888", api_key="config-key")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, None, None)
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://config:8888", timeout=30.0, api_key="config-key"
|
||||
)
|
||||
|
||||
def test_explicit_url_overrides_config(self):
|
||||
configure(hindsight_api_url="http://config:8888")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, "http://explicit:9999", None)
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://explicit:9999", timeout=30.0
|
||||
)
|
||||
|
||||
def test_explicit_api_key_overrides_config(self):
|
||||
configure(hindsight_api_url="http://config:8888", api_key="config-key")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, None, "explicit-key")
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://config:8888", timeout=30.0, api_key="explicit-key"
|
||||
)
|
||||
|
||||
def test_raises_without_url_or_config(self):
|
||||
with pytest.raises(HindsightError, match="No Hindsight API URL"):
|
||||
_resolve_client(None, None, None)
|
||||
|
||||
def test_raises_with_empty_config_no_url(self):
|
||||
# Config exists but has default URL, so this should NOT raise
|
||||
configure()
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
_resolve_client(None, None, None)
|
||||
mock_cls.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_hindsight_tools — factory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateHindsightTools:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_creates_three_tools_by_default(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(bank_id="test", client=client)
|
||||
assert len(tools) == 3
|
||||
|
||||
def test_tool_names(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(bank_id="test", client=client)
|
||||
names = {t.__name__ for t in tools}
|
||||
assert names == {"hindsight_retain", "hindsight_recall", "hindsight_reflect"}
|
||||
|
||||
def test_enable_retain_only(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test",
|
||||
client=client,
|
||||
enable_retain=True,
|
||||
enable_recall=False,
|
||||
enable_reflect=False,
|
||||
)
|
||||
assert len(tools) == 1
|
||||
assert tools[0].__name__ == "hindsight_retain"
|
||||
|
||||
def test_enable_recall_only(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test",
|
||||
client=client,
|
||||
enable_retain=False,
|
||||
enable_recall=True,
|
||||
enable_reflect=False,
|
||||
)
|
||||
assert len(tools) == 1
|
||||
assert tools[0].__name__ == "hindsight_recall"
|
||||
|
||||
def test_enable_reflect_only(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test",
|
||||
client=client,
|
||||
enable_retain=False,
|
||||
enable_recall=False,
|
||||
enable_reflect=True,
|
||||
)
|
||||
assert len(tools) == 1
|
||||
assert tools[0].__name__ == "hindsight_reflect"
|
||||
|
||||
def test_no_tools_when_all_disabled(self):
|
||||
client = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test",
|
||||
client=client,
|
||||
enable_retain=False,
|
||||
enable_recall=False,
|
||||
enable_reflect=False,
|
||||
)
|
||||
assert len(tools) == 0
|
||||
|
||||
def test_raises_without_client_or_config(self):
|
||||
with pytest.raises(HindsightError, match="No Hindsight API URL"):
|
||||
create_hindsight_tools(bank_id="test")
|
||||
|
||||
def test_falls_back_to_global_config(self):
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
tools = create_hindsight_tools(bank_id="test")
|
||||
assert len(tools) == 3
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://localhost:8888", timeout=30.0
|
||||
)
|
||||
|
||||
def test_api_key_passed_to_client(self):
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
create_hindsight_tools(
|
||||
bank_id="test",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="secret",
|
||||
)
|
||||
mock_cls.assert_called_once_with(
|
||||
base_url="http://localhost:8888", timeout=30.0, api_key="secret"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# retain tool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRetainTool:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def _retain_tool(self, client, **kwargs):
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="my-bank",
|
||||
client=client,
|
||||
enable_recall=False,
|
||||
enable_reflect=False,
|
||||
**kwargs,
|
||||
)
|
||||
return tools[0]
|
||||
|
||||
def test_retain_success(self):
|
||||
client = _mock_client()
|
||||
t = self._retain_tool(client)
|
||||
result = _call_tool(t, content="I like dark mode")
|
||||
assert result == "Memory stored successfully."
|
||||
client.retain.assert_called_once_with(
|
||||
bank_id="my-bank", content="I like dark mode"
|
||||
)
|
||||
|
||||
def test_retain_with_tags(self):
|
||||
client = _mock_client()
|
||||
t = self._retain_tool(client, tags=["env:test"])
|
||||
_call_tool(t, content="tagged content")
|
||||
client.retain.assert_called_once_with(
|
||||
bank_id="my-bank", content="tagged content", tags=["env:test"]
|
||||
)
|
||||
|
||||
def test_retain_config_tags(self):
|
||||
configure(hindsight_api_url="http://localhost:8888", tags=["config-tag"])
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test", enable_recall=False, enable_reflect=False
|
||||
)
|
||||
client = mock_cls.return_value
|
||||
_call_tool(tools[0], content="content")
|
||||
assert client.retain.call_args[1]["tags"] == ["config-tag"]
|
||||
|
||||
def test_retain_explicit_tags_override_config(self):
|
||||
configure(hindsight_api_url="http://localhost:8888", tags=["config-tag"])
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_cls.return_value = _mock_client()
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="test",
|
||||
tags=["explicit-tag"],
|
||||
enable_recall=False,
|
||||
enable_reflect=False,
|
||||
)
|
||||
client = mock_cls.return_value
|
||||
_call_tool(tools[0], content="content")
|
||||
assert client.retain.call_args[1]["tags"] == ["explicit-tag"]
|
||||
|
||||
def test_retain_creates_bank(self):
|
||||
client = _mock_client()
|
||||
t = self._retain_tool(client)
|
||||
_call_tool(t, content="content")
|
||||
client.create_bank.assert_called_once_with(bank_id="my-bank", name="my-bank")
|
||||
|
||||
def test_retain_creates_bank_only_once(self):
|
||||
client = _mock_client()
|
||||
t = self._retain_tool(client)
|
||||
_call_tool(t, content="first")
|
||||
_call_tool(t, content="second")
|
||||
client.create_bank.assert_called_once()
|
||||
|
||||
def test_retain_bank_already_exists(self):
|
||||
client = _mock_client()
|
||||
client.create_bank.side_effect = Exception("already exists")
|
||||
t = self._retain_tool(client)
|
||||
result = _call_tool(t, content="content")
|
||||
assert result == "Memory stored successfully."
|
||||
|
||||
def test_retain_failure_raises_hindsight_error(self):
|
||||
client = _mock_client()
|
||||
client.retain.side_effect = RuntimeError("network error")
|
||||
t = self._retain_tool(client)
|
||||
with pytest.raises(HindsightError, match="Retain failed"):
|
||||
_call_tool(t, content="content")
|
||||
|
||||
def test_retain_hindsight_error_not_wrapped(self):
|
||||
client = _mock_client()
|
||||
client.retain.side_effect = HindsightError("original error")
|
||||
t = self._retain_tool(client)
|
||||
with pytest.raises(HindsightError, match="original error"):
|
||||
_call_tool(t, content="content")
|
||||
|
||||
def test_retain_failure_logs_error(self, caplog):
|
||||
client = _mock_client()
|
||||
client.retain.side_effect = RuntimeError("network error")
|
||||
t = self._retain_tool(client)
|
||||
with caplog.at_level(logging.ERROR), pytest.raises(HindsightError):
|
||||
_call_tool(t, content="content")
|
||||
assert "Retain failed" in caplog.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# recall tool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRecallTool:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def _recall_tool(self, client, **kwargs):
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="my-bank",
|
||||
client=client,
|
||||
enable_retain=False,
|
||||
enable_reflect=False,
|
||||
**kwargs,
|
||||
)
|
||||
return tools[0]
|
||||
|
||||
def test_recall_returns_numbered_results(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact1", "fact2", "fact3"])
|
||||
t = self._recall_tool(client)
|
||||
result = _call_tool(t, query="preferences")
|
||||
assert result == "1. fact1\n2. fact2\n3. fact3"
|
||||
|
||||
def test_recall_no_results(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response([])
|
||||
t = self._recall_tool(client)
|
||||
result = _call_tool(t, query="unknown")
|
||||
assert result == "No relevant memories found."
|
||||
|
||||
def test_recall_none_results(self):
|
||||
client = _mock_client()
|
||||
response = MagicMock()
|
||||
response.results = None
|
||||
client.recall.return_value = response
|
||||
t = self._recall_tool(client)
|
||||
result = _call_tool(t, query="unknown")
|
||||
assert result == "No relevant memories found."
|
||||
|
||||
def test_recall_passes_budget_and_max_tokens(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact"])
|
||||
t = self._recall_tool(client, budget="high", max_tokens=2048)
|
||||
_call_tool(t, query="query")
|
||||
call_kwargs = client.recall.call_args[1]
|
||||
assert call_kwargs["budget"] == "high"
|
||||
assert call_kwargs["max_tokens"] == 2048
|
||||
|
||||
def test_recall_default_budget(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact"])
|
||||
t = self._recall_tool(client)
|
||||
_call_tool(t, query="query")
|
||||
assert client.recall.call_args[1]["budget"] == "mid"
|
||||
|
||||
def test_recall_with_tags(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact"])
|
||||
t = self._recall_tool(
|
||||
client, recall_tags=["scope:global"], recall_tags_match="all"
|
||||
)
|
||||
_call_tool(t, query="query")
|
||||
call_kwargs = client.recall.call_args[1]
|
||||
assert call_kwargs["tags"] == ["scope:global"]
|
||||
assert call_kwargs["tags_match"] == "all"
|
||||
|
||||
def test_recall_without_tags_omits_tag_kwargs(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact"])
|
||||
t = self._recall_tool(client)
|
||||
_call_tool(t, query="query")
|
||||
call_kwargs = client.recall.call_args[1]
|
||||
assert "tags" not in call_kwargs
|
||||
assert "tags_match" not in call_kwargs
|
||||
|
||||
def test_recall_failure_raises_hindsight_error(self):
|
||||
client = _mock_client()
|
||||
client.recall.side_effect = RuntimeError("network error")
|
||||
t = self._recall_tool(client)
|
||||
with pytest.raises(HindsightError, match="Recall failed"):
|
||||
_call_tool(t, query="query")
|
||||
|
||||
def test_recall_hindsight_error_not_wrapped(self):
|
||||
client = _mock_client()
|
||||
client.recall.side_effect = HindsightError("original error")
|
||||
t = self._recall_tool(client)
|
||||
with pytest.raises(HindsightError, match="original error"):
|
||||
_call_tool(t, query="query")
|
||||
|
||||
def test_recall_failure_logs_error(self, caplog):
|
||||
client = _mock_client()
|
||||
client.recall.side_effect = RuntimeError("network error")
|
||||
t = self._recall_tool(client)
|
||||
with caplog.at_level(logging.ERROR), pytest.raises(HindsightError):
|
||||
_call_tool(t, query="query")
|
||||
assert "Recall failed" in caplog.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reflect tool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestReflectTool:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def _reflect_tool(self, client, **kwargs):
|
||||
tools = create_hindsight_tools(
|
||||
bank_id="my-bank",
|
||||
client=client,
|
||||
enable_retain=False,
|
||||
enable_recall=False,
|
||||
**kwargs,
|
||||
)
|
||||
return tools[0]
|
||||
|
||||
def test_reflect_returns_text(self):
|
||||
client = _mock_client()
|
||||
client.reflect.return_value = _mock_reflect_response("Synthesized answer")
|
||||
t = self._reflect_tool(client)
|
||||
result = _call_tool(t, query="What are my preferences?")
|
||||
assert result == "Synthesized answer"
|
||||
|
||||
def test_reflect_empty_text_returns_fallback(self):
|
||||
client = _mock_client()
|
||||
client.reflect.return_value = _mock_reflect_response("")
|
||||
t = self._reflect_tool(client)
|
||||
result = _call_tool(t, query="query")
|
||||
assert result == "No relevant memories found."
|
||||
|
||||
def test_reflect_none_text_returns_fallback(self):
|
||||
client = _mock_client()
|
||||
client.reflect.return_value = _mock_reflect_response(None)
|
||||
t = self._reflect_tool(client)
|
||||
result = _call_tool(t, query="query")
|
||||
assert result == "No relevant memories found."
|
||||
|
||||
def test_reflect_passes_budget(self):
|
||||
client = _mock_client()
|
||||
client.reflect.return_value = _mock_reflect_response("answer")
|
||||
t = self._reflect_tool(client, budget="high")
|
||||
_call_tool(t, query="query")
|
||||
assert client.reflect.call_args[1]["budget"] == "high"
|
||||
|
||||
def test_reflect_default_budget(self):
|
||||
client = _mock_client()
|
||||
client.reflect.return_value = _mock_reflect_response("answer")
|
||||
t = self._reflect_tool(client)
|
||||
_call_tool(t, query="query")
|
||||
assert client.reflect.call_args[1]["budget"] == "mid"
|
||||
|
||||
def test_reflect_failure_raises_hindsight_error(self):
|
||||
client = _mock_client()
|
||||
client.reflect.side_effect = RuntimeError("network error")
|
||||
t = self._reflect_tool(client)
|
||||
with pytest.raises(HindsightError, match="Reflect failed"):
|
||||
_call_tool(t, query="query")
|
||||
|
||||
def test_reflect_hindsight_error_not_wrapped(self):
|
||||
client = _mock_client()
|
||||
client.reflect.side_effect = HindsightError("original error")
|
||||
t = self._reflect_tool(client)
|
||||
with pytest.raises(HindsightError, match="original error"):
|
||||
_call_tool(t, query="query")
|
||||
|
||||
def test_reflect_failure_logs_error(self, caplog):
|
||||
client = _mock_client()
|
||||
client.reflect.side_effect = RuntimeError("network error")
|
||||
t = self._reflect_tool(client)
|
||||
with caplog.at_level(logging.ERROR), pytest.raises(HindsightError):
|
||||
_call_tool(t, query="query")
|
||||
assert "Reflect failed" in caplog.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# memory_instructions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMemoryInstructions:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_returns_formatted_memories(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["pref1", "pref2"])
|
||||
result = memory_instructions(bank_id="test", client=client)
|
||||
assert result == "Relevant memories:\n\n1. pref1\n2. pref2"
|
||||
|
||||
def test_returns_empty_string_when_no_results(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response([])
|
||||
result = memory_instructions(bank_id="test", client=client)
|
||||
assert result == ""
|
||||
|
||||
def test_respects_max_results(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(
|
||||
["a", "b", "c", "d", "e", "f"]
|
||||
)
|
||||
result = memory_instructions(bank_id="test", client=client, max_results=3)
|
||||
lines = result.strip().split("\n")
|
||||
assert len(lines) == 5 # prefix + blank line + 3 results
|
||||
assert lines[-1] == "3. c"
|
||||
|
||||
def test_custom_prefix(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response(["fact"])
|
||||
result = memory_instructions(bank_id="test", client=client, prefix="Context:\n")
|
||||
assert result.startswith("Context:\n")
|
||||
|
||||
def test_passes_query_and_budget(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response([])
|
||||
memory_instructions(
|
||||
bank_id="test", client=client, query="custom query", budget="high"
|
||||
)
|
||||
call_kwargs = client.recall.call_args[1]
|
||||
assert call_kwargs["query"] == "custom query"
|
||||
assert call_kwargs["budget"] == "high"
|
||||
|
||||
def test_default_budget_is_low(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response([])
|
||||
memory_instructions(bank_id="test", client=client)
|
||||
assert client.recall.call_args[1]["budget"] == "low"
|
||||
|
||||
def test_passes_tags(self):
|
||||
client = _mock_client()
|
||||
client.recall.return_value = _mock_recall_response([])
|
||||
memory_instructions(
|
||||
bank_id="test", client=client, tags=["scope:global"], tags_match="all"
|
||||
)
|
||||
call_kwargs = client.recall.call_args[1]
|
||||
assert call_kwargs["tags"] == ["scope:global"]
|
||||
assert call_kwargs["tags_match"] == "all"
|
||||
|
||||
def test_returns_empty_on_exception(self):
|
||||
client = _mock_client()
|
||||
client.recall.side_effect = RuntimeError("network error")
|
||||
result = memory_instructions(bank_id="test", client=client)
|
||||
assert result == ""
|
||||
|
||||
def test_raises_without_client_or_config(self):
|
||||
with pytest.raises(HindsightError, match="No Hindsight API URL"):
|
||||
memory_instructions(bank_id="test")
|
||||
|
||||
def test_falls_back_to_global_config(self):
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
with patch("hindsight_strands.tools.Hindsight") as mock_cls:
|
||||
mock_instance = _mock_client()
|
||||
mock_instance.recall.return_value = _mock_recall_response(["fact"])
|
||||
mock_cls.return_value = mock_instance
|
||||
result = memory_instructions(bank_id="test")
|
||||
assert "fact" in result
|
||||
1955
hindsight-integrations/strands/uv.lock
Normal file
1955
hindsight-integrations/strands/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|||
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ai-sdk" "chat" "openclaw" "langgraph" "nemoclaw")
|
||||
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ai-sdk" "chat" "openclaw" "langgraph" "nemoclaw" "strands")
|
||||
|
||||
usage() {
|
||||
print_error "Usage: $0 <integration> <version>"
|
||||
|
|
|
|||
Loading…
Reference in a new issue