letschatty 0.4.351__py3-none-any.whl → 0.4.352__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 +4 -4
- letschatty/models/ai_microservices/expected_output.py +2 -29
- letschatty/models/ai_microservices/lambda_events.py +28 -155
- letschatty/models/ai_microservices/lambda_invokation_types.py +1 -4
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +1 -3
- letschatty/models/analytics/events/__init__.py +3 -3
- letschatty/models/analytics/events/chat_based_events/chat_client.py +19 -0
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
- letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
- letschatty/models/analytics/events/event_type_to_classes.py +7 -3
- letschatty/models/analytics/events/event_types.py +11 -50
- letschatty/models/chat/chat.py +13 -2
- letschatty/models/chat/chat_with_assets.py +6 -1
- letschatty/models/chat/client.py +0 -2
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +365 -33
- letschatty/models/company/__init__.py +10 -1
- 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 +0 -4
- 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 +0 -1
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +1 -28
- letschatty/models/company/assets/automation.py +10 -19
- letschatty/models/company/assets/chat_assets.py +3 -2
- letschatty/models/company/assets/company_assets.py +2 -0
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/company/empresa.py +4 -1
- letschatty/models/company/integrations/product_sync_status.py +28 -0
- letschatty/models/company/integrations/shopify/company_shopify_integration.py +62 -0
- letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +18 -0
- letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +40 -0
- letschatty/models/company/integrations/sync_status_enum.py +9 -0
- letschatty/models/company/integrations/tienda_nube/company_tienda_nube_integration.py +62 -0
- letschatty/models/company/integrations/tienda_nube/tienda_nube_product_sync_status.py +18 -0
- letschatty/models/company/integrations/tienda_nube/tienda_nube_webhook_topics.py +46 -0
- letschatty/models/data_base/collection_interface.py +29 -101
- letschatty/models/data_base/mongo_connection.py +9 -92
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +1 -14
- letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +2 -5
- letschatty/services/chat/chat_service.py +47 -11
- letschatty/services/chatty_assets/__init__.py +0 -12
- letschatty/services/chatty_assets/asset_service.py +13 -190
- letschatty/services/chatty_assets/base_container.py +2 -3
- letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
- letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
- letschatty/services/events/events_manager.py +1 -218
- letschatty/services/factories/analytics/events_factory.py +30 -66
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -46
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +4 -6
- letschatty/services/validators/analytics_validator.py +11 -0
- {letschatty-0.4.351.dist-info → letschatty-0.4.352.dist-info}/METADATA +1 -1
- {letschatty-0.4.351.dist-info → letschatty-0.4.352.dist-info}/RECORD +56 -83
- letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
- letschatty/services/chatty_assets/assets_collections.py +0 -137
- letschatty/services/chatty_assets/collections/__init__.py +0 -38
- letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
- letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
- letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
- letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
- letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
- letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
- letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
- letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
- letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
- letschatty/services/chatty_assets/collections/product_collection.py +0 -20
- letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
- letschatty/services/chatty_assets/collections/source_collection.py +0 -21
- letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
- letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
- letschatty/services/chatty_assets/collections/user_collection.py +0 -20
- letschatty/services/chatty_assets/example_usage.py +0 -44
- letschatty/services/chatty_assets/services/__init__.py +0 -37
- letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
- letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
- letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
- letschatty/services/chatty_assets/services/chat_service.py +0 -25
- letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
- letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
- letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
- letschatty/services/chatty_assets/services/flow_service.py +0 -25
- letschatty/services/chatty_assets/services/product_service.py +0 -30
- letschatty/services/chatty_assets/services/sale_service.py +0 -25
- letschatty/services/chatty_assets/services/source_service.py +0 -28
- letschatty/services/chatty_assets/services/tag_service.py +0 -32
- letschatty/services/chatty_assets/services/topic_service.py +0 -31
- letschatty/services/chatty_assets/services/user_service.py +0 -32
- letschatty/services/events/__init__.py +0 -6
- letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
- {letschatty-0.4.351.dist-info → letschatty-0.4.352.dist-info}/LICENSE +0 -0
- {letschatty-0.4.351.dist-info → letschatty-0.4.352.dist-info}/WHEEL +0 -0
|
@@ -64,8 +64,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
64
64
|
self.update_previews_thread()
|
|
65
65
|
self.load_from_db_thread(company_id=None)
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
async def insert(self, item: T, execution_context: ExecutionContext) -> T:
|
|
67
|
+
def insert(self, item: T, execution_context: ExecutionContext) -> T:
|
|
69
68
|
"""
|
|
70
69
|
Add an item to the container and insert it into the database collection.
|
|
71
70
|
|
|
@@ -78,12 +77,12 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
78
77
|
"""
|
|
79
78
|
logger.debug(f"{self.__class__.__name__} inserting item {item}")
|
|
80
79
|
inserted_item = super().insert(item)
|
|
81
|
-
|
|
80
|
+
self.collection.insert(inserted_item)
|
|
82
81
|
execution_context.set_event_time(inserted_item.created_at)
|
|
83
82
|
self.update_previews_thread()
|
|
84
83
|
return inserted_item
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
def update(self, id: str, new_item: T, execution_context: ExecutionContext) -> T:
|
|
87
86
|
"""
|
|
88
87
|
Update an item in the container and in the database collection.
|
|
89
88
|
|
|
@@ -106,18 +105,18 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
106
105
|
if id != updated_item.id:
|
|
107
106
|
logger.error(f"Item id {id} does not match updated item id {updated_item.id}")
|
|
108
107
|
raise ValueError(f"Item id {id} does not match updated item id {updated_item.id}")
|
|
109
|
-
|
|
108
|
+
self.collection.update(updated_item)
|
|
110
109
|
execution_context.set_event_time(updated_item.updated_at)
|
|
111
110
|
self.update_preview(updated_item)
|
|
112
111
|
self.update_previews_thread()
|
|
113
112
|
return updated_item
|
|
114
113
|
|
|
115
114
|
except NotFoundError as e:
|
|
116
|
-
outdated_item =
|
|
115
|
+
outdated_item = self.collection.get_by_id(id)
|
|
117
116
|
if outdated_item:
|
|
118
117
|
updated_item = outdated_item.update(new_item)
|
|
119
118
|
self.items[id] = updated_item
|
|
120
|
-
|
|
119
|
+
self.collection.update(updated_item)
|
|
121
120
|
execution_context.set_event_time(updated_item.updated_at)
|
|
122
121
|
self.update_previews_thread()
|
|
123
122
|
return updated_item
|
|
@@ -126,7 +125,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
126
125
|
f"Item with id {id} not found in {self.__class__.__name__} nor in collection DB"
|
|
127
126
|
)
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
def delete(self, id: str, execution_context: ExecutionContext,deletion_type : DeletionType = DeletionType.LOGICAL) -> T:
|
|
130
129
|
"""
|
|
131
130
|
Delete an item from the container and the collection.
|
|
132
131
|
|
|
@@ -143,16 +142,16 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
143
142
|
deleted_item = super().delete(id)
|
|
144
143
|
self.delete_preview(id)
|
|
145
144
|
execution_context.set_event_time(datetime.now(ZoneInfo("UTC")))
|
|
146
|
-
|
|
145
|
+
self.collection.delete(id, deletion_type)
|
|
147
146
|
return deleted_item
|
|
148
147
|
except NotFoundError as e:
|
|
149
|
-
|
|
148
|
+
self.collection.delete(id, deletion_type)
|
|
150
149
|
self.delete_preview(id)
|
|
151
150
|
self.update_previews_thread()
|
|
152
151
|
execution_context.set_event_time(datetime.now(ZoneInfo("UTC")))
|
|
153
|
-
return
|
|
152
|
+
return self.collection.get_by_id(id)
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
def get_by_id(self, id: str) -> T:
|
|
156
155
|
"""
|
|
157
156
|
Get an item from the container.
|
|
158
157
|
|
|
@@ -175,7 +174,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
175
174
|
# #if they are supposed to be in memory, we raise an error since it shouldn't be in the collection DB
|
|
176
175
|
# raise NotFoundError(f"Item with id {id} not found in {self.__class__.__name__} nor in collection DB")
|
|
177
176
|
logger.debug(f"{self.__class__.__name__} getting item {id} not found in container, trying to get from collection")
|
|
178
|
-
item =
|
|
177
|
+
item = self.collection.get_by_id(id)
|
|
179
178
|
if item:
|
|
180
179
|
if item.deleted_at is not None:
|
|
181
180
|
return item
|
|
@@ -240,7 +239,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
240
239
|
logger.debug(f"Clearing previews cache of {self.__class__.__name__}")
|
|
241
240
|
self.set_preview_items([])
|
|
242
241
|
|
|
243
|
-
|
|
242
|
+
def get_all(self, company_id: Optional[StrObjectId]) -> List[T]:
|
|
244
243
|
# Get items from memory
|
|
245
244
|
logger.debug(f"{self.__class__.__name__} getting all items from memory and collection")
|
|
246
245
|
memory_items = super().get_all(company_id=company_id)
|
|
@@ -248,7 +247,7 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
248
247
|
memory_ids = [ObjectId(item.id) for item in memory_items]
|
|
249
248
|
# Build the query for collection items
|
|
250
249
|
query = {"deleted_at": None, "_id": {"$nin": memory_ids}}
|
|
251
|
-
collection_items =
|
|
250
|
+
collection_items = self.collection.get_docs(query=query, company_id=company_id)
|
|
252
251
|
all_items = memory_items + collection_items
|
|
253
252
|
return sorted(all_items, key=lambda x: x.created_at, reverse=True)
|
|
254
253
|
|
|
@@ -262,8 +261,6 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
262
261
|
def update_previews_thread(self):
|
|
263
262
|
"""We start a thread to update the previews cache so it doesn't block the main thread"""
|
|
264
263
|
# self.update_previews_cache()
|
|
265
|
-
if not self.cache_config.keep_previews_always_in_memory:
|
|
266
|
-
return
|
|
267
264
|
thread = threading.Thread(target=self.update_previews_cache)
|
|
268
265
|
thread.start()
|
|
269
266
|
|
|
@@ -278,13 +275,13 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
278
275
|
self.set_preview_items(collection_items)
|
|
279
276
|
return collection_items
|
|
280
277
|
|
|
281
|
-
|
|
278
|
+
def get_by_query(self, query: dict, company_id: Optional[StrObjectId]) -> List[T]:
|
|
282
279
|
logger.debug(f"{self.__class__.__name__} getting items by query {query} from collection")
|
|
283
|
-
return
|
|
280
|
+
return self.collection.get_docs(query=query, company_id=company_id)
|
|
284
281
|
|
|
285
|
-
|
|
282
|
+
def get_deleted(self, company_id: Optional[StrObjectId]) -> List[T]:
|
|
286
283
|
logger.debug(f"{self.__class__.__name__} getting deleted items from collection")
|
|
287
|
-
return
|
|
284
|
+
return self.collection.get_docs(query={"deleted_at": {"$ne": None}}, company_id=company_id)
|
|
288
285
|
|
|
289
286
|
def load_from_db_thread(self, company_id: Optional[StrObjectId]):
|
|
290
287
|
"""We start a thread to load the items from the database so it doesn't block the main thread"""
|
|
@@ -295,27 +292,21 @@ class ChattyAssetContainerWithCollection(ChattyAssetBaseContainer[T, P], ABC):
|
|
|
295
292
|
thread.start()
|
|
296
293
|
|
|
297
294
|
def load_from_db(self, company_id: Optional[StrObjectId]):
|
|
298
|
-
"""Pass company_id=None to load all items from the database.
|
|
295
|
+
"""Pass company_id=None to load all items from the database."""
|
|
299
296
|
logger.debug(f"{self.__class__.__name__} loading items from collection")
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
docs = list(self.collection.collection.find(filter=query))
|
|
305
|
-
# Create instances once and reuse
|
|
306
|
-
loaded_items = [self.collection.create_instance(doc) for doc in docs]
|
|
307
|
-
self.items = {item.id: item for item in loaded_items}
|
|
308
|
-
|
|
309
|
-
async def restore(self, id: str, execution_context: ExecutionContext) -> T:
|
|
297
|
+
# self.items = {item.id: item for item in self.collection.get_docs(query={}, company_id=company_id)}
|
|
298
|
+
self.items = {item.id: item for item in self.collection.get_docs(query={"deleted_at": None}, company_id=company_id)}
|
|
299
|
+
|
|
300
|
+
def restore(self, id: str, execution_context: ExecutionContext) -> T:
|
|
310
301
|
logger.debug(f"{self.__class__.__name__} restoring item {id} with execution context {execution_context}")
|
|
311
302
|
if id in self.items:
|
|
312
303
|
raise ValueError(f"Item with id {id} already exists in {self.__class__.__name__}")
|
|
313
|
-
restored_item =
|
|
304
|
+
restored_item = self.collection.get_by_id(id)
|
|
314
305
|
if restored_item is None:
|
|
315
306
|
raise NotFoundError(f"Item with id {id} not found in collection DB")
|
|
316
307
|
restored_item.deleted_at = None
|
|
317
308
|
restored_item.update_now()
|
|
318
309
|
execution_context.set_event_time(restored_item.updated_at)
|
|
319
310
|
self.items[id] = restored_item
|
|
320
|
-
|
|
321
|
-
return restored_item
|
|
311
|
+
self.collection.update(restored_item)
|
|
312
|
+
return restored_item
|
|
@@ -269,15 +269,4 @@ class ContinuousConversationHelper:
|
|
|
269
269
|
central_notif_content = ChattyContentCentral(body=body, status=CentralNotificationStatus.WARNING, calls_to_action=[cta.value for cta in cc.calls_to_action])
|
|
270
270
|
central_notif = CentralNotificationFactory.continuous_conversation_status(cc=cc, content=central_notif_content)
|
|
271
271
|
ChatService.add_central_notification(central_notification=central_notif, chat=chat)
|
|
272
|
-
return cc
|
|
273
|
-
|
|
274
|
-
@staticmethod
|
|
275
|
-
def handle_failed_template_cc(chat: Chat, cc: ContinuousConversation, error_details: str) -> ContinuousConversation:
|
|
276
|
-
"""This is for the handling of a failed template CC"""
|
|
277
|
-
cc.set_status(status=ContinuousConversationStatus.FAILED)
|
|
278
|
-
body=f"Continuous conversation failed to be sent: {error_details}"
|
|
279
|
-
logger.debug(f"{body} | CC status: {cc.status} | CC id: {cc.id} | chat id: {chat.identifier}")
|
|
280
|
-
central_notif_content = ChattyContentCentral(body=body, status=CentralNotificationStatus.ERROR, calls_to_action=[cta.value for cta in cc.calls_to_action])
|
|
281
|
-
central_notif = CentralNotificationFactory.continuous_conversation_status(cc=cc, content=central_notif_content)
|
|
282
|
-
ChatService.add_central_notification(central_notification=central_notif, chat=chat)
|
|
283
272
|
return cc
|
|
@@ -1,219 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
Events Manager - Handles queuing and publishing events to EventBridge
|
|
1
|
+
from letschatty.models.base_models.singleton import SingletonMeta
|
|
3
2
|
|
|
4
|
-
This is a generic implementation that can be configured for different environments.
|
|
5
|
-
"""
|
|
6
|
-
from ...models.base_models.singleton import SingletonMeta
|
|
7
|
-
from ...models.analytics.events.base import Event, EventType
|
|
8
|
-
from typing import List, Optional, Callable
|
|
9
|
-
import logging
|
|
10
|
-
import boto3
|
|
11
|
-
import queue
|
|
12
|
-
import threading
|
|
13
|
-
import time
|
|
14
|
-
from datetime import datetime
|
|
15
|
-
from zoneinfo import ZoneInfo
|
|
16
|
-
import os
|
|
17
|
-
import json
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger("EventsManager")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class EventsManager(metaclass=SingletonMeta):
|
|
23
|
-
"""
|
|
24
|
-
Manages event queuing and publishing to AWS EventBridge.
|
|
25
|
-
|
|
26
|
-
Can be configured via environment variables or init parameters.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self,
|
|
30
|
-
event_bus_name: Optional[str] = None,
|
|
31
|
-
source: Optional[str] = None,
|
|
32
|
-
publish_events: Optional[bool] = None,
|
|
33
|
-
failed_events_callback: Optional[Callable] = None):
|
|
34
|
-
"""
|
|
35
|
-
Initialize EventsManager.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
event_bus_name: AWS EventBridge event bus name (or uses env var)
|
|
39
|
-
source: Source identifier for events (or uses env var)
|
|
40
|
-
publish_events: Whether to publish events (or uses env var)
|
|
41
|
-
failed_events_callback: Optional callback for handling failed events
|
|
42
|
-
"""
|
|
43
|
-
self.events_queue: queue.Queue[Event] = queue.Queue()
|
|
44
|
-
self.eventbridge_client = boto3.client('events', region_name='us-east-1')
|
|
45
|
-
|
|
46
|
-
# Configuration - prefer parameters, fall back to env vars
|
|
47
|
-
self.event_bus_name = event_bus_name or os.getenv('CHATTY_EVENT_BUS_NAME', 'chatty-events')
|
|
48
|
-
self.source = source or os.getenv('CHATTY_EVENT_SOURCE')
|
|
49
|
-
if not self.source:
|
|
50
|
-
raise ValueError("Source must be provided either as a parameter or through the CHATTY_EVENT_SOURCE environment variable.")
|
|
51
|
-
self.publish_events = publish_events if publish_events is not None else os.getenv('PUBLISH_EVENTS_TO_EVENTBRIDGE', 'true').lower() == 'true'
|
|
52
|
-
|
|
53
|
-
self.max_retries = 3
|
|
54
|
-
self.thread_lock = threading.Lock()
|
|
55
|
-
self.thread_running = False
|
|
56
|
-
self.max_thread_runtime = 300
|
|
57
|
-
self.failed_events_callback = failed_events_callback
|
|
58
|
-
|
|
59
|
-
logger.debug(f"EventsManager initialized: bus={self.event_bus_name}, source={self.source}, publish={self.publish_events}")
|
|
60
|
-
|
|
61
|
-
def queue_events(self, events: List[Event]):
|
|
62
|
-
"""Queue events and spawn a thread to publish them if one isn't already running"""
|
|
63
|
-
if not self.publish_events:
|
|
64
|
-
logger.debug("Event publishing disabled, skipping")
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
for event in events:
|
|
68
|
-
logger.debug(f"Queueing event: {event.type.value} {event.company_id}")
|
|
69
|
-
logger.debug(f"Event: {event.model_dump_json()}")
|
|
70
|
-
self.events_queue.put(event)
|
|
71
|
-
|
|
72
|
-
logger.debug(f"Queued {len(events)} events")
|
|
73
|
-
if events:
|
|
74
|
-
logger.debug(f"1° event: {events[0].model_dump_json()}")
|
|
75
|
-
|
|
76
|
-
# Only start a new thread if one isn't already running
|
|
77
|
-
with self.thread_lock:
|
|
78
|
-
if not self.thread_running:
|
|
79
|
-
logger.debug("Starting publisher thread")
|
|
80
|
-
self.thread_running = True
|
|
81
|
-
thread = threading.Thread(
|
|
82
|
-
target=self._process_queue,
|
|
83
|
-
daemon=True,
|
|
84
|
-
name="EventBridge-Publisher"
|
|
85
|
-
)
|
|
86
|
-
thread.start()
|
|
87
|
-
logger.debug("Started publisher thread")
|
|
88
|
-
else:
|
|
89
|
-
logger.debug("Publisher thread already running, using existing thread")
|
|
90
|
-
|
|
91
|
-
def _process_queue(self):
|
|
92
|
-
"""Process all events in the queue and then terminate"""
|
|
93
|
-
try:
|
|
94
|
-
start_time = time.time()
|
|
95
|
-
while not self.events_queue.empty():
|
|
96
|
-
logger.debug("Processing queue")
|
|
97
|
-
events_batch = []
|
|
98
|
-
if time.time() - start_time > self.max_thread_runtime:
|
|
99
|
-
logger.warning(f"Thread ran for more than {self.max_thread_runtime}s - terminating")
|
|
100
|
-
break
|
|
101
|
-
|
|
102
|
-
# Collect up to 10 events (EventBridge limit)
|
|
103
|
-
for _ in range(10):
|
|
104
|
-
try:
|
|
105
|
-
event = self.events_queue.get(timeout=0.5)
|
|
106
|
-
events_batch.append(event)
|
|
107
|
-
self.events_queue.task_done()
|
|
108
|
-
except queue.Empty:
|
|
109
|
-
logger.debug("Queue is empty")
|
|
110
|
-
break
|
|
111
|
-
|
|
112
|
-
# Publish this batch
|
|
113
|
-
if events_batch:
|
|
114
|
-
self._publish_batch(events_batch)
|
|
115
|
-
|
|
116
|
-
except Exception as e:
|
|
117
|
-
logger.exception(f"Error in publisher thread: {str(e)}")
|
|
118
|
-
|
|
119
|
-
finally:
|
|
120
|
-
# Mark thread as completed
|
|
121
|
-
with self.thread_lock:
|
|
122
|
-
self.thread_running = False
|
|
123
|
-
|
|
124
|
-
def _publish_batch(self, events: List[Event]):
|
|
125
|
-
"""Send a batch of events to EventBridge with retries"""
|
|
126
|
-
if not events:
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
entries = []
|
|
130
|
-
for event in events:
|
|
131
|
-
entry = {
|
|
132
|
-
'Source': self.source,
|
|
133
|
-
'DetailType': event.type.value,
|
|
134
|
-
'Detail': json.dumps(event.model_dump_json()),
|
|
135
|
-
'EventBusName': self.event_bus_name
|
|
136
|
-
}
|
|
137
|
-
logger.debug(f"Appending event: {event.type.value}")
|
|
138
|
-
entries.append(entry)
|
|
139
|
-
|
|
140
|
-
for retry in range(self.max_retries):
|
|
141
|
-
try:
|
|
142
|
-
logger.debug(f"Sending {len(entries)} events to EventBridge")
|
|
143
|
-
logger.debug(f"Entries: {entries}")
|
|
144
|
-
response = self.eventbridge_client.put_events(Entries=entries)
|
|
145
|
-
logger.debug(f"Response: {response}")
|
|
146
|
-
|
|
147
|
-
if response.get('FailedEntryCount', 0) == 0:
|
|
148
|
-
logger.info(f"Successfully published {len(events)} events")
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
# Handle partial failures
|
|
152
|
-
failed_entries: List[dict] = []
|
|
153
|
-
failed_events: List[Event] = []
|
|
154
|
-
|
|
155
|
-
for i, result in enumerate(response.get('Entries', [])):
|
|
156
|
-
if 'ErrorCode' in result:
|
|
157
|
-
failed_entries.append(entries[i])
|
|
158
|
-
failed_events.append(events[i])
|
|
159
|
-
logger.error(f"Failed to publish event: {events[i].type.value}")
|
|
160
|
-
|
|
161
|
-
if retry < self.max_retries - 1 and failed_entries:
|
|
162
|
-
logger.info(f"Retrying {len(failed_entries)} events")
|
|
163
|
-
entries = failed_entries
|
|
164
|
-
events = failed_events
|
|
165
|
-
else:
|
|
166
|
-
# Store failed events via callback if provided
|
|
167
|
-
if self.failed_events_callback and failed_events:
|
|
168
|
-
failed_events_with_errors = []
|
|
169
|
-
for i, event in enumerate(failed_events):
|
|
170
|
-
result = response.get('Entries', [])[i]
|
|
171
|
-
failed_event_data = {
|
|
172
|
-
"event": event.model_dump_json(),
|
|
173
|
-
"error_code": result.get('ErrorCode'),
|
|
174
|
-
"error_message": result.get('ErrorMessage'),
|
|
175
|
-
"retry_count": self.max_retries,
|
|
176
|
-
"timestamp": datetime.now(ZoneInfo("UTC"))
|
|
177
|
-
}
|
|
178
|
-
failed_events_with_errors.append(failed_event_data)
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
self.failed_events_callback(failed_events_with_errors)
|
|
182
|
-
except Exception as e:
|
|
183
|
-
logger.error(f"Error calling failed_events_callback: {e}")
|
|
184
|
-
|
|
185
|
-
logger.error(f"Gave up on {len(failed_entries)} events after {self.max_retries} attempts")
|
|
186
|
-
return
|
|
187
|
-
|
|
188
|
-
except Exception as e:
|
|
189
|
-
if retry < self.max_retries - 1:
|
|
190
|
-
logger.warning(f"Error publishing events (attempt {retry+1}/{self.max_retries}): {str(e)}")
|
|
191
|
-
time.sleep(0.5 * (2 ** retry)) # Exponential backoff
|
|
192
|
-
else:
|
|
193
|
-
logger.exception(f"Failed to publish events after {self.max_retries} attempts")
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
def flush(self):
|
|
197
|
-
"""Wait for all queued events to be processed"""
|
|
198
|
-
# If no thread is running but we have events, start one
|
|
199
|
-
with self.thread_lock:
|
|
200
|
-
if not self.thread_running and not self.events_queue.empty():
|
|
201
|
-
self.thread_running = True
|
|
202
|
-
thread = threading.Thread(
|
|
203
|
-
target=self._process_queue,
|
|
204
|
-
daemon=True,
|
|
205
|
-
name="EventBridge-Publisher"
|
|
206
|
-
)
|
|
207
|
-
thread.start()
|
|
208
|
-
|
|
209
|
-
# Wait for queue to be empty
|
|
210
|
-
try:
|
|
211
|
-
self.events_queue.join()
|
|
212
|
-
return True
|
|
213
|
-
except Exception:
|
|
214
|
-
logger.warning("Error waiting for events queue to complete")
|
|
215
|
-
return False
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# Singleton instance
|
|
219
|
-
events_manager = EventsManager()
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Optional, Dict, Any
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from ....models.analytics.events import *
|
|
4
|
-
from ....models.analytics.events.chat_based_events.ai_agent_execution_event import AIAgentExecutionEvent
|
|
5
4
|
from ....models.base_models.chatty_asset_model import ChattyAssetModel
|
|
6
5
|
from ....models.company.assets.chat_assets import AssignedAssetToChat
|
|
7
6
|
from ....models.company.empresa import EmpresaModel
|
|
@@ -322,7 +321,7 @@ class EventFactory:
|
|
|
322
321
|
return events
|
|
323
322
|
|
|
324
323
|
@staticmethod
|
|
325
|
-
def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel:
|
|
324
|
+
def funnel_stage_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, chat_funnel: ChatFunnel, funnel_transition: StageTransition, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
|
|
326
325
|
events = []
|
|
327
326
|
base_data = EventFactory._create_base_customer_event_data(
|
|
328
327
|
chat_with_assets=chat_with_assets,
|
|
@@ -335,8 +334,10 @@ class EventFactory:
|
|
|
335
334
|
funnel_data = FunnelEventData(
|
|
336
335
|
**base_data.model_dump(),
|
|
337
336
|
funnel_id=chat_funnel.funnel_id,
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
chat_funnel_id=chat_funnel.id,
|
|
338
|
+
stage_transition=funnel_transition,
|
|
339
|
+
time_in_funnel_seconds=chat_funnel.time_in_funnel_seconds,
|
|
340
|
+
time_in_last_stage_seconds=chat_funnel.time_in_current_stage_seconds
|
|
340
341
|
)
|
|
341
342
|
event = ChatFunnelEvent(
|
|
342
343
|
specversion=EventFactory.package_version(),
|
|
@@ -414,6 +415,30 @@ class EventFactory:
|
|
|
414
415
|
events.append(event)
|
|
415
416
|
return events
|
|
416
417
|
|
|
418
|
+
@staticmethod
|
|
419
|
+
def chat_client_updated_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str, executor_type: ExecutorType, executor_id: StrObjectId, event_type: EventType, time: datetime, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
|
|
420
|
+
events = []
|
|
421
|
+
base_data = EventFactory._create_base_customer_event_data(
|
|
422
|
+
chat_with_assets=chat_with_assets,
|
|
423
|
+
company_info=company_info,
|
|
424
|
+
executor_type=executor_type,
|
|
425
|
+
executor_id=executor_id,
|
|
426
|
+
company_snapshot=company_snapshot,
|
|
427
|
+
agent_snapshot=agent_snapshot
|
|
428
|
+
)
|
|
429
|
+
event = ChatClientEvent(
|
|
430
|
+
specversion=EventFactory.package_version(),
|
|
431
|
+
type=event_type,
|
|
432
|
+
time=time,
|
|
433
|
+
data=base_data,
|
|
434
|
+
source="chatty_api.webapp",
|
|
435
|
+
company_id=company_info.id,
|
|
436
|
+
frozen_company_name=company_info.frozen_name,
|
|
437
|
+
trace_id=trace_id
|
|
438
|
+
)
|
|
439
|
+
events.append(event)
|
|
440
|
+
return events
|
|
441
|
+
|
|
417
442
|
@staticmethod
|
|
418
443
|
def chat_created_events(chat_with_assets: ChatWithAssets, company_info: EmpresaModel, trace_id: str,executor_type: ExecutorType, executor_id: StrObjectId, event_type: EventType, time: datetime, contact_point_id: Optional[StrObjectId], created_from: ChatCreatedFrom, company_snapshot: Optional[CompanyChatsSnapshot] = None, agent_snapshot: Optional[AgentChatsSnapshot] = None) -> List[Event]:
|
|
419
444
|
events = []
|
|
@@ -612,65 +637,4 @@ class EventFactory:
|
|
|
612
637
|
trace_id=trace_id
|
|
613
638
|
)
|
|
614
639
|
events.append(event)
|
|
615
|
-
return events
|
|
616
|
-
|
|
617
|
-
@staticmethod
|
|
618
|
-
def ai_agent_execution_event(
|
|
619
|
-
event_type: EventType,
|
|
620
|
-
chat_id: StrObjectId,
|
|
621
|
-
company_id: StrObjectId,
|
|
622
|
-
frozen_company_name: str,
|
|
623
|
-
ai_agent_id: StrObjectId,
|
|
624
|
-
chain_of_thought_id: StrObjectId,
|
|
625
|
-
trigger: str,
|
|
626
|
-
source: str = "chatty.api",
|
|
627
|
-
decision_type: Optional[str] = None,
|
|
628
|
-
error_message: Optional[str] = None,
|
|
629
|
-
duration_ms: Optional[int] = None,
|
|
630
|
-
user_rating: Optional[int] = None,
|
|
631
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
632
|
-
trace_id: Optional[str] = None
|
|
633
|
-
) -> AIAgentExecutionEvent:
|
|
634
|
-
"""
|
|
635
|
-
Create an AI agent execution event using the modularized AIAgentEventFactory.
|
|
636
|
-
|
|
637
|
-
This method delegates to AIAgentEventFactory to maintain modularity while
|
|
638
|
-
keeping event creation centralized through EventsFactory.
|
|
639
|
-
|
|
640
|
-
Args:
|
|
641
|
-
event_type: The type of event (from EventType enum)
|
|
642
|
-
chat_id: ID of the chat where the event occurred
|
|
643
|
-
company_id: ID of the company
|
|
644
|
-
frozen_company_name: Company name snapshot for analytics
|
|
645
|
-
ai_agent_id: ID of the AI agent asset
|
|
646
|
-
chain_of_thought_id: ID of the chain of thought execution
|
|
647
|
-
trigger: What triggered the execution (USER_MESSAGE, FOLLOW_UP, etc.)
|
|
648
|
-
source: Event source (e.g., 'chatty.api', 'chatty.lambda')
|
|
649
|
-
decision_type: Type of decision if applicable
|
|
650
|
-
error_message: Error message if this is an error event
|
|
651
|
-
duration_ms: Duration of the operation in milliseconds
|
|
652
|
-
user_rating: User rating (1-5 stars) if applicable
|
|
653
|
-
metadata: Additional event-specific data
|
|
654
|
-
trace_id: Trace ID for tracking event flows
|
|
655
|
-
|
|
656
|
-
Returns:
|
|
657
|
-
AIAgentExecutionEvent ready to be queued to EventBridge
|
|
658
|
-
"""
|
|
659
|
-
from .ai_agent_event_factory import AIAgentEventFactory
|
|
660
|
-
|
|
661
|
-
return AIAgentEventFactory.create_event(
|
|
662
|
-
event_type=event_type,
|
|
663
|
-
chat_id=chat_id,
|
|
664
|
-
company_id=company_id,
|
|
665
|
-
frozen_company_name=frozen_company_name,
|
|
666
|
-
ai_agent_id=ai_agent_id,
|
|
667
|
-
chain_of_thought_id=chain_of_thought_id,
|
|
668
|
-
trigger=trigger,
|
|
669
|
-
source=source,
|
|
670
|
-
decision_type=decision_type,
|
|
671
|
-
error_message=error_message,
|
|
672
|
-
duration_ms=duration_ms,
|
|
673
|
-
user_rating=user_rating,
|
|
674
|
-
metadata=metadata,
|
|
675
|
-
trace_id=trace_id
|
|
676
|
-
)
|
|
640
|
+
return events
|
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
from letschatty.models import StrObjectId
|
|
3
|
-
from letschatty.models.ai_microservices.lambda_events import
|
|
4
|
-
DoubleCheckerForIncomingMessagesAnswerCallbackEvent,
|
|
5
|
-
DoubleCheckerForIncomingMessagesAnswerEvent,
|
|
6
|
-
FixBuggedAiAgentsCallsInChatsEvent,
|
|
7
|
-
GetChainOfThoughtsByChatIdEvent,
|
|
8
|
-
QualityTestEventData,
|
|
9
|
-
QualityTestsForUpdatedAIComponentEvent,
|
|
10
|
-
QualityTestsForUpdatedAIComponentEventData,
|
|
11
|
-
CancelExecutionEvent,
|
|
12
|
-
ManualTriggerEvent,
|
|
13
|
-
AssignAIAgentToChatEvent,
|
|
14
|
-
RemoveAIAgentFromChatEvent,
|
|
15
|
-
SmartFollowUpDecisionOutputEvent,
|
|
16
|
-
UpdateAIAgentModeInChatEvent,
|
|
17
|
-
EscalateAIAgentInChatEvent,
|
|
18
|
-
UnescalateAIAgentInChatEvent,
|
|
19
|
-
GetChatWithPromptForIncomingMessageEvent,
|
|
20
|
-
GetChatWithPromptForFollowUpEvent,
|
|
21
|
-
UpdateAIAgentPrequalStatusInChatEvent,
|
|
22
|
-
)
|
|
3
|
+
from letschatty.models.ai_microservices.lambda_events import DoubleCheckerForIncomingMessagesAnswerCallbackEvent, DoubleCheckerForIncomingMessagesAnswerEvent, FixBuggedAiAgentsCallsInChatsEvent, QualityTestEventData, QualityTestsForUpdatedAIComponentEvent, QualityTestsForUpdatedAIComponentEventData
|
|
23
4
|
from letschatty.models.ai_microservices.lambda_invokation_types import InvokationType
|
|
24
|
-
from letschatty.models.ai_microservices import AllQualityTestEvent, AllQualityTestEventData, QualityTestEvent, QualityTestInteractionCallbackEvent, SmartTaggingCallbackEvent, QualityTestCallbackEvent, LambdaAiEvent, SmartTaggingEvent, SmartTaggingPromptEvent
|
|
5
|
+
from letschatty.models.ai_microservices import AllQualityTestEvent, AllQualityTestEventData, FollowUpEvent, IncomingMessageEvent, QualityTestEvent, QualityTestInteractionCallbackEvent, SmartTaggingCallbackEvent, IncomingMessageCallbackEvent, QualityTestCallbackEvent, LambdaAiEvent, SmartTaggingEvent, SmartTaggingPromptEvent
|
|
25
6
|
from letschatty.models.base_models.ai_agent_component import AiAgentComponent
|
|
26
7
|
from letschatty.models.company.assets.ai_agents_v2.chat_example import ChatExample
|
|
27
8
|
from letschatty.models.company.assets.ai_agents_v2.chat_example_test import ChatExampleTestCase
|
|
@@ -38,10 +19,16 @@ class LambdaEventFactory:
|
|
|
38
19
|
return SmartTaggingEvent(**event_data)
|
|
39
20
|
case InvokationType.SMART_TAGGING_CALLBACK:
|
|
40
21
|
return SmartTaggingCallbackEvent(**event_data)
|
|
22
|
+
case InvokationType.INCOMING_MESSAGE:
|
|
23
|
+
return IncomingMessageEvent(**event_data)
|
|
41
24
|
case InvokationType.SINGLE_QUALITY_TEST:
|
|
42
25
|
return QualityTestEvent(**event_data)
|
|
43
26
|
case InvokationType.ALL_QUALITY_TEST:
|
|
44
27
|
return AllQualityTestEvent(**event_data)
|
|
28
|
+
case InvokationType.INCOMING_MESSAGE_CALLBACK:
|
|
29
|
+
return IncomingMessageCallbackEvent(**event_data)
|
|
30
|
+
case InvokationType.FOLLOW_UP:
|
|
31
|
+
return FollowUpEvent(**event_data)
|
|
45
32
|
case InvokationType.SINGLE_QUALITY_TEST_CALLBACK:
|
|
46
33
|
return QualityTestCallbackEvent(**event_data)
|
|
47
34
|
case InvokationType.SMART_TAGGING_PROMPT:
|
|
@@ -54,31 +41,6 @@ class LambdaEventFactory:
|
|
|
54
41
|
return DoubleCheckerForIncomingMessagesAnswerEvent(**event_data)
|
|
55
42
|
case InvokationType.DOUBLE_CHECKER_FOR_INCOMING_MESSAGES_ANSWER_CALLBACK:
|
|
56
43
|
return DoubleCheckerForIncomingMessagesAnswerCallbackEvent(**event_data)
|
|
57
|
-
case InvokationType.MANUAL_TRIGGER:
|
|
58
|
-
return ManualTriggerEvent(**event_data)
|
|
59
|
-
case InvokationType.CANCEL_EXECUTION:
|
|
60
|
-
return CancelExecutionEvent(**event_data)
|
|
61
|
-
case InvokationType.ASSIGN_AI_AGENT_TO_CHAT:
|
|
62
|
-
return AssignAIAgentToChatEvent(**event_data)
|
|
63
|
-
case InvokationType.REMOVE_AI_AGENT_FROM_CHAT:
|
|
64
|
-
return RemoveAIAgentFromChatEvent(**event_data)
|
|
65
|
-
case InvokationType.UPDATE_AI_AGENT_MODE_IN_CHAT:
|
|
66
|
-
return UpdateAIAgentModeInChatEvent(**event_data)
|
|
67
|
-
case InvokationType.UPDATE_AI_AGENT_PREQUAL_STATUS_IN_CHAT:
|
|
68
|
-
return UpdateAIAgentPrequalStatusInChatEvent(**event_data)
|
|
69
|
-
case InvokationType.ESCALATE_AI_AGENT_IN_CHAT:
|
|
70
|
-
return EscalateAIAgentInChatEvent(**event_data)
|
|
71
|
-
case InvokationType.UNESCALATE_AI_AGENT_IN_CHAT:
|
|
72
|
-
return UnescalateAIAgentInChatEvent(**event_data)
|
|
73
|
-
case InvokationType.GET_CHAIN_OF_THOUGHTS_BY_CHAT_ID:
|
|
74
|
-
return GetChainOfThoughtsByChatIdEvent(**event_data)
|
|
75
|
-
case InvokationType.SMART_FOLLOW_UP_DECISION_OUTPUT:
|
|
76
|
-
return SmartFollowUpDecisionOutputEvent(**event_data)
|
|
77
|
-
|
|
78
|
-
case InvokationType.GET_CHAT_WITH_PROMPT_INCOMING_MESSAGE:
|
|
79
|
-
return GetChatWithPromptForIncomingMessageEvent(**event_data)
|
|
80
|
-
case InvokationType.GET_CHAT_WITH_PROMPT_FOLLOW_UP:
|
|
81
|
-
return GetChatWithPromptForFollowUpEvent(**event_data)
|
|
82
44
|
case _:
|
|
83
45
|
raise ValueError(f"Invalid event type: {event_type}")
|
|
84
46
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from letschatty.models.messages.chatty_messages.button import ChattyContentButton
|
|
2
1
|
from ...models.messages.chatty_messages.schema import ChattyContent, ChattyContentText, ChattyContentContacts, ChattyContentLocation, ChattyContentImage, ChattyContentVideo, ChattyContentAudio, ChattyContentDocument, ChattyContentSticker, ChattyContentReaction
|
|
3
2
|
class MessageTextOrCaptionOrPreview:
|
|
4
3
|
@staticmethod
|
|
@@ -6,9 +5,9 @@ class MessageTextOrCaptionOrPreview:
|
|
|
6
5
|
if isinstance(message_content, ChattyContentText):
|
|
7
6
|
return message_content.body
|
|
8
7
|
elif isinstance(message_content, ChattyContentContacts):
|
|
9
|
-
return
|
|
10
|
-
elif
|
|
11
|
-
return
|
|
8
|
+
return "👥 Mensaje de tipo contacto"
|
|
9
|
+
elif isinstance(message_content, ChattyContentLocation):
|
|
10
|
+
return "📍 Mensaje de tipo ubicación"
|
|
12
11
|
elif isinstance(message_content, ChattyContentImage):
|
|
13
12
|
return "🖼️ Mensaje de tipo imagen"
|
|
14
13
|
elif isinstance(message_content, ChattyContentVideo):
|
|
@@ -23,7 +22,6 @@ class MessageTextOrCaptionOrPreview:
|
|
|
23
22
|
return "😀 Mensaje de tipo sticker"
|
|
24
23
|
elif isinstance(message_content, ChattyContentReaction):
|
|
25
24
|
return "❤️ Mensaje de tipo reacción"
|
|
26
|
-
elif isinstance(message_content, ChattyContentButton):
|
|
27
|
-
return f"{message_content.payload}"
|
|
28
25
|
else:
|
|
26
|
+
|
|
29
27
|
return "Vista previa del mensaje"
|
|
@@ -10,6 +10,10 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
class SourcesAndTopicsValidator:
|
|
12
12
|
"""This class provides methods to validate sources and topics."""
|
|
13
|
+
@staticmethod
|
|
14
|
+
def normalize_source_name(name: str) -> str:
|
|
15
|
+
return name.strip().lower()
|
|
16
|
+
|
|
13
17
|
@staticmethod
|
|
14
18
|
def source_validation_check(sources : Dict[str,Source], topics : Dict[str,Topic], source : Source, existing_source_id : str | None = None) -> None:
|
|
15
19
|
"""Checks validation of a source against other sources and topics."""
|
|
@@ -28,9 +32,16 @@ class SourcesAndTopicsValidator:
|
|
|
28
32
|
"""This method checks that no duplicated source is created.
|
|
29
33
|
For Pure Ads, it checks that the ad_id is unique and doesn't exist already.
|
|
30
34
|
For Other Sources, it checks that exact trigger doesn't exist already. """
|
|
35
|
+
normalized_name = SourcesAndTopicsValidator.normalize_source_name(source.name)
|
|
36
|
+
existing_source_name = None
|
|
37
|
+
if existing_source_id and existing_source_id in sources:
|
|
38
|
+
existing_source_name = SourcesAndTopicsValidator.normalize_source_name(sources[existing_source_id].name)
|
|
39
|
+
should_check_name = existing_source_name is None or normalized_name != existing_source_name
|
|
31
40
|
for source_id, source_to_check in sources.items():
|
|
32
41
|
if source_id == existing_source_id:
|
|
33
42
|
continue
|
|
43
|
+
if should_check_name and SourcesAndTopicsValidator.normalize_source_name(source_to_check.name) == normalized_name:
|
|
44
|
+
raise ConflictedSource(f":warning: *Conflict while adding source: Name already exists* \n New source '{source.name}' \n- Id {source.id} \n Existing source '{source_to_check.name}' \n- Id {source_to_check.id}", conflicting_source_id=source_to_check.id)
|
|
34
45
|
if source == source_to_check: #type: ignore
|
|
35
46
|
if isinstance(source, OtherSource):
|
|
36
47
|
raise ConflictedSource(f":warning: *Conflict while adding source: Trigger already exists* \n New source '{source.name}' \n- Id {source.id} \n- Trigger {source.trigger[:30]} \n Existing source '{source_to_check.name}' \n- Id {source_to_check.id} \n- Trigger '{source_to_check.trigger[:30]}'", conflicting_source_id=source_to_check.id)
|