feat: add new conversation command and enhance command handling
- Introduced the `/new` command to start a new conversation, resetting the history. - Updated command handling in the CLI and various platform adapters (Discord, Slack, Telegram) to support the new command. - Added help command functionality to list available commands, improving user guidance. - Enhanced command mapping for better integration across platforms, ensuring consistent command behavior.
This commit is contained in:
parent
53e13fe1f1
commit
3191a9ba11
5 changed files with 217 additions and 8 deletions
3
cli.py
3
cli.py
|
|
@ -560,6 +560,7 @@ COMMANDS = {
|
|||
"/personality": "Set a predefined personality",
|
||||
"/clear": "Clear screen and reset conversation (fresh start)",
|
||||
"/history": "Show conversation history",
|
||||
"/new": "Start a new conversation (reset history)",
|
||||
"/reset": "Reset conversation only (keep screen)",
|
||||
"/retry": "Retry the last message (resend to agent)",
|
||||
"/undo": "Remove the last user/assistant exchange",
|
||||
|
|
@ -1399,7 +1400,7 @@ class HermesCLI:
|
|||
print(" ✨ (◕‿◕)✨ Fresh start! Screen cleared and conversation reset.\n")
|
||||
elif cmd_lower == "/history":
|
||||
self.show_history()
|
||||
elif cmd_lower == "/reset":
|
||||
elif cmd_lower in ("/reset", "/new"):
|
||||
self.reset_conversation()
|
||||
elif cmd_lower.startswith("/model"):
|
||||
# Use original case so model names like "Anthropic/Claude-Opus-4" are preserved
|
||||
|
|
|
|||
|
|
@ -366,6 +366,16 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="new", description="Start a new conversation")
|
||||
async def slash_new(interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
event = self._build_slash_event(interaction, "/reset")
|
||||
await self.handle_message(event)
|
||||
try:
|
||||
await interaction.followup.send("New conversation started~", ephemeral=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="reset", description="Reset your Hermes session")
|
||||
async def slash_reset(interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
|
@ -376,6 +386,48 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="model", description="Show or change the model")
|
||||
@discord.app_commands.describe(name="Model name (e.g. anthropic/claude-sonnet-4). Leave empty to see current.")
|
||||
async def slash_model(interaction: discord.Interaction, name: str = ""):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
event = self._build_slash_event(interaction, f"/model {name}".strip())
|
||||
await self.handle_message(event)
|
||||
try:
|
||||
await interaction.followup.send("Done~", ephemeral=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="personality", description="Set a personality")
|
||||
@discord.app_commands.describe(name="Personality name. Leave empty to list available.")
|
||||
async def slash_personality(interaction: discord.Interaction, name: str = ""):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
event = self._build_slash_event(interaction, f"/personality {name}".strip())
|
||||
await self.handle_message(event)
|
||||
try:
|
||||
await interaction.followup.send("Done~", ephemeral=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="retry", description="Retry your last message")
|
||||
async def slash_retry(interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
event = self._build_slash_event(interaction, "/retry")
|
||||
await self.handle_message(event)
|
||||
try:
|
||||
await interaction.followup.send("Retrying~", ephemeral=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="undo", description="Remove the last exchange")
|
||||
async def slash_undo(interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
event = self._build_slash_event(interaction, "/undo")
|
||||
await self.handle_message(event)
|
||||
try:
|
||||
await interaction.followup.send("Done~", ephemeral=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@tree.command(name="status", description="Show Hermes session status")
|
||||
async def slash_status(interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
|
|
|||
|
|
@ -327,13 +327,19 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
user_id = command.get("user_id", "")
|
||||
channel_id = command.get("channel_id", "")
|
||||
|
||||
# Map common slash subcommands to gateway commands
|
||||
if text in ("new", "reset"):
|
||||
text = "/reset"
|
||||
elif text == "status":
|
||||
text = "/status"
|
||||
elif text == "stop":
|
||||
text = "/stop"
|
||||
# Map subcommands to gateway commands
|
||||
subcommand_map = {
|
||||
"new": "/reset", "reset": "/reset",
|
||||
"status": "/status", "stop": "/stop",
|
||||
"help": "/help",
|
||||
"model": "/model", "personality": "/personality",
|
||||
"retry": "/retry", "undo": "/undo",
|
||||
}
|
||||
first_word = text.split()[0] if text else ""
|
||||
if first_word in subcommand_map:
|
||||
# Preserve arguments after the subcommand
|
||||
rest = text[len(first_word):].strip()
|
||||
text = f"{subcommand_map[first_word]} {rest}".strip() if rest else subcommand_map[first_word]
|
||||
elif text:
|
||||
pass # Treat as a regular question
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -101,6 +101,23 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
await self._app.start()
|
||||
await self._app.updater.start_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
# Register bot commands so Telegram shows a hint menu when users type /
|
||||
try:
|
||||
from telegram import BotCommand
|
||||
await self._bot.set_my_commands([
|
||||
BotCommand("new", "Start a new conversation"),
|
||||
BotCommand("reset", "Reset conversation history"),
|
||||
BotCommand("model", "Show or change the model"),
|
||||
BotCommand("personality", "Set a personality"),
|
||||
BotCommand("retry", "Retry your last message"),
|
||||
BotCommand("undo", "Remove the last exchange"),
|
||||
BotCommand("status", "Show session info"),
|
||||
BotCommand("stop", "Stop the running agent"),
|
||||
BotCommand("help", "Show available commands"),
|
||||
])
|
||||
except Exception as e:
|
||||
print(f"[{self.name}] Could not register command menu: {e}")
|
||||
|
||||
self._running = True
|
||||
print(f"[{self.name}] Connected and polling for updates")
|
||||
return True
|
||||
|
|
|
|||
133
gateway/run.py
133
gateway/run.py
|
|
@ -318,12 +318,27 @@ class GatewayRunner:
|
|||
if command in ["new", "reset"]:
|
||||
return await self._handle_reset_command(event)
|
||||
|
||||
if command == "help":
|
||||
return await self._handle_help_command(event)
|
||||
|
||||
if command == "status":
|
||||
return await self._handle_status_command(event)
|
||||
|
||||
if command == "stop":
|
||||
return await self._handle_stop_command(event)
|
||||
|
||||
if command == "model":
|
||||
return await self._handle_model_command(event)
|
||||
|
||||
if command == "personality":
|
||||
return await self._handle_personality_command(event)
|
||||
|
||||
if command == "retry":
|
||||
return await self._handle_retry_command(event)
|
||||
|
||||
if command == "undo":
|
||||
return await self._handle_undo_command(event)
|
||||
|
||||
# Check for pending exec approval responses
|
||||
session_key_preview = f"agent:main:{source.platform.value}:{source.chat_type}:{source.chat_id}" if source.chat_type != "dm" else f"agent:main:{source.platform.value}:dm"
|
||||
if session_key_preview in self._pending_approvals:
|
||||
|
|
@ -587,6 +602,124 @@ class GatewayRunner:
|
|||
else:
|
||||
return "No active task to stop."
|
||||
|
||||
async def _handle_help_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /help command - list available commands."""
|
||||
return (
|
||||
"📖 **Hermes Commands**\n"
|
||||
"\n"
|
||||
"`/new` — Start a new conversation\n"
|
||||
"`/reset` — Reset conversation history\n"
|
||||
"`/status` — Show session info\n"
|
||||
"`/stop` — Interrupt the running agent\n"
|
||||
"`/model [name]` — Show or change the model\n"
|
||||
"`/personality [name]` — Set a personality\n"
|
||||
"`/retry` — Retry your last message\n"
|
||||
"`/undo` — Remove the last exchange\n"
|
||||
"`/help` — Show this message"
|
||||
)
|
||||
|
||||
async def _handle_model_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /model command - show or change the current model."""
|
||||
args = event.get_command_args().strip()
|
||||
current = os.getenv("HERMES_MODEL", "anthropic/claude-opus-4.6")
|
||||
|
||||
if not args:
|
||||
return f"🤖 **Current model:** `{current}`\n\nTo change: `/model provider/model-name`"
|
||||
|
||||
os.environ["HERMES_MODEL"] = args
|
||||
return f"🤖 Model changed to `{args}`\n_(takes effect on next message)_"
|
||||
|
||||
async def _handle_personality_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /personality command - list or set a personality."""
|
||||
args = event.get_command_args().strip().lower()
|
||||
|
||||
try:
|
||||
import yaml
|
||||
config_path = Path.home() / '.hermes' / 'config.yaml'
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
personalities = config.get("agent", {}).get("personalities", {})
|
||||
else:
|
||||
personalities = {}
|
||||
except Exception:
|
||||
personalities = {}
|
||||
|
||||
if not personalities:
|
||||
return "No personalities configured in `~/.hermes/config.yaml`"
|
||||
|
||||
if not args:
|
||||
lines = ["🎭 **Available Personalities**\n"]
|
||||
for name, prompt in personalities.items():
|
||||
preview = prompt[:50] + "..." if len(prompt) > 50 else prompt
|
||||
lines.append(f"• `{name}` — {preview}")
|
||||
lines.append(f"\nUsage: `/personality <name>`")
|
||||
return "\n".join(lines)
|
||||
|
||||
if args in personalities:
|
||||
os.environ["HERMES_PERSONALITY"] = personalities[args]
|
||||
return f"🎭 Personality set to **{args}**\n_(takes effect on next message)_"
|
||||
|
||||
available = ", ".join(f"`{n}`" for n in personalities.keys())
|
||||
return f"Unknown personality: `{args}`\n\nAvailable: {available}"
|
||||
|
||||
async def _handle_retry_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /retry command - re-send the last user message."""
|
||||
source = event.source
|
||||
session_entry = self.session_store.get_or_create_session(source)
|
||||
history = self.session_store.load_transcript(session_entry.session_id)
|
||||
|
||||
# Find the last user message
|
||||
last_user_msg = None
|
||||
last_user_idx = None
|
||||
for i in range(len(history) - 1, -1, -1):
|
||||
if history[i].get("role") == "user":
|
||||
last_user_msg = history[i].get("content", "")
|
||||
last_user_idx = i
|
||||
break
|
||||
|
||||
if not last_user_msg:
|
||||
return "No previous message to retry."
|
||||
|
||||
# Truncate history to before the last user message
|
||||
truncated = history[:last_user_idx]
|
||||
session_entry.conversation_history = truncated
|
||||
|
||||
# Re-send by creating a fake text event with the old message
|
||||
retry_event = MessageEvent(
|
||||
text=last_user_msg,
|
||||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
raw_message=event.raw_message,
|
||||
)
|
||||
|
||||
# Let the normal message handler process it
|
||||
await self._handle_message(retry_event)
|
||||
return None # Response sent through normal flow
|
||||
|
||||
async def _handle_undo_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /undo command - remove the last user/assistant exchange."""
|
||||
source = event.source
|
||||
session_entry = self.session_store.get_or_create_session(source)
|
||||
history = self.session_store.load_transcript(session_entry.session_id)
|
||||
|
||||
# Find the last user message and remove everything from it onward
|
||||
last_user_idx = None
|
||||
for i in range(len(history) - 1, -1, -1):
|
||||
if history[i].get("role") == "user":
|
||||
last_user_idx = i
|
||||
break
|
||||
|
||||
if last_user_idx is None:
|
||||
return "Nothing to undo."
|
||||
|
||||
removed_msg = history[last_user_idx].get("content", "")
|
||||
removed_count = len(history) - last_user_idx
|
||||
session_entry.conversation_history = history[:last_user_idx]
|
||||
|
||||
preview = removed_msg[:40] + "..." if len(removed_msg) > 40 else removed_msg
|
||||
return f"↩️ Undid {removed_count} message(s).\nRemoved: \"{preview}\""
|
||||
|
||||
def _set_session_env(self, context: SessionContext) -> None:
|
||||
"""Set environment variables for the current session."""
|
||||
os.environ["HERMES_SESSION_PLATFORM"] = context.source.platform.value
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue