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
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
from ..base_models.singleton import SingletonMeta
|
|
2
2
|
from pymongo import MongoClient
|
|
3
|
-
from motor.motor_asyncio import AsyncIOMotorClient
|
|
4
3
|
from typing import Optional
|
|
5
|
-
from urllib.parse import quote_plus
|
|
6
4
|
import os
|
|
7
5
|
import atexit
|
|
8
|
-
import asyncio
|
|
9
|
-
import logging
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
6
|
|
|
13
7
|
class MongoConnection(metaclass=SingletonMeta):
|
|
14
8
|
def __init__(
|
|
@@ -16,103 +10,26 @@ class MongoConnection(metaclass=SingletonMeta):
|
|
|
16
10
|
username: Optional[str] = None,
|
|
17
11
|
password: Optional[str] = None,
|
|
18
12
|
uri_base: Optional[str] = None,
|
|
19
|
-
instance: Optional[str] = None
|
|
20
|
-
verify_on_init: bool = True
|
|
13
|
+
instance: Optional[str] = None
|
|
21
14
|
):
|
|
22
15
|
self.username = username or os.getenv('MONGO_USERNAME')
|
|
23
16
|
self.password = password or os.getenv('MONGO_PASSWORD')
|
|
24
17
|
self.uri_base = uri_base or os.getenv('MONGO_URI_BASE')
|
|
25
18
|
self.instance = instance or os.getenv('MONGO_INSTANCE_COMPONENT')
|
|
26
|
-
|
|
19
|
+
|
|
27
20
|
if not all([self.username, self.password, self.uri_base, self.instance]):
|
|
28
21
|
raise ValueError("Missing required MongoDB connection parameters")
|
|
29
|
-
|
|
30
|
-
|
|
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)
|
|
22
|
+
|
|
23
|
+
uri = f"{self.uri_base}://{self.username}:{self.password}@{self.instance}.mongodb.net"
|
|
37
24
|
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)"""
|
|
78
25
|
try:
|
|
79
|
-
|
|
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
|
|
26
|
+
self.client.admin.command('ping')
|
|
110
27
|
except Exception as e:
|
|
111
|
-
|
|
112
|
-
raise ConnectionError(f"Failed to
|
|
28
|
+
self.client.close()
|
|
29
|
+
raise ConnectionError(f"Failed to connect to MongoDB: {str(e)}")
|
|
113
30
|
|
|
31
|
+
atexit.register(self.close)
|
|
32
|
+
|
|
114
33
|
def close(self) -> None:
|
|
115
34
|
if hasattr(self, 'client'):
|
|
116
35
|
self.client.close()
|
|
117
|
-
if hasattr(self, 'async_client'):
|
|
118
|
-
self.async_client.close()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, model_validator, ValidationInfo
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from urllib.parse import urlparse, unquote
|
|
3
4
|
from .content_media import ChattyContentMedia
|
|
4
5
|
|
|
5
6
|
class ChattyContentDocument(ChattyContentMedia):
|
|
@@ -8,5 +9,6 @@ class ChattyContentDocument(ChattyContentMedia):
|
|
|
8
9
|
@model_validator(mode='before')
|
|
9
10
|
def validate_filename(cls, data: dict, info: ValidationInfo):
|
|
10
11
|
if not data.get("filename") and data.get("url"):
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
parsed = urlparse(data["url"])
|
|
13
|
+
data["filename"] = unquote(parsed.path.split("/")[-1])
|
|
14
|
+
return data
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator, HttpUrl
|
|
2
2
|
from typing import Optional
|
|
3
|
+
from urllib.parse import quote
|
|
3
4
|
class ChattyContentMedia(BaseModel):
|
|
4
5
|
id: Optional[str] = Field(description="Unique identifier for the image. Also known as media_id", default="")
|
|
5
6
|
url: str = Field(description="URL of the media from S3")
|
|
@@ -11,9 +12,9 @@ class ChattyContentMedia(BaseModel):
|
|
|
11
12
|
def validate_url(cls, v):
|
|
12
13
|
if not v:
|
|
13
14
|
raise ValueError("URL is required")
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
encoded = quote(str(v), safe=":/?&=%#")
|
|
16
|
+
HttpUrl(encoded)
|
|
17
|
+
return encoded
|
|
16
18
|
|
|
17
19
|
def get_body_or_caption(self) -> str:
|
|
18
20
|
return self.caption
|
|
19
|
-
|
|
@@ -5,7 +5,6 @@ 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
|
|
9
8
|
logger = logging.getLogger("logger")
|
|
10
9
|
|
|
11
10
|
class Context(BaseModel):
|
|
@@ -230,16 +229,4 @@ class OpenAIError(CustomException):
|
|
|
230
229
|
|
|
231
230
|
class NewerAvailableMessageToBeProcessedByAiAgent(CustomException):
|
|
232
231
|
def __init__(self, message="Duplicated incoming message call for ai agent", status_code=400, **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)
|
|
232
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
@@ -14,16 +14,13 @@ 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
|
-
|
|
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)
|
|
17
|
+
expected_interval_minutes = follow_up_strategy.get_interval_for_followup(smart_follow_up_state.consecutive_count)
|
|
21
18
|
last_message_timestamp = chat.last_message_timestamp
|
|
22
19
|
if last_message_timestamp is None:
|
|
23
20
|
raise HumanInterventionRequired("There's no last message in the chat, can't validate the minimum time since last message for the smart follow up")
|
|
24
21
|
time_since_last_message = datetime.now(ZoneInfo('UTC')) - last_message_timestamp
|
|
25
22
|
if time_since_last_message.total_seconds() < expected_interval_minutes * 60:
|
|
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 #{
|
|
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 #{smart_follow_up_state.consecutive_count}")
|
|
27
24
|
return True
|
|
28
25
|
|
|
29
26
|
|
|
@@ -211,6 +211,25 @@ 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}. Motivo: {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
|
+
|
|
214
233
|
@staticmethod
|
|
215
234
|
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
235
|
"""
|
|
@@ -266,36 +285,46 @@ class ChatService:
|
|
|
266
285
|
return next((state for state in chat.flow_states if state.is_smart_follow_up), None)
|
|
267
286
|
|
|
268
287
|
@staticmethod
|
|
269
|
-
def create_sale(
|
|
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:
|
|
270
296
|
"""
|
|
271
297
|
Add a sale to the chat.
|
|
272
298
|
"""
|
|
273
299
|
if next((sale for sale in chat.client.sales if sale.asset_id == sale.id), None) is not None:
|
|
274
300
|
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 [])
|
|
275
303
|
assigned_asset = SaleAssignedToChat(
|
|
276
304
|
asset_type=ChatAssetType.SALE,
|
|
277
305
|
asset_id=sale.id,
|
|
278
306
|
assigned_at=sale.created_at,
|
|
279
307
|
assigned_by=execution_context.executor.id,
|
|
280
|
-
product_id=product.id
|
|
308
|
+
product_id=product.id if product else None,
|
|
309
|
+
product_ids=assigned_product_ids
|
|
281
310
|
)
|
|
282
311
|
execution_context.set_event_time(assigned_asset.assigned_at)
|
|
283
312
|
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 {
|
|
313
|
+
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))
|
|
314
|
+
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
315
|
return assigned_asset
|
|
287
316
|
|
|
288
317
|
@staticmethod
|
|
289
|
-
def update_sale(chat
|
|
318
|
+
def update_sale(chat: Chat, execution_context: ExecutionContext, sale: Sale, product_label: str) -> Sale:
|
|
290
319
|
"""
|
|
291
320
|
Update a sale for the chat.
|
|
292
321
|
"""
|
|
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 {
|
|
322
|
+
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))
|
|
323
|
+
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
324
|
return sale
|
|
296
325
|
|
|
297
326
|
@staticmethod
|
|
298
|
-
def delete_sale(chat
|
|
327
|
+
def delete_sale(chat: Chat, execution_context: ExecutionContext, sale_id: StrObjectId, product_label: str) -> SaleAssignedToChat:
|
|
299
328
|
"""
|
|
300
329
|
Logically remove a sale from the chat.
|
|
301
330
|
"""
|
|
@@ -303,8 +332,8 @@ class ChatService:
|
|
|
303
332
|
assigned_asset_to_remove = next(sale for sale in chat.client.sales if sale.asset_id == sale_id)
|
|
304
333
|
chat.client.sales.remove(assigned_asset_to_remove)
|
|
305
334
|
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 {
|
|
335
|
+
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))
|
|
336
|
+
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
337
|
return assigned_asset_to_remove
|
|
309
338
|
except StopIteration:
|
|
310
339
|
raise NotFoundError(message=f"Sale with id {sale_id} not found in chat {chat.id}")
|
|
@@ -873,9 +902,16 @@ class ChatService:
|
|
|
873
902
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
874
903
|
logger.info(f"Updated collected data for chat {chat.id}: {', '.join(updated_fields)}")
|
|
875
904
|
|
|
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]
|
|
876
912
|
ChatService.add_central_notification_from_text(
|
|
877
913
|
chat=chat,
|
|
878
|
-
body=f"
|
|
914
|
+
body=f"Datos del cliente recopilados: {', '.join(display_fields)}",
|
|
879
915
|
subtype=MessageSubtype.CLIENT_INFO_UPDATED,
|
|
880
916
|
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
881
917
|
)
|
|
@@ -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__}")
|