forked from Rockachopa/Timmy-time-dashboard
This commit is contained in:
@@ -155,6 +155,56 @@ class SessionLogger:
|
||||
"decisions": sum(1 for e in entries if e.get("type") == "decision"),
|
||||
}
|
||||
|
||||
def search(self, query: str, role: str | None = None, limit: int = 10) -> list[dict]:
|
||||
"""Search across all session logs for entries matching a query.
|
||||
|
||||
Args:
|
||||
query: Case-insensitive substring to search for.
|
||||
role: Optional role filter ("user", "timmy", "system").
|
||||
limit: Maximum number of results to return.
|
||||
|
||||
Returns:
|
||||
List of matching entries (most recent first), each with
|
||||
type, timestamp, and relevant content fields.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
matches: list[dict] = []
|
||||
|
||||
# Collect all session files, sorted newest first
|
||||
log_files = sorted(self.logs_dir.glob("session_*.jsonl"), reverse=True)
|
||||
|
||||
for log_file in log_files:
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
try:
|
||||
with open(log_file) as f:
|
||||
# Read all lines, reverse so newest entries come first
|
||||
lines = [ln for ln in f if ln.strip()]
|
||||
for line in reversed(lines):
|
||||
if len(matches) >= limit:
|
||||
break
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Role filter
|
||||
if role and entry.get("role") != role:
|
||||
continue
|
||||
|
||||
# Search in text-bearing fields
|
||||
searchable = " ".join(
|
||||
str(entry.get(k, ""))
|
||||
for k in ("content", "error", "decision", "rationale", "result", "tool")
|
||||
).lower()
|
||||
if query_lower in searchable:
|
||||
entry["_source_file"] = log_file.name
|
||||
matches.append(entry)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
# Global session logger instance
|
||||
_session_logger: SessionLogger | None = None
|
||||
@@ -187,3 +237,53 @@ def flush_session_logs() -> str:
|
||||
logger = get_session_logger()
|
||||
path = logger.flush()
|
||||
return str(path)
|
||||
|
||||
|
||||
def session_history(query: str, role: str = "", limit: int = 10) -> str:
|
||||
"""Search Timmy's past conversation history.
|
||||
|
||||
Find messages, tool calls, errors, and decisions from past sessions
|
||||
that match the query. Results are returned most-recent first.
|
||||
|
||||
Args:
|
||||
query: What to search for (case-insensitive substring match).
|
||||
role: Optional filter by role — "user", "timmy", or "" for all.
|
||||
limit: Maximum results to return (default 10).
|
||||
|
||||
Returns:
|
||||
Formatted string of matching session entries.
|
||||
"""
|
||||
sl = get_session_logger()
|
||||
# Flush buffer first so current session is searchable
|
||||
sl.flush()
|
||||
results = sl.search(query, role=role or None, limit=limit)
|
||||
if not results:
|
||||
return f"No session history found matching '{query}'."
|
||||
|
||||
lines = [f"Found {len(results)} result(s) for '{query}':\n"]
|
||||
for entry in results:
|
||||
ts = entry.get("timestamp", "?")[:19]
|
||||
etype = entry.get("type", "?")
|
||||
source = entry.get("_source_file", "")
|
||||
|
||||
if etype == "message":
|
||||
who = entry.get("role", "?")
|
||||
text = entry.get("content", "")[:200]
|
||||
lines.append(f"[{ts}] {who}: {text}")
|
||||
elif etype == "tool_call":
|
||||
tool = entry.get("tool", "?")
|
||||
result = entry.get("result", "")[:100]
|
||||
lines.append(f"[{ts}] tool:{tool} → {result}")
|
||||
elif etype == "error":
|
||||
err = entry.get("error", "")[:200]
|
||||
lines.append(f"[{ts}] ERROR: {err}")
|
||||
elif etype == "decision":
|
||||
dec = entry.get("decision", "")[:200]
|
||||
lines.append(f"[{ts}] DECIDED: {dec}")
|
||||
else:
|
||||
lines.append(f"[{ts}] {etype}: {json.dumps(entry)[:200]}")
|
||||
|
||||
if source:
|
||||
lines[-1] += f" ({source})"
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -586,6 +586,14 @@ def _register_introspection_tools(toolkit: Toolkit) -> None:
|
||||
logger.warning("Tool execution failed (Introspection tools registration): %s", exc)
|
||||
logger.debug("Introspection tools not available")
|
||||
|
||||
try:
|
||||
from timmy.session_logger import session_history
|
||||
|
||||
toolkit.register(session_history, name="session_history")
|
||||
except (ImportError, AttributeError) as exc:
|
||||
logger.warning("Tool execution failed (session_history registration): %s", exc)
|
||||
logger.debug("session_history tool not available")
|
||||
|
||||
|
||||
def _register_delegation_tools(toolkit: Toolkit) -> None:
|
||||
"""Register inter-agent delegation tools."""
|
||||
@@ -824,6 +832,11 @@ def _introspection_tool_catalog() -> dict:
|
||||
"description": "Check status of memory tiers (hot memory, vault)",
|
||||
"available_in": ["orchestrator"],
|
||||
},
|
||||
"session_history": {
|
||||
"name": "Session History",
|
||||
"description": "Search past conversation logs for messages, tool calls, errors, and decisions",
|
||||
"available_in": ["orchestrator"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user