From 8345fb85d28160f895795c8f31a183093109648b Mon Sep 17 00:00:00 2001 From: Alex Kulikov Date: Mon, 20 Apr 2026 16:44:37 +0100 Subject: [PATCH] feat: search thread embedding via cli --- context_use/cli/commands/__init__.py | 2 + context_use/cli/commands/search.py | 65 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 context_use/cli/commands/search.py diff --git a/context_use/cli/commands/__init__.py b/context_use/cli/commands/__init__.py index db4c4c1..2283c90 100644 --- a/context_use/cli/commands/__init__.py +++ b/context_use/cli/commands/__init__.py @@ -10,6 +10,7 @@ from context_use.cli.commands.pipeline import PipelineCommand from context_use.cli.commands.proxy import ProxyCommand from context_use.cli.commands.reset import ResetCommand +from context_use.cli.commands.search import SearchCommand TOP_LEVEL_COMMANDS: list[type[BaseCommand]] = [ ProxyCommand, @@ -17,6 +18,7 @@ IngestCommand, DescribeCommand, EmbedCommand, + SearchCommand, ResetCommand, ] diff --git a/context_use/cli/commands/search.py b/context_use/cli/commands/search.py new file mode 100644 index 0000000..4629320 --- /dev/null +++ b/context_use/cli/commands/search.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import argparse +from typing import TYPE_CHECKING + +from context_use.cli import output as out +from context_use.cli.base import ApiCommand +from context_use.cli.config import Config + +if TYPE_CHECKING: + from context_use import ContextUse + + +class SearchCommand(ApiCommand): + name = "search" + help = "Semantic search over embedded threads" + description = ( + "Search thread embeddings by semantic similarity. " + "Requires that 'embed' has been run first." + ) + llm_mode = "sync" + + def add_arguments(self, parser: argparse.ArgumentParser) -> None: + parser.add_argument("query", help="Semantic search query") + parser.add_argument( + "--top-k", type=int, default=10, help="Number of results (default: 10)" + ) + parser.add_argument( + "--interaction-types", + type=str, + default=None, + help="Comma-separated list of interaction types to filter by", + ) + + async def run( + self, + cfg: Config, + ctx: ContextUse, + args: argparse.Namespace, + ) -> None: + interaction_types: list[str] | None = None + if args.interaction_types: + interaction_types = [t.strip() for t in args.interaction_types.split(",")] + + results = await ctx.search_threads( + query=args.query, + top_k=args.top_k, + interaction_types=interaction_types, + ) + + if not results: + out.warn("No matching threads found.") + return + + out.header(f"Search results ({len(results)})") + print() + for i, r in enumerate(results, 1): + sim = out.dim(f"similarity={r.similarity:.4f}") + ts = r.asat.strftime("%Y-%m-%d %H:%M") + print(f" {i}. [{r.interaction_type}] {ts} {sim}") + preview = r.content[:200].replace("\n", " ") + if len(r.content) > 200: + preview += "…" + print(f" {preview}") + print()