feat: unified slash commands (/new, /help) across all channels

This commit is contained in:
Re-bin 2026-02-13 03:30:21 +00:00
parent 0fc4f109bf
commit 903caaa642
5 changed files with 38 additions and 74 deletions

View File

@ -16,7 +16,7 @@
⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines. ⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines.
📏 Real-time line count: **3,562 lines** (run `bash core_agent_lines.sh` to verify anytime) 📏 Real-time line count: **3,578 lines** (run `bash core_agent_lines.sh` to verify anytime)
## 📢 News ## 📢 News

View File

@ -164,7 +164,20 @@ class AgentLoop:
logger.info(f"Processing message from {msg.channel}:{msg.sender_id}: {preview}") logger.info(f"Processing message from {msg.channel}:{msg.sender_id}: {preview}")
# Get or create session # Get or create session
session = self.sessions.get_or_create(session_key or msg.session_key) key = session_key or msg.session_key
session = self.sessions.get_or_create(key)
# Handle slash commands
cmd = msg.content.strip().lower()
if cmd == "/new":
await self._consolidate_memory(session, archive_all=True)
session.clear()
self.sessions.save(session)
return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id,
content="🐈 New session started. Memory consolidated.")
if cmd == "/help":
return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id,
content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands")
# Consolidate memory before processing if session is too large # Consolidate memory before processing if session is too large
if len(session.messages) > self.memory_window: if len(session.messages) > self.memory_window:
@ -363,11 +376,17 @@ class AgentLoop:
content=final_content content=final_content
) )
async def _consolidate_memory(self, session) -> None: async def _consolidate_memory(self, session, archive_all: bool = False) -> None:
"""Consolidate old messages into MEMORY.md + HISTORY.md, then trim session.""" """Consolidate old messages into MEMORY.md + HISTORY.md, then trim session."""
if not session.messages:
return
memory = MemoryStore(self.workspace) memory = MemoryStore(self.workspace)
keep_count = min(10, max(2, self.memory_window // 2)) if archive_all:
old_messages = session.messages[:-keep_count] # Everything except recent ones old_messages = session.messages
keep_count = 0
else:
keep_count = min(10, max(2, self.memory_window // 2))
old_messages = session.messages[:-keep_count]
if not old_messages: if not old_messages:
return return
logger.info(f"Memory consolidation started: {len(session.messages)} messages, archiving {len(old_messages)}, keeping {keep_count}") logger.info(f"Memory consolidation started: {len(session.messages)} messages, archiving {len(old_messages)}, keeping {keep_count}")
@ -404,12 +423,10 @@ Respond with ONLY valid JSON, no markdown fences."""
], ],
model=self.model, model=self.model,
) )
import json as _json
text = (response.content or "").strip() text = (response.content or "").strip()
# Strip markdown fences that LLMs often add despite instructions
if text.startswith("```"): if text.startswith("```"):
text = text.split("\n", 1)[-1].rsplit("```", 1)[0].strip() text = text.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
result = _json.loads(text) result = json.loads(text)
if entry := result.get("history_entry"): if entry := result.get("history_entry"):
memory.append_history(entry) memory.append_history(entry)
@ -417,8 +434,7 @@ Respond with ONLY valid JSON, no markdown fences."""
if update != current_memory: if update != current_memory:
memory.write_long_term(update) memory.write_long_term(update)
# Trim session to recent messages session.messages = session.messages[-keep_count:] if keep_count else []
session.messages = session.messages[-keep_count:]
self.sessions.save(session) self.sessions.save(session)
logger.info(f"Memory consolidation done, session trimmed to {len(session.messages)} messages") logger.info(f"Memory consolidation done, session trimmed to {len(session.messages)} messages")
except Exception as e: except Exception as e:

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Any, TYPE_CHECKING from typing import Any
from loguru import logger from loguru import logger
@ -12,9 +12,6 @@ from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel from nanobot.channels.base import BaseChannel
from nanobot.config.schema import Config from nanobot.config.schema import Config
if TYPE_CHECKING:
from nanobot.session.manager import SessionManager
class ChannelManager: class ChannelManager:
""" """
@ -26,10 +23,9 @@ class ChannelManager:
- Route outbound messages - Route outbound messages
""" """
def __init__(self, config: Config, bus: MessageBus, session_manager: "SessionManager | None" = None): def __init__(self, config: Config, bus: MessageBus):
self.config = config self.config = config
self.bus = bus self.bus = bus
self.session_manager = session_manager
self.channels: dict[str, BaseChannel] = {} self.channels: dict[str, BaseChannel] = {}
self._dispatch_task: asyncio.Task | None = None self._dispatch_task: asyncio.Task | None = None
@ -46,7 +42,6 @@ class ChannelManager:
self.config.channels.telegram, self.config.channels.telegram,
self.bus, self.bus,
groq_api_key=self.config.providers.groq.api_key, groq_api_key=self.config.providers.groq.api_key,
session_manager=self.session_manager,
) )
logger.info("Telegram channel enabled") logger.info("Telegram channel enabled")
except ImportError as e: except ImportError as e:

View File

@ -4,8 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
import re import re
from typing import TYPE_CHECKING
from loguru import logger from loguru import logger
from telegram import BotCommand, Update from telegram import BotCommand, Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
@ -16,9 +14,6 @@ from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel from nanobot.channels.base import BaseChannel
from nanobot.config.schema import TelegramConfig from nanobot.config.schema import TelegramConfig
if TYPE_CHECKING:
from nanobot.session.manager import SessionManager
def _markdown_to_telegram_html(text: str) -> str: def _markdown_to_telegram_html(text: str) -> str:
""" """
@ -95,7 +90,7 @@ class TelegramChannel(BaseChannel):
# Commands registered with Telegram's command menu # Commands registered with Telegram's command menu
BOT_COMMANDS = [ BOT_COMMANDS = [
BotCommand("start", "Start the bot"), BotCommand("start", "Start the bot"),
BotCommand("reset", "Reset conversation history"), BotCommand("new", "Start a new conversation"),
BotCommand("help", "Show available commands"), BotCommand("help", "Show available commands"),
] ]
@ -104,12 +99,10 @@ class TelegramChannel(BaseChannel):
config: TelegramConfig, config: TelegramConfig,
bus: MessageBus, bus: MessageBus,
groq_api_key: str = "", groq_api_key: str = "",
session_manager: SessionManager | None = None,
): ):
super().__init__(config, bus) super().__init__(config, bus)
self.config: TelegramConfig = config self.config: TelegramConfig = config
self.groq_api_key = groq_api_key self.groq_api_key = groq_api_key
self.session_manager = session_manager
self._app: Application | None = None self._app: Application | None = None
self._chat_ids: dict[str, int] = {} # Map sender_id to chat_id for replies self._chat_ids: dict[str, int] = {} # Map sender_id to chat_id for replies
self._typing_tasks: dict[str, asyncio.Task] = {} # chat_id -> typing loop task self._typing_tasks: dict[str, asyncio.Task] = {} # chat_id -> typing loop task
@ -132,8 +125,8 @@ class TelegramChannel(BaseChannel):
# Add command handlers # Add command handlers
self._app.add_handler(CommandHandler("start", self._on_start)) self._app.add_handler(CommandHandler("start", self._on_start))
self._app.add_handler(CommandHandler("reset", self._on_reset)) self._app.add_handler(CommandHandler("new", self._forward_command))
self._app.add_handler(CommandHandler("help", self._on_help)) self._app.add_handler(CommandHandler("help", self._forward_command))
# Add message handler for text, photos, voice, documents # Add message handler for text, photos, voice, documents
self._app.add_handler( self._app.add_handler(
@ -229,40 +222,15 @@ class TelegramChannel(BaseChannel):
"Type /help to see available commands." "Type /help to see available commands."
) )
async def _on_reset(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def _forward_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /reset command — clear conversation history.""" """Forward slash commands to the bus for unified handling in AgentLoop."""
if not update.message or not update.effective_user: if not update.message or not update.effective_user:
return return
await self._handle_message(
chat_id = str(update.message.chat_id) sender_id=str(update.effective_user.id),
session_key = f"{self.name}:{chat_id}" chat_id=str(update.message.chat_id),
content=update.message.text,
if self.session_manager is None:
logger.warning("/reset called but session_manager is not available")
await update.message.reply_text("⚠️ Session management is not available.")
return
session = self.session_manager.get_or_create(session_key)
msg_count = len(session.messages)
session.clear()
self.session_manager.save(session)
logger.info(f"Session reset for {session_key} (cleared {msg_count} messages)")
await update.message.reply_text("🔄 Conversation history cleared. Let's start fresh!")
async def _on_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /help command — show available commands."""
if not update.message:
return
help_text = (
"🐈 <b>nanobot commands</b>\n\n"
"/start — Start the bot\n"
"/reset — Reset conversation history\n"
"/help — Show this help message\n\n"
"Just send me a text message to chat!"
) )
await update.message.reply_text(help_text, parse_mode="HTML")
async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle incoming messages (text, photos, voice, documents).""" """Handle incoming messages (text, photos, voice, documents)."""

View File

@ -28,7 +28,6 @@ app = typer.Typer(
console = Console() console = Console()
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"} EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}
NEW_SESSION_COMMANDS = {"/new", "/reset"}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# CLI input: prompt_toolkit for editing, paste, history, and display # CLI input: prompt_toolkit for editing, paste, history, and display
@ -112,11 +111,6 @@ def _is_exit_command(command: str) -> bool:
return command.lower() in EXIT_COMMANDS return command.lower() in EXIT_COMMANDS
def _is_new_session_command(command: str) -> bool:
"""Return True when input should clear the session history."""
return command.lower() in NEW_SESSION_COMMANDS
async def _read_interactive_input_async() -> str: async def _read_interactive_input_async() -> str:
"""Read user input using prompt_toolkit (handles paste, history, display). """Read user input using prompt_toolkit (handles paste, history, display).
@ -375,7 +369,7 @@ def gateway(
) )
# Create channel manager # Create channel manager
channels = ChannelManager(config, bus, session_manager=session_manager) channels = ChannelManager(config, bus)
if channels.enabled_channels: if channels.enabled_channels:
console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}") console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
@ -490,15 +484,6 @@ def agent(
console.print("\nGoodbye!") console.print("\nGoodbye!")
break break
if _is_new_session_command(command):
session = agent_loop.sessions.get_or_create(session_id)
session.clear()
agent_loop.sessions.save(session)
console.print(
f"\n[green]{__logo__} Started new session. History cleared.[/green]\n"
)
continue
with _thinking_ctx(): with _thinking_ctx():
response = await agent_loop.process_direct(user_input, session_id) response = await agent_loop.process_direct(user_input, session_id)
_print_agent_response(response, render_markdown=markdown) _print_agent_response(response, render_markdown=markdown)