diff --git a/README.md b/README.md index ef16273..f5d3e7c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ⚡️ 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 diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index a660436..80aeac4 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -164,7 +164,20 @@ class AgentLoop: logger.info(f"Processing message from {msg.channel}:{msg.sender_id}: {preview}") # 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 if len(session.messages) > self.memory_window: @@ -363,11 +376,17 @@ class AgentLoop: 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.""" + if not session.messages: + return memory = MemoryStore(self.workspace) - keep_count = min(10, max(2, self.memory_window // 2)) - old_messages = session.messages[:-keep_count] # Everything except recent ones + if archive_all: + 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: return 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, ) - import json as _json text = (response.content or "").strip() - # Strip markdown fences that LLMs often add despite instructions if text.startswith("```"): text = text.split("\n", 1)[-1].rsplit("```", 1)[0].strip() - result = _json.loads(text) + result = json.loads(text) if entry := result.get("history_entry"): memory.append_history(entry) @@ -417,8 +434,7 @@ Respond with ONLY valid JSON, no markdown fences.""" if update != current_memory: memory.write_long_term(update) - # Trim session to recent messages - session.messages = session.messages[-keep_count:] + session.messages = session.messages[-keep_count:] if keep_count else [] self.sessions.save(session) logger.info(f"Memory consolidation done, session trimmed to {len(session.messages)} messages") except Exception as e: diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 464fa97..e860d26 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from typing import Any, TYPE_CHECKING +from typing import Any from loguru import logger @@ -12,9 +12,6 @@ from nanobot.bus.queue import MessageBus from nanobot.channels.base import BaseChannel from nanobot.config.schema import Config -if TYPE_CHECKING: - from nanobot.session.manager import SessionManager - class ChannelManager: """ @@ -26,10 +23,9 @@ class ChannelManager: - 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.bus = bus - self.session_manager = session_manager self.channels: dict[str, BaseChannel] = {} self._dispatch_task: asyncio.Task | None = None @@ -46,7 +42,6 @@ class ChannelManager: self.config.channels.telegram, self.bus, groq_api_key=self.config.providers.groq.api_key, - session_manager=self.session_manager, ) logger.info("Telegram channel enabled") except ImportError as e: diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 1abd600..32f8c67 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -4,8 +4,6 @@ from __future__ import annotations import asyncio import re -from typing import TYPE_CHECKING - from loguru import logger from telegram import BotCommand, Update 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.config.schema import TelegramConfig -if TYPE_CHECKING: - from nanobot.session.manager import SessionManager - def _markdown_to_telegram_html(text: str) -> str: """ @@ -95,7 +90,7 @@ class TelegramChannel(BaseChannel): # Commands registered with Telegram's command menu BOT_COMMANDS = [ BotCommand("start", "Start the bot"), - BotCommand("reset", "Reset conversation history"), + BotCommand("new", "Start a new conversation"), BotCommand("help", "Show available commands"), ] @@ -104,12 +99,10 @@ class TelegramChannel(BaseChannel): config: TelegramConfig, bus: MessageBus, groq_api_key: str = "", - session_manager: SessionManager | None = None, ): super().__init__(config, bus) self.config: TelegramConfig = config self.groq_api_key = groq_api_key - self.session_manager = session_manager self._app: Application | None = None 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 @@ -132,8 +125,8 @@ class TelegramChannel(BaseChannel): # Add command handlers self._app.add_handler(CommandHandler("start", self._on_start)) - self._app.add_handler(CommandHandler("reset", self._on_reset)) - self._app.add_handler(CommandHandler("help", self._on_help)) + self._app.add_handler(CommandHandler("new", self._forward_command)) + self._app.add_handler(CommandHandler("help", self._forward_command)) # Add message handler for text, photos, voice, documents self._app.add_handler( @@ -229,40 +222,15 @@ class TelegramChannel(BaseChannel): "Type /help to see available commands." ) - async def _on_reset(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Handle /reset command — clear conversation history.""" + async def _forward_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Forward slash commands to the bus for unified handling in AgentLoop.""" if not update.message or not update.effective_user: return - - chat_id = str(update.message.chat_id) - session_key = f"{self.name}:{chat_id}" - - 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 = ( - "🐈 nanobot commands\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 self._handle_message( + sender_id=str(update.effective_user.id), + chat_id=str(update.message.chat_id), + content=update.message.text, ) - await update.message.reply_text(help_text, parse_mode="HTML") async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle incoming messages (text, photos, voice, documents).""" diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index eb16782..4580fed 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -28,7 +28,6 @@ app = typer.Typer( console = Console() EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"} -NEW_SESSION_COMMANDS = {"/new", "/reset"} # --------------------------------------------------------------------------- # 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 -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: """Read user input using prompt_toolkit (handles paste, history, display). @@ -375,7 +369,7 @@ def gateway( ) # Create channel manager - channels = ChannelManager(config, bus, session_manager=session_manager) + channels = ChannelManager(config, bus) if channels.enabled_channels: console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}") @@ -490,15 +484,6 @@ def agent( console.print("\nGoodbye!") 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(): response = await agent_loop.process_direct(user_input, session_id) _print_agent_response(response, render_markdown=markdown)