Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
643 changes: 635 additions & 8 deletions botspot/components/new/contact_manager.py

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions botspot/components/new/queue_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,110 @@ async def add_item(self, item: T, user_id: Optional[int] = None):
doc = self.enrich_item(item, user_id)
await self.collection.insert_one(doc)

async def update_record(self, record_id: Any, data: Dict[str, Any]) -> bool:
"""
Update a record by its ID.

Args:
record_id: The record ID to update
data: Dictionary containing the fields to update

Returns:
True if update was successful, False otherwise
"""
try:
# Update timestamp if enabled
if self.use_timestamp:
data["timestamp"] = datetime.now()

# Update the document
result = await self.collection.update_one(
{"_id": record_id},
{"$set": data}
)
return result.modified_count > 0
except Exception as e:
logger.error(f"Error updating record: {e}")
return False

async def find(self, query: Dict[str, Any], user_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
"""
Find a single record matching the given query.

Args:
query: MongoDB query dictionary
user_id: Optional user ID to filter by

Returns:
The record as a dictionary if found, None otherwise
"""
try:
if not self.single_user_mode and user_id is None:
from botspot.core.errors import QueuePermissionError
raise QueuePermissionError("user_id is required unless single_user_mode is enabled")

if user_id is not None:
query["user_id"] = user_id

return await self.collection.find_one(query)
except Exception as e:
logger.error(f"Error finding record: {e}")
return None

async def find_many(self, query: Dict[str, Any], user_id: Optional[int] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Find multiple records matching the given query.

Args:
query: MongoDB query dictionary
user_id: Optional user ID to filter by
limit: Maximum number of records to return

Returns:
List of records as dictionaries
"""
try:
if not self.single_user_mode and user_id is None:
from botspot.core.errors import QueuePermissionError
raise QueuePermissionError("user_id is required unless single_user_mode is enabled")

if user_id is not None:
query["user_id"] = user_id

cursor = self.collection.find(query)
if limit is not None:
cursor = cursor.limit(limit)
return await cursor.to_list(length=limit)
except Exception as e:
logger.error(f"Error finding records: {e}")
return []

async def delete_record(self, record_id: Any, user_id: Optional[int] = None) -> bool:
"""
Delete a record by its ID.

Args:
record_id: The record ID to delete
user_id: Optional user ID to verify ownership

Returns:
True if deletion was successful, False otherwise
"""
try:
if not self.single_user_mode and user_id is None:
from botspot.core.errors import QueuePermissionError
raise QueuePermissionError("user_id is required unless single_user_mode is enabled")

query = {"_id": record_id}
if user_id is not None:
query["user_id"] = user_id

result = await self.collection.delete_one(query)
return result.deleted_count > 0
except Exception as e:
logger.error(f"Error deleting record: {e}")
return False

async def get_items(
self,
user_id: Optional[int] = None,
Expand Down Expand Up @@ -123,6 +227,41 @@ async def get_records(
cursor = cursor.limit(limit)
return await cursor.to_list(length=limit)

async def get_random_record(self, user_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
"""
Get a random record from the queue.

Args:
user_id: Optional user ID to filter by

Returns:
A random record as a dictionary, or None if no records found
"""
try:
if not self.single_user_mode and user_id is None:
from botspot.core.errors import QueuePermissionError
raise QueuePermissionError("user_id is required unless single_user_mode is enabled")

# Build the pipeline
pipeline = []

# Add user filter if needed
if user_id is not None:
pipeline.append({"$match": {"user_id": user_id}})

# Add sample stage to get a random document
pipeline.append({"$sample": {"size": 1}})

# Execute the aggregation
cursor = self.collection.aggregate(pipeline)
async for doc in cursor:
return doc

return None
except Exception as e:
logger.error(f"Error getting random record: {e}")
return None


class QueueManager:
def __init__(self, settings: QueueManagerSettings, single_user_mode: Optional[bool] = None):
Expand Down
30 changes: 30 additions & 0 deletions botspot/contact_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from botspot.components.new.contact_manager import (
add_contact,
delete_contact,
find_contacts,
get_contact_by_id,
get_random_contact,
parse_contact_with_llm,
search_contacts,
update_contact,
get_contacts_by_owner,
get_contacts_by_telegram,
get_contacts_by_phone,
get_contacts_by_email,
)

__all__ = [
"add_contact",
"update_contact",
"delete_contact",
"get_contact_by_id",
"find_contacts",
"search_contacts",
"get_random_contact",
"parse_contact_with_llm",
"get_contacts_by_owner",
"get_contacts_by_telegram",
"get_contacts_by_phone",
"get_contacts_by_email",
]

7 changes: 7 additions & 0 deletions botspot/core/bot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
auto_archive,
chat_binder,
chat_fetcher,
contact_manager,
llm_provider,
queue_manager,
)
Expand Down Expand Up @@ -66,6 +67,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)

if self.settings.queue_manager.enabled:
self.deps.queue_manager = queue_manager.initialize(self.settings.queue_manager)
Expand Down Expand Up @@ -120,6 +124,9 @@ 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)

if self.settings.queue_manager.enabled:
queue_manager.setup_dispatcher(dp)
Expand Down
2 changes: 2 additions & 0 deletions botspot/core/botspot_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from botspot.components.new.auto_archive import AutoArchiveSettings
from botspot.components.new.chat_binder import ChatBinderSettings
from botspot.components.new.chat_fetcher import ChatFetcherSettings
from botspot.components.new.contact_manager import ContactManagerSettings
from botspot.components.new.llm_provider import LLMProviderSettings
from botspot.components.new.queue_manager import QueueManagerSettings
from botspot.components.qol.bot_commands_menu import BotCommandsMenuSettings
Expand Down Expand Up @@ -63,6 +64,7 @@ def friends(self) -> List[str]:
chat_binder: ChatBinderSettings = ChatBinderSettings()
chat_fetcher: ChatFetcherSettings = ChatFetcherSettings()
llm_provider: LLMProviderSettings = LLMProviderSettings()
contact_manager: ContactManagerSettings = ContactManagerSettings()
queue_manager: QueueManagerSettings = QueueManagerSettings()
auto_archive: AutoArchiveSettings = AutoArchiveSettings()

Expand Down
12 changes: 12 additions & 0 deletions botspot/core/dependency_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from botspot.components.new.auto_archive import AutoArchive
from botspot.components.new.chat_binder import ChatBinder
from botspot.components.new.chat_fetcher import ChatFetcher
from botspot.components.new.contact_manager import ContactManager
from botspot.components.new.llm_provider import LLMProvider
from botspot.components.new.queue_manager import QueueManager

Expand All @@ -39,6 +40,7 @@ def __init__(
self._user_manager = None
self._chat_binder = None
self._llm_provider = None
self._contact_manager = None
self._queue_manager = None
self._chat_fetcher = None
self._auto_archive = None
Expand Down Expand Up @@ -141,6 +143,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

@property
def queue_manager(self) -> "QueueManager":
Expand Down
2 changes: 2 additions & 0 deletions botspot/utils/deps_getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from botspot.components.main.telethon_manager import get_telethon_manager
from botspot.components.new.chat_binder import get_chat_binder
from botspot.components.new.chat_fetcher import get_chat_fetcher
from botspot.components.new.contact_manager import get_contact_manager
from botspot.components.new.llm_provider import get_llm_provider
from botspot.components.new.queue_manager import get_queue_manager

Expand Down Expand Up @@ -78,6 +79,7 @@ async def get_telethon_client(
"get_mongo_client",
"get_chat_binder",
"get_chat_fetcher",
"get_contact_manager",
"get_queue_manager",
"get_llm_provider",
]
58 changes: 58 additions & 0 deletions dev/workalong_contact_manager/rework_protocol.md
Original file line number Diff line number Diff line change
@@ -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

21 changes: 21 additions & 0 deletions dev/workalong_contact_manager/spec.md
Original file line number Diff line number Diff line change
@@ -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
Loading