Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

273 lines
10 KiB
Python

"""Slash commands for partner chat surfaces."""
from __future__ import annotations
from dataclasses import dataclass
import shlex
from typing import Any, Callable
from deeptutor.agents._shared.tool_composition import default_optional_tools
from deeptutor.partners.bus.events import InboundMessage
from deeptutor.services.partners.sessions import PartnerSessionStore
@dataclass(frozen=True)
class PartnerCommandSpec:
command: str
description: str
arg_hint: str = ""
@dataclass(frozen=True)
class PartnerCommandResult:
content: str
BUILTIN_PARTNER_COMMANDS: tuple[PartnerCommandSpec, ...] = (
PartnerCommandSpec("/help", "Show available partner commands."),
PartnerCommandSpec("/new", "Archive this conversation and start a fresh one."),
PartnerCommandSpec("/branch", "Archive this conversation and continue in a copy."),
PartnerCommandSpec("/stop", "Stop the reply that's currently being generated."),
PartnerCommandSpec("/sessions", "List this partner's conversations and their IDs."),
PartnerCommandSpec("/resume", "Reopen an archived conversation.", "<session ID>"),
PartnerCommandSpec("/delete", "Delete a conversation permanently.", "<session ID>"),
PartnerCommandSpec("/status", "Show partner, session, model, and tool status."),
PartnerCommandSpec("/history", "Show recent messages in this conversation.", "[n]"),
PartnerCommandSpec("/tool", "Show or change enabled tools.", "[on|off <name>|reset]"),
)
def partner_command_palette() -> list[dict[str, str]]:
return [
{
"command": spec.command,
"description": spec.description,
"arg_hint": spec.arg_hint,
}
for spec in BUILTIN_PARTNER_COMMANDS
]
def build_partner_help_text() -> str:
lines = ["Partner commands:"]
for spec in BUILTIN_PARTNER_COMMANDS:
command = f"{spec.command} {spec.arg_hint}".rstrip()
lines.append(f"{command} - {spec.description}")
lines.append("/clear - Alias for /new.")
return "\n".join(lines)
def looks_like_partner_command(text: str) -> bool:
stripped = text.strip()
return len(stripped) > 1 and stripped.startswith("/") and stripped[1].isalpha()
class PartnerCommandHandler:
def __init__(
self,
*,
partner_id: str,
config: Any,
store: PartnerSessionStore,
save_config: Callable[[str, Any], None] | None = None,
) -> None:
self.partner_id = partner_id
self.config = config
self.store = store
self.save_config = save_config
def dispatch(self, msg: InboundMessage) -> PartnerCommandResult | None:
raw = msg.content.strip()
if not looks_like_partner_command(raw):
return None
try:
parts = shlex.split(raw)
except ValueError as exc:
return PartnerCommandResult(f"Could not parse command: {exc}")
if not parts:
return None
command = parts[0].lower().split("@", 1)[0]
args = parts[1:]
if command == "/help":
return PartnerCommandResult(build_partner_help_text())
if command in {"/new", "/clear"}:
return self._new(msg)
if command == "/branch":
return self._branch(msg)
if command == "/stop":
return PartnerCommandResult("There's nothing being generated to stop.")
if command == "/sessions":
return self._sessions()
if command == "/resume":
return self._resume(args)
if command == "/delete":
return self._delete(args)
if command == "/status":
return self._status(msg)
if command == "/history":
return self._history(msg, args)
if command == "/tool":
return self._tool(args)
return PartnerCommandResult(f"Unknown command: {parts[0]}\n\n{build_partner_help_text()}")
def _new(self, msg: InboundMessage) -> PartnerCommandResult:
archived = self.store.archive(msg.session_key)
if archived:
return PartnerCommandResult(
"Started a new conversation.\n"
f"Archived {archived['message_count']} message(s) as `{archived['session_key']}`."
)
return PartnerCommandResult("Started a new conversation. No prior messages to archive.")
def _branch(self, msg: InboundMessage) -> PartnerCommandResult:
# Branching to a *copy* needs a new session key, which only the web app
# mints; on IM there is one session per chat, so degrade to /new.
archived = self.store.archive(msg.session_key)
if archived:
return PartnerCommandResult(
f"Archived this conversation as `{archived['session_key']}` and started fresh. "
"To keep the full history in a new branch, use the web app."
)
return PartnerCommandResult("Nothing to branch yet.")
def _sessions(self) -> PartnerCommandResult:
sessions = self.store.list_sessions()
if not sessions:
return PartnerCommandResult("No conversations yet.")
lines = ["Conversations:"]
for session in sessions[:30]:
flag = " (archived)" if session.get("archived") else ""
title = str(session.get("title") or "").strip() or "(untitled)"
lines.append(
f"- `{session['session_key']}`{flag}{title} · {session['message_count']} msg"
)
lines.append("\nUse /resume <session ID> or /delete <session ID>.")
return PartnerCommandResult("\n".join(lines))
def _resume(self, args: list[str]) -> PartnerCommandResult:
if not args:
return PartnerCommandResult("Usage: /resume <session ID>")
key = args[0]
self.store.set_archived(key, False)
return PartnerCommandResult(
f"Conversation `{key}` is active again. In the web app it reopens automatically."
)
def _delete(self, args: list[str]) -> PartnerCommandResult:
if not args:
return PartnerCommandResult("Usage: /delete <session ID>")
key = args[0]
removed = self.store.delete_session(key)
return PartnerCommandResult(
f"Deleted conversation `{key}`." if removed else f"No conversation `{key}` found."
)
def _status(self, msg: InboundMessage) -> PartnerCommandResult:
selection = getattr(self.config, "llm_selection", None) or {}
model = (
(selection.get("model_id") if isinstance(selection, dict) else None)
or getattr(self.config, "model", None)
or "default"
)
tools = self._current_tools()
messages = self.store.messages(msg.session_key, limit=10_000)
lines = [
"Partner status:",
f"- Partner: {getattr(self.config, 'name', self.partner_id)} (`{self.partner_id}`)",
f"- Channel: {msg.channel}",
f"- Session: `{msg.session_key}`",
f"- Model: `{model}`",
f"- Messages in current conversation: {len(messages)}",
f"- Tools: {', '.join(f'`{name}`' for name in tools) if tools else '(none)'}",
]
return PartnerCommandResult("\n".join(lines))
def _history(self, msg: InboundMessage, args: list[str]) -> PartnerCommandResult:
count = 10
if args:
try:
count = max(1, min(int(args[0]), 50))
except ValueError:
return PartnerCommandResult("Usage: /history [count]")
records = self.store.messages(msg.session_key, limit=count)
visible = [self._format_message(record) for record in records]
visible = [line for line in visible if line]
if not visible:
return PartnerCommandResult("No conversation history yet.")
return PartnerCommandResult(f"Last {len(visible)} message(s):\n" + "\n".join(visible))
def _tool(self, args: list[str]) -> PartnerCommandResult:
available = default_optional_tools()
current = self._current_tools()
if not args:
return PartnerCommandResult(self._format_tools(current, available))
action = args[0].lower()
if action == "reset":
setattr(self.config, "enabled_tools", None)
self._persist_config()
return PartnerCommandResult(self._format_tools(self._current_tools(), available))
if action not in {"on", "off"} or len(args) < 2:
return PartnerCommandResult("Usage: /tool [on|off <name>|reset]")
name = args[1]
if name not in available:
return PartnerCommandResult(
f"Unknown tool `{name}`.\nAvailable: {', '.join(f'`{tool}`' for tool in available)}"
)
next_tools = list(current)
if action == "on" and name not in next_tools:
next_tools.append(name)
elif action == "off" and name in next_tools:
next_tools.remove(name)
setattr(self.config, "enabled_tools", next_tools)
self._persist_config()
return PartnerCommandResult(self._format_tools(next_tools, available))
def _current_tools(self) -> list[str]:
configured = getattr(self.config, "enabled_tools", None)
if configured is None:
return default_optional_tools()
available = set(default_optional_tools())
return [str(name) for name in configured if str(name) in available]
def _persist_config(self) -> None:
if self.save_config is not None:
self.save_config(self.partner_id, self.config)
@staticmethod
def _format_message(record: dict[str, Any]) -> str:
role = str(record.get("role") or "")
if role not in {"user", "assistant"}:
return ""
content = str(record.get("content") or "").strip()
if not content:
return ""
if len(content) > 200:
content = content[:199] + "..."
label = "You" if role == "user" else "Partner"
return f"{label}: {content}"
@staticmethod
def _format_tools(current: list[str], available: list[str]) -> str:
return "\n".join(
[
"Tools:",
f"- Enabled: {', '.join(f'`{name}`' for name in current) if current else '(none)'}",
f"- Available: {', '.join(f'`{name}`' for name in available) if available else '(none)'}",
]
)
__all__ = [
"PartnerCommandHandler",
"PartnerCommandResult",
"PartnerCommandSpec",
"build_partner_help_text",
"looks_like_partner_command",
"partner_command_palette",
]