* 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>
51 lines
1.7 KiB
Python
51 lines
1.7 KiB
Python
"""Async compatibility helpers.
|
|
|
|
CrewAI runs inside an async event loop. The Hindsight client's sync
|
|
methods internally call ``loop.run_until_complete()``, which fails
|
|
when a loop is already running or when ``asyncio.get_event_loop()``
|
|
returns a foreign loop from another thread.
|
|
|
|
This module provides a dedicated worker thread with a persistent
|
|
event loop for all Hindsight API calls, ensuring the aiohttp session
|
|
stays bound to a single, stable loop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import concurrent.futures
|
|
import threading
|
|
from typing import Any, Callable
|
|
|
|
_thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
|
|
_thread_init_lock = threading.Lock()
|
|
_initialized_threads: set[int] = set()
|
|
|
|
|
|
def _ensure_thread_loop() -> None:
|
|
"""Ensure the current thread has a persistent event loop."""
|
|
tid = threading.get_ident()
|
|
if tid not in _initialized_threads:
|
|
with _thread_init_lock:
|
|
if tid not in _initialized_threads:
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
_initialized_threads.add(tid)
|
|
|
|
|
|
def call_sync(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
"""Call a sync Hindsight client method safely.
|
|
|
|
Runs the call in a dedicated thread pool where each thread has
|
|
its own persistent event loop. This avoids:
|
|
- Nested ``run_until_complete`` when CrewAI's loop is running
|
|
- Cross-thread loop references that cause "Event loop is closed"
|
|
- aiohttp session/loop binding issues
|
|
"""
|
|
|
|
def _run() -> Any:
|
|
_ensure_thread_loop()
|
|
return fn(*args, **kwargs)
|
|
|
|
future = _thread_pool.submit(_run)
|
|
return future.result(timeout=60)
|