letschatty 0.4.342__py3-none-any.whl → 0.4.344__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 +2 -29
- letschatty/models/ai_microservices/lambda_events.py +28 -137
- letschatty/models/ai_microservices/lambda_invokation_types.py +1 -3
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +1 -3
- letschatty/models/analytics/events/__init__.py +2 -3
- 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 +6 -3
- letschatty/models/analytics/events/event_types.py +9 -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 +7 -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 +2 -14
- 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 +2 -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 +12 -36
- letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +40 -0
- letschatty/models/company/integrations/sync_status_enum.py +9 -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 +67 -12
- 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 +6 -66
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -25
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +4 -6
- letschatty/services/validators/analytics_validator.py +11 -0
- {letschatty-0.4.342.dist-info → letschatty-0.4.344.dist-info}/METADATA +1 -1
- {letschatty-0.4.342.dist-info → letschatty-0.4.344.dist-info}/RECORD +52 -84
- 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.342.dist-info → letschatty-0.4.344.dist-info}/LICENSE +0 -0
- {letschatty-0.4.342.dist-info → letschatty-0.4.344.dist-info}/WHEEL +0 -0
|
@@ -28,9 +28,10 @@ from ...models.chat.scheduled_messages import ScheduledMessageStatus
|
|
|
28
28
|
from ...models.utils.types.identifier import StrObjectId
|
|
29
29
|
from ...models.utils.custom_exceptions.custom_exceptions import AssetAlreadyAssigned, MessageNotFoundError, NotFoundError, MessageAlreadyInChat, MetaErrorNotification, ChatAlreadyAssigned, AlreadyCompleted, ErrorToMantainSafety
|
|
30
30
|
from ..factories.messages.central_notification_factory import CentralNotificationFactory
|
|
31
|
+
from ..factories.messages.chatty_message_factory import from_message_draft
|
|
31
32
|
from ...models.messages.chatty_messages.base.message_draft import ChattyContentAudio, MessageDraft
|
|
32
33
|
from ...models.messages.chatty_messages.schema.chatty_content.content_central import CentralNotificationStatus
|
|
33
|
-
from ...models.messages.chatty_messages.schema import ChattyContext
|
|
34
|
+
from ...models.messages.chatty_messages.schema import ChattyContext, ChattyContentText
|
|
34
35
|
from ...models.utils.types.message_types import MessageType
|
|
35
36
|
from .conversation_topics_service import ConversationTopicsService
|
|
36
37
|
import logging
|
|
@@ -211,6 +212,43 @@ class ChatService:
|
|
|
211
212
|
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
213
|
return chat.chatty_ai_agent
|
|
213
214
|
|
|
215
|
+
@staticmethod
|
|
216
|
+
def escalate_chatty_ai_agent(
|
|
217
|
+
chat: Chat,
|
|
218
|
+
execution_context: ExecutionContext,
|
|
219
|
+
message: Optional[str] = None,
|
|
220
|
+
reason: Optional[str] = None
|
|
221
|
+
) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Mark the chat's AI agent as requiring human intervention and add a central notification.
|
|
224
|
+
"""
|
|
225
|
+
if chat.chatty_ai_agent and not chat.chatty_ai_agent.requires_human_intervention:
|
|
226
|
+
chat.chatty_ai_agent.requires_human_intervention = True
|
|
227
|
+
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
228
|
+
body = "El chat fue escalado a un agente humano"
|
|
229
|
+
if reason:
|
|
230
|
+
body = f"{body}. Motivo: {reason}"
|
|
231
|
+
ChatService.add_central_notification_from_text(
|
|
232
|
+
chat=chat,
|
|
233
|
+
body=body,
|
|
234
|
+
subtype=MessageSubtype.CHATTY_AI_AGENT_NOTIFICATION,
|
|
235
|
+
content_status=CentralNotificationStatus.WARNING,
|
|
236
|
+
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
237
|
+
)
|
|
238
|
+
if message:
|
|
239
|
+
outgoing_context = ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
240
|
+
outgoing_message = from_message_draft(
|
|
241
|
+
MessageDraft(
|
|
242
|
+
type=MessageType.TEXT,
|
|
243
|
+
content=ChattyContentText(body=message),
|
|
244
|
+
context=outgoing_context,
|
|
245
|
+
subtype=MessageSubtype.NONE,
|
|
246
|
+
is_incoming_message=False
|
|
247
|
+
),
|
|
248
|
+
sent_by=execution_context.executor.id
|
|
249
|
+
)
|
|
250
|
+
ChatService.add_message(chat=chat, message=outgoing_message)
|
|
251
|
+
|
|
214
252
|
@staticmethod
|
|
215
253
|
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:
|
|
216
254
|
"""
|
|
@@ -266,36 +304,46 @@ class ChatService:
|
|
|
266
304
|
return next((state for state in chat.flow_states if state.is_smart_follow_up), None)
|
|
267
305
|
|
|
268
306
|
@staticmethod
|
|
269
|
-
def create_sale(
|
|
307
|
+
def create_sale(
|
|
308
|
+
chat: Chat,
|
|
309
|
+
execution_context: ExecutionContext,
|
|
310
|
+
sale: Sale,
|
|
311
|
+
product: Optional[Product],
|
|
312
|
+
product_ids: Optional[List[StrObjectId]] = None,
|
|
313
|
+
product_label: Optional[str] = None
|
|
314
|
+
) -> SaleAssignedToChat:
|
|
270
315
|
"""
|
|
271
316
|
Add a sale to the chat.
|
|
272
317
|
"""
|
|
273
318
|
if next((sale for sale in chat.client.sales if sale.asset_id == sale.id), None) is not None:
|
|
274
319
|
raise AssetAlreadyAssigned(f"Sale with id {sale.id} already assigned to chat {chat.id}")
|
|
320
|
+
label = product_label or (product.name if product else "multiples productos")
|
|
321
|
+
assigned_product_ids = product_ids or ([product.id] if product else [])
|
|
275
322
|
assigned_asset = SaleAssignedToChat(
|
|
276
323
|
asset_type=ChatAssetType.SALE,
|
|
277
324
|
asset_id=sale.id,
|
|
278
325
|
assigned_at=sale.created_at,
|
|
279
326
|
assigned_by=execution_context.executor.id,
|
|
280
|
-
product_id=product.id
|
|
327
|
+
product_id=product.id if product else None,
|
|
328
|
+
product_ids=assigned_product_ids
|
|
281
329
|
)
|
|
282
330
|
execution_context.set_event_time(assigned_asset.assigned_at)
|
|
283
331
|
bisect.insort(chat.client.sales, assigned_asset)
|
|
284
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {
|
|
285
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
332
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta de {label}", description=f"Venta de {label} creada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_ADDED))
|
|
333
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {label} agregada al chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_ADDED)
|
|
286
334
|
return assigned_asset
|
|
287
335
|
|
|
288
336
|
@staticmethod
|
|
289
|
-
def update_sale(chat
|
|
337
|
+
def update_sale(chat: Chat, execution_context: ExecutionContext, sale: Sale, product_label: str) -> Sale:
|
|
290
338
|
"""
|
|
291
339
|
Update a sale for the chat.
|
|
292
340
|
"""
|
|
293
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {
|
|
294
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
341
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta actualizada de {product_label}", description=f"Venta de {product_label} actualizada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_UPDATED))
|
|
342
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product_label} actualizada en el chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_UPDATED)
|
|
295
343
|
return sale
|
|
296
344
|
|
|
297
345
|
@staticmethod
|
|
298
|
-
def delete_sale(chat
|
|
346
|
+
def delete_sale(chat: Chat, execution_context: ExecutionContext, sale_id: StrObjectId, product_label: str) -> SaleAssignedToChat:
|
|
299
347
|
"""
|
|
300
348
|
Logically remove a sale from the chat.
|
|
301
349
|
"""
|
|
@@ -303,8 +351,8 @@ class ChatService:
|
|
|
303
351
|
assigned_asset_to_remove = next(sale for sale in chat.client.sales if sale.asset_id == sale_id)
|
|
304
352
|
chat.client.sales.remove(assigned_asset_to_remove)
|
|
305
353
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
306
|
-
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {
|
|
307
|
-
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {
|
|
354
|
+
ChatService.create_highlight(chat=chat, execution_context=execution_context, highlight_data=HighlightRequestData(title=f"🛍️ Venta eliminada de {product_label}", description=f"Venta de {product_label} eliminada por {execution_context.executor.name}", starred=False, subtype=MessageSubtype.SALE_DELETED))
|
|
355
|
+
ChatService.add_central_notification_from_text(chat=chat, body=f"Venta de {product_label} eliminada del chat {chat.id} por {execution_context.executor.name}", subtype=MessageSubtype.SALE_DELETED)
|
|
308
356
|
return assigned_asset_to_remove
|
|
309
357
|
except StopIteration:
|
|
310
358
|
raise NotFoundError(message=f"Sale with id {sale_id} not found in chat {chat.id}")
|
|
@@ -873,9 +921,16 @@ class ChatService:
|
|
|
873
921
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
874
922
|
logger.info(f"Updated collected data for chat {chat.id}: {', '.join(updated_fields)}")
|
|
875
923
|
|
|
924
|
+
field_label_map = {
|
|
925
|
+
"name": "nombre",
|
|
926
|
+
"email": "email",
|
|
927
|
+
"phone": "telefono",
|
|
928
|
+
"document_id": "dni",
|
|
929
|
+
}
|
|
930
|
+
display_fields = [field_label_map.get(field, field) for field in updated_fields]
|
|
876
931
|
ChatService.add_central_notification_from_text(
|
|
877
932
|
chat=chat,
|
|
878
|
-
body=f"
|
|
933
|
+
body=f"Datos del cliente recopilados: {', '.join(display_fields)}",
|
|
879
934
|
subtype=MessageSubtype.CLIENT_INFO_UPDATED,
|
|
880
935
|
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
881
936
|
)
|
|
@@ -1,14 +1,2 @@
|
|
|
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
|
-
)
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TypeVar, Generic, Type, Callable, Protocol, Optional
|
|
3
|
-
|
|
4
|
-
from bson import ObjectId
|
|
5
|
-
from letschatty.models.utils.types import StrObjectId
|
|
2
|
+
from typing import TypeVar, Generic, Type, Callable, Protocol, Optional
|
|
6
3
|
from .base_container_with_collection import ChattyAssetCollectionInterface, ChattyAssetContainerWithCollection, CacheConfig
|
|
7
4
|
from ...models.base_models import ChattyAssetModel
|
|
8
5
|
from ...models.base_models.chatty_asset_model import ChattyAssetPreview
|
|
9
6
|
from ...models.data_base.mongo_connection import MongoConnection
|
|
10
7
|
import logging
|
|
11
8
|
import os
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from ...models.analytics.events.base import EventType
|
|
15
|
-
from ...models.company.empresa import EmpresaModel
|
|
16
|
-
from ...models.execution.execution import ExecutionContext
|
|
17
|
-
from ...models.company.assets.company_assets import CompanyAssetType
|
|
18
|
-
from ...models.utils.types.deletion_type import DeletionType
|
|
19
|
-
|
|
20
9
|
logger = logging.getLogger("AssetService")
|
|
21
10
|
|
|
22
11
|
# Protocol for assets that specify their preview type
|
|
@@ -55,75 +44,17 @@ class AssetCollection(Generic[T, P], ChattyAssetCollectionInterface[T, P]):
|
|
|
55
44
|
raise ValueError(f"Data must be a dictionary, got {type(data)}: {data}")
|
|
56
45
|
return self._create_instance_method(data)
|
|
57
46
|
|
|
58
|
-
|
|
59
47
|
class AssetService(Generic[T, P], ChattyAssetContainerWithCollection[T, P]):
|
|
60
|
-
"""
|
|
61
|
-
Generic service for handling CRUD operations for any Chatty asset.
|
|
62
|
-
|
|
63
|
-
Supports optional automatic event handling for API implementations.
|
|
64
|
-
Set these class attributes to enable events:
|
|
65
|
-
- asset_type_enum: CompanyAssetType (e.g., CompanyAssetType.PRODUCTS)
|
|
66
|
-
- event_type_created: EventType (e.g., EventType.PRODUCT_CREATED)
|
|
67
|
-
- event_type_updated: EventType (e.g., EventType.PRODUCT_UPDATED)
|
|
68
|
-
- event_type_deleted: EventType (e.g., EventType.PRODUCT_DELETED)
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
# Optional: Set these in subclasses to enable automatic event handling
|
|
72
|
-
asset_type_enum: ClassVar[Optional['CompanyAssetType']] = None
|
|
73
|
-
event_type_created: ClassVar[Optional['EventType']] = None
|
|
74
|
-
event_type_updated: ClassVar[Optional['EventType']] = None
|
|
75
|
-
event_type_deleted: ClassVar[Optional['EventType']] = None
|
|
76
|
-
|
|
77
|
-
collection: AssetCollection[T, P] # Type annotation for better type checking
|
|
48
|
+
"""Generic service for handling CRUD operations for any Chatty asset"""
|
|
78
49
|
|
|
79
50
|
def __init__(self,
|
|
80
|
-
|
|
51
|
+
collection_name: str,
|
|
52
|
+
asset_type: Type[T],
|
|
53
|
+
connection: MongoConnection,
|
|
54
|
+
create_instance_method: Callable[[dict], T],
|
|
55
|
+
preview_type: Optional[Type[P]] = None,
|
|
81
56
|
cache_config: CacheConfig = CacheConfig.default()):
|
|
82
|
-
""
|
|
83
|
-
Initialize AssetService with a pre-configured collection.
|
|
84
|
-
|
|
85
|
-
The item_type and preview_type are automatically extracted from the collection,
|
|
86
|
-
eliminating redundancy and simplifying the API.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
collection: Pre-configured AssetCollection subclass
|
|
90
|
-
cache_config: Cache configuration
|
|
91
|
-
"""
|
|
92
|
-
logger.debug(f"AssetService {self.__class__.__name__} initializing with collection")
|
|
93
|
-
super().__init__(
|
|
94
|
-
item_type=collection.type,
|
|
95
|
-
preview_type=collection.preview_type,
|
|
96
|
-
collection=collection,
|
|
97
|
-
cache_config=cache_config,
|
|
98
|
-
)
|
|
99
|
-
logger.debug(f"AssetService {self.__class__.__name__} initialized")
|
|
100
|
-
|
|
101
|
-
@classmethod
|
|
102
|
-
def from_config(cls,
|
|
103
|
-
collection_name: str,
|
|
104
|
-
asset_type: Type[T],
|
|
105
|
-
connection: MongoConnection,
|
|
106
|
-
create_instance_method: Callable[[dict], T],
|
|
107
|
-
preview_type: Optional[Type[P]] = None,
|
|
108
|
-
cache_config: CacheConfig = CacheConfig.default()) -> 'AssetService[T, P]':
|
|
109
|
-
"""
|
|
110
|
-
Create an AssetService using the legacy configuration pattern.
|
|
111
|
-
|
|
112
|
-
This class method is provided for backward compatibility.
|
|
113
|
-
New code should use pre-configured AssetCollection subclasses.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
collection_name: MongoDB collection name
|
|
117
|
-
asset_type: The asset model type
|
|
118
|
-
connection: MongoDB connection
|
|
119
|
-
create_instance_method: Factory method to create asset instances
|
|
120
|
-
preview_type: Optional preview type
|
|
121
|
-
cache_config: Cache configuration
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
AssetService instance
|
|
125
|
-
"""
|
|
126
|
-
logger.debug(f"AssetService creating from config for {collection_name}")
|
|
57
|
+
logger.debug(f"AssetService {self.__class__.__name__} initializing for {collection_name}")
|
|
127
58
|
asset_collection = AssetCollection(
|
|
128
59
|
collection=collection_name,
|
|
129
60
|
asset_type=asset_type,
|
|
@@ -131,102 +62,13 @@ class AssetService(Generic[T, P], ChattyAssetContainerWithCollection[T, P]):
|
|
|
131
62
|
create_instance_method=create_instance_method,
|
|
132
63
|
preview_type=preview_type
|
|
133
64
|
)
|
|
134
|
-
|
|
65
|
+
super().__init__(
|
|
66
|
+
item_type=asset_type,
|
|
67
|
+
preview_type=preview_type,
|
|
135
68
|
collection=asset_collection,
|
|
136
|
-
cache_config=cache_config
|
|
69
|
+
cache_config=cache_config,
|
|
137
70
|
)
|
|
138
|
-
|
|
139
|
-
def _should_handle_events(self) -> bool:
|
|
140
|
-
"""Check if this service should handle events automatically"""
|
|
141
|
-
return (self.asset_type_enum is not None and
|
|
142
|
-
self.event_type_created is not None and
|
|
143
|
-
self.event_type_updated is not None and
|
|
144
|
-
self.event_type_deleted is not None)
|
|
145
|
-
|
|
146
|
-
def _queue_event(self, item: T, event_type: 'EventType', execution_context: 'ExecutionContext', company_info: 'EmpresaModel'):
|
|
147
|
-
"""Queue an event for this asset if event handling is enabled"""
|
|
148
|
-
if not self._should_handle_events() or not self.asset_type_enum:
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
from ...services.factories.analytics.events_factory import EventFactory
|
|
153
|
-
from ...services.events import events_manager
|
|
154
|
-
|
|
155
|
-
# Type guard - company_id should exist on ChattyAssetModel
|
|
156
|
-
if not hasattr(item, 'company_id'):
|
|
157
|
-
logger.warning(f"Asset {type(item).__name__} missing company_id, skipping event")
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
events = EventFactory.asset_events(
|
|
161
|
-
company_id=item.company_id, # type: ignore[attr-defined]
|
|
162
|
-
executor_id=execution_context.executor.id,
|
|
163
|
-
asset=item,
|
|
164
|
-
asset_type=self.asset_type_enum,
|
|
165
|
-
event_type=event_type,
|
|
166
|
-
time=execution_context.time,
|
|
167
|
-
trace_id=execution_context.trace_id,
|
|
168
|
-
executor_type=execution_context.executor.type,
|
|
169
|
-
company_info=company_info
|
|
170
|
-
)
|
|
171
|
-
events_manager.queue_events(events)
|
|
172
|
-
except ImportError:
|
|
173
|
-
# Events not available (microservice context) - skip
|
|
174
|
-
pass
|
|
175
|
-
|
|
176
|
-
# All methods are now async-only for better performance
|
|
177
|
-
async def insert(self, item: T, execution_context: 'ExecutionContext', company_info: Optional['EmpresaModel'] = None) -> T:
|
|
178
|
-
"""Insert with automatic event handling if configured"""
|
|
179
|
-
result = await super().insert(item, execution_context)
|
|
180
|
-
if company_info and self._should_handle_events() and self.event_type_created:
|
|
181
|
-
self._queue_event(result, self.event_type_created, execution_context, company_info)
|
|
182
|
-
return result
|
|
183
|
-
|
|
184
|
-
async def update(self, id: str, new_item: T, execution_context: 'ExecutionContext', company_info: Optional['EmpresaModel'] = None) -> T:
|
|
185
|
-
"""Update with automatic event handling if configured"""
|
|
186
|
-
result = await super().update(id, new_item, execution_context)
|
|
187
|
-
if company_info and self._should_handle_events() and self.event_type_updated:
|
|
188
|
-
self._queue_event(result, self.event_type_updated, execution_context, company_info)
|
|
189
|
-
return result
|
|
190
|
-
|
|
191
|
-
async def delete(self, id: str, execution_context: 'ExecutionContext', company_info: Optional['EmpresaModel'] = None, deletion_type: Optional['DeletionType'] = None) -> T:
|
|
192
|
-
"""Delete with automatic event handling if configured"""
|
|
193
|
-
from ...models.utils.types.deletion_type import DeletionType as DT
|
|
194
|
-
result = await super().delete(id, execution_context, deletion_type or DT.LOGICAL)
|
|
195
|
-
if company_info and self._should_handle_events() and self.event_type_deleted:
|
|
196
|
-
self._queue_event(result, self.event_type_deleted, execution_context, company_info)
|
|
197
|
-
return result
|
|
198
|
-
|
|
199
|
-
async def restore(self, id: str, execution_context: 'ExecutionContext', company_info: Optional['EmpresaModel'] = None) -> T:
|
|
200
|
-
"""Restore with automatic event handling if configured"""
|
|
201
|
-
result = await super().restore(id, execution_context)
|
|
202
|
-
if company_info and self._should_handle_events() and self.event_type_updated:
|
|
203
|
-
self._queue_event(result, self.event_type_updated, execution_context, company_info)
|
|
204
|
-
return result
|
|
205
|
-
|
|
206
|
-
# Generic convenience methods
|
|
207
|
-
async def create_asset(self, data: dict, execution_context: 'ExecutionContext', company_info: 'EmpresaModel') -> T:
|
|
208
|
-
"""
|
|
209
|
-
Generic create method - creates instance from dict and inserts with events.
|
|
210
|
-
Can be called as create_asset or aliased to create_product/create_tag/etc.
|
|
211
|
-
"""
|
|
212
|
-
data["company_id"] = execution_context.company_id
|
|
213
|
-
item = self.collection.create_instance(data)
|
|
214
|
-
return await self.insert(item, execution_context, company_info)
|
|
215
|
-
|
|
216
|
-
async def update_asset(self, id: str, data: dict, execution_context: 'ExecutionContext', company_info: 'EmpresaModel') -> T:
|
|
217
|
-
"""
|
|
218
|
-
Generic update method - creates instance from dict and updates with events.
|
|
219
|
-
Can be called as update_asset or aliased to update_product/update_tag/etc.
|
|
220
|
-
"""
|
|
221
|
-
new_item = self.collection.create_instance(data)
|
|
222
|
-
return await self.update(id, new_item, execution_context, company_info)
|
|
223
|
-
|
|
224
|
-
async def delete_asset(self, id: str, execution_context: 'ExecutionContext', company_info: 'EmpresaModel') -> T:
|
|
225
|
-
"""
|
|
226
|
-
Generic delete method - deletes with events.
|
|
227
|
-
Can be called as delete_asset or aliased to delete_product/delete_tag/etc.
|
|
228
|
-
"""
|
|
229
|
-
return await self.delete(id, execution_context, company_info)
|
|
71
|
+
logger.debug(f"AssetService {self.__class__.__name__} initialized for {collection_name}")
|
|
230
72
|
|
|
231
73
|
def get_preview_type(self) -> Type[P]:
|
|
232
74
|
"""Get the preview type from the asset class if it has one"""
|
|
@@ -239,22 +81,3 @@ class AssetService(Generic[T, P], ChattyAssetContainerWithCollection[T, P]):
|
|
|
239
81
|
preview_type = self.get_preview_type()
|
|
240
82
|
return super().get_preview_by_id(id, company_id, preview_type)
|
|
241
83
|
|
|
242
|
-
# Additional async read methods (passthrough to base class)
|
|
243
|
-
async def get_by_id(self, id: str) -> T:
|
|
244
|
-
"""Get by ID"""
|
|
245
|
-
return await super().get_by_id(id)
|
|
246
|
-
|
|
247
|
-
async def get_all(self, company_id: str) -> List[T]:
|
|
248
|
-
"""Get all for company"""
|
|
249
|
-
return await super().get_all(company_id)
|
|
250
|
-
|
|
251
|
-
async def get_by_query(self, query: dict, company_id: Optional[str]) -> List[T]:
|
|
252
|
-
"""Get by query"""
|
|
253
|
-
return await super().get_by_query(query, company_id)
|
|
254
|
-
|
|
255
|
-
async def get_item_dumped(self, id: str) -> dict:
|
|
256
|
-
"""Get item by ID and return as JSON serialized dict for frontend"""
|
|
257
|
-
from ...models.utils.types.serializer_type import SerializerType
|
|
258
|
-
item = await self.get_by_id(id)
|
|
259
|
-
return item.model_dump_json(serializer=SerializerType.FRONTEND)
|
|
260
|
-
|
|
@@ -123,9 +123,8 @@ class ChattyAssetBaseContainer(Generic[T, P], ABC):
|
|
|
123
123
|
else:
|
|
124
124
|
return list(self.items.values())
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return {item.id: item for item in items}
|
|
126
|
+
def get_all_dict_id_item(self, company_id:Optional[StrObjectId]) -> Dict[StrObjectId, T]:
|
|
127
|
+
return {item.id: item for item in self.get_all(company_id)}
|
|
129
128
|
|
|
130
129
|
def get_all_previews(self, company_id:Optional[StrObjectId]) -> List[P]:
|
|
131
130
|
logger.debug(f"Getting all previews for {self.__class__.__name__}")
|
|
@@ -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
|