diff --git a/src/tool_implementations.py b/src/tool_implementations.py index 092747452..e45fde1f9 100644 --- a/src/tool_implementations.py +++ b/src/tool_implementations.py @@ -46,6 +46,9 @@ from src.tools.cookbook import ( # noqa: F401 _scan_running_model_processes, _cookbook_kill_session, _MODEL_PROCESS_PATTERNS, ) +# Search domain extracted to src/tools/search.py (slice 1, #4082/#4071). +# Re-imported here so this module stays a working facade. +from src.tools.search import do_search_chats # noqa: F401 logger = logging.getLogger(__name__) @@ -119,50 +122,6 @@ def _parse_tool_args(content): args = args["body"] return args -# --------------------------------------------------------------------------- -# Search chats -# --------------------------------------------------------------------------- - -async def do_search_chats(query: str, limit: int = 20, owner: str | None = None) -> Dict: - """Search past session transcripts for the calling user's sessions only. - - Without an owner filter this used to leak EVERY user's chat history - into the agent's `search_chats` results (v2 review HIGH-11). The - caller in `tool_execution.execute_tool_block` now plumbs the owner - through; legacy callers without owner pass through as before but - will only see legacy/null-owner rows. - """ - try: - from src.session_search import search_session_messages - - results = search_session_messages(query, limit=limit, owner=owner) - if not results: - return {"results": f"No chats found matching \"{query}\"."} - - # Group by session to avoid duplicate links - seen_sessions = {} - for result in results: - if result.session_id not in seen_sessions: - seen_sessions[result.session_id] = result - - lines = [f"Found {len(seen_sessions)} session(s) matching \"{query}\":\n"] - for sid, result in seen_sessions.items(): - lines.append(f"- **{result.session_name}** (#{sid})") - lines.append(f" Link: [Open chat](#{sid})") - lines.append(f" Match ({result.role}): {result.content_snippet}") - if result.context_before: - before = result.context_before[-1] - lines.append(f" Before ({before['role']}): {before['content'][:180]}") - if result.context_after: - after = result.context_after[0] - lines.append(f" After ({after['role']}): {after['content'][:180]}") - lines.append("") - - return {"results": "\n".join(lines)} - except Exception as e: - logger.error(f"search_chats failed: {e}") - return {"error": str(e), "exit_code": 1} - # --------------------------------------------------------------------------- # Notes / checklists management tool diff --git a/src/tools/__init__.py b/src/tools/__init__.py index 50c427baf..5f48777f0 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -21,3 +21,4 @@ from src.tools.cookbook import ( # noqa: F401 _scan_running_model_processes, _cookbook_kill_session, _MODEL_PROCESS_PATTERNS, ) +from src.tools.search import do_search_chats # noqa: F401 diff --git a/src/tools/search.py b/src/tools/search.py new file mode 100644 index 000000000..882951333 --- /dev/null +++ b/src/tools/search.py @@ -0,0 +1,51 @@ +"""Search-domain tool implementations. + +Extracted from tool_implementations.py as part of slice 1 (#4082/#4071). +Holds the search_chats tool. +``src.tool_implementations`` re-exports these for backward compatibility. +""" +import logging +from typing import Dict + +logger = logging.getLogger(__name__) + + +async def do_search_chats(query: str, limit: int = 20, owner: str | None = None) -> Dict: + """Search past session transcripts for the calling user's sessions only. + + Without an owner filter this used to leak EVERY user's chat history + into the agent's `search_chats` results (v2 review HIGH-11). The + caller in `tool_execution.execute_tool_block` now plumbs the owner + through; legacy callers without owner pass through as before but + will only see legacy/null-owner rows. + """ + try: + from src.session_search import search_session_messages + + results = search_session_messages(query, limit=limit, owner=owner) + if not results: + return {"results": f"No chats found matching \"{query}\"."} + + # Group by session to avoid duplicate links + seen_sessions = {} + for result in results: + if result.session_id not in seen_sessions: + seen_sessions[result.session_id] = result + + lines = [f"Found {len(seen_sessions)} session(s) matching \"{query}\":\n"] + for sid, result in seen_sessions.items(): + lines.append(f"- **{result.session_name}** (#{sid})") + lines.append(f" Link: [Open chat](#{sid})") + lines.append(f" Match ({result.role}): {result.content_snippet}") + if result.context_before: + before = result.context_before[-1] + lines.append(f" Before ({before['role']}): {before['content'][:180]}") + if result.context_after: + after = result.context_after[0] + lines.append(f" After ({after['role']}): {after['content'][:180]}") + lines.append("") + + return {"results": "\n".join(lines)} + except Exception as e: + logger.error(f"search_chats failed: {e}") + return {"error": str(e), "exit_code": 1}