feat(backend): search finds word prefixes, earlier versions and deleted messages, with filters and cursor paging
This commit is contained in:
1 parent
42b4e1d7e6
commit
d45c96c259
5 files changed
+377
-88
No files matched your search
@@ -38,7 +38,7 @@ from utils.read.feed import CURSOR_PATTERN, FeedChatKind, FeedFilter, FeedKind
|
||||
from utils.read.models import DEFAULT_LIMIT, Page
|
||||
from utils.read.period import DEFAULT_TZ, MAX_DAYS, known_timezone
|
||||
from utils.read.read_receipts import read_info
|
||||
from utils.search.models import SearchFilters
|
||||
from utils.search.models import ChatKind, SearchFilters
|
||||
from utils.search.repository import search_messages
|
||||
from utils.storage import ContentAddressedStorage
|
||||
from utils.watch_rules import WatchKind, WatchParamsError, clean_name, normalize_params
|
||||
@@ -72,8 +72,9 @@ Typical flows:
|
||||
"📝 [your private note, NOT in Telegram]" are the user's own annotations
|
||||
attached locally via the web UI; they never existed in Telegram. Treat them
|
||||
as private notes from the user to you, not as chat content.
|
||||
- Find something: `search_messages_tool` (full-text over message text and STT
|
||||
transcripts; supports chat/sender/date filters and regex).
|
||||
- Find something: `search_messages_tool` (words by prefix over message text,
|
||||
STT transcripts and earlier versions of edited messages; filters by chat,
|
||||
sender, chat kind, media, deleted, edited, dates, regex).
|
||||
- People: `get_person` gives one person in full: profile changes, shared groups,
|
||||
joins and leaves, deleted message count, the private chat.
|
||||
- Forensics: `get_feed` lists what was deleted or edited in Telegram across
|
||||
@@ -99,7 +100,8 @@ Watched bots:
|
||||
Writes: everything is read-only except `set_watch` and the share links
|
||||
`download_media` creates (visible and revocable in the web UI).
|
||||
|
||||
Paging: list/search tools accept `limit` and `offset`.
|
||||
Paging: list tools accept `limit` and `offset`; `search_messages_tool` returns
|
||||
`next_cursor` to pass back as `cursor`.
|
||||
"""
|
||||
|
||||
mcp: FastMCP = FastMCP("beavergram", instructions=INSTRUCTIONS)
|
||||
@@ -138,27 +140,49 @@ async def search_messages_tool(
|
||||
query: str | None = None,
|
||||
chat_id: int | None = None,
|
||||
sender_id: int | None = None,
|
||||
kind: ChatKind | None = None,
|
||||
has_media: bool | None = None,
|
||||
deleted: bool | None = None,
|
||||
edited: bool | None = None,
|
||||
date_from: datetime | None = None,
|
||||
date_to: datetime | None = None,
|
||||
regex: str | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
offset: int = 0,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Full-text search over message text and STT transcripts."""
|
||||
) -> dict[str, Any]:
|
||||
"""Search messages by words, newest first, including deleted ones.
|
||||
|
||||
Words match by prefix (3+ letters) and by stem, Russian and English.
|
||||
`"exact phrase"`, `-word` and `or` work as in web search. Besides the
|
||||
current text it searches voice transcripts and earlier versions of edited
|
||||
messages: a hit with `source="version"` matched only an old text, given in
|
||||
`version_text`. Every hit carries `deleted_at` and `edited_at`.
|
||||
|
||||
Filters: `chat_id`, `sender_id` (peer id), `kind` (private, group,
|
||||
channel), `has_media`, `deleted` / `edited` (true for only those, false to
|
||||
exclude them), `date_from` / `date_to`, `regex` over the current text. With
|
||||
no `query` it lists messages that pass the filters.
|
||||
|
||||
Paging: pass `next_cursor` from the previous result as `cursor`; it is null
|
||||
on the last page.
|
||||
"""
|
||||
filters = SearchFilters(
|
||||
account_id=account_id,
|
||||
query=query,
|
||||
chat_id=chat_id,
|
||||
sender_id=sender_id,
|
||||
kind=kind,
|
||||
has_media=has_media,
|
||||
deleted=deleted,
|
||||
edited=edited,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
regex=regex,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return _dump(await search_messages(await _pool(), filters))
|
||||
page = await search_messages(await _pool(), filters)
|
||||
return page.model_dump(mode="json", exclude={"terms"})
|
||||
|
||||
|
||||
@mcp.tool
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncpg
|
||||
from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from utils.search.models import SearchFilters, SearchHit
|
||||
from utils.search.models import SearchFilters, SearchPage
|
||||
from utils.search.repository import search_messages
|
||||
|
||||
router = APIRouter(prefix="/api/search", tags=["search"], route_class=DishkaRoute)
|
||||
@@ -13,5 +13,5 @@ router = APIRouter(prefix="/api/search", tags=["search"], route_class=DishkaRout
|
||||
@router.get("")
|
||||
async def search(
|
||||
pool: FromDishka[asyncpg.Pool], filters: Annotated[SearchFilters, Query()]
|
||||
) -> list[SearchHit]:
|
||||
) -> SearchPage:
|
||||
return await search_messages(pool, filters)
|
||||
Reference in new issue
Block a user