feat: unified slash commands (/new, /help) across all channels
This commit is contained in:
parent
0fc4f109bf
commit
903caaa642
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
if archive_all:
|
||||||
|
old_messages = session.messages
|
||||||
|
keep_count = 0
|
||||||
|
else:
|
||||||
keep_count = min(10, max(2, self.memory_window // 2))
|
keep_count = min(10, max(2, self.memory_window // 2))
|
||||||
old_messages = session.messages[:-keep_count] # Everything except recent ones
|
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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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)."""
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user