fleet-memory/hindsight-integrations/crewai/tests/test_tools.py
Ben 41db2960c5
feat: add CrewAI integration for persistent crew memory (#319)
* feat: add CrewAI integration for persistent crew memory

Implements a CrewAI ExternalMemory storage backend that maps CrewAI's
Storage interface (save/search/reset) to Hindsight's retain/recall/delete
APIs, giving crews long-term memory with fact extraction, entity tracking,
and temporal awareness across runs.

Key features:
- HindsightStorage: drop-in Storage backend for CrewAI ExternalMemory
- HindsightReflectTool: BaseTool exposing Hindsight's reflect API
- Per-agent memory banks with customizable bank resolver
- Async compatibility layer for CrewAI's threading model
- 35 unit tests, docs site page, example script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move CrewAI example to hindsight-cookbook

Move research_crew.py example from hindsight-integrations/crewai/examples/
to the cookbook repo and update the integration README to link there instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add GitHub Actions test job for CrewAI integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add uv.lock for frozen installs in CI

The test-crewai-integration CI job uses `uv sync --frozen` which
requires a committed lock file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:05:52 +01:00

97 lines
3.5 KiB
Python

"""Unit tests for HindsightReflectTool."""
from unittest.mock import MagicMock, patch
from hindsight_crewai import HindsightReflectTool, configure, reset_config
from hindsight_crewai.errors import HindsightError
import pytest
def _passthrough(fn, *args, **kwargs):
"""Replace call_sync with direct call for testing."""
return fn(*args, **kwargs)
class TestReflectTool:
def setup_method(self):
reset_config()
configure(hindsight_api_url="http://localhost:8888")
def teardown_method(self):
reset_config()
@patch("hindsight_crewai.tools.call_sync", side_effect=_passthrough)
def test_reflect_returns_text(self, _mock_cs):
tool = HindsightReflectTool(bank_id="test-bank")
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.text = "The user is a Python developer who..."
mock_client.reflect.return_value = mock_result
tool._local.client = mock_client
result = tool._run("What do you know about the user?")
assert result == "The user is a Python developer who..."
mock_client.reflect.assert_called_once()
@patch("hindsight_crewai.tools.call_sync", side_effect=_passthrough)
def test_reflect_empty_returns_fallback(self, _mock_cs):
tool = HindsightReflectTool(bank_id="test-bank")
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.text = ""
mock_client.reflect.return_value = mock_result
tool._local.client = mock_client
result = tool._run("anything")
assert "No relevant memories" in result
@patch("hindsight_crewai.tools.call_sync", side_effect=_passthrough)
def test_reflect_passes_context(self, _mock_cs):
tool = HindsightReflectTool(
bank_id="test-bank",
reflect_context="Agent is a delivery robot.",
)
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.text = "Synthesized answer"
mock_client.reflect.return_value = mock_result
tool._local.client = mock_client
tool._run("Where is Alice?")
call_kwargs = mock_client.reflect.call_args[1]
assert call_kwargs["context"] == "Agent is a delivery robot."
assert call_kwargs["bank_id"] == "test-bank"
assert call_kwargs["budget"] == "mid"
@patch("hindsight_crewai.tools.call_sync", side_effect=_passthrough)
def test_reflect_passes_budget(self, _mock_cs):
tool = HindsightReflectTool(bank_id="test-bank", budget="high")
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.text = "answer"
mock_client.reflect.return_value = mock_result
tool._local.client = mock_client
tool._run("query")
call_kwargs = mock_client.reflect.call_args[1]
assert call_kwargs["budget"] == "high"
@patch("hindsight_crewai.tools.call_sync", side_effect=_passthrough)
def test_reflect_raises_hindsight_error_on_failure(self, _mock_cs):
tool = HindsightReflectTool(bank_id="test-bank")
mock_client = MagicMock()
mock_client.reflect.side_effect = RuntimeError("timeout")
tool._local.client = mock_client
with pytest.raises(HindsightError, match="Reflect failed"):
tool._run("query")
def test_tool_metadata(self):
tool = HindsightReflectTool(bank_id="test-bank")
assert tool.name == "hindsight_reflect"
assert "reflect" in tool.description.lower()
assert "memories" in tool.description.lower()