"""Relations agent — read-only tool wrapping MemoryMiddleware.query_relations.""" from __future__ import annotations from typing import Any from langchain_core.tools import tool from app.core.memory_middleware import MemoryMiddleware from app.db import async_session # Injected at tool-factory time by _brief_research_tools(); not a module-level global. # Each tool closure captures the user_id bound at factory time. def make_query_relations_tool(user_id: str, trace_id: str | None = None) -> Any: """Return a query_relations tool bound to *user_id*.""" @tool async def query_relations( subject_label: str = "", predicate: str = "", object_label: str = "", limit: int = 10, ) -> str: """Query the relational memory graph for entity relationships. Returns rows where subject ↔ predicate ↔ object match the given filters. All parameters are optional — omit to retrieve all relations up to limit. subject_label: entity label on the left side (e.g. a client name, "Acme Corp"). predicate: relationship type (e.g. "mentioned_in", "works_at", "related_to"). object_label: entity label on the right side (e.g. a project name, "Website Redesign"). limit: max rows to return (default 10). """ import logging logger = logging.getLogger(__name__) logger.info( "relations_agent: query_relations trace=%s user=%s subject=%r predicate=%r object=%r", trace_id or "-", user_id, subject_label, predicate, object_label, ) async with async_session() as db: memory = MemoryMiddleware(db) rows = await memory.query_relations( user_id=user_id, subject=subject_label or None, predicate=predicate or None, object_=object_label or None, limit=limit, ) if not rows: return "No relational memory entries found for the given filters." lines = [ f"- {r.subject_label} —[{r.predicate}]→ {r.object_label}" + (f" (confidence: {r.confidence:.2f})" if r.confidence is not None else "") for r in rows ] return f"Found {len(rows)} relation(s):\n" + "\n".join(lines) return query_relations