Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **`/memory/context` ranking — full-content signal + cleaner graph fade.**
Retrieval is still keyword-mediated (fuzzy + embedding over keyword
entities), but an entity's BODY now contributes a fair, additive signal:
`seed = keyword_score + content_weight × c`, where `c` (0–1) is a
body-only match (index-backed stemmed full-text + per-word
`word_similarity`, with a size damper so long documents must match
better). It can reinforce a keyword hit or surface a keyword-missed one,
never lower a score; `content_weight=0` reproduces the old keyword-only
behavior. This replaces a hard-coded 0.2 "discovery" fallback that
double-penalized and discarded agreement. Retired/redirected wiki stubs
are now excluded from results; the per-query reservation is
importance-aware; and the final list is ordered by true rank. Graph
hops now fade purely by the compounding per-edge `relevance × importance`
product (the redundant 1.0/0.8/0.6 depth layer was removed). No public
response field renamed.

### Fixed

- **Unit tests no longer demand the test stack.** The live-API guard in
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ See [BRAINDB_GUIDE.md](BRAINDB_GUIDE.md) for full API reference with curl exampl
1. **Multi-query search** — pass `queries: ["topic1", "topic2"]` to search multiple angles at once. Each query is matched against keyword entities by both pg_trgm trigram similarity AND query-embedding-vs-keyword-embedding cosine similarity; results are merged with the geometric mean (configurable `missing_signal_penalty` when only one signal fires).
2. **Per-search-term reservation (L1 diversity quota)** — each query you pass gets a guaranteed share of the result slots filled from THAT query's own top-ranked entities. Bare-keyword queries (`"Petros"`) reliably surface specific facts even when paired with broader semantic angles.
3. **Per-keyword reservation (L2 diversity quota)** — each dominant matched keyword gets a halving slot allowance (50% / 25% / 12.5% ..., floor 1). Stops one popular hub keyword (e.g. `user-profile` tagging 100 facts) from monopolising top-N.
4. **Graph traversal** up to 3 hops via relations, relevance fading: `1.0 → 0.6 → 0.3`.
4. **Graph traversal** up to 3 hops via relations. Each hop multiplies by the edge's `relevance_score × importance_score`; that product (< 1) compounds along the path, so distance fades naturally — weak-edge paths die fast, strong-edge paths persist.
5. **Temporal decay** — memories fade over time, strengthen on access.
6. **Final rank** = `combined_score × effective_importance × accumulated_relevance`. The LLM-visible cap stays at the caller's `max_results` (default 30); the scoring pool internally considers up to 500 candidates per query so narrow keywords are never excluded before they're evaluated.
6. **Final rank** = `combined_score × effective_importance × accumulated_relevance`, where `accumulated_relevance` is that compounded per-hop edge product (relevance × importance along the path; 1.0 for a direct hit). The LLM-visible cap stays at the caller's `max_results` (default 30); the scoring pool internally considers up to 500 candidates per query so narrow keywords are never excluded before they're evaluated.
7. **Always-on rules** injected regardless of query.

Single `query` (string) still works for backward compatibility.
Expand Down
21 changes: 19 additions & 2 deletions braindb/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,30 @@ async def recall_memory(
req = ContextRequest(queries=queries, max_results=max_results)
with get_conn() as conn:
result = assemble_context(conn, req)
# EVERY item must survive into the rendered output — the per-query
# quota guarantees membership, and a blind tail-truncate would undo
# that guarantee for low-ranked reserved items (items are sorted by
# final_rank, so reserved picks sit at the tail). So: budget the
# content preview per item and shrink previews, never drop items.
n = max(1, len(result.items))
per_item_overhead = 130 # header + id + keywords lines, roughly
content_budget = max(
150, (MAX_OUTPUT_CHARS - 800) // n - per_item_overhead
)

def _clip(text: str, cap: int) -> str:
s = text or ""
if len(s) <= cap:
return s
return s[:cap] + f"...(+{len(s) - cap} chars; get_entity for full)"

lines = [f"Found {result.total_found} items:"]
for item in result.items:
lines.append(
f"[{item.entity_type}] rank={item.final_rank:.3f} src={item.source or '-'}\n"
f" id: {item.id}\n"
f" content: {item.content}\n"
f" keywords: {', '.join(item.keywords)}"
f" content: {_clip(item.content, content_budget)}\n"
f" keywords: {_clip(', '.join(item.keywords), 120)}"
)
for rule in result.always_on_rules:
lines.append(f"[RULE priority={rule.ext.get('priority')}] {rule.content}")
Expand Down
12 changes: 12 additions & 0 deletions braindb/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ class Settings(BaseSettings):
# Scoring
missing_signal_penalty: float = 0.5 # multiplier when only text OR only embedding matches (0-1)

# Content witness — the `w` in `seed = keyword_score + w * c`, where `c`
# (0-1) measures whether the entity BODY literally talks about the query
# (stemmed full-text + per-word trigram fuzz, both index-backed). The
# term is additive and boost-only: it can reinforce or introduce an
# entity, never lower one. 0.0 disables the signal entirely (pure
# keyword-mediated behavior).
content_weight: float = 0.3
# Size damper for the content witness: body-derived evidence is scaled by
# 1 / (1 + log10(1 + chars/pivot)) — a big document must match BETTER
# than a short fact to earn the same c. Bigger pivot = gentler damping.
content_size_pivot: int = 2000

# Scoring-pool caps. These bound the CANDIDATE pool that feeds ranking
# (pure SQL/vector work — cheap, runs once per query). They are NOT the
# LLM-visible cap; the caller's `max_results` truncates the FINAL sorted
Expand Down
7 changes: 7 additions & 0 deletions braindb/schemas/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class ContextRequest(BaseModel):
min_relevance: float = Field(default=0.05, ge=0.0, le=1.0)
include_always_on_rules: bool = True
min_importance: float = Field(default=0.0, ge=0.0, le=1.0)
# Diagnostic only: when true, the response carries a per-entity score
# breakdown in ContextResponse.explain. Never set by the agent tools or
# the frontend — intended for manual tuning/debugging sessions.
explain: bool = False

@model_validator(mode="after")
def at_least_one_query(self):
Expand Down Expand Up @@ -58,3 +62,6 @@ class ContextResponse(BaseModel):
items: list[SearchResultItem]
always_on_rules: list[SearchResultItem] = []
total_found: int
# Per-entity score constituents, keyed by entity id (str). Populated only
# when ContextRequest.explain=true; null otherwise.
explain: dict[str, Any] | None = None
Loading
Loading