letschatty 0.4.341__py3-none-any.whl → 0.4.342__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letschatty/models/ai_microservices/__init__.py +3 -3
- letschatty/models/ai_microservices/expected_output.py +29 -2
- letschatty/models/ai_microservices/lambda_events.py +137 -28
- letschatty/models/ai_microservices/lambda_invokation_types.py +3 -1
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +3 -1
- letschatty/models/analytics/events/__init__.py +3 -2
- letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +71 -0
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +13 -69
- letschatty/models/analytics/events/company_based_events/asset_events.py +2 -9
- letschatty/models/analytics/events/event_type_to_classes.py +3 -6
- letschatty/models/analytics/events/event_types.py +50 -9
- letschatty/models/chat/chat.py +2 -13
- letschatty/models/chat/chat_with_assets.py +1 -6
- letschatty/models/chat/client.py +2 -0
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +33 -365
- letschatty/models/company/__init__.py +1 -2
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +4 -0
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
- letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +1 -0
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +14 -2
- letschatty/models/company/assets/automation.py +19 -10
- letschatty/models/company/assets/chat_assets.py +2 -3
- letschatty/models/company/assets/company_assets.py +0 -2
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/data_base/collection_interface.py +101 -29
- letschatty/models/data_base/mongo_connection.py +92 -9
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +2 -4
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +3 -4
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +14 -1
- letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +5 -2
- letschatty/services/chat/chat_service.py +11 -47
- letschatty/services/chatty_assets/__init__.py +12 -0
- letschatty/services/chatty_assets/asset_service.py +190 -13
- letschatty/services/chatty_assets/assets_collections.py +137 -0
- letschatty/services/chatty_assets/base_container.py +3 -2
- letschatty/services/chatty_assets/base_container_with_collection.py +35 -26
- letschatty/services/chatty_assets/collections/__init__.py +38 -0
- letschatty/services/chatty_assets/collections/ai_agent_collection.py +19 -0
- letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +32 -0
- letschatty/services/chatty_assets/collections/ai_component_collection.py +21 -0
- letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +30 -0
- letschatty/services/chatty_assets/collections/chat_collection.py +21 -0
- letschatty/services/chatty_assets/collections/contact_point_collection.py +21 -0
- letschatty/services/chatty_assets/collections/fast_answer_collection.py +21 -0
- letschatty/services/chatty_assets/collections/filter_criteria_collection.py +18 -0
- letschatty/services/chatty_assets/collections/flow_collection.py +20 -0
- letschatty/services/chatty_assets/collections/product_collection.py +20 -0
- letschatty/services/chatty_assets/collections/sale_collection.py +20 -0
- letschatty/services/chatty_assets/collections/source_collection.py +21 -0
- letschatty/services/chatty_assets/collections/tag_collection.py +19 -0
- letschatty/services/chatty_assets/collections/topic_collection.py +21 -0
- letschatty/services/chatty_assets/collections/user_collection.py +20 -0
- letschatty/services/chatty_assets/example_usage.py +44 -0
- letschatty/services/chatty_assets/services/__init__.py +37 -0
- letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +73 -0
- letschatty/services/chatty_assets/services/ai_agent_service.py +23 -0
- letschatty/services/chatty_assets/services/chain_of_thought_service.py +70 -0
- letschatty/services/chatty_assets/services/chat_service.py +25 -0
- letschatty/services/chatty_assets/services/contact_point_service.py +29 -0
- letschatty/services/chatty_assets/services/fast_answer_service.py +32 -0
- letschatty/services/chatty_assets/services/filter_criteria_service.py +30 -0
- letschatty/services/chatty_assets/services/flow_service.py +25 -0
- letschatty/services/chatty_assets/services/product_service.py +30 -0
- letschatty/services/chatty_assets/services/sale_service.py +25 -0
- letschatty/services/chatty_assets/services/source_service.py +28 -0
- letschatty/services/chatty_assets/services/tag_service.py +32 -0
- letschatty/services/chatty_assets/services/topic_service.py +31 -0
- letschatty/services/chatty_assets/services/user_service.py +32 -0
- letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +11 -0
- letschatty/services/events/__init__.py +6 -0
- letschatty/services/events/events_manager.py +218 -1
- letschatty/services/factories/analytics/ai_agent_event_factory.py +161 -0
- letschatty/services/factories/analytics/events_factory.py +66 -6
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +25 -8
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +6 -4
- letschatty/services/validators/analytics_validator.py +0 -11
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/METADATA +1 -1
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/RECORD +82 -46
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/LICENSE +0 -0
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/WHEEL +0 -0
|
@@ -10,7 +10,7 @@ from datetime import datetime
|
|
|
10
10
|
from zoneinfo import ZoneInfo
|
|
11
11
|
from bson import ObjectId
|
|
12
12
|
import json
|
|
13
|
-
from typing import Dict, Any, Optional
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
14
|
from letschatty.models.utils.types.serializer_type import SerializerType
|
|
15
15
|
from letschatty.models.company.assets.ai_agents_v2.chatty_ai_mode import ChattyAIMode
|
|
16
16
|
|
|
@@ -67,8 +67,7 @@ class AssignedAssetToChat(BaseModel):
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
class SaleAssignedToChat(AssignedAssetToChat):
|
|
70
|
-
product_id:
|
|
71
|
-
product_ids: List[StrObjectId] = Field(default_factory=list)
|
|
70
|
+
product_id: StrObjectId = Field(frozen=True)
|
|
72
71
|
|
|
73
72
|
class ContactPointAssignedToChat(AssignedAssetToChat):
|
|
74
73
|
source_id: StrObjectId = Field(frozen=True)
|
|
@@ -5,7 +5,7 @@ from ...utils.types.identifier import StrObjectId
|
|
|
5
5
|
|
|
6
6
|
class Sale(CompanyAssetModel):
|
|
7
7
|
chat_id: StrObjectId
|
|
8
|
-
product_id:
|
|
8
|
+
product_id: StrObjectId
|
|
9
9
|
quantity: int
|
|
10
10
|
total_amount: float
|
|
11
11
|
currency: str
|
|
@@ -16,7 +16,7 @@ class Sale(CompanyAssetModel):
|
|
|
16
16
|
creator_id: StrObjectId
|
|
17
17
|
|
|
18
18
|
class SaleRequest(BaseModel):
|
|
19
|
-
product_id:
|
|
19
|
+
product_id: StrObjectId
|
|
20
20
|
quantity: int
|
|
21
21
|
creator_id: StrObjectId
|
|
22
22
|
total_amount: float
|
|
@@ -36,4 +36,4 @@ class SaleRequest(BaseModel):
|
|
|
36
36
|
"paid_amount": 100,
|
|
37
37
|
"installments": 1,
|
|
38
38
|
"details": {}
|
|
39
|
-
}
|
|
39
|
+
}
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Dict, List, Generic, TypeVar, Type, Optional,
|
|
|
4
4
|
from bson.objectid import ObjectId
|
|
5
5
|
from pymongo.collection import Collection
|
|
6
6
|
from pymongo.database import Database
|
|
7
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorCollection
|
|
7
8
|
|
|
8
9
|
from letschatty.models.chat.chat import Chat
|
|
9
10
|
from ...models.base_models.chatty_asset_model import ChattyAssetModel, CompanyAssetModel, ChattyAssetPreview
|
|
@@ -26,58 +27,116 @@ P = TypeVar('P', bound=ChattyAssetPreview)
|
|
|
26
27
|
class ChattyAssetCollectionInterface(Generic[T, P], ABC):
|
|
27
28
|
def __init__(self, database: str, collection: str, connection: MongoConnection, type: Type[T], preview_type: Optional[Type[P]] = None):
|
|
28
29
|
logger.info(f"Initializing collection {collection} in database {database}")
|
|
30
|
+
# Sync database and collection (existing)
|
|
29
31
|
self.db: Database = connection.client[database]
|
|
30
32
|
self.collection: Collection = connection.client[database][collection]
|
|
33
|
+
|
|
34
|
+
# NEW: Async database and collection
|
|
35
|
+
# Store connection reference to ensure we use current event loop
|
|
36
|
+
self._connection = connection
|
|
37
|
+
self._database_name = database
|
|
38
|
+
self._collection_name = collection
|
|
39
|
+
self._async_db: Optional[AsyncIOMotorDatabase] = None
|
|
40
|
+
self._async_collection: Optional[AsyncIOMotorCollection] = None
|
|
41
|
+
|
|
31
42
|
self.type = type
|
|
32
43
|
self.preview_type = preview_type
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def async_db(self) -> AsyncIOMotorDatabase:
|
|
47
|
+
"""Get async database, ensuring it uses the current event loop"""
|
|
48
|
+
# Always ensure connection's async client is using current loop (for Lambda compatibility)
|
|
49
|
+
self._connection._ensure_async_client_loop()
|
|
50
|
+
# Recreate database reference to ensure it uses the current client
|
|
51
|
+
self._async_db = self._connection.async_client[self._database_name]
|
|
52
|
+
return self._async_db
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def async_collection(self) -> AsyncIOMotorCollection:
|
|
56
|
+
"""Get async collection, ensuring it uses the current event loop"""
|
|
57
|
+
# Always ensure connection's async client is using current loop (for Lambda compatibility)
|
|
58
|
+
self._connection._ensure_async_client_loop()
|
|
59
|
+
# Recreate collection reference to ensure it uses the current client
|
|
60
|
+
self._async_collection = self._connection.async_client[self._database_name][self._collection_name]
|
|
61
|
+
return self._async_collection
|
|
33
62
|
@abstractmethod
|
|
34
63
|
def create_instance(self, data: dict) -> T:
|
|
35
64
|
"""Factory method to create instance from data"""
|
|
36
65
|
pass
|
|
37
66
|
|
|
38
|
-
|
|
67
|
+
# All methods are now async-only for better performance
|
|
68
|
+
async def insert(self, asset: T) -> StrObjectId:
|
|
69
|
+
"""Async insert operation"""
|
|
39
70
|
if not isinstance(asset, self.type):
|
|
40
71
|
raise ValueError(f"Asset must be of type {self.type.__name__}")
|
|
41
72
|
document = asset.model_dump_json(serializer=SerializerType.DATABASE)
|
|
42
73
|
logger.debug(f"Inserting document: {document}")
|
|
43
|
-
result = self.
|
|
74
|
+
result = await self.async_collection.insert_one(document)
|
|
44
75
|
if not result.inserted_id:
|
|
45
76
|
raise Exception("Failed to insert document")
|
|
46
77
|
logger.debug(f"Inserted document with id {result.inserted_id}")
|
|
47
78
|
return result.inserted_id
|
|
48
79
|
|
|
49
|
-
def update(self, asset: T) -> StrObjectId:
|
|
80
|
+
async def update(self, asset: T) -> StrObjectId:
|
|
81
|
+
"""Async update operation"""
|
|
50
82
|
logger.debug(f"Updating document with id {asset.id}")
|
|
51
83
|
if not isinstance(asset, self.type):
|
|
52
84
|
raise ValueError(f"Asset must be of type {self.type.__name__}")
|
|
53
85
|
asset.update_now()
|
|
54
86
|
document = asset.model_dump_json(serializer=SerializerType.DATABASE)
|
|
55
|
-
document.pop('_id', None)
|
|
56
|
-
result = self.
|
|
87
|
+
document.pop('_id', None)
|
|
88
|
+
result = await self.async_collection.update_one(
|
|
89
|
+
{"_id": ObjectId(asset.id)},
|
|
90
|
+
{"$set": document}
|
|
91
|
+
)
|
|
57
92
|
if result.matched_count == 0:
|
|
58
93
|
raise NotFoundError(f"No document found with id {asset.id}")
|
|
59
94
|
if result.modified_count == 0:
|
|
60
95
|
logger.debug(f"No changes were made to the document with id {asset.id} probably because the values were the same")
|
|
61
96
|
return asset.id
|
|
62
97
|
|
|
63
|
-
def get_by_id(self, doc_id: str) -> T:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
async def get_by_id(self, doc_id: str) -> T:
|
|
99
|
+
"""Get by ID operation"""
|
|
100
|
+
logger.debug(f"Getting document with id {doc_id} from collection {self.async_collection.name}")
|
|
101
|
+
doc = await self.async_collection.find_one({"_id": ObjectId(doc_id)})
|
|
67
102
|
if doc:
|
|
68
103
|
return self.create_instance(doc)
|
|
69
104
|
else:
|
|
70
|
-
raise NotFoundError(f"No document found with id {doc_id} in
|
|
105
|
+
raise NotFoundError(f"No document found with id {doc_id} in collection")
|
|
71
106
|
|
|
72
|
-
def get_docs(self, company_id:Optional[StrObjectId], query
|
|
73
|
-
|
|
107
|
+
async def get_docs(self, company_id: Optional[StrObjectId], query={}, limit=0) -> List[T]:
|
|
108
|
+
"""Get multiple documents operation"""
|
|
109
|
+
logger.debug(f"Getting documents from collection with company_id {company_id} and query {query}")
|
|
74
110
|
if company_id:
|
|
75
|
-
query = query.copy()
|
|
111
|
+
query = query.copy()
|
|
76
112
|
query["company_id"] = company_id
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
cursor = self.async_collection.find(filter=query)
|
|
114
|
+
if limit:
|
|
115
|
+
cursor = cursor.limit(limit)
|
|
116
|
+
docs = await cursor.to_list(length=limit if limit > 0 else None)
|
|
117
|
+
logger.debug(f"Found {len(docs)} documents")
|
|
79
118
|
return [self.create_instance(doc) for doc in docs]
|
|
80
119
|
|
|
120
|
+
async def delete(self, doc_id: str, deletion_type: DeletionType = DeletionType.LOGICAL) -> StrObjectId:
|
|
121
|
+
"""Delete operation"""
|
|
122
|
+
logger.debug(f"Deleting document with id {doc_id} - deletion type: {deletion_type}")
|
|
123
|
+
if deletion_type == DeletionType.LOGICAL:
|
|
124
|
+
result = await self.async_collection.update_one(
|
|
125
|
+
{"_id": ObjectId(doc_id)},
|
|
126
|
+
{"$set": {"deleted_at": datetime.now(ZoneInfo("UTC")), "updated_at": datetime.now(ZoneInfo("UTC"))}}
|
|
127
|
+
)
|
|
128
|
+
if result.modified_count == 0:
|
|
129
|
+
raise NotFoundError(f"No document found with id {doc_id}")
|
|
130
|
+
return doc_id
|
|
131
|
+
elif deletion_type == DeletionType.PHYSICAL:
|
|
132
|
+
result = await self.async_collection.delete_one({"_id": ObjectId(doc_id)})
|
|
133
|
+
if result.deleted_count == 0:
|
|
134
|
+
raise NotFoundError(f"No document found with id {doc_id}")
|
|
135
|
+
return doc_id
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"Invalid deletion type: {deletion_type}")
|
|
138
|
+
|
|
139
|
+
# Additional methods - keeping these sync as they're less critical
|
|
81
140
|
def get_preview_docs(self, projection = {}, all=True) -> List[P]:
|
|
82
141
|
"""We get the previews of all the documents in the collection for all companies"""
|
|
83
142
|
if not self.preview_type:
|
|
@@ -98,18 +157,31 @@ class ChattyAssetCollectionInterface(Generic[T, P], ABC):
|
|
|
98
157
|
docs = self.collection.find(query)
|
|
99
158
|
return [self.create_instance(doc) for doc in docs]
|
|
100
159
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
160
|
+
async def get_by_ids(self, ids: List[StrObjectId]) -> List[T]:
|
|
161
|
+
"""
|
|
162
|
+
Get multiple assets by their IDs in a single query.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
ids: List of asset IDs
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of assets objects
|
|
169
|
+
"""
|
|
170
|
+
if not ids:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
# Convert string IDs to ObjectIds
|
|
174
|
+
object_ids = [ObjectId(id) for id in ids]
|
|
175
|
+
|
|
176
|
+
# Query for all filter criteria with matching IDs
|
|
177
|
+
query = {
|
|
178
|
+
"_id": {"$in": object_ids},
|
|
179
|
+
"deleted_at": None
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Use the sync collection directly (inherited from ChattyAssetCollectionInterface)
|
|
183
|
+
docs = await self.async_collection.find(query).to_list(length=None)
|
|
184
|
+
|
|
185
|
+
# Create FilterCriteria instances
|
|
186
|
+
return [self.create_instance(doc) for doc in docs]
|
|
115
187
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
from ..base_models.singleton import SingletonMeta
|
|
2
2
|
from pymongo import MongoClient
|
|
3
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
3
4
|
from typing import Optional
|
|
5
|
+
from urllib.parse import quote_plus
|
|
4
6
|
import os
|
|
5
7
|
import atexit
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
6
12
|
|
|
7
13
|
class MongoConnection(metaclass=SingletonMeta):
|
|
8
14
|
def __init__(
|
|
@@ -10,26 +16,103 @@ class MongoConnection(metaclass=SingletonMeta):
|
|
|
10
16
|
username: Optional[str] = None,
|
|
11
17
|
password: Optional[str] = None,
|
|
12
18
|
uri_base: Optional[str] = None,
|
|
13
|
-
instance: Optional[str] = None
|
|
19
|
+
instance: Optional[str] = None,
|
|
20
|
+
verify_on_init: bool = True
|
|
14
21
|
):
|
|
15
22
|
self.username = username or os.getenv('MONGO_USERNAME')
|
|
16
23
|
self.password = password or os.getenv('MONGO_PASSWORD')
|
|
17
24
|
self.uri_base = uri_base or os.getenv('MONGO_URI_BASE')
|
|
18
25
|
self.instance = instance or os.getenv('MONGO_INSTANCE_COMPONENT')
|
|
19
|
-
|
|
26
|
+
|
|
20
27
|
if not all([self.username, self.password, self.uri_base, self.instance]):
|
|
21
28
|
raise ValueError("Missing required MongoDB connection parameters")
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
# URL-encode username and password to handle special characters per RFC 3986
|
|
31
|
+
encoded_username = quote_plus(self.username)
|
|
32
|
+
encoded_password = quote_plus(self.password)
|
|
33
|
+
|
|
34
|
+
uri = f"{self.uri_base}://{encoded_username}:{encoded_password}@{self.instance}.mongodb.net"
|
|
35
|
+
|
|
36
|
+
# Sync client (existing)
|
|
24
37
|
self.client = MongoClient(uri)
|
|
38
|
+
|
|
39
|
+
# NEW: Async client for async operations
|
|
40
|
+
# Don't pass io_loop - Motor will automatically use the current event loop
|
|
41
|
+
# This is important for Lambda where the event loop changes between invocations
|
|
42
|
+
self.async_client = AsyncIOMotorClient(uri)
|
|
43
|
+
|
|
44
|
+
# Verify connections if requested
|
|
45
|
+
if verify_on_init:
|
|
46
|
+
try:
|
|
47
|
+
# Try to get running loop
|
|
48
|
+
loop = asyncio.get_running_loop()
|
|
49
|
+
# If we get here, there's a running loop
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Event loop is already running. Skipping connection verification in __init__. "
|
|
52
|
+
"Call verify_connection_async() from async context to verify connection."
|
|
53
|
+
)
|
|
54
|
+
self._connection_verified = False
|
|
55
|
+
except RuntimeError:
|
|
56
|
+
# No running loop, safe to use run_until_complete
|
|
57
|
+
try:
|
|
58
|
+
# Test sync client
|
|
59
|
+
self.client.admin.command('ping')
|
|
60
|
+
|
|
61
|
+
# Test async client in sync context
|
|
62
|
+
loop = asyncio.new_event_loop()
|
|
63
|
+
asyncio.set_event_loop(loop)
|
|
64
|
+
loop.run_until_complete(self.async_client.admin.command('ping'))
|
|
65
|
+
self._connection_verified = True
|
|
66
|
+
loop.close()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
self.client.close()
|
|
69
|
+
self.async_client.close()
|
|
70
|
+
raise ConnectionError(f"Failed to connect to MongoDB: {str(e)}")
|
|
71
|
+
else:
|
|
72
|
+
self._connection_verified = False
|
|
73
|
+
|
|
74
|
+
atexit.register(self.close)
|
|
75
|
+
|
|
76
|
+
def _ensure_async_client_loop(self):
|
|
77
|
+
"""Ensure async client is using the current event loop (for Lambda compatibility)"""
|
|
25
78
|
try:
|
|
26
|
-
|
|
79
|
+
current_loop = asyncio.get_running_loop()
|
|
80
|
+
# Check if client's loop is closed or different
|
|
81
|
+
client_loop = getattr(self.async_client, '_io_loop', None)
|
|
82
|
+
if client_loop is not None:
|
|
83
|
+
try:
|
|
84
|
+
# Try to check if the loop is closed
|
|
85
|
+
if client_loop.is_closed():
|
|
86
|
+
# Recreate client with current loop
|
|
87
|
+
logger.warning("Async client's event loop is closed, recreating client")
|
|
88
|
+
old_client = self.async_client
|
|
89
|
+
uri = f"{self.uri_base}://{quote_plus(self.username)}:{quote_plus(self.password)}@{self.instance}.mongodb.net"
|
|
90
|
+
self.async_client = AsyncIOMotorClient(uri)
|
|
91
|
+
try:
|
|
92
|
+
old_client.close()
|
|
93
|
+
except:
|
|
94
|
+
pass
|
|
95
|
+
except AttributeError:
|
|
96
|
+
# _io_loop might not exist in newer Motor versions
|
|
97
|
+
pass
|
|
98
|
+
except RuntimeError:
|
|
99
|
+
# No running loop, which is fine - Motor will handle it
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
async def verify_connection_async(self) -> bool:
|
|
103
|
+
"""Verify MongoDB connection asynchronously. Safe to call from async context."""
|
|
104
|
+
try:
|
|
105
|
+
# Ensure we're using the current event loop
|
|
106
|
+
self._ensure_async_client_loop()
|
|
107
|
+
await self.async_client.admin.command('ping')
|
|
108
|
+
self._connection_verified = True
|
|
109
|
+
return True
|
|
27
110
|
except Exception as e:
|
|
28
|
-
|
|
29
|
-
raise ConnectionError(f"Failed to
|
|
111
|
+
logger.error(f"Failed to verify MongoDB connection: {e}")
|
|
112
|
+
raise ConnectionError(f"Failed to verify MongoDB connection: {e}")
|
|
30
113
|
|
|
31
|
-
atexit.register(self.close)
|
|
32
|
-
|
|
33
114
|
def close(self) -> None:
|
|
34
115
|
if hasattr(self, 'client'):
|
|
35
116
|
self.client.close()
|
|
117
|
+
if hasattr(self, 'async_client'):
|
|
118
|
+
self.async_client.close()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, model_validator, ValidationInfo
|
|
2
2
|
from typing import Optional
|
|
3
|
-
from urllib.parse import urlparse, unquote
|
|
4
3
|
from .content_media import ChattyContentMedia
|
|
5
4
|
|
|
6
5
|
class ChattyContentDocument(ChattyContentMedia):
|
|
@@ -9,6 +8,5 @@ class ChattyContentDocument(ChattyContentMedia):
|
|
|
9
8
|
@model_validator(mode='before')
|
|
10
9
|
def validate_filename(cls, data: dict, info: ValidationInfo):
|
|
11
10
|
if not data.get("filename") and data.get("url"):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return data
|
|
11
|
+
data["filename"] = data["url"].split("/")[-1]
|
|
12
|
+
return data
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator, HttpUrl
|
|
2
2
|
from typing import Optional
|
|
3
|
-
from urllib.parse import quote
|
|
4
3
|
class ChattyContentMedia(BaseModel):
|
|
5
4
|
id: Optional[str] = Field(description="Unique identifier for the image. Also known as media_id", default="")
|
|
6
5
|
url: str = Field(description="URL of the media from S3")
|
|
@@ -12,9 +11,9 @@ class ChattyContentMedia(BaseModel):
|
|
|
12
11
|
def validate_url(cls, v):
|
|
13
12
|
if not v:
|
|
14
13
|
raise ValueError("URL is required")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return encoded
|
|
14
|
+
HttpUrl(v)
|
|
15
|
+
return v
|
|
18
16
|
|
|
19
17
|
def get_body_or_caption(self) -> str:
|
|
20
18
|
return self.caption
|
|
19
|
+
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
from datetime import timedelta
|
|
6
6
|
|
|
7
7
|
from letschatty.models.utils.definitions import Area
|
|
8
|
+
from pydantic_core.core_schema import custom_error_schema
|
|
8
9
|
logger = logging.getLogger("logger")
|
|
9
10
|
|
|
10
11
|
class Context(BaseModel):
|
|
@@ -229,4 +230,16 @@ class OpenAIError(CustomException):
|
|
|
229
230
|
|
|
230
231
|
class NewerAvailableMessageToBeProcessedByAiAgent(CustomException):
|
|
231
232
|
def __init__(self, message="Duplicated incoming message call for ai agent", status_code=400, **context_data):
|
|
232
|
-
super().__init__(message, status_code=status_code, **context_data)
|
|
233
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
234
|
+
|
|
235
|
+
class MissingAIAgentInChat(CustomException):
|
|
236
|
+
def __init__(self, message="Missing AI agent in chat", status_code=400, **context_data):
|
|
237
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
238
|
+
|
|
239
|
+
class ChattyAIModeOff(CustomException):
|
|
240
|
+
def __init__(self, message="Chatty AI agent is in OFF mode", status_code=400, **context_data):
|
|
241
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
242
|
+
|
|
243
|
+
class ChatWithActiveContinuousConversation(CustomException):
|
|
244
|
+
def __init__(self, message="Chat has active continuous conversation", status_code=400, **context_data):
|
|
245
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
@@ -14,13 +14,16 @@ class SmartFollowUpContextBuilder(ContextBuilder):
|
|
|
14
14
|
|
|
15
15
|
@staticmethod
|
|
16
16
|
def check_minimum_time_since_last_message(chat: Chat, follow_up_strategy: FollowUpStrategy,smart_follow_up_state: FlowStateAssignedToChat) -> bool:
|
|
17
|
-
|
|
17
|
+
# consecutive_count is 0-indexed (0 = no follow-ups sent yet), but get_interval_for_followup expects 1-indexed
|
|
18
|
+
# So we add 1 to get the interval for the follow-up we're about to send
|
|
19
|
+
next_followup_number = smart_follow_up_state.consecutive_count + 1
|
|
20
|
+
expected_interval_minutes = follow_up_strategy.get_interval_for_followup(next_followup_number)
|
|
18
21
|
last_message_timestamp = chat.last_message_timestamp
|
|
19
22
|
if last_message_timestamp is None:
|
|
20
23
|
raise HumanInterventionRequired("There's no last message in the chat, can't validate the minimum time since last message for the smart follow up")
|
|
21
24
|
time_since_last_message = datetime.now(ZoneInfo('UTC')) - last_message_timestamp
|
|
22
25
|
if time_since_last_message.total_seconds() < expected_interval_minutes * 60:
|
|
23
|
-
raise PostponeFollowUp(time_delta= timedelta(seconds=expected_interval_minutes * 60 - time_since_last_message.total_seconds()), message=f"Se pospuso el Smart Follow Up porque no ha pasado el tiempo mínimo esperado de {expected_interval_minutes/60} horas para el seguimiento #{
|
|
26
|
+
raise PostponeFollowUp(time_delta= timedelta(seconds=expected_interval_minutes * 60 - time_since_last_message.total_seconds()), message=f"Se pospuso el Smart Follow Up porque no ha pasado el tiempo mínimo esperado de {expected_interval_minutes/60} horas para el seguimiento #{next_followup_number}")
|
|
24
27
|
return True
|
|
25
28
|
|
|
26
29
|
|
|
@@ -211,25 +211,6 @@ class ChatService:
|
|
|
211
211
|
ChatService.add_central_notification_from_text(chat=chat, body=f"Agente de IA {chatty_ai_agent.name} actualizado en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.CHATTY_AI_AGENT_UPDATED)
|
|
212
212
|
return chat.chatty_ai_agent
|
|
213
213
|
|
|
214
|
-
@staticmethod
|
|
215
|
-
def escalate_chatty_ai_agent(chat: Chat, execution_context: ExecutionContext, message: Optional[str] = None) -> None:
|
|
216
|
-
"""
|
|
217
|
-
Mark the chat's AI agent as requiring human intervention and add a central notification.
|
|
218
|
-
"""
|
|
219
|
-
if chat.chatty_ai_agent and not chat.chatty_ai_agent.requires_human_intervention:
|
|
220
|
-
chat.chatty_ai_agent.requires_human_intervention = True
|
|
221
|
-
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
222
|
-
body = "El chat fue escalado a un agente humano"
|
|
223
|
-
if message:
|
|
224
|
-
body = f"{body}: {message}"
|
|
225
|
-
ChatService.add_central_notification_from_text(
|
|
226
|
-
chat=chat,
|
|
227
|
-
body=body,
|
|
228
|
-
subtype=MessageSubtype.CHATTY_AI_AGENT_NOTIFICATION,
|
|
229
|
-
content_status=CentralNotificationStatus.WARNING,
|
|
230
|
-
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
231
|
-
)
|
|
232
|
-
|
|
233
214
|
@staticmethod
|
|
234
215
|
def add_workflow_link(chat : Chat, link : LinkItem, flow:FlowPreview, execution_context: ExecutionContext, description: str, last_incoming_message_id: Optional[str] = None, next_call: Optional[datetime] = None) -> FlowStateAssignedToChat:
|
|
235
216
|
"""
|
|
@@ -285,46 +266,36 @@ class ChatService:
|
|
|
285
266
|
return next((state for state in chat.flow_states if state.is_smart_follow_up), None)
|
|
286
267
|
|
|
287
268
|
@staticmethod
|
|
288
|
-
def create_sale(
|
|
289
|
-
chat: Chat,
|
|
290
|
-
execution_context: ExecutionContext,
|
|
291
|
-
sale: Sale,
|
|
292
|
-
product: Optional[Product],
|
|
293
|
-
product_ids: Optional[List[StrObjectId]] = None,
|
|
294
|
-
product_label: Optional[str] = None
|
|
295
|
-
) -> SaleAssignedToChat:
|
|
269
|
+
def create_sale(chat : Chat, execution_context: ExecutionContext, sale : Sale, product : Product) -> SaleAssignedToChat:
|
|
296
270
|
"""
|
|
297
271
|
Add a sale to the chat.
|
|
298
272
|
"""
|
|
299
273
|
if next((sale for sale in chat.client.sales if sale.asset_id == sale.id), None) is not None:
|
|
300
274
|
raise AssetAlreadyAssigned(f"Sale with id {sale.id} already assigned to chat {chat.id}")
|
|
301
|
-
label = product_label or (product.name if product else "multiples productos")
|
|
302
|
-
assigned_product_ids = product_ids or ([product.id] if product else [])
|
|
303
275
|
assigned_asset = SaleAssignedToChat(
|
|
304
276
|
asset_type=ChatAssetType.SALE,
|
|
305
277
|
asset_id=sale.id,
|
|
306
278
|
assigned_at=sale.created_at,
|
|
307
279
|
assigned_by=execution_context.executor.id,
|
|
308
|
-
product_id=product.id
|
|
309
|
-
product_ids=assigned_product_ids
|
|
280
|
+
product_id=product.id
|
|
310
281
|
)
|
|
311
282
|
execution_context.set_event_time(assigned_asset.assigned_at)
|
|
312
283
|
bisect.insort(chat.client.sales, assigned_asset)
|
|
313
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {
|
|
314
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
284
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {product.name}", description=f"Venta de {product.name} creada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_ADDED))
|
|
285
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} agregada al chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_ADDED)
|
|
315
286
|
return assigned_asset
|
|
316
287
|
|
|
317
288
|
@staticmethod
|
|
318
|
-
def update_sale(chat: Chat, execution_context: ExecutionContext, sale: Sale,
|
|
289
|
+
def update_sale(chat : Chat, execution_context: ExecutionContext, sale : Sale, product : Product) -> Sale:
|
|
319
290
|
"""
|
|
320
291
|
Update a sale for the chat.
|
|
321
292
|
"""
|
|
322
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {
|
|
323
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
293
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {product.name}", description=f"Venta de {product.name} actualizada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_UPDATED))
|
|
294
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} actualizada en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_UPDATED)
|
|
324
295
|
return sale
|
|
325
296
|
|
|
326
297
|
@staticmethod
|
|
327
|
-
def delete_sale(chat: Chat, execution_context: ExecutionContext, sale_id: StrObjectId,
|
|
298
|
+
def delete_sale(chat : Chat, execution_context: ExecutionContext, sale_id : StrObjectId, product : Product) -> SaleAssignedToChat:
|
|
328
299
|
"""
|
|
329
300
|
Logically remove a sale from the chat.
|
|
330
301
|
"""
|
|
@@ -332,8 +303,8 @@ class ChatService:
|
|
|
332
303
|
assigned_asset_to_remove = next(sale for sale in chat.client.sales if sale.asset_id == sale_id)
|
|
333
304
|
chat.client.sales.remove(assigned_asset_to_remove)
|
|
334
305
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
335
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {
|
|
336
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
306
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {product.name}", description=f"Venta de {product.name} eliminada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_DELETED))
|
|
307
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product.name} eliminada del chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_DELETED)
|
|
337
308
|
return assigned_asset_to_remove
|
|
338
309
|
except StopIteration:
|
|
339
310
|
raise NotFoundError(message=f"Sale with id {sale_id} not found in chat {chat.id}")
|
|
@@ -902,16 +873,9 @@ class ChatService:
|
|
|
902
873
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
903
874
|
logger.info(f"Updated collected data for chat {chat.id}: {', '.join(updated_fields)}")
|
|
904
875
|
|
|
905
|
-
field_label_map = {
|
|
906
|
-
"name": "nombre",
|
|
907
|
-
"email": "email",
|
|
908
|
-
"phone": "telefono",
|
|
909
|
-
"document_id": "dni",
|
|
910
|
-
}
|
|
911
|
-
display_fields = [field_label_map.get(field, field) for field in updated_fields]
|
|
912
876
|
ChatService.add_central_notification_from_text(
|
|
913
877
|
chat=chat,
|
|
914
|
-
body=f"
|
|
878
|
+
body=f"Collected customer data: {', '.join(updated_fields)}",
|
|
915
879
|
subtype=MessageSubtype.CLIENT_INFO_UPDATED,
|
|
916
880
|
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
917
881
|
)
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
from .base_container import ChattyAssetBaseContainer
|
|
2
2
|
from .base_container_with_collection import ChattyAssetContainerWithCollection
|
|
3
|
+
from .assets_collections import AssetsCollections
|
|
4
|
+
from .services import (
|
|
5
|
+
ProductService,
|
|
6
|
+
TagService,
|
|
7
|
+
UserService,
|
|
8
|
+
ChatService,
|
|
9
|
+
SourceService,
|
|
10
|
+
FlowService,
|
|
11
|
+
SaleService,
|
|
12
|
+
ContactPointService,
|
|
13
|
+
AiAgentService
|
|
14
|
+
)
|