mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix(cli): fix build-context Rich formatter to use real nested ContextResult shape
_display_build_context was reading top-level item.get("title")/item.get("type")
on each results[i], but the real GraphContext.model_dump() shape wraps every item
as a ContextResult with primary_result + related_results nested inside. This
made every related note render as an empty tree node.
Fix:
- Rewrite _display_build_context to iterate context_items, build a primary-result
node from item["primary_result"], then add each item["related_results"] entry as
a child with relation_type/type/title rendered.
- Update _display_search_results to use the real SearchResponse key "current_page"
(not "page"), pass query from the CLI argument, add Score and Snippet columns
(score + matched_chunk/content truncated to 200 chars) as the issue requested.
- Update test fixtures in test_cli_tool_rich_output.py to match the real payload
shapes (nested ContextResult, current_page, score/matched_chunk fields, no
updated_at in recent-activity).
- Fix test_build_context_json_flag_overrides_tty assertion to navigate the nested
primary_result/related_results shape.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
This commit is contained in:
@@ -67,15 +67,23 @@ def _print_json(result: Any) -> None:
|
||||
# --- Rich formatters ---
|
||||
|
||||
|
||||
def _display_search_results(result: dict[str, Any]) -> None:
|
||||
"""Render search-notes results as a Rich table."""
|
||||
def _display_search_results(result: dict[str, Any], query: str = "") -> None:
|
||||
"""Render search-notes results as a Rich table.
|
||||
|
||||
Real SearchResponse.model_dump() shape:
|
||||
results: list of SearchResult dicts (title, type, permalink, score, matched_chunk, content)
|
||||
current_page: int (NOT "page")
|
||||
page_size: int
|
||||
total: int
|
||||
has_more: bool
|
||||
"""
|
||||
results = result.get("results", [])
|
||||
total = result.get("total", len(results))
|
||||
query = result.get("query") or ""
|
||||
page = result.get("page", 1)
|
||||
page_size = result.get("page_size", len(results))
|
||||
# Real key is "current_page"; fall back to "page" for forward-compat.
|
||||
page = result.get("current_page") or result.get("page", 1)
|
||||
page_size = result.get("page_size", len(results)) or 1
|
||||
|
||||
title = f"Search results for [bold cyan]{query}[/bold cyan]" if query else "Search results"
|
||||
title = f"Search: [bold cyan]{query}[/bold cyan]" if query else "Search results"
|
||||
subtitle = f"{total} result(s) • page {page} of {max(1, -(-total // page_size))}"
|
||||
|
||||
if not results:
|
||||
@@ -85,13 +93,21 @@ def _display_search_results(result: dict[str, Any]) -> None:
|
||||
table = Table(show_header=True, header_style="bold", expand=False)
|
||||
table.add_column("Type", style="dim", width=12)
|
||||
table.add_column("Title", style="bold cyan")
|
||||
table.add_column("Score", style="yellow", width=7)
|
||||
table.add_column("Permalink", style="green")
|
||||
table.add_column("Snippet", style="dim", max_width=60)
|
||||
|
||||
for item in results:
|
||||
item_type = item.get("type", "")
|
||||
item_title = item.get("title") or item.get("permalink", "")
|
||||
permalink = item.get("permalink", "")
|
||||
table.add_row(item_type, item_title, permalink)
|
||||
score = item.get("score")
|
||||
score_str = f"{score:.2f}" if score is not None else ""
|
||||
# Prefer matched_chunk as the most relevant snippet; fall back to content.
|
||||
raw_snippet = item.get("matched_chunk") or item.get("content") or ""
|
||||
# Truncate to ~200 chars so the table stays readable.
|
||||
snippet = raw_snippet[:200].replace("\n", " ") if raw_snippet else ""
|
||||
table.add_row(item_type, item_title, score_str, permalink, snippet)
|
||||
|
||||
console.print(Panel(table, title=title, subtitle=subtitle, expand=False))
|
||||
|
||||
@@ -116,33 +132,59 @@ def _display_read_note(result: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def _display_build_context(result: dict[str, Any]) -> None:
|
||||
"""Render build-context result as a Rich tree."""
|
||||
"""Render build-context result as a Rich tree.
|
||||
|
||||
Real GraphContext.model_dump() shape:
|
||||
results: list of ContextResult dicts, each with:
|
||||
primary_result: EntitySummary | RelationSummary | ObservationSummary
|
||||
observations: list of ObservationSummary
|
||||
related_results: list of EntitySummary | RelationSummary | ObservationSummary
|
||||
metadata: {"uri": ..., ...}
|
||||
page/page_size/has_more
|
||||
|
||||
Each summary has: type, title (EntitySummary/RelationSummary), permalink,
|
||||
and relation_type (RelationSummary only).
|
||||
"""
|
||||
metadata = result.get("metadata", {})
|
||||
uri = metadata.get("uri", "")
|
||||
results = result.get("results", [])
|
||||
total = len(results)
|
||||
context_items: list[dict[str, Any]] = list(result.get("results", []))
|
||||
|
||||
label = f"[bold cyan]{uri}[/bold cyan]" if uri else "Context"
|
||||
tree = Tree(f"[bold]Context:[/bold] {label}")
|
||||
|
||||
if not results:
|
||||
if not context_items:
|
||||
tree.add("[dim]No related content found.[/dim]")
|
||||
else:
|
||||
for item in results:
|
||||
item_title = item.get("title") or item.get("permalink", "")
|
||||
relation = item.get("relation_type", "")
|
||||
item_type = item.get("type", "")
|
||||
for context_result in context_items:
|
||||
# --- Primary result node ---
|
||||
primary = context_result.get("primary_result", {})
|
||||
p_title = primary.get("title") or primary.get("permalink", "")
|
||||
p_type = primary.get("type", "")
|
||||
primary_label = f"[cyan]{p_title}[/cyan]"
|
||||
if p_type:
|
||||
primary_label = f"[dim]{p_type}[/dim] {primary_label}"
|
||||
primary_node = tree.add(primary_label)
|
||||
|
||||
parts = []
|
||||
if relation:
|
||||
parts.append(f"[yellow]{relation}[/yellow]")
|
||||
if item_type:
|
||||
parts.append(f"[dim]{item_type}[/dim]")
|
||||
parts.append(f"[cyan]{item_title}[/cyan]")
|
||||
# --- Related items as children ---
|
||||
related: list[dict[str, Any]] = list(context_result.get("related_results", []))
|
||||
for rel_item in related:
|
||||
rel_title = rel_item.get("title") or rel_item.get("permalink", "")
|
||||
rel_type = rel_item.get("type", "")
|
||||
relation = rel_item.get("relation_type", "")
|
||||
|
||||
tree.add(" ".join(parts))
|
||||
parts = []
|
||||
if relation:
|
||||
parts.append(f"[yellow]{relation}[/yellow]")
|
||||
if rel_type:
|
||||
parts.append(f"[dim]{rel_type}[/dim]")
|
||||
parts.append(f"[cyan]{rel_title}[/cyan]")
|
||||
primary_node.add(" ".join(parts))
|
||||
|
||||
subtitle = f"{total} related item(s)"
|
||||
# Count total related items across all primary results.
|
||||
total_related = sum(
|
||||
len(cr.get("related_results", [])) for cr in context_items
|
||||
)
|
||||
subtitle = f"{len(context_items)} primary • {total_related} related"
|
||||
console.print(Panel(tree, subtitle=subtitle, expand=False))
|
||||
|
||||
|
||||
@@ -845,7 +887,7 @@ def search_notes(
|
||||
if json_output or not _use_rich():
|
||||
_print_json(result)
|
||||
else:
|
||||
_display_search_results(result)
|
||||
_display_search_results(result, query=query or "")
|
||||
except ValueError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user