From 6d68fa83f5358469765aa393492a5824751b80d2 Mon Sep 17 00:00:00 2001 From: Petr Lavrov Date: Fri, 21 Mar 2025 03:35:45 +0300 Subject: [PATCH 1/3] update CLAUDE.md --- CLAUDE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 3dfad56..41de419 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,25 @@ +# Contact Manager (for People Bot) + +- **Main Feature**: Manages user contacts with a basic schema (name, phone, email, telegram, birthday) stored in MongoDB. +- **Demo Bot**: Send a message (e.g., "Jane, 555-5678"); it auto-parses and saves as a contact, asking for missing fields if needed. +- **Useful Bot**: **People Bot** - Auto-ingests contacts from chat messages, parses with LLM, stores them, and lets you retrieve random contacts. + +## Developer Notes + +0) need to decide whether to implement contact manager on top of queue manager or as a completely separate + +1) for contacts - people should never get out of the queue + +2) AI loves to invent features here. Forbid it. Make sure it is minimal possible implementation to start with - and then extend that + +3) need some way to easily ingest contacts: from telegram, from iphone, from gmail, from my old notion / remnote / obsidian. + +4) I guess we do this on top of queue manager. Just need data model. The question is how will we add fields later? + +5) how to make this extensible into a full-fledged crm? + +6) bring over features to send messages to people - from ef random coffee bot. /Users/petrlavrov/work/experiments/ef-community-bot-rc-notifier + # Botspot Development Guide ## Build & Test Commands From 317cda80555b913d07cda04dfeac70c0fdf5b241 Mon Sep 17 00:00:00 2001 From: Petr Lavrov Date: Mon, 24 Mar 2025 01:36:48 +0300 Subject: [PATCH 2/3] contact manager: Spec and workalong --- CLAUDE.md | 21 ------- .../rework_protocol.md | 58 +++++++++++++++++++ dev/workalong_contact_manager/spec.md | 21 +++++++ dev/workalong_contact_manager/workalong.md | 50 ++++++++++++++++ 4 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 dev/workalong_contact_manager/rework_protocol.md create mode 100644 dev/workalong_contact_manager/spec.md create mode 100644 dev/workalong_contact_manager/workalong.md diff --git a/CLAUDE.md b/CLAUDE.md index 41de419..327d0bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,24 +1,3 @@ -# Contact Manager (for People Bot) - -- **Main Feature**: Manages user contacts with a basic schema (name, phone, email, telegram, birthday) stored in MongoDB. -- **Demo Bot**: Send a message (e.g., "Jane, 555-5678"); it auto-parses and saves as a contact, asking for missing fields if needed. -- **Useful Bot**: **People Bot** - Auto-ingests contacts from chat messages, parses with LLM, stores them, and lets you retrieve random contacts. - -## Developer Notes - -0) need to decide whether to implement contact manager on top of queue manager or as a completely separate - -1) for contacts - people should never get out of the queue - -2) AI loves to invent features here. Forbid it. Make sure it is minimal possible implementation to start with - and then extend that - -3) need some way to easily ingest contacts: from telegram, from iphone, from gmail, from my old notion / remnote / obsidian. - -4) I guess we do this on top of queue manager. Just need data model. The question is how will we add fields later? - -5) how to make this extensible into a full-fledged crm? - -6) bring over features to send messages to people - from ef random coffee bot. /Users/petrlavrov/work/experiments/ef-community-bot-rc-notifier # Botspot Development Guide diff --git a/dev/workalong_contact_manager/rework_protocol.md b/dev/workalong_contact_manager/rework_protocol.md new file mode 100644 index 0000000..e28f6e4 --- /dev/null +++ b/dev/workalong_contact_manager/rework_protocol.md @@ -0,0 +1,58 @@ +This is a protocol for refactoring / reworking code components with claude-code or other +ai tools. +The main goal of this protocol is to ensure the codebase doesn't bloat and grow riddled +with duplicated code during AI refactoring / reworks. + +# Pre-rework + +- Commit. Make sure everything is committed + - Optional: Create a new branch for the rework +- Optional: Check tests, if present. + +# Simple protocol: + +If the task is clear, go directly to coding phase + +# Complex protocol: + +If there's a complex task - first, read spec.md or ask user for a list of requirements + +- After that, evaluate the current solution on the match with specs + - Which additional featuers were implemented that are not present in the spec + - What is the likely reason were they added + - + - Which components / features explicitly listed in the spec are missing + - How difficult it is to add this + - write to workalong.md + - proceed to coding + +## Coding: + +- Before coding, lay out a plan to the user in clear terms. + - Which components / features will be added + - Which modified + - Which moved / removed + - Make an explicit enumeration for user to specify which steps are approved and + which are declined + - Each item should be formulated in as simple terms as possible, 1 line maximum per + item, not longer than a few words +- Always remove duplicate code or alternative / previous implementations of the same + feature + - After making a change, call git diff and make sure file is in a desired target + state and the changes you planned are correctly reflected +- proceed with implementing each item one by one and track progress with checkboxes in + workalong.md + - [x] item 1 - keep to original item descriptions, DO NOT ADD SUB-ITEMS. List which + files were affected for this feature. + +AI Issue resolution + +1) Failed deletions + Sometimes AI applier fails to delete code according to instructions + This results in really confusing situations with duplicate functions / methods + present in multiple places across the codebase + To mitigate that + +- Check file diff after each modifications and make sure it reflects the changes you've + made + diff --git a/dev/workalong_contact_manager/spec.md b/dev/workalong_contact_manager/spec.md new file mode 100644 index 0000000..21b8264 --- /dev/null +++ b/dev/workalong_contact_manager/spec.md @@ -0,0 +1,21 @@ +# Contact Manager (for People Bot) + +- **Main Feature**: Manages user contacts with a basic schema (name, phone, email, telegram, birthday) stored in MongoDB. +- **Demo Bot**: Send a message (e.g., "Jane, 555-5678"); it auto-parses and saves as a contact, asking for missing fields if needed. +- **Useful Bot**: **People Bot** - Auto-ingests contacts from chat messages, parses with LLM, stores them, and lets you retrieve random contacts. + +## Developer Notes + +0) need to decide whether to implement contact manager on top of queue manager or as a completely separate + +1) for contacts - people should never get out of the queue + +2) AI loves to invent features here. Forbid it. Make sure it is minimal possible implementation to start with - and then extend that + +3) need some way to easily ingest contacts: from telegram, from iphone, from gmail, from my old notion / remnote / obsidian. + +4) I guess we do this on top of queue manager. Just need data model. The question is how will we add fields later? + +5) how to make this extensible into a full-fledged crm? + +6) bring over features to send messages to people - from ef random coffee bot. /Users/petrlavrov/work/experiments/ef-community-bot-rc-notifier diff --git a/dev/workalong_contact_manager/workalong.md b/dev/workalong_contact_manager/workalong.md new file mode 100644 index 0000000..cc954d9 --- /dev/null +++ b/dev/workalong_contact_manager/workalong.md @@ -0,0 +1,50 @@ +# Contact Manager Implementation Analysis + +## Features/Methods/Fields Added Beyond the Spec + +1. **Settings beyond basic enablement:** + - `message_parser_enabled` - Toggle for auto-parsing contacts from messages + - `random_contact_enabled` - Toggle for random contact feature + - `allow_everyone` - Access control setting + - `collection` - MongoDB collection name setting + +2. **Contact Model Fields beyond basic schema:** + - `user_id` - Telegram user ID if applicable + - `notes` - Additional notes field + - `created_at` - Timestamp when contact was created + - `updated_at` - Timestamp when contact was last updated + - `owner_id` - Who added this contact + +3. **Additional Methods in ContactManager:** + - `update_contact()` - Update existing contacts + - `delete_contact()` - Delete contacts + - `get_contact_by_id()` - Get specific contact by ID + - `find_contacts()` - Generic method to find contacts by any criteria + - `search_contacts()` - Specialized search method with text matching + +4. **Commands and User Interface:** + - `/find_contact` command - Search functionality + - Detailed response formatting with emoji and markdown + +5. **Other Features:** + - Visibility control through command menu + - Contact ownership and per-user contacts + +## Features Mentioned in Spec but Not Implemented + +1. **Queue Manager Integration:** + - Notes mention "implement contact manager on top of queue manager" (line 9) + - "for contacts - people should never get out of the queue" (line 11) + - Not implemented as a queue-based system + +2. **External Data Import:** + - "need some way to easily ingest contacts: from telegram, from iphone, from gmail, from my old notion / remnote / obsidian" (line 15) + - No import functionality from external sources implemented + +3. **Message Feature:** + - "bring over features to send messages to people - from ef random coffee bot" (line 21) + - No functionality to send messages to contacts + +4. **Extensibility for Full CRM:** + - "how to make this extensible into a full-fledged crm?" (line 19) + - While the implementation is somewhat extensible, no explicit CRM-focused extensibility mechanisms \ No newline at end of file From 2d0be6e5f55b4237735adf8adc11aa0485c2c83c Mon Sep 17 00:00:00 2001 From: Petr Lavrov Date: Mon, 24 Mar 2025 01:39:46 +0300 Subject: [PATCH 3/3] contact manager: dump changes --- botspot/components/new/contact_manager.py | 444 +++++++++++++++++- botspot/core/bot_manager.py | 8 +- botspot/core/botspot_settings.py | 2 + botspot/core/dependency_manager.py | 12 + botspot/utils/deps_getters.py | 4 + .../contact_manager_demo/README.md | 53 +++ .../contact_manager_demo/bot.py | 58 +++ .../contact_manager_demo/sample.env | 37 ++ 8 files changed, 611 insertions(+), 7 deletions(-) create mode 100644 examples/components_examples/contact_manager_demo/README.md create mode 100644 examples/components_examples/contact_manager_demo/bot.py create mode 100644 examples/components_examples/contact_manager_demo/sample.env diff --git a/botspot/components/new/contact_manager.py b/botspot/components/new/contact_manager.py index a8a44ab..7335624 100644 --- a/botspot/components/new/contact_manager.py +++ b/botspot/components/new/contact_manager.py @@ -1,13 +1,35 @@ -from typing import TYPE_CHECKING +from datetime import date, datetime, timezone +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from aiogram import Dispatcher, Router +from aiogram.filters import Command +from aiogram.types import Message +from pydantic import BaseModel, Field from pydantic_settings import BaseSettings +from botspot import commands_menu +from botspot.commands_menu import Visibility +from botspot.utils.admin_filter import AdminFilter +from botspot.utils.deps_getters import get_database +from botspot.utils.internal import get_logger +from botspot.utils.user_ops import UserLike, get_chat_id + if TYPE_CHECKING: - from motor.motor_asyncio import AsyncIOMotorCollection # noqa: F401 + from motor.motor_asyncio import AsyncIOMotorCollection, AsyncIOMotorDatabase # noqa: F401 + +logger = get_logger() + +# --------------------------------------------- +# region Settings and Model Definitions +# --------------------------------------------- class ContactManagerSettings(BaseSettings): enabled: bool = False + collection: str = "contacts" + message_parser_enabled: bool = True + random_contact_enabled: bool = True + allow_everyone: bool = False # If False, only friends and admins can use class Config: env_prefix = "BOTSPOT_CONTACT_MANAGER_" @@ -16,20 +38,430 @@ class Config: extra = "ignore" +class Contact(BaseModel): + """Contact information model.""" + + name: str + user_id: Optional[int] = None # Telegram user ID if applicable + phone: Optional[str] = None # Phone number + email: Optional[str] = None # Email address + telegram: Optional[str] = None # Telegram username + birthday: Optional[date] = None # Birthday + notes: Optional[str] = None # Any additional notes + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + owner_id: Optional[int] = None # Who added this contact (user_id) + + @property + def display_info(self) -> str: + """Return formatted contact information.""" + parts = [f"📇 **{self.name}**"] + + if self.phone: + parts.append(f"📱 Phone: {self.phone}") + if self.email: + parts.append(f"📧 Email: {self.email}") + if self.telegram: + username = self.telegram.strip('@') + parts.append(f"📨 Telegram: @{username}") + if self.birthday: + parts.append(f"🎂 Birthday: {self.birthday.strftime('%d %B')}") + if self.notes: + parts.append(f"📝 Notes: {self.notes}") + + return "\n".join(parts) + + +# --------------------------------------------- +# endregion Settings and Model Definitions +# --------------------------------------------- + + +# --------------------------------------------- +# region Contact Manager Implementation +# --------------------------------------------- + class ContactManager: - pass + """Manager for user contacts.""" + + def __init__(self, db: "AsyncIOMotorDatabase", collection: str): + """Initialize contact manager with database connection.""" + self.db = db + self.collection = collection + + @property + def contacts_collection(self) -> "AsyncIOMotorCollection": + """Get the contacts collection.""" + return self.db[self.collection] + + async def add_contact(self, contact: Contact) -> bool: + """Add a new contact to the database.""" + try: + contact.updated_at = datetime.now(timezone.utc) + result = await self.contacts_collection.insert_one(contact.model_dump()) + logger.info(f"Added contact: {contact.name} with ID {result.inserted_id}") + return True + except Exception as e: + logger.error(f"Error adding contact {contact.name}: {e}") + return False + + async def update_contact(self, contact_id: str, data: Dict[str, Any]) -> bool: + """Update an existing contact.""" + try: + # Add updated timestamp + data["updated_at"] = datetime.now(timezone.utc) + result = await self.contacts_collection.update_one( + {"_id": contact_id}, + {"$set": data} + ) + return result.modified_count > 0 + except Exception as e: + logger.error(f"Error updating contact {contact_id}: {e}") + return False + + async def delete_contact(self, contact_id: str) -> bool: + """Delete a contact by ID.""" + try: + result = await self.contacts_collection.delete_one({"_id": contact_id}) + return result.deleted_count > 0 + except Exception as e: + logger.error(f"Error deleting contact {contact_id}: {e}") + return False + + async def get_contact_by_id(self, contact_id: str) -> Optional[Contact]: + """Get a contact by ID.""" + try: + data = await self.contacts_collection.find_one({"_id": contact_id}) + return Contact(**data) if data else None + except Exception as e: + logger.error(f"Error getting contact {contact_id}: {e}") + return None + + async def find_contacts( + self, + query: Dict[str, Any], + limit: int = 10, + owner_id: Optional[int] = None + ) -> List[Contact]: + """Find contacts matching the query.""" + try: + # If owner_id is provided, filter by owner + if owner_id is not None: + query["owner_id"] = owner_id + + cursor = self.contacts_collection.find(query).limit(limit) + contacts = [] + + async for doc in cursor: + contacts.append(Contact(**doc)) + + return contacts + except Exception as e: + logger.error(f"Error finding contacts: {e}") + return [] + + async def search_contacts( + self, + text: str, + owner_id: Optional[int] = None, + limit: int = 10 + ) -> List[Contact]: + """Search for contacts by name, email, etc.""" + query = { + "$or": [ + {"name": {"$regex": text, "$options": "i"}}, + {"email": {"$regex": text, "$options": "i"}}, + {"phone": {"$regex": text, "$options": "i"}}, + {"telegram": {"$regex": text, "$options": "i"}}, + {"notes": {"$regex": text, "$options": "i"}} + ] + } + + return await self.find_contacts(query, limit, owner_id) + + async def get_random_contact(self, owner_id: Optional[int] = None) -> Optional[Contact]: + """Get a random contact.""" + try: + pipeline = [] + + # If owner_id is provided, filter by owner + if owner_id is not None: + pipeline.append({"$match": {"owner_id": owner_id}}) + + # Add sample stage to get random document + pipeline.append({"$sample": {"size": 1}}) + + cursor = self.contacts_collection.aggregate(pipeline) + async for doc in cursor: + return Contact(**doc) + + return None + except Exception as e: + logger.error(f"Error getting random contact: {e}") + return None + + async def parse_contact_with_llm( + self, + text: str, + user_id: Optional[int] = None + ) -> Optional[Contact]: + """ + Parse contact information from text using LLM. + + Args: + text: Text message to parse + user_id: User ID who sent the message (for ownership) + + Returns: + Contact object if parsing successful, else None + """ + try: + from botspot.components.new.llm_provider import get_llm_provider + from botspot.utils.deps_getters import get_llm_provider + + # Get LLM provider + llm = get_llm_provider() + + # System message for the LLM + system_message = """ + You are a contact information parser. Extract contact details from the user's message. + Return ONLY a valid JSON object with the following structure: + { + "name": "Person's name", + "phone": "Phone number (if found)", + "email": "Email (if found)", + "telegram": "Telegram username (if found)", + "birthday": "Birthday in YYYY-MM-DD format (if found)", + "notes": "Any additional information" + } + + If a field is not found, set it to null. + The name field is required - make your best guess if not explicit. + """ + + # Use structured output for better parsing + result = await llm.aquery_llm_structured( + prompt=text, + output_schema=Contact, + user=user_id, + system_message=system_message, + temperature=0.2, # Lower temperature for more deterministic output + ) + + # Add owner ID + result.owner_id = user_id + + return result + except Exception as e: + logger.error(f"Error parsing contact with LLM: {e}") + return None + +# --------------------------------------------- +# endregion Contact Manager Implementation +# --------------------------------------------- -def setup_dispatcher(dp): +# --------------------------------------------- +# region Command Handlers +# --------------------------------------------- + +def setup_command_handlers(dp: Dispatcher, manager: ContactManager, settings: ContactManagerSettings): + """Set up command handlers for contact management.""" + router = Router(name="contact_manager") + + # Add command to add contact + @commands_menu.add_command("add_contact", "Add a new contact", Visibility.PUBLIC) + @router.message(Command("add_contact")) + async def add_contact_cmd(message: Message): + """Add a new contact.""" + # Extract contact info after the command + if len(message.text.split()) <= 1: + await message.reply( + "Please provide contact information after the command. Example:\n" + "/add_contact John Doe, phone: 555-1234, email: john@example.com" + ) + return + + # Extract contact text (everything after the command) + contact_text = message.text.split(maxsplit=1)[1] + + # Parse contact with LLM + contact = await manager.parse_contact_with_llm(contact_text, message.from_user.id) + + if not contact: + await message.reply("I couldn't parse the contact information. Please try again with more details.") + return + + # Check if any required fields are missing + missing_fields = [] + if not contact.phone and not contact.email and not contact.telegram: + missing_fields.append("a way to contact (phone, email, or telegram)") + + # If missing fields, ask user to provide them + if missing_fields: + missing_info = ", ".join(missing_fields) + await message.reply( + f"I need {missing_info} for this contact. Please add this information and try again." + ) + return + + # Add contact to database + success = await manager.add_contact(contact) + + if success: + await message.reply( + f"✅ Contact added successfully!\n\n{contact.display_info}", + parse_mode="Markdown" + ) + else: + await message.reply("❌ Failed to add contact. Please try again.") + + # Add command to find contact + @commands_menu.add_command("find_contact", "Find contacts by name, phone, etc.", Visibility.PUBLIC) + @router.message(Command("find_contact")) + async def find_contact_cmd(message: Message): + """Find contacts matching a search term.""" + if len(message.text.split()) <= 1: + await message.reply( + "Please provide a search term after the command. Example:\n" + "/find_contact John" + ) + return + + # Extract search text + search_text = message.text.split(maxsplit=1)[1] + + # Search for contacts + contacts = await manager.search_contacts(search_text, message.from_user.id) + + if not contacts: + await message.reply(f"No contacts found matching '{search_text}'.") + return + + # Format results + if len(contacts) == 1: + await message.reply( + f"Found 1 contact:\n\n{contacts[0].display_info}", + parse_mode="Markdown" + ) + else: + results = "\n\n".join([c.display_info for c in contacts[:5]]) + count_msg = f"Found {len(contacts)} contacts" + (", showing first 5:" if len(contacts) > 5 else ":") + await message.reply( + f"{count_msg}\n\n{results}", + parse_mode="Markdown" + ) + + # Add command for random contact + if settings.random_contact_enabled: + @commands_menu.add_command("random_contact", "Get a random contact", Visibility.PUBLIC) + @router.message(Command("random_contact")) + async def random_contact_cmd(message: Message): + """Get a random contact.""" + contact = await manager.get_random_contact(message.from_user.id) + + if not contact: + await message.reply("You don't have any contacts saved yet.") + return + + await message.reply( + f"Random contact:\n\n{contact.display_info}", + parse_mode="Markdown" + ) + + # Handle message parsing if enabled + if settings.message_parser_enabled: + @router.message() + async def parse_contact_message(message: Message): + """Parse potential contact information from regular messages.""" + # Skip commands + if message.text and message.text.startswith('/'): + return + + # Simple heuristic: Look for some indicators of contact info + # This is intentionally simple as the LLM will do the heavy lifting + if not message.text or not any(kw in message.text.lower() for kw in [ + 'phone', 'email', 'telegram', '@', 'contact', 'reach', 'name' + ]): + return + + # Try to parse as contact + contact = await manager.parse_contact_with_llm(message.text, message.from_user.id) + + # If we have a name and at least one contact method, offer to save + if contact and contact.name and (contact.phone or contact.email or contact.telegram): + await message.reply( + f"I detected contact information. Would you like to save this contact?\n\n" + f"{contact.display_info}\n\n" + f"Reply with /add_contact to confirm.", + parse_mode="Markdown" + ) + + # Include router in dispatcher + dp.include_router(router) + +# --------------------------------------------- +# endregion Command Handlers +# --------------------------------------------- + + +# --------------------------------------------- +# region Initialization and Setup +# --------------------------------------------- + +def setup_dispatcher(dp: Dispatcher, **kwargs): + """Setup contact manager component in the dispatcher.""" + settings = ContactManagerSettings(**kwargs) + + if not settings.enabled: + logger.info("Contact Manager component is disabled") + return dp + + # Make sure dependencies are available + from botspot.core.dependency_manager import get_dependency_manager + + deps = get_dependency_manager() + + if deps.contact_manager is None: + logger.warning("Contact Manager component is not initialized") + return dp + + logger.info("Setting up Contact Manager component") + + # Set up command handlers + setup_command_handlers(dp, deps.contact_manager, settings) + return dp def initialize(settings: ContactManagerSettings) -> ContactManager: - pass + """Initialize the Contact Manager component.""" + if not settings.enabled: + logger.info("Contact Manager component is disabled") + return None + + logger.info("Initializing Contact Manager component") + + # Get MongoDB database + db = get_database() + + # Create Contact Manager instance + manager = ContactManager(db, settings.collection) + + return manager -def get_contact_manager(): +def get_contact_manager() -> ContactManager: + """Get the Contact Manager from dependency manager.""" from botspot.core.dependency_manager import get_dependency_manager deps = get_dependency_manager() + + if deps.contact_manager is None: + raise RuntimeError("Contact Manager is not initialized") + return deps.contact_manager + +# --------------------------------------------- +# endregion Initialization and Setup +# --------------------------------------------- diff --git a/botspot/core/bot_manager.py b/botspot/core/bot_manager.py index 885fe8f..79ed7d4 100644 --- a/botspot/core/bot_manager.py +++ b/botspot/core/bot_manager.py @@ -13,7 +13,7 @@ from botspot.components.features import user_interactions from botspot.components.main import event_scheduler, single_user_mode, telethon_manager, trial_mode from botspot.components.middlewares import error_handler -from botspot.components.new import chat_binder, llm_provider +from botspot.components.new import chat_binder, contact_manager, llm_provider from botspot.components.qol import bot_commands_menu, bot_info, print_bot_url from botspot.core.botspot_settings import BotspotSettings from botspot.core.dependency_manager import DependencyManager @@ -60,6 +60,9 @@ def __init__( if self.settings.llm_provider.enabled: self.deps.llm_provider = llm_provider.initialize(self.settings.llm_provider) + + if self.settings.contact_manager.enabled: + self.deps.contact_manager = contact_manager.initialize(self.settings.contact_manager) def setup_dispatcher(self, dp: Dispatcher): """Setup dispatcher with components""" @@ -105,3 +108,6 @@ def setup_dispatcher(self, dp: Dispatcher): if self.settings.llm_provider.enabled: llm_provider.setup_dispatcher(dp) + + if self.settings.contact_manager.enabled: + contact_manager.setup_dispatcher(dp) diff --git a/botspot/core/botspot_settings.py b/botspot/core/botspot_settings.py index 7c1d186..48e92e0 100644 --- a/botspot/core/botspot_settings.py +++ b/botspot/core/botspot_settings.py @@ -13,6 +13,7 @@ from botspot.components.main.trial_mode import TrialModeSettings from botspot.components.middlewares.error_handler import ErrorHandlerSettings from botspot.components.new.chat_binder import ChatBinderSettings +from botspot.components.new.contact_manager import ContactManagerSettings from botspot.components.new.llm_provider import LLMProviderSettings from botspot.components.qol.bot_commands_menu import BotCommandsMenuSettings from botspot.components.qol.bot_info import BotInfoSettings @@ -59,6 +60,7 @@ def friends(self) -> List[str]: admin_filter: AdminFilterSettings = AdminFilterSettings() chat_binder: ChatBinderSettings = ChatBinderSettings() llm_provider: LLMProviderSettings = LLMProviderSettings() + contact_manager: ContactManagerSettings = ContactManagerSettings() class Config: env_prefix = "BOTSPOT_" diff --git a/botspot/core/dependency_manager.py b/botspot/core/dependency_manager.py index 88bf491..0a8aacc 100644 --- a/botspot/core/dependency_manager.py +++ b/botspot/core/dependency_manager.py @@ -15,6 +15,7 @@ from botspot.components.data.user_data import UserManager from botspot.components.main.telethon_manager import TelethonManager + from botspot.components.new.contact_manager import ContactManager from botspot.components.new.llm_provider import LLMProvider @@ -38,6 +39,7 @@ def __init__( self._user_manager = None self._chat_binder = None self._llm_provider = None + self._contact_manager = None self.__dict__.update(kwargs) @property @@ -137,6 +139,16 @@ def llm_provider(self) -> "LLMProvider": @llm_provider.setter def llm_provider(self, value): self._llm_provider = value + + @property + def contact_manager(self) -> "ContactManager": + if self._contact_manager is None: + raise RuntimeError("Contact Manager is not initialized") + return self._contact_manager + + @contact_manager.setter + def contact_manager(self, value): + self._contact_manager = value @classmethod def is_initialized(cls) -> bool: diff --git a/botspot/utils/deps_getters.py b/botspot/utils/deps_getters.py index b508dc4..3fac394 100644 --- a/botspot/utils/deps_getters.py +++ b/botspot/utils/deps_getters.py @@ -12,6 +12,8 @@ from botspot.components.data.user_data import get_user_manager from botspot.components.main.event_scheduler import get_scheduler from botspot.components.main.telethon_manager import get_telethon_manager +from botspot.components.new.contact_manager import get_contact_manager +from botspot.components.new.llm_provider import get_llm_provider if TYPE_CHECKING: from aiogram import Bot, Dispatcher @@ -70,4 +72,6 @@ async def get_telethon_client( "get_telethon_manager", "get_telethon_client", "get_mongo_client", + "get_contact_manager", + "get_llm_provider", ] diff --git a/examples/components_examples/contact_manager_demo/README.md b/examples/components_examples/contact_manager_demo/README.md new file mode 100644 index 0000000..5278b13 --- /dev/null +++ b/examples/components_examples/contact_manager_demo/README.md @@ -0,0 +1,53 @@ +# Contact Manager Demo + +This example demonstrates how to use the Contact Manager component to create a simple contact management bot. + +## Features + +- Add contacts with a basic schema (name, phone, email, telegram, birthday) +- Auto-parse contact information from regular messages using LLM +- Search contacts by name, email, phone, or other fields +- Get random contacts from your saved list + +## Setup + +1. Copy `sample.env` to `.env` and update with your settings: + ```bash + cp sample.env .env + ``` + +2. Edit `.env` file to add your Telegram bot token and other settings + +3. Make sure MongoDB is running (required for this component) + +4. Install dependencies: + ```bash + poetry install + ``` + +5. Run the bot: + ```bash + python bot.py + ``` + +## Usage + +- `/add_contact [contact info]` - Add a new contact + Example: `/add_contact John Doe, phone: 555-1234, email: john@example.com` + +- `/find_contact [search term]` - Find contacts matching search term + Example: `/find_contact John` + +- `/random_contact` - Get a random contact from your saved list + +- The bot will also automatically detect potential contact information in regular messages and offer to save it. + +## Behind the Scenes + +This demo uses: +- **MongoDB** for storing contacts +- **LLM Provider** for parsing contact details from text +- **User Data** for managing user access +- **Bot Commands Menu** for registering commands + +The Contact Manager component is designed to be extensible for more advanced CRM-like functionality in the future. \ No newline at end of file diff --git a/examples/components_examples/contact_manager_demo/bot.py b/examples/components_examples/contact_manager_demo/bot.py new file mode 100644 index 0000000..7cb07a0 --- /dev/null +++ b/examples/components_examples/contact_manager_demo/bot.py @@ -0,0 +1,58 @@ +import asyncio +import logging +import os + +from aiogram import Bot, Dispatcher +from aiogram.enums import ParseMode +from aiogram.filters import Command +from aiogram.types import Message +from dotenv import load_dotenv + +from botspot.core.bot_manager import BotManager +from botspot.utils.deps_getters import get_contact_manager + + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Load environment variables +load_dotenv() + + +async def main(): + # Initialize Bot and Dispatcher + bot = Bot(token=os.getenv("BOT_TOKEN"), parse_mode=ParseMode.MARKDOWN) + dp = Dispatcher() + + # Initialize BotManager with our bot and dispatcher + manager = BotManager(bot=bot, dispatcher=dp) + + # Set up the dispatcher with all enabled components + manager.setup_dispatcher(dp) + + # Add custom command handler (optional, Contact Manager already adds its own handlers) + @dp.message(Command("contacts_help")) + async def contacts_help_cmd(message: Message): + help_text = """ +📇 **Contact Manager Help** + +This bot helps you manage your contacts. You can: + +- `/add_contact [info]` - Add a new contact + Example: `/add_contact John Doe, phone: 555-1234, email: john@example.com` + +- `/find_contact [search]` - Find contacts + Example: `/find_contact John` + +- `/random_contact` - Get a random contact + +You can also simply send a message with contact info, and I'll offer to save it! + """ + await message.reply(help_text) + + # Start the bot + await dp.start_polling(bot) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/components_examples/contact_manager_demo/sample.env b/examples/components_examples/contact_manager_demo/sample.env new file mode 100644 index 0000000..77b444b --- /dev/null +++ b/examples/components_examples/contact_manager_demo/sample.env @@ -0,0 +1,37 @@ +# Bot token (required) +BOT_TOKEN=your_bot_token_here + +# MongoDB settings (required for Contact Manager) +BOTSPOT_MONGO_DATABASE_ENABLED=true +BOTSPOT_MONGO_DATABASE_CONN_STR=mongodb://localhost:27017 +BOTSPOT_MONGO_DATABASE_DATABASE=botspot_contacts + +# User data settings (required for Contact Manager) +BOTSPOT_USER_DATA_ENABLED=true +BOTSPOT_USER_DATA_COLLECTION=users + +# Contact Manager settings +BOTSPOT_CONTACT_MANAGER_ENABLED=true +BOTSPOT_CONTACT_MANAGER_COLLECTION=contacts +BOTSPOT_CONTACT_MANAGER_MESSAGE_PARSER_ENABLED=true +BOTSPOT_CONTACT_MANAGER_RANDOM_CONTACT_ENABLED=true +BOTSPOT_CONTACT_MANAGER_ALLOW_EVERYONE=true + +# LLM Provider settings (required for Contact Manager) +BOTSPOT_LLM_PROVIDER_ENABLED=true +BOTSPOT_LLM_PROVIDER_DEFAULT_MODEL=claude-3.7 +BOTSPOT_LLM_PROVIDER_ALLOW_EVERYONE=true + +# Admin and friends settings +BOTSPOT_ADMINS_STR=@your_username +BOTSPOT_FRIENDS_STR= + +# Bot Commands Menu +BOTSPOT_BOT_COMMANDS_MENU_ENABLED=true + +# QoL Components +BOTSPOT_BOT_INFO_ENABLED=true +BOTSPOT_PRINT_BOT_URL_ENABLED=true + +# Error Handler +BOTSPOT_ERROR_HANDLING_ENABLED=true \ No newline at end of file