feat(ui): interactive graph, strip context menus, days by activity, message times, limit status words
This commit is contained in:
@@ -161,7 +161,10 @@ def build_entries(
|
||||
return entries
|
||||
|
||||
|
||||
def messages_from_entries(entries: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
def messages_from_entries(
|
||||
entries: Iterable[Mapping[str, Any]], *, with_ts: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Messages as the model saw them; ``with_ts`` adds when each one was written."""
|
||||
out: list[dict[str, Any]] = []
|
||||
last_message_id: str | None = None
|
||||
for entry in entries:
|
||||
@@ -180,7 +183,13 @@ def messages_from_entries(entries: Iterable[Mapping[str, Any]]) -> list[dict[str
|
||||
):
|
||||
out[-1]["content"].extend(results)
|
||||
else:
|
||||
out.append({"role": "user", "content": _user_content(content)})
|
||||
message_out: dict[str, Any] = {
|
||||
"role": "user",
|
||||
"content": _user_content(content),
|
||||
}
|
||||
if with_ts:
|
||||
message_out["ts"] = entry.get("timestamp")
|
||||
out.append(message_out)
|
||||
last_message_id = None
|
||||
continue
|
||||
blocks = _assistant_blocks(content)
|
||||
@@ -193,7 +202,10 @@ def messages_from_entries(entries: Iterable[Mapping[str, Any]]) -> list[dict[str
|
||||
):
|
||||
out[-1]["content"].extend(blocks)
|
||||
else:
|
||||
out.append({"role": "assistant", "content": blocks})
|
||||
message_out: dict[str, Any] = {"role": "assistant", "content": blocks}
|
||||
if with_ts:
|
||||
message_out["ts"] = entry.get("timestamp")
|
||||
out.append(message_out)
|
||||
last_message_id = message_id
|
||||
return out
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
from beaver_gateway.agents.claude import ClaudeAgent
|
||||
from beaver_gateway.conversations.kinds import Kind
|
||||
|
||||
__all__ = ["compose", "files_touched", "granules", "skill_sets"]
|
||||
__all__ = ["bash_paths", "compose", "files_touched", "granules", "skill_sets"]
|
||||
|
||||
CHARS_PER_TOKEN = 3.2
|
||||
READ_TOOLS = frozenset({"Read", "NotebookRead"})
|
||||
@@ -21,6 +21,40 @@ WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit", "NotebookEdit"})
|
||||
SEARCH_TOOLS = frozenset({"Glob", "Grep"})
|
||||
PATH_KEYS = ("file_path", "notebook_path", "path", "file", "filename")
|
||||
_FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
||||
# A shell command names files as quoted strings or bare words; only notes
|
||||
# and plain text files count, code and binaries are the agent's own business.
|
||||
_SHELL_TOKEN = re.compile(r'"([^"\n]+)"|\'([^\'\n]+)\'|([^\s"\'`;|&()<>]+)')
|
||||
_NOTE_SUFFIXES = (".md", ".canvas", ".txt", ".csv", ".json", ".yaml", ".yml")
|
||||
_SHELL_CD = re.compile(r'(?:^|&&|;|\|\|)\s*cd\s+("([^"]+)"|\'([^\']+)\'|(\S+))')
|
||||
_SHELL_WRITES = re.compile(
|
||||
r"write_text|write_bytes|>>?\s*[\"\']?[^\s&|]|\btee\b|\bsed\s+-i|\bmv\b|\brm\b"
|
||||
r"|\btouch\b|\bcp\b|open\([^)]*[\"\'][wa]"
|
||||
)
|
||||
|
||||
|
||||
def bash_paths(command: str, cwd: Path | None) -> tuple[list[str], Path | None, bool]:
|
||||
"""Files a shell command names, where it leaves the shell, whether it writes.
|
||||
|
||||
Relative names resolve against ``cwd``, or against the directory of a
|
||||
leading ``cd`` in the same command. Absolute names stay as they are.
|
||||
"""
|
||||
here = cwd
|
||||
for match in _SHELL_CD.finditer(command):
|
||||
target = match.group(2) or match.group(3) or match.group(4) or ""
|
||||
target = target.strip()
|
||||
if not target:
|
||||
continue
|
||||
here = Path(target) if target.startswith("/") else (here or Path()) / target
|
||||
found: list[str] = []
|
||||
for match in _SHELL_TOKEN.finditer(command):
|
||||
raw = (match.group(1) or match.group(2) or match.group(3) or "").strip()
|
||||
if not raw.lower().endswith(_NOTE_SUFFIXES) or "*" in raw:
|
||||
continue
|
||||
path = Path(raw) if raw.startswith("/") else (here or Path()) / raw
|
||||
text = str(path)
|
||||
if text not in found:
|
||||
found.append(text)
|
||||
return found, here, bool(_SHELL_WRITES.search(command))
|
||||
|
||||
|
||||
def _tokens(chars: int) -> int:
|
||||
@@ -56,27 +90,38 @@ def files_touched(
|
||||
"""Files named in tool inputs, with how they were touched, plus tool counts."""
|
||||
files: dict[str, dict[str, Any]] = {}
|
||||
counts: Counter[str] = Counter()
|
||||
# The shell keeps its directory between calls; follow it.
|
||||
shell_cwd = cwd
|
||||
|
||||
def touch(raw: str, how: str, stamp: str) -> None:
|
||||
key = _relative(raw, cwd)
|
||||
row = files.setdefault(
|
||||
key, {"path": key, "reads": 0, "writes": 0, "other": 0, "last_at": stamp}
|
||||
)
|
||||
row[how] += 1
|
||||
row["last_at"] = max(row["last_at"], stamp)
|
||||
|
||||
for name, tool_input, stamp in _tool_uses(entries):
|
||||
counts[name] += 1
|
||||
if not isinstance(tool_input, dict):
|
||||
continue
|
||||
if name == "Bash" and isinstance(tool_input.get("command"), str):
|
||||
paths, shell_cwd, writes = bash_paths(tool_input["command"], shell_cwd)
|
||||
for raw in paths:
|
||||
touch(raw, "writes" if writes else "reads", stamp)
|
||||
continue
|
||||
raw = next(
|
||||
(v for k in PATH_KEYS if isinstance(v := tool_input.get(k), str) and v),
|
||||
None,
|
||||
)
|
||||
if raw is None or (name in SEARCH_TOOLS and "file_path" not in tool_input):
|
||||
continue
|
||||
key = _relative(raw, cwd)
|
||||
row = files.setdefault(
|
||||
key, {"path": key, "reads": 0, "writes": 0, "other": 0, "last_at": stamp}
|
||||
)
|
||||
if name in READ_TOOLS:
|
||||
row["reads"] += 1
|
||||
touch(raw, "reads", stamp)
|
||||
elif name in WRITE_TOOLS:
|
||||
row["writes"] += 1
|
||||
touch(raw, "writes", stamp)
|
||||
else:
|
||||
row["other"] += 1
|
||||
row["last_at"] = max(row["last_at"], stamp)
|
||||
touch(raw, "other", stamp)
|
||||
ordered = sorted(files.values(), key=lambda r: r["last_at"], reverse=True)
|
||||
return ordered, dict(counts)
|
||||
|
||||
|
||||
@@ -474,7 +474,9 @@ class Rows(State):
|
||||
return await load_messages(
|
||||
session, conversation_id=cast("int", conv.id)
|
||||
)
|
||||
return messages_from_entries(cast("Any", await self.entries(conv)))
|
||||
return messages_from_entries(
|
||||
cast("Any", await self.entries(conv)), with_ts=True
|
||||
)
|
||||
|
||||
async def entries(self, conv: Conversation, *, subpath: str = "") -> list[Any]:
|
||||
if conv.session_id is None:
|
||||
|
||||
@@ -750,7 +750,25 @@ def build_app( # noqa: PLR0915
|
||||
)
|
||||
gateway = _sum_usage(await _usage_rows(runtime, since, now))
|
||||
gateway["since"] = since.isoformat(timespec="seconds")
|
||||
windows.append({**_limit_public(row), "gateway": gateway})
|
||||
# The API sends a figure only when a window nears its limit;
|
||||
# the last one inside this window is the best lower bound.
|
||||
known = next(
|
||||
(
|
||||
r
|
||||
for r in rows
|
||||
if r.window == window
|
||||
and r.utilization is not None
|
||||
and _aware(r.ts) >= since
|
||||
),
|
||||
None,
|
||||
)
|
||||
windows.append(
|
||||
{
|
||||
**_limit_public(row),
|
||||
"gateway": gateway,
|
||||
"last_known": _limit_public(known) if known else None,
|
||||
}
|
||||
)
|
||||
windows.sort(
|
||||
key=lambda w: (WINDOWS.get(w["window"], timedelta.max), w["window"])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user