Skip to content

feat: FactStore-first memory_search for ticker-scoped queries (closes #159)#169

Open
luceinaltis wants to merge 2 commits into
mainfrom
feat/159-memory-search-factstore
Open

feat: FactStore-first memory_search for ticker-scoped queries (closes #159)#169
luceinaltis wants to merge 2 commits into
mainfrom
feat/159-memory-search-factstore

Conversation

@luceinaltis
Copy link
Copy Markdown
Owner

Summary

Closes #159.

pipeline.memory_search() previously passed the raw user query directly to BM25/vector search — it had no awareness of Intent.tickers or the FactStore. Ticker-scoped questions like "how's my AAPL thesis going?" relied on keyword overlap with past Markdown summaries instead of a direct lookup against the structured thesis table.

This PR turns memory_search into the two-stage retrieval described in the issue:

  1. Structured first. When a FactStore is configured and the intent carries tickers, run get_open_theses(tickers) (and get_findings(ticker) when the store exposes it — gated on hasattr so we're forward-compatible with feat: extract and persist Findings from ToolResults (FactStore Phase 2) #157 / PR feat: extract and persist Findings from ToolResults (FactStore Phase 2) #166). If any structured records come back, return them directly as the sole result — no free-text search is performed.
  2. Free-text fallback. Otherwise fall back to the existing MemorySearcher.search(query) path. If there's no searcher either, return an empty results list — same graceful-degradation behavior as before.

What changed

  • qracer/tools/pipeline.py

    • memory_search(query, searcher=None, *, fact_store=None, tickers=None, **kwargs) — new kwargs; old keyword-only callers are still compatible.
    • New _structured_lookup(fact_store, tickers) helper builds the theses/findings payload and swallows store-side exceptions so a broken FactStore always falls back to free-text search.
    • Result shape now carries a "source" field ("fact_store", "memory_searcher", or "none") so analysts / downstream formatters can tell which path won.
  • qracer/conversation/dispatcher.py

    • invoke_tool / invoke_tools accept an optional fact_store kwarg and forward it into pipeline.memory_search alongside intent.tickers.
  • qracer/conversation/handlers.py

    • QuickPathHandler, StandardHandler: pass their existing self._fact_store into invoke_tools.
    • ComparisonHandler: gains an optional fact_store constructor arg and threads it through the per-ticker invoke_tools fan-out.
  • qracer/conversation/engine.py

    • Wires fact_store=fact_store into both ComparisonHandler constructions (init + update_registries hot-swap).

Scope mapping (#159)

Scope item Status
pipeline.memory_search() accepts fact_store
Structured lookup first when tickers present _structured_lookup runs before free-text
Return structured results directly when sufficient ✅ short-circuits when theses or findings non-empty
Fall back to MemorySearcher.search(query) otherwise ✅ stage 2
Thread fact_store through dispatcher → pipeline invoke_tool/invoke_tools kwargs
Tests updated ✅ see below

Test plan

  • uv run pytest tests/tools/test_pipeline.py tests/conversation/test_engine.py63 passed (5 new TestMemorySearch cases + updated dispatcher assertion + new test_memory_search_forwards_fact_store_and_tickers).
  • uv run pytest full suite — 784 passed, 14 skipped.
  • uv run ruff check on changed files — clean.
  • uv run pyright on changed files — 0 errors.

New tests cover

  • Structured theses from FactStore short-circuit the free-text search (source=fact_store, theses populated, results empty).
  • Empty structured hit → falls back to MemorySearcher.search() (source=memory_searcher, searcher called exactly once).
  • No tickers → skip FactStore path entirely.
  • FactStore.get_open_theses raises → fall back to free-text instead of bubbling.
  • When the store exposes get_findings, findings are included in the payload.
  • Dispatcher passes fact_store + intent.tickers into pipeline.memory_search.

Manual verification

from qracer.memory.fact_store import FactStore
from qracer.models import TradeThesis
from qracer.tools.pipeline import memory_search

store = FactStore()
store.save_thesis(
    TradeThesis(
        ticker="AAPL", entry_zone=(175, 180), target_price=200, stop_loss=165,
        risk_reward_ratio=2.5, catalyst="AI revenue", catalyst_date="Q2 2026",
        conviction=8, summary="Long AAPL.",
    ),
    session_id="sess_001",
)
result = await memory_search("how's my AAPL thesis?", fact_store=store, tickers=["AAPL"])
# result.data["source"] == "fact_store"
# result.data["theses"][0]["ticker"] == "AAPL"

claude added 2 commits April 15, 2026 00:16
…159)

Cross-session memory recall for ticker queries ("how's my AAPL thesis?")
previously relied on BM25/vector overlap with Markdown summaries. This
PR turns `memory_search` into a two-stage lookup:

1. If a FactStore and `intent.tickers` are available, query open theses
   (and findings, when the FactStore exposes them) first.
2. Fall back to the existing MemorySearcher free-text path otherwise.

The structured path is deterministic, so ticker-scoped questions get
direct recall instead of depending on LLM/keyword overlap.

- `pipeline.memory_search`: new `fact_store` + `tickers` kwargs, two
  stages, structured results short-circuit when non-empty. Findings
  lookup gated on hasattr for forward-compat with #157.
- `dispatcher.invoke_tool[s]`: forward `fact_store` to pipeline, passing
  `intent.tickers` through.
- `QuickPathHandler`, `ComparisonHandler`, `StandardHandler`: thread
  the existing/new `fact_store` into invoke_tools.
- `ConversationEngine`: wire `fact_store` into `ComparisonHandler`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: upgrade memory_search to use FactStore ticker lookup before text search

2 participants