Connect Feather DB's living context engine to any LLM — Claude, Gemini, OpenAI, Groq, Mistral, or any MCP-compatible agent — in under 10 lines of code.
Version: v0.6.0 · GitHub · PyPI
- How it works
- Installation
- Claude (Anthropic)
- OpenAI and compatible APIs
- Gemini (Google)
- MCP Server — Claude Desktop & Cursor
- LangChain
- LlamaIndex
- Tool Reference — all 14 tools
- Choosing an embedder
- Building a knowledge graph to query
- Production patterns
Every connector follows the same pattern:
Your question
│
▼
LLM (Claude / Gemini / GPT-4o / Llama …)
│ decides it needs context → calls a Feather tool
▼
Feather DB Connector
│ translates the tool call → db.search() / context_chain() / …
│ returns structured JSON back to the LLM
▼
Feather DB (your .feather file on disk)
│ HNSW vector search + typed graph + adaptive decay
▼
LLM writes its final answer, grounded in retrieved context
The LLM decides which tools to call and in what order. The connector handles the translation and all multi-turn rounds. You call one method — run_loop() — and get back the final answer.
Round 1 → LLM calls feather_search("FD CTR drop")
← Feather returns top-4 nodes as JSON
Round 2 → LLM calls feather_context_chain("budget day competitor", hops=2)
← Feather returns 8 nodes + 7 causal edges
Round 3 → LLM writes final answer (no more tool calls)
← run_loop() returns the text
Each round is handled automatically. run_loop() exits when the LLM stops calling tools.
# Core (always needed)
pip install feather-db numpy
# Per provider — install only what you use
pip install anthropic # Claude
pip install openai # OpenAI / Azure / Groq / Mistral / Together
pip install google-genai # Gemini
pip install mcp # MCP server (Claude Desktop / Cursor)
pip install langchain langchain-core # LangChain
pip install llama-index llama-index-core # LlamaIndex
# Or install all optional deps at once
pip install "feather-db[all]"import anthropic
from feather_db.integrations import ClaudeConnector
conn = ClaudeConnector(
db_path = "my.feather",
dim = 3072,
embedder = my_embed_fn, # text → np.ndarray
)
client = anthropic.Anthropic(api_key="sk-ant-...")
result = conn.run_loop(
client,
messages = [{"role": "user", "content": "Why is our FD CTR dropping?"}],
model = "claude-opus-4-6",
)
print(result)result = conn.run_loop(
client,
messages = [{"role": "user", "content": "Why is our FD CTR dropping?"}],
model = "claude-opus-4-6",
max_tokens = 4096,
max_rounds = 8, # max tool-call rounds before giving up
system = (
"You are a performance marketing analyst with access to a Feather DB "
"knowledge graph containing campaign intel, competitor signals, and "
"social trends. Always retrieve context before answering. Cite node IDs."
),
verbose = True, # prints tool calls and results as they happen
)If you want to manage the loop yourself:
import anthropic
response = client.messages.create(
model = "claude-opus-4-6",
max_tokens = 4096,
tools = conn.tools(), # list of tool definitions
messages = [{"role": "user", "content": "Why is our FD CTR dropping?"}],
)
done, tool_messages = conn.process_response(response)
# done=True → response.content has the final text
# done=False → append tool_messages to messages and call againFeather tools use Anthropic's input_schema format:
[
{
"name": "feather_search",
"description": "Semantic vector search over the Feather DB knowledge graph...",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural language search query"},
"k": {"type": "integer", "description": "Number of results (default 5)"},
...
},
"required": ["query"]
}
},
... # 14 tools total
]OpenAIConnector works with any OpenAI-spec API: OpenAI, Azure OpenAI, Groq, Mistral, Together AI, Ollama, and more. Change only the client — the connector is identical.
from openai import OpenAI
from feather_db.integrations import OpenAIConnector
conn = OpenAIConnector(db_path="my.feather", dim=3072, embedder=my_embed_fn)
client = OpenAI(api_key="sk-...")
result = conn.run_loop(
client,
messages = [{"role": "user", "content": "Why is our FD CTR dropping?"}],
model = "gpt-4o",
)from openai import OpenAI
client = OpenAI(
api_key = "gsk_...",
base_url = "https://api.groq.com/openai/v1",
)
result = conn.run_loop(
client,
messages = [{"role": "user", "content": "Why is our FD CTR dropping?"}],
model = "llama-3.3-70b-versatile",
)client = OpenAI(
api_key = "...",
base_url = "https://api.mistral.ai/v1",
)
result = conn.run_loop(client, messages, model="mistral-large-latest")client = OpenAI(
api_key = "ollama", # any string
base_url = "http://localhost:11434/v1",
)
result = conn.run_loop(client, messages, model="llama3.2")result = conn.run_loop(
client,
messages = [{"role": "user", "content": "Analyse FD campaign performance"}],
model = "gpt-4o",
system = "You are a marketing analyst. Use Feather tools before answering.",
max_rounds = 8,
max_tokens = 4096,
verbose = True,
)[
{
"type": "function",
"function": {
"name": "feather_search",
"description": "Semantic vector search over the Feather DB knowledge graph...",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural language search query"},
"k": {"type": "integer", "description": "Number of results (default 5)"},
...
},
"required": ["query"]
}
}
},
... # 14 tools total
]Use Gemini's own 3072-dim multimodal embedder for both ingestion and queries. Text, image descriptions, and video transcripts all land in the same vector space.
from google import genai
from feather_db.integrations import GeminiConnector, GeminiEmbedder
# 1. Create the embedder
emb = GeminiEmbedder(api_key="AIza...")
# 2. Create the connector — use the same embedder for queries
conn = GeminiConnector(
db_path = "my.feather",
dim = emb.dim, # 3072 for Gemini Embedding 2
embedder = emb.embed_text,
)
# 3. Create a Gemini client and chat session
client = genai.Client(api_key="AIza...")
chat = client.chats.create(
model = "gemini-2.0-flash",
config = conn.chat_config(), # attaches all 14 tools
)
# 4. Run
result = conn.run_loop(chat, "Which competitor moves should I worry about?")
print(result)from feather_db.integrations import GeminiConnector
conn = GeminiConnector(
db_path = "my.feather",
dim = 768, # match your embedder's output dimension
embedder = my_embed_fn,
)chat = client.chats.create(
model = "gemini-2.0-flash",
config = conn.chat_config(
system = (
"You are a performance marketing analyst. "
"Use Feather DB tools to retrieve context before answering."
)
),
)
result = conn.run_loop(chat, "Analyse FD campaign performance", max_rounds=8)emb = GeminiEmbedder(api_key="AIza...")
# Embed different modalities — all land in the same 3072-dim space
text_vec = emb.embed_text("FD 8.5% interest rate campaign")
image_vec = emb.embed_image(image_path="ad_creative.jpg")
video_vec = emb.embed_video_transcript("0:00 Are your savings safe? 0:12 Guaranteed 8.5%")
# Cross-modal: search text, find image creatives
results = db.search(text_vec, k=5) # returns text AND image nodes
# Offline / mock mode (no API key needed for testing)
emb_mock = GeminiEmbedder(mock=True)[
types.Tool(function_declarations=[
types.FunctionDeclaration(
name = "feather_search",
description = "Semantic vector search over the Feather DB knowledge graph...",
parameters = types.Schema(
type = "OBJECT",
properties = {
"query": types.Schema(type="STRING", description="Natural language search query"),
"k": types.Schema(type="INTEGER", description="Number of results"),
...
},
required = ["query"]
)
),
... # 14 tools total
])
]The MCP server exposes all 14 Feather tools as first-class MCP tool definitions. Any MCP-compatible agent — Claude Desktop, Cursor, or a custom agent — can connect without writing any Python code.
pip install mcp feather-dbfeather-serve --db my.feather --dim 3072Options:
| Flag | Default | Description |
|---|---|---|
--db |
required | Path to .feather file (created if absent) |
--dim |
3072 |
Vector dimension |
--name |
feather-db |
Server name shown in agent UIs |
--embedder |
built-in mock | Path to a Python file exporting embed(text) -> list[float] |
Add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"feather": {
"command": "feather-serve",
"args": ["--db", "/path/to/my.feather", "--dim", "3072"]
}
}
}Restart Claude Desktop. All 14 Feather tools appear in the tool picker immediately.
Add to .cursor/mcp.json in your project root:
{
"mcpServers": {
"feather": {
"command": "feather-serve",
"args": ["--db", "/path/to/my.feather", "--dim", "3072"]
}
}
}Create my_embedder.py:
# Must export a function named `embed`
from sentence_transformers import SentenceTransformer
_model = SentenceTransformer("all-MiniLM-L6-v2")
def embed(text: str) -> list[float]:
return _model.encode(text).tolist()Run with:
feather-serve --db my.feather --dim 384 --embedder my_embedder.pyfrom feather_db.integrations.mcp_server import create_server
import asyncio
from mcp.server.stdio import stdio_server
server = create_server(db_path="my.feather", dim=3072)
asyncio.run(stdio_server(server))Drop-in VectorStore for use anywhere LangChain expects a vector store.
from feather_db.integrations import FeatherVectorStore
store = FeatherVectorStore(
db_path = "my.feather",
dim = 3072,
embed_fn = my_embed_fn,
namespace = "langchain",
)
# Add documents
store.add_texts(
texts = ["FD rate 8.5%", "Competitor launched 8.75%"],
metadatas = [{"product": "FD"}, {"source": "competitor"}],
)
# Search
docs = store.similarity_search("FD interest rate", k=5)
for doc in docs:
print(doc.page_content, doc.metadata["score"])
# Search with scores
results = store.similarity_search_with_score("FD interest rate", k=5)
# Create from existing texts
store = FeatherVectorStore.from_texts(
texts = ["FD rate 8.5%", "Competitor launched 8.75%"],
embedding = my_embedding_model,
db_path = "my.feather",
)
# Create from LangChain Documents
store = FeatherVectorStore.from_documents(documents, embedding=model)Conversation memory that retrieves semantically relevant history — not just last-N turns — with adaptive decay so recent and frequently accessed turns surface first.
from feather_db.integrations import FeatherMemory
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
memory = FeatherMemory(
db_path = "my.feather",
dim = 3072,
embed_fn = my_embed_fn,
k = 5, # retrieve top-5 relevant past turns
namespace = "memory",
memory_key = "history",
)
chain = ConversationChain(
llm = ChatOpenAI(model="gpt-4o"),
memory = memory,
)
response = chain.predict(input="Why is FD CTR dropping?")Wraps context_chain() as a LangChain BaseRetriever. Returns graph-expanded nodes, not just top-k similarity — traces causal connections automatically.
from feather_db.integrations import FeatherRetriever
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
retriever = FeatherRetriever(
db_path = "my.feather",
dim = 3072,
embed_fn = my_embed_fn,
k = 5,
hops = 2, # expand 2 graph hops from seed nodes
modality = "text",
)
qa_chain = RetrievalQA.from_chain_type(
llm = ChatOpenAI(model="gpt-4o"),
retriever = retriever,
)
answer = qa_chain.run("What caused the FD CTR spike on Budget Day?")Drop-in VectorStore for LlamaIndex.
from feather_db.integrations import FeatherVectorStoreIndex
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core.vector_stores.types import VectorStoreQuery
store = FeatherVectorStoreIndex(
db_path = "my.feather",
dim = 3072,
embed_fn = my_embed_fn,
namespace = "llamaindex",
)
# Add LlamaIndex nodes
store.add(nodes)
# Query directly
query = VectorStoreQuery(query_embedding=vec, similarity_top_k=5)
result = store.query(query)
for node_with_score in result.nodes:
print(node_with_score.node.text, node_with_score.score)
# Use as a LlamaIndex storage context
storage_context = StorageContext.from_defaults(vector_store=store)
index = VectorStoreIndex(nodes, storage_context=storage_context)
query_engine = index.as_query_engine()
response = query_engine.query("Why did FD CTR spike?")Load an existing .feather file as LlamaIndex Documents.
from feather_db.integrations import FeatherReader
from llama_index.core import VectorStoreIndex
reader = FeatherReader()
docs = reader.load_data(
db_path = "my.feather",
dim = 3072,
namespace_filter = "marketing", # optional
min_importance = 0.5, # optional
)
# Build a LlamaIndex index from existing Feather data
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()
response = query_engine.query("What competitor threats exist for FD?")All 14 tools are available across every connector — Claude, OpenAI, Gemini, and MCP.
Semantic vector search. Converts the query to an embedding and returns the k most similar nodes.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Natural language search query |
k |
integer | no | Results to return (default 5) |
namespace |
string | no | Filter by namespace |
entity |
string | no | Filter by entity type (e.g. competitor_intel) |
product |
string | no | Filter by product attribute (e.g. FD, CC) |
Same as feather_search but post-processes results with Maximal Marginal Relevance to balance relevance and diversity. Use when you need a broad view rather than the single most relevant cluster.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Natural language search query |
k |
integer | no | Results (default 5) |
diversity |
number | no | 0.0 = pure similarity, 1.0 = max diversity (default 0.5) |
Two-phase retrieval: vector search for k seeds (hop=0) then BFS graph expansion for hops levels. Returns all reached nodes with hop distance. Use to trace root causes.
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Seed query |
k |
integer | no | Vector-search seeds (default 3) |
hops |
integer | no | Graph expansion hops (default 2) |
Fetch full metadata for a node by ID — content, attributes, edges in and out.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | Node ID |
Get direct graph neighbours — nodes connected by typed edges.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | Source node ID |
rel_type |
string | no | Filter by edge type (e.g. caused_by) |
direction |
string | no | outgoing (default), incoming, or both |
Score breakdown explaining why a node would be retrieved for a query. Returns similarity, stickiness (recall bonus), recency, importance, confidence, and the final formula.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | Node ID to explain |
query |
string | yes | Query text to score against |
Ingest a new intelligence node. Use when the agent discovers information worth persisting.
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Full text of the intelligence note |
entity_type |
string | yes | Type label (e.g. competitor_intel, strategy_intel) |
product |
string | no | Product this intel relates to |
importance |
number | no | Relevance weight 0–1 (default 0.8) |
Create a typed weighted edge between two existing nodes.
| Parameter | Type | Required | Description |
|---|---|---|---|
from_id |
integer | yes | Source node ID |
to_id |
integer | yes | Target node ID |
rel_type |
string | yes | caused_by, supports, contradicts, derived_from, references, same_ad, related_to |
weight |
number | no | Edge weight 0–1 (default 0.8) |
Soft-delete a node — removes it from search results but preserves graph edges.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
integer | yes | Node ID to forget |
Chronological node list (newest first) for a product or entity type.
| Parameter | Type | Required | Description |
|---|---|---|---|
product |
string | no | Filter by product |
entity_type |
string | no | Filter by entity type |
limit |
integer | no | Max nodes (default 10) |
Knowledge graph health report — tier distribution, orphan nodes, recall histogram, avg importance/confidence.
| Parameter | Type | Required | Description |
|---|---|---|---|
modality |
string | no | Modality to inspect (default text) |
Cluster similar nodes in a namespace and merge each cluster into a summary node. Reduces noise, improves retrieval quality over time.
| Parameter | Type | Required | Description |
|---|---|---|---|
namespace |
string | yes | Namespace to consolidate |
since_hours |
number | no | Only consolidate recent nodes (default 24) |
threshold |
number | no | Cosine similarity to cluster (default 0.85) |
Retrieve all nodes in a named episode, ordered by timestamp.
| Parameter | Type | Required | Description |
|---|---|---|---|
episode_id |
string | yes | Episode identifier |
Scan and soft-delete all nodes that have exceeded their TTL (time-to-live). Call periodically to keep the graph clean.
No parameters required.
The embedder converts text queries (and ingested content) into vectors. Use the same embedder for both ingestion and querying.
from feather_db.integrations import GeminiEmbedder
emb = GeminiEmbedder(api_key="AIza...")
# dim=3072, multimodal (text + image + video in same space)
embed_fn = emb.embed_textfrom openai import OpenAI
import numpy as np
client = OpenAI()
def embed_fn(text: str) -> np.ndarray:
resp = client.embeddings.create(model="text-embedding-3-large", input=text)
return np.array(resp.data[0].embedding, dtype=np.float32)
# dim=3072 for text-embedding-3-largefrom sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2") # dim=384
def embed_fn(text: str) -> np.ndarray:
return model.encode(text, normalize_embeddings=True).astype(np.float32)All connectors include a built-in deterministic mock embedder — no API key or model required. Leave embedder=None to use it:
conn = ClaudeConnector(db_path="my.feather", dim=3072) # uses mock embedderBefore querying, you need nodes in the DB. Here is the typical ingestion pattern:
import feather_db, numpy as np, time
db = feather_db.DB.open("my.feather", dim=3072)
def add_node(id, content, entity_type, product=None, importance=0.9):
vec = embed_fn(content)
meta = feather_db.Metadata()
meta.timestamp = int(time.time())
meta.importance = importance
meta.confidence = 0.9
meta.type = feather_db.ContextType.FACT
meta.source = "pipeline-v1"
meta.content = content
meta.namespace_id = "my_brand"
meta.set_attribute("entity_type", entity_type)
if product:
meta.set_attribute("product", product)
db.add(id=id, vec=vec, meta=meta)
# Add intelligence nodes
add_node(1001, "FD video ad — CTR 3.2%, ROAS 4.1. Senior couple hook.", "ad_creative", "FD")
add_node(9001, "Competitor launched 8.75% APY FD on Budget Day.", "competitor_intel","FD")
add_node(9101, "Budget Day: RBI held repo rate. FD appeal increased.", "market_signal", "FD")
# Add causal edges
db.link(9001, 1001, "contradicts", 0.9) # competitor undercuts our rate
db.link(9101, 1001, "supports", 0.85) # budget day supports FD demand
# Working memory with TTL (auto-expires in 1 hour)
meta_temp = feather_db.Metadata()
meta_temp.content = "Live social trend: #BudgetDay2026 trending"
meta_temp.ttl = 3600 # expires in 1 hour
meta_temp.namespace_id = "live_feed"
db.add(id=8001, vec=embed_fn(meta_temp.content), meta=meta_temp)
db.save()# Create once at startup, reuse across requests
conn = ClaudeConnector(db_path="production.feather", dim=3072, embedder=embed_fn)
# Each request gets a fresh message list but the same connector
def handle_request(user_question: str) -> str:
return conn.run_loop(
client,
messages = [{"role": "user", "content": user_question}],
model = "claude-opus-4-6",
)# Each brand / customer gets their own namespace
def build_connector(brand_id: str) -> ClaudeConnector:
return ClaudeConnector(
db_path = f"{brand_id}.feather",
dim = 3072,
embedder = embed_fn,
)from feather_db import WatchManager, ContradictionDetector
wm = WatchManager()
cd = ContradictionDetector()
# Alert when a new node matches a critical pattern
wm.watch(
db,
query_text = "competitor launched new FD product",
threshold = 0.75,
callback = lambda nid, sim: alert_team(f"⚡ New competitor intel: node {nid}"),
embed_fn = embed_fn,
)
def ingest(content: str, entity_type: str):
# Add to DB
nid = next_id()
db.add(nid, embed_fn(content), make_meta(content, entity_type))
# Check triggers
wm.check_triggers(db, nid, embed_fn=embed_fn)
# Check for contradictions
cd.check(db, nid, auto_link=True)import schedule, time
from feather_db import MemoryManager
def maintain():
db.forget_expired() # remove TTL nodes
MemoryManager.consolidate(db, namespace="live_feed") # merge similar nodes
MemoryManager.assign_tiers(db) # classify hot/warm/cold
schedule.every(6).hours.do(maintain)
while True:
schedule.run_pending()
time.sleep(60)from feather_db import MemoryManager
# After an agent response, explain why node 1001 was retrieved
breakdown = MemoryManager.why_retrieved(db, node_id=1001, query_vec=query_vec)
print(f"Retrieved because: {breakdown['formula']}")
# → Retrieved because: ((1-0.3) × 0.81 + 0.3 × 0.94) × 0.95 = 0.84from feather_db.integrations import (
ClaudeConnector, # Anthropic tool_use format
OpenAIConnector, # OpenAI function-calling format (+ Groq, Mistral, etc.)
GeminiConnector, # Google FunctionDeclaration format
GeminiEmbedder, # Gemini Embedding 2 (3072-dim, multimodal)
FeatherVectorStore, # LangChain VectorStore
FeatherMemory, # LangChain BaseMemory
FeatherRetriever, # LangChain BaseRetriever
FeatherVectorStoreIndex, # LlamaIndex VectorStore
FeatherReader, # LlamaIndex BaseReader
)
# MCP server
# feather-serve --db my.feather --dim 3072