letschatty 0.4.280__py3-none-any.whl → 0.4.343__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 +35 -1
- letschatty/models/ai_microservices/lambda_events.py +85 -45
- letschatty/models/ai_microservices/lambda_invokation_types.py +6 -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 +13 -50
- letschatty/models/chat/chat.py +14 -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 +3 -1
- letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py +58 -0
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +46 -2
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +93 -1
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +111 -0
- letschatty/models/company/assets/ai_agents_v2/statuses.py +33 -0
- letschatty/models/company/assets/assignment/__init__.py +14 -0
- letschatty/models/company/assets/assignment/assignment_assets.py +75 -0
- letschatty/models/company/assets/automation.py +10 -19
- letschatty/models/company/assets/chat_assets.py +12 -2
- letschatty/models/company/assets/company_assets.py +3 -0
- letschatty/models/company/assets/launch/__init__.py +12 -0
- letschatty/models/company/assets/launch/launch.py +128 -0
- letschatty/models/company/assets/launch/scheduled_communication.py +44 -0
- letschatty/models/company/assets/launch/subscription.py +63 -0
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/company/assets/users/user.py +5 -1
- letschatty/models/company/company_messaging_settgins.py +2 -1
- letschatty/models/company/form_field.py +182 -12
- 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 +24 -13
- letschatty/services/ai_agents/smart_follow_up_service_v2.py +10 -0
- letschatty/services/chat/chat_service.py +79 -14
- 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 -23
- letschatty/services/users/user_factory.py +14 -8
- letschatty/services/validators/analytics_validator.py +11 -0
- {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/METADATA +1 -1
- {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/RECORD +56 -83
- {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/WHEEL +1 -1
- 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.280.dist-info → letschatty-0.4.343.dist-info}/LICENSE +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):
|
|
@@ -60,6 +59,30 @@ class WhatsAppPayloadValidationError(Exception):
|
|
|
60
59
|
def __init__(self, message="WhatsApp payload validation error", status_code=400, **context_data):
|
|
61
60
|
super().__init__(message, status_code=status_code, **context_data)
|
|
62
61
|
|
|
62
|
+
|
|
63
|
+
class ChatWithActiveContinuousConversation(CustomException):
|
|
64
|
+
"""
|
|
65
|
+
Raised when an AI agent is triggered on a chat that has an active Continuous Conversation.
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, message="Chat has an active continuous conversation", status_code=400, **context_data):
|
|
68
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChattyAIModeOff(CustomException):
|
|
72
|
+
"""
|
|
73
|
+
Raised when an AI agent is in OFF mode and cannot be triggered.
|
|
74
|
+
"""
|
|
75
|
+
def __init__(self, message="Chatty AI agent is OFF", status_code=400, **context_data):
|
|
76
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MissingAIAgentInChat(CustomException):
|
|
80
|
+
"""
|
|
81
|
+
Raised when a chat has no AI agent assigned but one is required for the operation.
|
|
82
|
+
"""
|
|
83
|
+
def __init__(self, message="AI agent not assigned to chat", status_code=404, **context_data):
|
|
84
|
+
super().__init__(message, status_code=status_code, **context_data)
|
|
85
|
+
|
|
63
86
|
class UnsuportedChannel(CustomException):
|
|
64
87
|
def __init__(self, message="Channel not supported", status_code=400, **context_data):
|
|
65
88
|
super().__init__(message, status_code=status_code, **context_data)
|
|
@@ -206,16 +229,4 @@ class OpenAIError(CustomException):
|
|
|
206
229
|
|
|
207
230
|
class NewerAvailableMessageToBeProcessedByAiAgent(CustomException):
|
|
208
231
|
def __init__(self, message="Duplicated incoming message call for ai agent", status_code=400, **context_data):
|
|
209
|
-
super().__init__(message, status_code=status_code, **context_data)
|
|
210
|
-
|
|
211
|
-
class MissingAIAgentInChat(CustomException):
|
|
212
|
-
def __init__(self, message="Missing AI agent in chat", status_code=400, **context_data):
|
|
213
|
-
super().__init__(message, status_code=status_code, **context_data)
|
|
214
|
-
|
|
215
|
-
class ChattyAIModeOff(CustomException):
|
|
216
|
-
def __init__(self, message="Chatty AI agent is in OFF mode", status_code=400, **context_data):
|
|
217
|
-
super().__init__(message, status_code=status_code, **context_data)
|
|
218
|
-
|
|
219
|
-
class ChatWithActiveContinuousConversation(CustomException):
|
|
220
|
-
def __init__(self, message="Chat has active continuous conversation", status_code=400, **context_data):
|
|
221
232
|
super().__init__(message, status_code=status_code, **context_data)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
from letschatty.models.chat.chat import Chat, FlowStateAssignedToChat
|
|
3
|
+
from letschatty.models.chat.flow_link_state import StateTrigger
|
|
3
4
|
from letschatty.models.company.assets.ai_agents_v2.follow_up_strategy import FollowUpStrategy
|
|
4
5
|
from letschatty.models.company.assets.ai_agents_v2.ai_agents_decision_output import SmartFollowUpDecision, SmartFollowUpDecisionAction
|
|
5
6
|
from letschatty.services.chat.chat_service import ChatService
|
|
@@ -85,5 +86,14 @@ class SmartFollowUpService:
|
|
|
85
86
|
SmartFollowUpService.validate_next_call_time_or_default(decision, smart_follow_up_state.consecutive_count + 1, follow_up_strategy)
|
|
86
87
|
smart_follow_up_state.next_call = decision.next_call_time_value
|
|
87
88
|
ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
|
|
89
|
+
elif decision.action == SmartFollowUpDecisionAction.POSTPONE_DELTA_TIME:
|
|
90
|
+
logger.debug(f"Postponing smart follow up for chat {chat.id} by delta time {decision.next_call_time_value}")
|
|
91
|
+
SmartFollowUpService.validate_next_call_time_or_default(decision, smart_follow_up_state.consecutive_count + 1, follow_up_strategy)
|
|
92
|
+
smart_follow_up_state.next_call = decision.next_call_time_value
|
|
93
|
+
ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
|
|
94
|
+
elif decision.action == SmartFollowUpDecisionAction.POSTPONE_TILL_UPDATE:
|
|
95
|
+
logger.debug(f"Postponing smart follow up for chat {chat.id} till update")
|
|
96
|
+
smart_follow_up_state.trigger = StateTrigger.CHAT_UPDATE
|
|
97
|
+
ChatService.update_workflow_link(chat=chat, workflow_id=smart_follow_up_state.flow_id, workflow_link=smart_follow_up_state, execution_context=execution_context)
|
|
88
98
|
else:
|
|
89
99
|
raise ValueError(f"Invalid action: {decision.action}")
|
|
@@ -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}")
|
|
@@ -663,7 +711,10 @@ class ChatService:
|
|
|
663
711
|
if client_data.external_id is not None:
|
|
664
712
|
chat.client.external_id = client_data.external_id
|
|
665
713
|
if client_data.lead_form_data is not None:
|
|
666
|
-
|
|
714
|
+
# Merge with existing lead_form_data instead of replacing
|
|
715
|
+
if chat.client.lead_form_data is None:
|
|
716
|
+
chat.client.lead_form_data = {}
|
|
717
|
+
chat.client.lead_form_data.update(client_data.lead_form_data)
|
|
667
718
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
668
719
|
ChatService.add_central_notification_from_text(chat=chat, body=f"La info del cliente fue actualizada por {execution_context.executor.name}", subtype=MessageSubtype.CLIENT_INFO_UPDATED)
|
|
669
720
|
return chat
|
|
@@ -846,6 +897,13 @@ class ChatService:
|
|
|
846
897
|
chat.client.email = collected_data.email
|
|
847
898
|
updated_fields.append("email")
|
|
848
899
|
|
|
900
|
+
if collected_data.phone:
|
|
901
|
+
if chat.client.lead_form_data is None:
|
|
902
|
+
chat.client.lead_form_data = {}
|
|
903
|
+
if chat.client.lead_form_data.get("phone") != collected_data.phone:
|
|
904
|
+
chat.client.lead_form_data["phone"] = collected_data.phone
|
|
905
|
+
updated_fields.append("phone")
|
|
906
|
+
|
|
849
907
|
if collected_data.document_id and chat.client.document_id != collected_data.document_id:
|
|
850
908
|
chat.client.document_id = collected_data.document_id
|
|
851
909
|
updated_fields.append("document_id")
|
|
@@ -863,11 +921,18 @@ class ChatService:
|
|
|
863
921
|
execution_context.set_event_time(datetime.now(tz=ZoneInfo("UTC")))
|
|
864
922
|
logger.info(f"Updated collected data for chat {chat.id}: {', '.join(updated_fields)}")
|
|
865
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]
|
|
866
931
|
ChatService.add_central_notification_from_text(
|
|
867
932
|
chat=chat,
|
|
868
|
-
body=f"
|
|
933
|
+
body=f"Datos del cliente recopilados: {', '.join(display_fields)}",
|
|
869
934
|
subtype=MessageSubtype.CLIENT_INFO_UPDATED,
|
|
870
935
|
context=ChattyContext(chain_of_thought_id=execution_context.chain_of_thought_id)
|
|
871
936
|
)
|
|
872
937
|
|
|
873
|
-
return chat.client.lead_form_data
|
|
938
|
+
return chat.client.lead_form_data
|
|
@@ -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
|
-
)
|