Skip to main content
khora.integrations.llamaindex wires khora behind three LlamaIndex surfaces in one extra:
  • KhoraRetriever: BaseRetriever for any QueryEngine / agent that takes a retriever. Async-only, see “Sync is not implemented” below.
  • KhoraMemoryBlock: BaseMemoryBlock[str] factory for long-term semantic memory inside llama_index.core.memory.Memory.
  • KhoraChatStore: deprecated legacy BaseChatStore for ChatMemoryBuffer users. New code should use KhoraMemoryBlock.

Install

This pulls llama-index-core>=0.14,<0.15. The pin is intentionally narrow because LlamaIndex has shipped breaking changes on minor bumps before (BaseMemoryBlock reshape across 0.11 → 0.12 → 0.14, BaseMemory.putaput). Plan one maintenance PR per LlamaIndex minor release. The nightly skew job in CI catches breaks against the latest tagged minor. The adapter is also registered under the khora.integrations entry-point group (factory: KhoraRetriever), so khora.integrations.discover() returns it without explicit registration.

Quickstart

example.py
The block above is enforced byte-identical against examples/integrations/llamaindex/example.py by tools/check_examples_drift.py (CI gate).

KhoraRetriever

Each returned NodeWithScore’s node.metadata carries:

Sync is not implemented

KhoraRetriever._retrieve raises NotImplementedError. The reason is specific to this adapter: khora’s recall is async-native and the deadlock surface for bridging it through a thread inside a running event loop dominates the failure modes for this kind of plumbing. The fix is straightforward. Every LlamaIndex QueryEngine exposes aquery(...) / aretrieve(...). Use those. If you genuinely need a sync path (e.g. a notebook outside any event loop), wrap the call yourself:
We deliberately do not ship a nest_asyncio workaround. That’s a hidden reentrancy bomb under any real agent loop.

KhoraMemoryBlock

Long-term memory block for llama_index.core.memory.Memory. The factory returns a BaseMemoryBlock[str] instance:
Semantics:
  • _aget(messages) picks the last user-role message, calls Khora.recall(query, namespace=…, limit=similarity_top_k), and returns the rendered context wrapped in <khora_memory>…</khora_memory> so the prompt template can spot it.
  • _aput(messages) calls Khora.remember(content, namespace=…) once per message (skipping empty ones). The returned document_id is stamped onto message.additional_kwargs["khora_event_id"] so callers can round-trip a delete handle.
  • atruncate(content, tokens_to_truncate) returns None - khora is the persistent store, so dropping the in-flight payload loses nothing.

KhoraChatStore (deprecated)

Legacy BaseChatStore for ChatMemoryBuffer. Instantiation emits a DeprecationWarning. Provided only for compatibility with existing code. New agents should use KhoraMemoryBlock instead.
All seven BaseChatStore abstract sync methods are implemented and bridged through khora.integrations._sync.run_sync (which runs the coroutine on a dedicated daemon-thread loop and blocks the caller; see “Sync is not implemented” above for why the async methods are preferred on an event loop). get_keys() and the per-key list-by-index operations scan documents in the bound namespace and filter on metadata client-side (llamaindex_chat_key, llamaindex_chat_index). This is fine for bounded chat workloads (one key per conversation, dozens of messages each). Multi-tenant deployments with many active conversations in one namespace should partition by namespace_id instead.

Limits and future work

  • Filter pushdown to SQL for KhoraChatStore.get_messages: the current O(N_docs) scan is acceptable for bounded chat workloads but not for hot multi-tenant deployments.
  • No support for image / audio / tool-call blocks inside ChatMessage. Only the rendered text is persisted (via ChatMessage.content). The original additional_kwargs round-trips so the consumer can reconstruct non-text payloads from its own side channel.
  • KhoraRetriever returns chunks and (optionally) entities. It does not return relationships. LlamaIndex has no first-class relationship node type and forcing them into TextNode would pollute the response synthesizer. Use Khora.recall(...) directly if you need relationship data.