* 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>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""CrewAI Tool for Hindsight reflect operations.
|
|
|
|
Since CrewAI's Storage interface only has save/search/reset,
|
|
reflect is exposed as a Tool that agents can call explicitly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import threading
|
|
from typing import Any
|
|
|
|
from crewai.tools import BaseTool
|
|
from pydantic import Field, PrivateAttr
|
|
|
|
from ._compat import call_sync
|
|
from .config import get_config
|
|
from .errors import HindsightError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HindsightReflectTool(BaseTool):
|
|
"""CrewAI tool that generates disposition-aware answers from memory.
|
|
|
|
Unlike recall (search), reflect synthesizes a coherent, reasoned
|
|
response using the bank's personality/disposition and all relevant
|
|
memories. Use this when agents need a thoughtful, contextual answer
|
|
rather than raw memory facts.
|
|
|
|
Args:
|
|
bank_id: The Hindsight memory bank to reflect against.
|
|
hindsight_api_url: Override the configured API URL.
|
|
api_key: Override the configured API key.
|
|
budget: Reflect budget level (low/mid/high).
|
|
reflect_context: Additional context for reflect reasoning.
|
|
|
|
Example::
|
|
|
|
from hindsight_crewai import HindsightReflectTool
|
|
from crewai import Agent
|
|
|
|
reflect_tool = HindsightReflectTool(
|
|
bank_id="my-crew",
|
|
budget="mid",
|
|
)
|
|
agent = Agent(role="Analyst", tools=[reflect_tool], ...)
|
|
"""
|
|
|
|
name: str = "hindsight_reflect"
|
|
description: str = (
|
|
"Generate a thoughtful, synthesized answer about a topic by reflecting "
|
|
"on all relevant memories. Use this when you need a coherent summary "
|
|
"of what you know, not just raw facts. Input: a question or topic."
|
|
)
|
|
|
|
bank_id: str = Field(description="Hindsight memory bank ID")
|
|
hindsight_api_url: str | None = Field(default=None, description="Override API URL")
|
|
api_key: str | None = Field(default=None, description="Override API key")
|
|
budget: str = Field(default="mid", description="Reflect budget (low/mid/high)")
|
|
reflect_context: str | None = Field(
|
|
default=None, description="Additional context for reflect reasoning"
|
|
)
|
|
|
|
_local: Any = PrivateAttr(default_factory=threading.local)
|
|
|
|
def _get_client(self) -> Any:
|
|
"""Get or create a thread-local Hindsight client."""
|
|
client = getattr(self._local, "client", None)
|
|
if client is None:
|
|
from hindsight_client import Hindsight
|
|
|
|
config = get_config()
|
|
api_url = self.hindsight_api_url or (
|
|
config.hindsight_api_url if config else "http://localhost:8888"
|
|
)
|
|
api_key = self.api_key or (config.api_key if config else None)
|
|
|
|
client = Hindsight(
|
|
base_url=api_url,
|
|
api_key=api_key,
|
|
timeout=30.0,
|
|
)
|
|
self._local.client = client
|
|
return client
|
|
|
|
def _run(self, query: str) -> str:
|
|
"""Execute the reflect tool.
|
|
|
|
Args:
|
|
query: The question or topic to reflect on.
|
|
|
|
Returns:
|
|
The synthesized reflect response text.
|
|
|
|
Raises:
|
|
HindsightError: If the reflect operation fails.
|
|
"""
|
|
reflect_kwargs: dict[str, Any] = {
|
|
"bank_id": self.bank_id,
|
|
"query": query,
|
|
"budget": self.budget,
|
|
}
|
|
if self.reflect_context:
|
|
reflect_kwargs["context"] = self.reflect_context
|
|
|
|
def _reflect() -> Any:
|
|
return self._get_client().reflect(**reflect_kwargs)
|
|
|
|
try:
|
|
result = call_sync(_reflect)
|
|
text = result.text if hasattr(result, "text") else str(result)
|
|
|
|
if not text:
|
|
return "No relevant memories found to reflect on."
|
|
|
|
return text
|
|
|
|
except Exception as e:
|
|
logger.error(f"Reflect failed: {e}")
|
|
raise HindsightError(f"Reflect failed: {e}") from e
|