feat: add AutoGen integration for Hindsight (#719)
* feat: add AutoGen integration for Hindsight Adds hindsight-autogen package providing FunctionTool instances that give AutoGen agents persistent long-term memory via retain/recall/reflect APIs. - Package: hindsight_autogen with create_hindsight_tools() factory - 31 unit tests covering tool creation, invocation, config fallback, errors - Docs page and integrations.json entry - README with quickstart and configuration reference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback for autogen integration - Fix install instructions to include autogen-agentchat and autogen-ext[openai] - Add autogen.svg icon to prevent broken image in integrations grid - Change icon reference from .png to .svg in integrations.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add sleep between retain/recall and close clients in examples - Add time.sleep(3) between retain and recall to wait for async processing - Close Hindsight client and model client to avoid unclosed session warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use asyncio.sleep instead of time.sleep in async examples time.sleep blocks the event loop; asyncio.sleep yields control. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback - validation, defaults, release script - Add autogen to VALID_INTEGRATIONS in release-integration.sh - Remove unused verbose config field - Extract DEFAULT_BUDGET/MAX_TOKENS/RECALL_TAGS_MATCH constants in config.py, import from tools.py to eliminate default duplication - Add Literal types for budget and recall_tags_match validation - Modernize type hints to X | None with from __future__ import annotations - Add [tool.ruff] line-length = 120 to match monorepo convention - Add py.typed PEP 561 marker - Re-raise HindsightError before broad Exception catch - Expand asyncio.sleep(3) comment explaining when/why it's needed - Remove verbose from docs configure() reference table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
300d089b6a
commit
a757765ab2
15 changed files with 2643 additions and 1 deletions
222
hindsight-docs/docs/sdks/integrations/autogen.md
Normal file
222
hindsight-docs/docs/sdks/integrations/autogen.md
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
# AutoGen
|
||||||
|
|
||||||
|
Persistent long-term memory for [AutoGen](https://microsoft.github.io/autogen/) agents via Hindsight. Provides `FunctionTool` instances that plug directly into AutoGen's `AssistantAgent`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Memory Tools** — retain, recall, and reflect as AutoGen `FunctionTool` instances compatible with `AssistantAgent(tools=[...])`
|
||||||
|
- **Async-Native** — Uses `aretain`, `arecall`, `areflect` directly — works seamlessly in AutoGen's async runtime
|
||||||
|
- **Selective Tools** — Include only the tools you need with `include_retain/recall/reflect` flags
|
||||||
|
- **Tag-Based Scoping** — Partition memories by topic, session, or user with tags
|
||||||
|
- **Global Configuration** — Configure once with `configure()`, create tools anywhere
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install hindsight-autogen autogen-agentchat "autogen-ext[openai]"
|
||||||
|
```
|
||||||
|
|
||||||
|
`hindsight-autogen` pulls in `autogen-core` and `hindsight-client`. You also need `autogen-agentchat` for `AssistantAgent` and `autogen-ext[openai]` for the OpenAI model client.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from autogen_agentchat.agents import AssistantAgent
|
||||||
|
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
from hindsight_autogen import create_hindsight_tools
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
await client.acreate_bank(bank_id="user-123")
|
||||||
|
|
||||||
|
model_client = OpenAIChatCompletionClient(model="gpt-4o")
|
||||||
|
tools = create_hindsight_tools(client=client, bank_id="user-123")
|
||||||
|
|
||||||
|
agent = AssistantAgent(
|
||||||
|
name="assistant",
|
||||||
|
model_client=model_client,
|
||||||
|
tools=tools,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store a memory
|
||||||
|
result = await agent.run(task="Remember that I prefer dark mode")
|
||||||
|
print(result.messages[-1].content)
|
||||||
|
|
||||||
|
# Hindsight processes retained content asynchronously (fact extraction,
|
||||||
|
# entity resolution, embeddings). A brief pause ensures memories are
|
||||||
|
# searchable before the next recall. In production, this delay is only
|
||||||
|
# needed when retain and recall happen back-to-back in the same script.
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# Recall it later
|
||||||
|
result = await agent.run(task="What are my UI preferences?")
|
||||||
|
print(result.messages[-1].content)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
await client.aclose()
|
||||||
|
await model_client.close()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip Jupyter Notebooks
|
||||||
|
If you're running in a Jupyter notebook, you don't need `asyncio.run()` — just use `await` directly in cells since the notebook already has an active event loop.
|
||||||
|
:::
|
||||||
|
|
||||||
|
The agent gets 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
|
||||||
|
|
||||||
|
## Selecting Tools
|
||||||
|
|
||||||
|
Include only the tools you need:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
client=client,
|
||||||
|
bank_id="user-123",
|
||||||
|
include_retain=True,
|
||||||
|
include_recall=True,
|
||||||
|
include_reflect=False, # Omit reflect
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Configuration
|
||||||
|
|
||||||
|
Instead of passing a client to every call, configure once:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hindsight_autogen 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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now create tools without passing client — uses global config
|
||||||
|
tools = create_hindsight_tools(bank_id="user-123")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Scoping with Tags
|
||||||
|
|
||||||
|
Use tags to partition memories by topic, session, or user:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Store memories tagged by source
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
client=client,
|
||||||
|
bank_id="user-123",
|
||||||
|
tags=["source:chat", "session:abc"],
|
||||||
|
recall_tags=["source:chat"],
|
||||||
|
recall_tags_match="any",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Patterns
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Tools raise `HindsightError` on failure, which AutoGen surfaces to the agent as a tool error. Wrap agent calls for graceful degradation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hindsight_autogen.errors import HindsightError
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await agent.run(task="What do you remember about me?")
|
||||||
|
except HindsightError as e:
|
||||||
|
print(f"Memory operation failed: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bank Lifecycle
|
||||||
|
|
||||||
|
Create banks before first use and clean up when done:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def main():
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
|
# Create bank (idempotent)
|
||||||
|
await client.acreate_bank(bank_id="user-123")
|
||||||
|
|
||||||
|
tools = create_hindsight_tools(client=client, bank_id="user-123")
|
||||||
|
# ... use tools ...
|
||||||
|
|
||||||
|
# Optional: delete bank when no longer needed
|
||||||
|
await client.adelete_bank(bank_id="user-123")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Agent Teams
|
||||||
|
|
||||||
|
Give each agent its own memory bank, or share a bank across a team:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Per-agent memory
|
||||||
|
researcher_tools = create_hindsight_tools(client=client, bank_id="researcher-memory")
|
||||||
|
writer_tools = create_hindsight_tools(client=client, bank_id="writer-memory")
|
||||||
|
|
||||||
|
# Shared team memory
|
||||||
|
shared_tools = create_hindsight_tools(
|
||||||
|
client=client,
|
||||||
|
bank_id="team-shared",
|
||||||
|
tags=["team:content"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 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 (any/all/any\_strict/all\_strict) |
|
||||||
|
| `retain_metadata` | `None` | Default metadata dict for retain operations |
|
||||||
|
| `retain_document_id` | `None` | Default document\_id for retain (groups/upserts memories) |
|
||||||
|
| `recall_types` | `None` | Fact types to filter (world, experience, opinion, observation) |
|
||||||
|
| `recall_include_entities` | `False` | Include entity information in recall results |
|
||||||
|
| `reflect_context` | `None` | Additional context for reflect operations |
|
||||||
|
| `reflect_max_tokens` | `None` | Max tokens for reflect results (defaults to `max_tokens`) |
|
||||||
|
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
|
||||||
|
| `reflect_tags` | `None` | Tags to filter memories used in reflect (defaults to `recall_tags`) |
|
||||||
|
| `reflect_tags_match` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
|
||||||
|
| `include_retain` | `True` | Include the retain (store) tool |
|
||||||
|
| `include_recall` | `True` | Include the recall (search) tool |
|
||||||
|
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
|
||||||
|
|
||||||
|
### `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 |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python >= 3.10
|
||||||
|
- autogen-core >= 0.4.0
|
||||||
|
- hindsight-client >= 0.4.0
|
||||||
|
|
@ -170,6 +170,16 @@
|
||||||
"link": "/sdks/integrations/ag2",
|
"link": "/sdks/integrations/ag2",
|
||||||
"icon": "/img/icons/ag2.svg"
|
"icon": "/img/icons/ag2.svg"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "autogen",
|
||||||
|
"name": "AutoGen",
|
||||||
|
"description": "Give AutoGen agents persistent long-term memory with Hindsight FunctionTool instances for retain, recall, and reflect.",
|
||||||
|
"type": "official",
|
||||||
|
"by": "hindsight",
|
||||||
|
"category": "framework",
|
||||||
|
"link": "/sdks/integrations/autogen",
|
||||||
|
"icon": "/img/icons/autogen.svg"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "hindclaw",
|
"id": "hindclaw",
|
||||||
"name": "HindClaw",
|
"name": "HindClaw",
|
||||||
|
|
|
||||||
4
hindsight-docs/static/img/icons/autogen.svg
Normal file
4
hindsight-docs/static/img/icons/autogen.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect width="64" height="64" rx="12" fill="#0078D4"/>
|
||||||
|
<text x="32" y="38" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="white" text-anchor="middle">AG</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 268 B |
155
hindsight-integrations/autogen/README.md
Normal file
155
hindsight-integrations/autogen/README.md
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# hindsight-autogen
|
||||||
|
|
||||||
|
AutoGen integration for [Hindsight](https://github.com/vectorize-io/hindsight) — persistent long-term memory for AI agents.
|
||||||
|
|
||||||
|
Provides `FunctionTool` instances that give [AutoGen](https://microsoft.github.io/autogen/) agents the ability to store, search, and synthesize memories across conversations.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A running Hindsight instance ([self-hosted via Docker](https://github.com/vectorize-io/hindsight#quick-start) or [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup))
|
||||||
|
- Python 3.10+
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install hindsight-autogen autogen-agentchat "autogen-ext[openai]"
|
||||||
|
```
|
||||||
|
|
||||||
|
`hindsight-autogen` pulls in `autogen-core` and `hindsight-client`. You also need `autogen-agentchat` for `AssistantAgent` and `autogen-ext[openai]` for the OpenAI model client.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from autogen_agentchat.agents import AssistantAgent
|
||||||
|
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
from hindsight_autogen import create_hindsight_tools
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
await client.acreate_bank(bank_id="user-123")
|
||||||
|
|
||||||
|
model_client = OpenAIChatCompletionClient(model="gpt-4o")
|
||||||
|
tools = create_hindsight_tools(client=client, bank_id="user-123")
|
||||||
|
|
||||||
|
agent = AssistantAgent(
|
||||||
|
name="assistant",
|
||||||
|
model_client=model_client,
|
||||||
|
tools=tools,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store a memory
|
||||||
|
result = await agent.run(task="Remember that I prefer dark mode")
|
||||||
|
print(result.messages[-1].content)
|
||||||
|
|
||||||
|
# Hindsight processes retained content asynchronously (fact extraction,
|
||||||
|
# entity resolution, embeddings). A brief pause ensures memories are
|
||||||
|
# searchable before the next recall. In production, this delay is only
|
||||||
|
# needed when retain and recall happen back-to-back in the same script.
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# Recall it later
|
||||||
|
result = await agent.run(task="What are my UI preferences?")
|
||||||
|
print(result.messages[-1].content)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
await client.aclose()
|
||||||
|
await model_client.close()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent gets three tools:
|
||||||
|
|
||||||
|
- **`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
|
||||||
|
|
||||||
|
## Selecting Tools
|
||||||
|
|
||||||
|
Include only the tools you need:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
client=client,
|
||||||
|
bank_id="user-123",
|
||||||
|
include_retain=True,
|
||||||
|
include_recall=True,
|
||||||
|
include_reflect=False, # Omit reflect
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Configuration
|
||||||
|
|
||||||
|
Instead of passing a client to every call, configure once:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hindsight_autogen 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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now create tools without passing client
|
||||||
|
tools = create_hindsight_tools(bank_id="user-123")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Scoping with Tags
|
||||||
|
|
||||||
|
Use tags to partition memories by topic, session, or user:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Store memories tagged by source
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
client=client,
|
||||||
|
bank_id="user-123",
|
||||||
|
tags=["source:chat", "session:abc"],
|
||||||
|
recall_tags=["source:chat"],
|
||||||
|
recall_tags_match="any",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
| 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 (any/all/any\_strict/all\_strict) |
|
||||||
|
| `retain_metadata` | `None` | Default metadata dict for retain operations |
|
||||||
|
| `retain_document_id` | `None` | Default document\_id for retain (groups/upserts memories) |
|
||||||
|
| `recall_types` | `None` | Fact types to filter (world, experience, opinion, observation) |
|
||||||
|
| `recall_include_entities` | `False` | Include entity information in recall results |
|
||||||
|
| `reflect_context` | `None` | Additional context for reflect operations |
|
||||||
|
| `reflect_max_tokens` | `None` | Max tokens for reflect results (defaults to `max_tokens`) |
|
||||||
|
| `reflect_response_schema` | `None` | JSON schema to constrain reflect output format |
|
||||||
|
| `reflect_tags` | `None` | Tags to filter memories used in reflect (defaults to `recall_tags`) |
|
||||||
|
| `reflect_tags_match` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) |
|
||||||
|
| `include_retain` | `True` | Include the retain (store) tool |
|
||||||
|
| `include_recall` | `True` | Include the recall (search) tool |
|
||||||
|
| `include_reflect` | `True` | Include the reflect (synthesize) tool |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python >= 3.10
|
||||||
|
- autogen-core >= 0.4.0
|
||||||
|
- hindsight-client >= 0.4.0
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Integration docs](https://docs.hindsight.vectorize.io/docs/sdks/integrations/autogen)
|
||||||
|
- [Cookbook: AutoGen assistant with memory](https://docs.hindsight.vectorize.io/cookbook/recipes/autogen-assistant-agent)
|
||||||
|
- [Hindsight API docs](https://docs.hindsight.vectorize.io)
|
||||||
36
hindsight-integrations/autogen/hindsight_autogen/__init__.py
Normal file
36
hindsight-integrations/autogen/hindsight_autogen/__init__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Hindsight-AutoGen: Persistent memory tools for AutoGen agents.
|
||||||
|
|
||||||
|
Provides ``FunctionTool`` instances that give AutoGen agents long-term memory
|
||||||
|
via Hindsight's retain/recall/reflect APIs.
|
||||||
|
|
||||||
|
Basic usage::
|
||||||
|
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
from hindsight_autogen import create_hindsight_tools
|
||||||
|
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
tools = create_hindsight_tools(client=client, bank_id="user-123")
|
||||||
|
|
||||||
|
# Use with an AutoGen AssistantAgent
|
||||||
|
agent = AssistantAgent(name="assistant", model_client=model, tools=tools)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .config import (
|
||||||
|
HindsightAutoGenConfig,
|
||||||
|
configure,
|
||||||
|
get_config,
|
||||||
|
reset_config,
|
||||||
|
)
|
||||||
|
from .errors import HindsightError
|
||||||
|
from .tools import create_hindsight_tools
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"configure",
|
||||||
|
"get_config",
|
||||||
|
"reset_config",
|
||||||
|
"HindsightAutoGenConfig",
|
||||||
|
"HindsightError",
|
||||||
|
"create_hindsight_tools",
|
||||||
|
]
|
||||||
34
hindsight-integrations/autogen/hindsight_autogen/_client.py
Normal file
34
hindsight-integrations/autogen/hindsight_autogen/_client.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Shared Hindsight client resolution logic."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
|
||||||
|
from .config import get_config
|
||||||
|
from .errors import HindsightError
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
95
hindsight-integrations/autogen/hindsight_autogen/config.py
Normal file
95
hindsight-integrations/autogen/hindsight_autogen/config.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""Global configuration for Hindsight-AutoGen integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
|
||||||
|
HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
|
||||||
|
|
||||||
|
DEFAULT_BUDGET: Literal["low", "mid", "high"] = "mid"
|
||||||
|
DEFAULT_MAX_TOKENS = 4096
|
||||||
|
DEFAULT_RECALL_TAGS_MATCH: Literal["any", "all", "any_strict", "all_strict"] = "any"
|
||||||
|
|
||||||
|
Budget = Literal["low", "mid", "high"]
|
||||||
|
TagsMatch = Literal["any", "all", "any_strict", "all_strict"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HindsightAutoGenConfig:
|
||||||
|
"""Connection and default settings for the AutoGen 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).
|
||||||
|
"""
|
||||||
|
|
||||||
|
hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
|
||||||
|
api_key: str | None = None
|
||||||
|
budget: Budget = DEFAULT_BUDGET
|
||||||
|
max_tokens: int = DEFAULT_MAX_TOKENS
|
||||||
|
tags: list[str] | None = None
|
||||||
|
recall_tags: list[str] | None = None
|
||||||
|
recall_tags_match: TagsMatch = DEFAULT_RECALL_TAGS_MATCH
|
||||||
|
|
||||||
|
|
||||||
|
_global_config: HindsightAutoGenConfig | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def configure(
|
||||||
|
hindsight_api_url: str | None = None,
|
||||||
|
api_key: str | None = None,
|
||||||
|
budget: Budget = DEFAULT_BUDGET,
|
||||||
|
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
recall_tags: list[str] | None = None,
|
||||||
|
recall_tags_match: TagsMatch = DEFAULT_RECALL_TAGS_MATCH,
|
||||||
|
) -> HindsightAutoGenConfig:
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The configured HindsightAutoGenConfig.
|
||||||
|
"""
|
||||||
|
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 = HindsightAutoGenConfig(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _global_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> HindsightAutoGenConfig | 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-AutoGen error types."""
|
||||||
|
|
||||||
|
|
||||||
|
class HindsightError(Exception):
|
||||||
|
"""Exception raised when a Hindsight memory operation fails."""
|
||||||
|
|
||||||
|
pass
|
||||||
235
hindsight-integrations/autogen/hindsight_autogen/tools.py
Normal file
235
hindsight-integrations/autogen/hindsight_autogen/tools.py
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
"""AutoGen tool definitions for Hindsight memory operations.
|
||||||
|
|
||||||
|
Provides a factory function that creates AutoGen-compatible ``FunctionTool``
|
||||||
|
instances backed by Hindsight's retain/recall/reflect APIs. These tools can
|
||||||
|
be passed directly to ``AssistantAgent(tools=[...])``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from autogen_core.tools import FunctionTool
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
|
||||||
|
from ._client import resolve_client
|
||||||
|
from .config import (
|
||||||
|
DEFAULT_BUDGET,
|
||||||
|
DEFAULT_MAX_TOKENS,
|
||||||
|
DEFAULT_RECALL_TAGS_MATCH,
|
||||||
|
Budget,
|
||||||
|
TagsMatch,
|
||||||
|
get_config,
|
||||||
|
)
|
||||||
|
from .errors import HindsightError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_hindsight_tools(
|
||||||
|
*,
|
||||||
|
bank_id: str,
|
||||||
|
client: Hindsight | None = None,
|
||||||
|
hindsight_api_url: str | None = None,
|
||||||
|
api_key: str | None = None,
|
||||||
|
budget: Budget | None = None,
|
||||||
|
max_tokens: int | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
recall_tags: list[str] | None = None,
|
||||||
|
recall_tags_match: TagsMatch | None = None,
|
||||||
|
# Retain options
|
||||||
|
retain_metadata: dict[str, str] | None = None,
|
||||||
|
retain_document_id: str | None = None,
|
||||||
|
# Recall options
|
||||||
|
recall_types: list[str] | None = None,
|
||||||
|
recall_include_entities: bool = False,
|
||||||
|
# Reflect options
|
||||||
|
reflect_context: str | None = None,
|
||||||
|
reflect_max_tokens: int | None = None,
|
||||||
|
reflect_response_schema: dict[str, Any] | None = None,
|
||||||
|
reflect_tags: list[str] | None = None,
|
||||||
|
reflect_tags_match: TagsMatch | None = None,
|
||||||
|
include_retain: bool = True,
|
||||||
|
include_recall: bool = True,
|
||||||
|
include_reflect: bool = True,
|
||||||
|
) -> list[FunctionTool]:
|
||||||
|
"""Create Hindsight memory tools for an AutoGen agent.
|
||||||
|
|
||||||
|
Returns a list of ``FunctionTool`` instances compatible with AutoGen's
|
||||||
|
``AssistantAgent(tools=[...])``.
|
||||||
|
|
||||||
|
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).
|
||||||
|
retain_metadata: Default metadata dict for retain operations.
|
||||||
|
retain_document_id: Default document_id for retain (groups/upserts memories).
|
||||||
|
recall_types: Fact types to filter (world, experience, opinion, observation).
|
||||||
|
recall_include_entities: Include entity information in recall results.
|
||||||
|
reflect_context: Additional context for reflect operations.
|
||||||
|
reflect_max_tokens: Max tokens for reflect results (defaults to max_tokens).
|
||||||
|
reflect_response_schema: JSON schema to constrain reflect output format.
|
||||||
|
reflect_tags: Tags to filter memories used in reflect (defaults to recall_tags).
|
||||||
|
reflect_tags_match: Tag matching for reflect (defaults to recall_tags_match).
|
||||||
|
include_retain: Include the retain (store) tool.
|
||||||
|
include_recall: Include the recall (search) tool.
|
||||||
|
include_reflect: Include the reflect (synthesize) tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of AutoGen FunctionTool instances.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HindsightError: If no client or API URL can be resolved.
|
||||||
|
"""
|
||||||
|
resolved_client = resolve_client(client, hindsight_api_url, api_key)
|
||||||
|
|
||||||
|
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
|
||||||
|
if recall_tags_match is not None
|
||||||
|
else (config.recall_tags_match if config else DEFAULT_RECALL_TAGS_MATCH)
|
||||||
|
)
|
||||||
|
effective_budget = budget if budget is not None else (config.budget if config else DEFAULT_BUDGET)
|
||||||
|
effective_max_tokens = (
|
||||||
|
max_tokens if max_tokens is not None else (config.max_tokens if config else DEFAULT_MAX_TOKENS)
|
||||||
|
)
|
||||||
|
|
||||||
|
tools: list[FunctionTool] = []
|
||||||
|
|
||||||
|
if include_retain:
|
||||||
|
|
||||||
|
async 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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The information to store in memory.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retain_kwargs: dict[str, Any] = {"bank_id": bank_id, "content": content}
|
||||||
|
if effective_tags:
|
||||||
|
retain_kwargs["tags"] = effective_tags
|
||||||
|
if retain_metadata:
|
||||||
|
retain_kwargs["metadata"] = retain_metadata
|
||||||
|
if retain_document_id:
|
||||||
|
retain_kwargs["document_id"] = retain_document_id
|
||||||
|
await resolved_client.aretain(**retain_kwargs)
|
||||||
|
return "Memory stored successfully."
|
||||||
|
except HindsightError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Retain failed: %s", e)
|
||||||
|
raise HindsightError(f"Retain failed: {e}") from e
|
||||||
|
|
||||||
|
tools.append(
|
||||||
|
FunctionTool(
|
||||||
|
hindsight_retain,
|
||||||
|
description="Store information to long-term memory for later retrieval.",
|
||||||
|
name="hindsight_retain",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if include_recall:
|
||||||
|
|
||||||
|
async 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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: What to search for in memory.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
if recall_types:
|
||||||
|
recall_kwargs["types"] = recall_types
|
||||||
|
if recall_include_entities:
|
||||||
|
recall_kwargs["include_entities"] = True
|
||||||
|
response = await resolved_client.arecall(**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("Recall failed: %s", e)
|
||||||
|
raise HindsightError(f"Recall failed: {e}") from e
|
||||||
|
|
||||||
|
tools.append(
|
||||||
|
FunctionTool(
|
||||||
|
hindsight_recall,
|
||||||
|
description="Search long-term memory for relevant information.",
|
||||||
|
name="hindsight_recall",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if include_reflect:
|
||||||
|
|
||||||
|
async 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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The question to reflect on using stored memories.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
reflect_kwargs: dict[str, Any] = {
|
||||||
|
"bank_id": bank_id,
|
||||||
|
"query": query,
|
||||||
|
"budget": effective_budget,
|
||||||
|
}
|
||||||
|
if reflect_context:
|
||||||
|
reflect_kwargs["context"] = reflect_context
|
||||||
|
effective_reflect_max = reflect_max_tokens or effective_max_tokens
|
||||||
|
if effective_reflect_max:
|
||||||
|
reflect_kwargs["max_tokens"] = effective_reflect_max
|
||||||
|
if reflect_response_schema:
|
||||||
|
reflect_kwargs["response_schema"] = reflect_response_schema
|
||||||
|
# Reflect tags: use reflect-specific or fall back to recall tags
|
||||||
|
effective_reflect_tags = reflect_tags if reflect_tags is not None else effective_recall_tags
|
||||||
|
effective_reflect_tags_match = reflect_tags_match or effective_recall_tags_match
|
||||||
|
if effective_reflect_tags:
|
||||||
|
reflect_kwargs["tags"] = effective_reflect_tags
|
||||||
|
reflect_kwargs["tags_match"] = effective_reflect_tags_match
|
||||||
|
response = await resolved_client.areflect(**reflect_kwargs)
|
||||||
|
return response.text or "No relevant memories found."
|
||||||
|
except HindsightError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Reflect failed: %s", e)
|
||||||
|
raise HindsightError(f"Reflect failed: {e}") from e
|
||||||
|
|
||||||
|
tools.append(
|
||||||
|
FunctionTool(
|
||||||
|
hindsight_reflect,
|
||||||
|
description="Synthesize a thoughtful answer from long-term memories.",
|
||||||
|
name="hindsight_reflect",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tools
|
||||||
59
hindsight-integrations/autogen/pyproject.toml
Normal file
59
hindsight-integrations/autogen/pyproject.toml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
[project]
|
||||||
|
name = "hindsight-autogen"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "AutoGen integration for Hindsight - persistent memory tools for AI agents"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [
|
||||||
|
{ name = "Vectorize", email = "support@vectorize.io" }
|
||||||
|
]
|
||||||
|
keywords = [
|
||||||
|
"ai",
|
||||||
|
"memory",
|
||||||
|
"autogen",
|
||||||
|
"agents",
|
||||||
|
"hindsight",
|
||||||
|
]
|
||||||
|
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 = [
|
||||||
|
"autogen-core>=0.4.0",
|
||||||
|
"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/autogen"
|
||||||
|
Repository = "https://github.com/vectorize-io/hindsight"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["hindsight_autogen"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"autogen-agentchat>=0.4.0",
|
||||||
|
"pytest>=9.0.2",
|
||||||
|
"pytest-asyncio>=0.23.0",
|
||||||
|
"ruff>=0.4.0",
|
||||||
|
]
|
||||||
0
hindsight-integrations/autogen/tests/__init__.py
Normal file
0
hindsight-integrations/autogen/tests/__init__.py
Normal file
486
hindsight-integrations/autogen/tests/test_tools.py
Normal file
486
hindsight-integrations/autogen/tests/test_tools.py
Normal file
|
|
@ -0,0 +1,486 @@
|
||||||
|
"""Unit tests for Hindsight AutoGen tools."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from autogen_core import CancellationToken
|
||||||
|
from hindsight_autogen import (
|
||||||
|
configure,
|
||||||
|
create_hindsight_tools,
|
||||||
|
reset_config,
|
||||||
|
)
|
||||||
|
from hindsight_autogen.errors import HindsightError
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_client():
|
||||||
|
"""Create a mock Hindsight client with async methods."""
|
||||||
|
client = MagicMock()
|
||||||
|
client.aretain = AsyncMock()
|
||||||
|
client.arecall = AsyncMock()
|
||||||
|
client.areflect = AsyncMock()
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_recall_response(texts: list[str]):
|
||||||
|
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):
|
||||||
|
response = MagicMock()
|
||||||
|
response.text = text
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_retain_response():
|
||||||
|
response = MagicMock()
|
||||||
|
response.success = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def _run(tool, args: dict) -> str:
|
||||||
|
"""Invoke an AutoGen FunctionTool and return the string result."""
|
||||||
|
result = await tool.run_json(args, CancellationToken())
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateHindsightTools:
|
||||||
|
def setup_method(self):
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
def test_returns_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_include_retain_only(self):
|
||||||
|
client = _mock_client()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=True,
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
assert len(tools) == 1
|
||||||
|
assert tools[0].name == "hindsight_retain"
|
||||||
|
|
||||||
|
def test_include_recall_only(self):
|
||||||
|
client = _mock_client()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=True,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
assert len(tools) == 1
|
||||||
|
assert tools[0].name == "hindsight_recall"
|
||||||
|
|
||||||
|
def test_include_reflect_only(self):
|
||||||
|
client = _mock_client()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=True,
|
||||||
|
)
|
||||||
|
assert len(tools) == 1
|
||||||
|
assert tools[0].name == "hindsight_reflect"
|
||||||
|
|
||||||
|
def test_no_tools_when_all_excluded(self):
|
||||||
|
client = _mock_client()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
include_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_autogen._client.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_explicit_url_overrides_config(self):
|
||||||
|
configure(hindsight_api_url="http://config:8888")
|
||||||
|
with patch("hindsight_autogen._client.Hindsight") as mock_cls:
|
||||||
|
mock_cls.return_value = _mock_client()
|
||||||
|
create_hindsight_tools(bank_id="test", hindsight_api_url="http://explicit:9999")
|
||||||
|
mock_cls.assert_called_once_with(base_url="http://explicit:9999", timeout=30.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetainTool:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retain_stores_memory(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.return_value = _mock_retain_response()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test-bank",
|
||||||
|
client=client,
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
result = await _run(tools[0], {"content": "The user likes Python"})
|
||||||
|
assert result == "Memory stored successfully."
|
||||||
|
client.aretain.assert_called_once_with(bank_id="test-bank", content="The user likes Python")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retain_passes_tags(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.return_value = _mock_retain_response()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test-bank",
|
||||||
|
client=client,
|
||||||
|
tags=["source:chat"],
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"content": "some content"})
|
||||||
|
call_kwargs = client.aretain.call_args[1]
|
||||||
|
assert call_kwargs["tags"] == ["source:chat"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retain_passes_metadata(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.return_value = _mock_retain_response()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
retain_metadata={"source": "chat", "session": "abc"},
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"content": "content"})
|
||||||
|
call_kwargs = client.aretain.call_args[1]
|
||||||
|
assert call_kwargs["metadata"] == {"source": "chat", "session": "abc"}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retain_passes_document_id(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.return_value = _mock_retain_response()
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
retain_document_id="session-123",
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"content": "content"})
|
||||||
|
call_kwargs = client.aretain.call_args[1]
|
||||||
|
assert call_kwargs["document_id"] == "session-123"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retain_raises_hindsight_error(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.side_effect = RuntimeError("connection refused")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
with pytest.raises(HindsightError, match="Retain failed"):
|
||||||
|
await _run(tools[0], {"content": "content"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestRecallTool:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_returns_numbered_results(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["User likes Python", "User is in NYC"])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test-bank",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
result = await _run(tools[0], {"query": "user preferences"})
|
||||||
|
assert "1. User likes Python" in result
|
||||||
|
assert "2. User is in NYC" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_empty_results(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response([])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
result = await _run(tools[0], {"query": "anything"})
|
||||||
|
assert result == "No relevant memories found."
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_passes_budget_and_max_tokens(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
budget="high",
|
||||||
|
max_tokens=2048,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["budget"] == "high"
|
||||||
|
assert call_kwargs["max_tokens"] == 2048
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_passes_tags(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
recall_tags=["scope:user"],
|
||||||
|
recall_tags_match="all",
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["tags"] == ["scope:user"]
|
||||||
|
assert call_kwargs["tags_match"] == "all"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_passes_types(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
recall_types=["world", "experience"],
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["types"] == ["world", "experience"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_passes_include_entities(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
recall_include_entities=True,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["include_entities"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recall_raises_hindsight_error(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.side_effect = RuntimeError("timeout")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
with pytest.raises(HindsightError, match="Recall failed"):
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestReflectTool:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_returns_text(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response(
|
||||||
|
"The user is a Python developer who prefers functional patterns."
|
||||||
|
)
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test-bank",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
result = await _run(tools[0], {"query": "What do you know about the user?"})
|
||||||
|
assert result == "The user is a Python developer who prefers functional patterns."
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_empty_returns_fallback(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response("")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
result = await _run(tools[0], {"query": "anything"})
|
||||||
|
assert result == "No relevant memories found."
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_passes_budget(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response("answer")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
budget="high",
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.areflect.call_args[1]
|
||||||
|
assert call_kwargs["budget"] == "high"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_passes_context(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response("answer")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
reflect_context="The user is asking about project setup",
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.areflect.call_args[1]
|
||||||
|
assert call_kwargs["context"] == "The user is asking about project setup"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_passes_max_tokens_and_response_schema(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response("answer")
|
||||||
|
schema = {"type": "object", "properties": {"summary": {"type": "string"}}}
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
reflect_max_tokens=2048,
|
||||||
|
reflect_response_schema=schema,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.areflect.call_args[1]
|
||||||
|
assert call_kwargs["max_tokens"] == 2048
|
||||||
|
assert call_kwargs["response_schema"] == schema
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_passes_tags(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.return_value = _mock_reflect_response("answer")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
reflect_tags=["scope:global"],
|
||||||
|
reflect_tags_match="all",
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.areflect.call_args[1]
|
||||||
|
assert call_kwargs["tags"] == ["scope:global"]
|
||||||
|
assert call_kwargs["tags_match"] == "all"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_raises_hindsight_error(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.areflect.side_effect = RuntimeError("timeout")
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_recall=False,
|
||||||
|
)
|
||||||
|
with pytest.raises(HindsightError, match="Reflect failed"):
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigFallback:
|
||||||
|
def setup_method(self):
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
reset_config()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_config_budget_used_when_no_explicit(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
configure(
|
||||||
|
hindsight_api_url="http://localhost:8888",
|
||||||
|
budget="low",
|
||||||
|
)
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["budget"] == "low"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_explicit_budget_overrides_config(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.arecall.return_value = _mock_recall_response(["fact"])
|
||||||
|
configure(
|
||||||
|
hindsight_api_url="http://localhost:8888",
|
||||||
|
budget="low",
|
||||||
|
)
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
budget="high",
|
||||||
|
include_retain=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"query": "query"})
|
||||||
|
call_kwargs = client.arecall.call_args[1]
|
||||||
|
assert call_kwargs["budget"] == "high"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_config_tags_used_for_retain(self):
|
||||||
|
client = _mock_client()
|
||||||
|
client.aretain.return_value = _mock_retain_response()
|
||||||
|
configure(
|
||||||
|
hindsight_api_url="http://localhost:8888",
|
||||||
|
tags=["env:test"],
|
||||||
|
)
|
||||||
|
tools = create_hindsight_tools(
|
||||||
|
bank_id="test",
|
||||||
|
client=client,
|
||||||
|
include_recall=False,
|
||||||
|
include_reflect=False,
|
||||||
|
)
|
||||||
|
await _run(tools[0], {"content": "content"})
|
||||||
|
call_kwargs = client.aretain.call_args[1]
|
||||||
|
assert call_kwargs["tags"] == ["env:test"]
|
||||||
1299
hindsight-integrations/autogen/uv.lock
Normal file
1299
hindsight-integrations/autogen/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_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "hermes")
|
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "hermes" "autogen")
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
print_error "Usage: $0 <integration> <version>"
|
print_error "Usage: $0 <integration> <version>"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue