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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from letschatty.models.utils.types.identifier import StrObjectId
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LaunchSubscriptionStatus(StrEnum):
|
|
9
|
+
"""
|
|
10
|
+
Status of a user's subscription to a launch.
|
|
11
|
+
Note: Pre-qualification is tracked separately in ChattyAIAgentInChat.pre_qualify_status
|
|
12
|
+
"""
|
|
13
|
+
SUBSCRIBED = "subscribed" # Successfully subscribed
|
|
14
|
+
UNSUBSCRIBED = "unsubscribed" # User unsubscribed
|
|
15
|
+
ATTENDED = "attended" # User attended the launch
|
|
16
|
+
MISSED = "missed" # User missed the launch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LaunchSubscription(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Tracks a chat's subscription to a launch.
|
|
22
|
+
Embedded within the Launch model.
|
|
23
|
+
"""
|
|
24
|
+
chat_id: StrObjectId = Field(description="ID of the subscribed chat")
|
|
25
|
+
launch_id: StrObjectId = Field(description="ID of the associated launch")
|
|
26
|
+
status: LaunchSubscriptionStatus = Field(
|
|
27
|
+
default=LaunchSubscriptionStatus.SUBSCRIBED,
|
|
28
|
+
description="Current status of the subscription"
|
|
29
|
+
)
|
|
30
|
+
subscribed_at: datetime = Field(description="Timestamp when the user subscribed")
|
|
31
|
+
|
|
32
|
+
# Tracking (optional)
|
|
33
|
+
personal_access_link: Optional[str] = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description="Unique link for tracking user access to the launch (if tracking enabled)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Communications tracking
|
|
39
|
+
communications_sent: List[str] = Field(
|
|
40
|
+
default_factory=list,
|
|
41
|
+
description="List of IDs of communications already sent to this specific subscriber"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Welcome kit tracking
|
|
45
|
+
welcome_kit_sent: bool = Field(
|
|
46
|
+
default=False,
|
|
47
|
+
description="True if the welcome kit has been sent to this subscriber"
|
|
48
|
+
)
|
|
49
|
+
welcome_kit_sent_at: Optional[datetime] = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
description="Timestamp when the welcome kit was sent"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Status timestamps
|
|
55
|
+
attended_at: Optional[datetime] = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Timestamp when the user attended the launch"
|
|
58
|
+
)
|
|
59
|
+
unsubscribed_at: Optional[datetime] = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
description="Timestamp when the user unsubscribed"
|
|
62
|
+
)
|
|
63
|
+
|
|
@@ -5,7 +5,7 @@ from ...utils.types.identifier import StrObjectId
|
|
|
5
5
|
|
|
6
6
|
class Sale(CompanyAssetModel):
|
|
7
7
|
chat_id: StrObjectId
|
|
8
|
-
product_id: StrObjectId
|
|
8
|
+
product_id: Optional[StrObjectId] = Field(default=None)
|
|
9
9
|
quantity: int
|
|
10
10
|
total_amount: float
|
|
11
11
|
currency: str
|
|
@@ -16,7 +16,7 @@ class Sale(CompanyAssetModel):
|
|
|
16
16
|
creator_id: StrObjectId
|
|
17
17
|
|
|
18
18
|
class SaleRequest(BaseModel):
|
|
19
|
-
product_id: StrObjectId
|
|
19
|
+
product_id: Optional[StrObjectId] = Field(default=None)
|
|
20
20
|
quantity: int
|
|
21
21
|
creator_id: StrObjectId
|
|
22
22
|
total_amount: float
|
|
@@ -36,4 +36,4 @@ class SaleRequest(BaseModel):
|
|
|
36
36
|
"paid_amount": 100,
|
|
37
37
|
"installments": 1,
|
|
38
38
|
"details": {}
|
|
39
|
-
}
|
|
39
|
+
}
|
|
@@ -65,6 +65,7 @@ class UserPreview(ChattyAssetPreview):
|
|
|
65
65
|
roles: List[UserRole]
|
|
66
66
|
user_type : UserType
|
|
67
67
|
email : Optional[str] = Field(default=None)
|
|
68
|
+
auto_assign_chats: bool = Field(default=False)
|
|
68
69
|
|
|
69
70
|
@classmethod
|
|
70
71
|
def not_found(cls, id: StrObjectId, company_id: StrObjectId) -> 'UserPreview':
|
|
@@ -80,6 +81,7 @@ class UserPreview(ChattyAssetPreview):
|
|
|
80
81
|
photo_url="https://files-chatty.s3.us-east-1.amazonaws.com/Disen%CC%83o+sin+ti%CC%81tulo.png",
|
|
81
82
|
roles=[],
|
|
82
83
|
user_type=UserType.HUMAN,
|
|
84
|
+
auto_assign_chats=False
|
|
83
85
|
)
|
|
84
86
|
|
|
85
87
|
@classmethod
|
|
@@ -92,6 +94,7 @@ class UserPreview(ChattyAssetPreview):
|
|
|
92
94
|
projection["email"] = 1
|
|
93
95
|
projection["user_type"] = 1
|
|
94
96
|
projection["api_key_expires_at"] = 1
|
|
97
|
+
projection["auto_assign_chats"] = 1
|
|
95
98
|
return projection
|
|
96
99
|
|
|
97
100
|
@classmethod
|
|
@@ -107,7 +110,8 @@ class UserPreview(ChattyAssetPreview):
|
|
|
107
110
|
roles=asset.roles,
|
|
108
111
|
user_type=asset.user_type,
|
|
109
112
|
email=asset.email,
|
|
110
|
-
updated_at=asset.updated_at
|
|
113
|
+
updated_at=asset.updated_at,
|
|
114
|
+
auto_assign_chats=asset.auto_assign_chats
|
|
111
115
|
)
|
|
112
116
|
|
|
113
117
|
class User(CompanyAssetModel):
|
|
@@ -13,4 +13,5 @@ class MessagingSettings(BaseModel):
|
|
|
13
13
|
"""Messaging settings for the company"""
|
|
14
14
|
good_quality_score_definition: Optional[str] = Field(default=None, description="The definition of a good quality score")
|
|
15
15
|
products_info_level : ProductsInfoLevel = Field(default=ProductsInfoLevel.NAME, description="Whether to include all products info in the prompt or just the name / description")
|
|
16
|
-
ai_god_mode : Optional[ChattyAIMode] = Field(default=None, description="The mode of the ai god - null if not active")
|
|
16
|
+
ai_god_mode : Optional[ChattyAIMode] = Field(default=None, description="The mode of the ai god - null if not active")
|
|
17
|
+
tagger_instructions: Optional[str] = Field(default=None, description="The instructions for the tagger when using the tagger ai agent")
|
|
@@ -1,30 +1,88 @@
|
|
|
1
|
-
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Optional, Any
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
2
|
+
from typing import Optional, Any, ClassVar, List
|
|
3
|
+
from letschatty.models.base_models.chatty_asset_model import CompanyAssetModel, ChattyAssetPreview
|
|
4
|
+
import re
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
class
|
|
7
|
+
class FormFieldPreview(ChattyAssetPreview):
|
|
8
|
+
"""Preview model for FormField - used in list views"""
|
|
9
|
+
field_key: str = Field(description="Unique key identifier for the field")
|
|
10
|
+
is_system_field: bool = Field(default=False, description="True if this is a system/standard field")
|
|
11
|
+
question_example: Optional[str] = Field(default=None, description="Example question for AI")
|
|
12
|
+
description: Optional[str] = Field(default=None, description="Description of the field")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FormField(CompanyAssetModel):
|
|
6
16
|
"""
|
|
7
|
-
|
|
17
|
+
Company asset for data collection fields.
|
|
8
18
|
AI handles all validation - no regex or type constraints.
|
|
19
|
+
|
|
20
|
+
System fields (is_system_field=True) are standard fields that map directly
|
|
21
|
+
to Client model properties (name, email, phone, document_id, external_id).
|
|
22
|
+
|
|
23
|
+
When a system field is customized, a document is created in this same collection
|
|
24
|
+
with is_system_field=True and the same field_key as the base system field.
|
|
25
|
+
The field_key serves as the unique identifier for system field customizations.
|
|
26
|
+
|
|
27
|
+
field_key is FROZEN after creation - it cannot be changed.
|
|
9
28
|
"""
|
|
10
|
-
|
|
29
|
+
name: str = Field(description="Display name of the field (e.g., 'Email', 'Budget')")
|
|
30
|
+
field_key: str = Field(description="Unique key identifier for the field (e.g., 'email', 'budget'). Only letters, numbers, and underscores allowed. Frozen after creation.")
|
|
11
31
|
description: str = Field(
|
|
32
|
+
default="",
|
|
12
33
|
description="Description of what information this field aims to collect"
|
|
13
34
|
)
|
|
14
35
|
question_example: Optional[str] = Field(
|
|
15
36
|
default=None,
|
|
16
37
|
description="Example of how AI should ask for this information"
|
|
17
38
|
)
|
|
18
|
-
|
|
39
|
+
is_system_field: bool = Field(
|
|
19
40
|
default=False,
|
|
20
|
-
description="
|
|
41
|
+
description="True if this is a system/standard field (cannot be deleted)"
|
|
21
42
|
)
|
|
22
43
|
|
|
44
|
+
@field_validator('field_key', mode='before')
|
|
45
|
+
@classmethod
|
|
46
|
+
def normalize_field_key(cls, v: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Normalize field_key to only contain lowercase letters, numbers, and underscores.
|
|
49
|
+
- Converts to lowercase
|
|
50
|
+
- Replaces spaces and hyphens with underscores
|
|
51
|
+
- Removes any other special characters
|
|
52
|
+
- Strips leading/trailing whitespace
|
|
53
|
+
"""
|
|
54
|
+
if not v:
|
|
55
|
+
raise ValueError("field_key cannot be empty")
|
|
56
|
+
|
|
57
|
+
# Convert to lowercase and strip whitespace
|
|
58
|
+
normalized = v.lower().strip()
|
|
59
|
+
|
|
60
|
+
# Replace spaces and hyphens with underscores
|
|
61
|
+
normalized = normalized.replace(' ', '_').replace('-', '_')
|
|
62
|
+
|
|
63
|
+
# Remove any character that's not a letter, number, or underscore
|
|
64
|
+
normalized = re.sub(r'[^a-z0-9_]', '', normalized)
|
|
65
|
+
|
|
66
|
+
# Remove consecutive underscores
|
|
67
|
+
normalized = re.sub(r'_+', '_', normalized)
|
|
68
|
+
|
|
69
|
+
# Remove leading/trailing underscores
|
|
70
|
+
normalized = normalized.strip('_')
|
|
71
|
+
|
|
72
|
+
if not normalized:
|
|
73
|
+
raise ValueError("field_key must contain at least one letter or number")
|
|
74
|
+
|
|
75
|
+
return normalized
|
|
76
|
+
|
|
77
|
+
# Preview class for API responses
|
|
78
|
+
preview_class: ClassVar[type[FormFieldPreview]] = FormFieldPreview
|
|
79
|
+
|
|
23
80
|
@classmethod
|
|
24
81
|
def example_email(cls) -> dict:
|
|
25
82
|
"""Example email field"""
|
|
26
83
|
return {
|
|
27
|
-
"
|
|
84
|
+
"name": "Email",
|
|
85
|
+
"field_key": "email",
|
|
28
86
|
"description": "Customer's email address for communication",
|
|
29
87
|
"question_example": "Could you share your email so I can send you more information?",
|
|
30
88
|
"required": True
|
|
@@ -34,10 +92,115 @@ class FormField(BaseModel):
|
|
|
34
92
|
def example_budget(cls) -> dict:
|
|
35
93
|
"""Example budget field"""
|
|
36
94
|
return {
|
|
37
|
-
"
|
|
95
|
+
"name": "Budget",
|
|
96
|
+
"field_key": "budget",
|
|
38
97
|
"description": "Customer's budget allocation for the project",
|
|
39
|
-
"question_example": "What's your budget range for this project?"
|
|
40
|
-
|
|
98
|
+
"question_example": "What's your budget range for this project?"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SystemFormFields:
|
|
103
|
+
"""
|
|
104
|
+
Standard system fields available for all companies.
|
|
105
|
+
These map directly to Client model properties.
|
|
106
|
+
|
|
107
|
+
field_key is the unique identifier for system fields.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# System field keys (used as identifiers)
|
|
111
|
+
SYSTEM_KEYS = ["name", "email", "phone", "document_id", "external_id"]
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def get_all(cls) -> List[dict]:
|
|
115
|
+
"""Get all system form fields as dicts (for API responses)"""
|
|
116
|
+
return [
|
|
117
|
+
cls.name_field(),
|
|
118
|
+
cls.email_field(),
|
|
119
|
+
cls.phone_field(),
|
|
120
|
+
cls.document_id_field(),
|
|
121
|
+
cls.external_id_field(),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def get_all_keys(cls) -> List[str]:
|
|
126
|
+
"""Get all system field keys"""
|
|
127
|
+
return cls.SYSTEM_KEYS
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def is_system_field_key(cls, field_key: str) -> bool:
|
|
131
|
+
"""Check if a field_key belongs to a system field"""
|
|
132
|
+
return field_key in cls.SYSTEM_KEYS
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def get_by_key(cls, field_key: str) -> Optional[dict]:
|
|
136
|
+
"""Get a system field by its field_key"""
|
|
137
|
+
fields_map = {
|
|
138
|
+
"name": cls.name_field(),
|
|
139
|
+
"email": cls.email_field(),
|
|
140
|
+
"phone": cls.phone_field(),
|
|
141
|
+
"document_id": cls.document_id_field(),
|
|
142
|
+
"external_id": cls.external_id_field(),
|
|
143
|
+
}
|
|
144
|
+
return fields_map.get(field_key)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def name_field(cls) -> dict:
|
|
148
|
+
"""Standard name field"""
|
|
149
|
+
return {
|
|
150
|
+
"field_key": "name",
|
|
151
|
+
"name": "Nombre",
|
|
152
|
+
"description": "Nombre del cliente",
|
|
153
|
+
"question_example": "¿Cuál es tu nombre?",
|
|
154
|
+
"is_system_field": True,
|
|
155
|
+
"deleted_at": None
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def email_field(cls) -> dict:
|
|
160
|
+
"""Standard email field"""
|
|
161
|
+
return {
|
|
162
|
+
"field_key": "email",
|
|
163
|
+
"name": "Email",
|
|
164
|
+
"description": "Dirección de correo electrónico del cliente",
|
|
165
|
+
"question_example": "¿Podrías compartirme tu email para enviarte más información?",
|
|
166
|
+
"is_system_field": True,
|
|
167
|
+
"deleted_at": None
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def phone_field(cls) -> dict:
|
|
172
|
+
"""Standard phone field"""
|
|
173
|
+
return {
|
|
174
|
+
"field_key": "phone",
|
|
175
|
+
"name": "Teléfono",
|
|
176
|
+
"description": "Número de teléfono del cliente",
|
|
177
|
+
"question_example": "¿Cuál es tu número de teléfono?",
|
|
178
|
+
"is_system_field": True,
|
|
179
|
+
"deleted_at": None
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def document_id_field(cls) -> dict:
|
|
184
|
+
"""Standard document ID field (DNI/ID)"""
|
|
185
|
+
return {
|
|
186
|
+
"field_key": "document_id",
|
|
187
|
+
"name": "DNI / Documento",
|
|
188
|
+
"description": "Número de documento de identidad del cliente",
|
|
189
|
+
"question_example": "¿Cuál es tu número de DNI o documento?",
|
|
190
|
+
"is_system_field": True,
|
|
191
|
+
"deleted_at": None
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def external_id_field(cls) -> dict:
|
|
196
|
+
"""Standard external/CRM ID field"""
|
|
197
|
+
return {
|
|
198
|
+
"field_key": "external_id",
|
|
199
|
+
"name": "ID Externo / CRM",
|
|
200
|
+
"description": "Identificador externo del cliente en CRM u otro sistema",
|
|
201
|
+
"question_example": "¿Tienes un número de cliente o código de referencia?",
|
|
202
|
+
"is_system_field": True,
|
|
203
|
+
"deleted_at": None
|
|
41
204
|
}
|
|
42
205
|
|
|
43
206
|
|
|
@@ -49,7 +212,11 @@ class CollectedData(BaseModel):
|
|
|
49
212
|
name: Optional[str] = Field(default=None, description="Customer's name")
|
|
50
213
|
email: Optional[str] = Field(default=None, description="Customer's email address")
|
|
51
214
|
phone: Optional[str] = Field(default=None, description="Customer's phone number")
|
|
52
|
-
document_id: Optional[str] = Field(
|
|
215
|
+
document_id: Optional[str] = Field(
|
|
216
|
+
default=None,
|
|
217
|
+
alias="dni",
|
|
218
|
+
description="Customer's DNI/ID number"
|
|
219
|
+
)
|
|
53
220
|
|
|
54
221
|
# Generic key-value store for any other collected fields
|
|
55
222
|
additional_fields: dict[str, Any] = Field(
|
|
@@ -57,6 +224,9 @@ class CollectedData(BaseModel):
|
|
|
57
224
|
description="Additional collected fields as key-value pairs"
|
|
58
225
|
)
|
|
59
226
|
|
|
227
|
+
model_config = ConfigDict(
|
|
228
|
+
populate_by_name=True
|
|
229
|
+
)
|
|
60
230
|
|
|
61
231
|
@classmethod
|
|
62
232
|
def example(cls) -> dict:
|
|
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Dict, List, Generic, TypeVar, Type, Optional,
|
|
|
4
4
|
from bson.objectid import ObjectId
|
|
5
5
|
from pymongo.collection import Collection
|
|
6
6
|
from pymongo.database import Database
|
|
7
|
-
from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorCollection
|
|
8
7
|
|
|
9
8
|
from letschatty.models.chat.chat import Chat
|
|
10
9
|
from ...models.base_models.chatty_asset_model import ChattyAssetModel, CompanyAssetModel, ChattyAssetPreview
|
|
@@ -27,116 +26,58 @@ P = TypeVar('P', bound=ChattyAssetPreview)
|
|
|
27
26
|
class ChattyAssetCollectionInterface(Generic[T, P], ABC):
|
|
28
27
|
def __init__(self, database: str, collection: str, connection: MongoConnection, type: Type[T], preview_type: Optional[Type[P]] = None):
|
|
29
28
|
logger.info(f"Initializing collection {collection} in database {database}")
|
|
30
|
-
# Sync database and collection (existing)
|
|
31
29
|
self.db: Database = connection.client[database]
|
|
32
30
|
self.collection: Collection = connection.client[database][collection]
|
|
33
|
-
|
|
34
|
-
# NEW: Async database and collection
|
|
35
|
-
# Store connection reference to ensure we use current event loop
|
|
36
|
-
self._connection = connection
|
|
37
|
-
self._database_name = database
|
|
38
|
-
self._collection_name = collection
|
|
39
|
-
self._async_db: Optional[AsyncIOMotorDatabase] = None
|
|
40
|
-
self._async_collection: Optional[AsyncIOMotorCollection] = None
|
|
41
|
-
|
|
42
31
|
self.type = type
|
|
43
32
|
self.preview_type = preview_type
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def async_db(self) -> AsyncIOMotorDatabase:
|
|
47
|
-
"""Get async database, ensuring it uses the current event loop"""
|
|
48
|
-
# Always ensure connection's async client is using current loop (for Lambda compatibility)
|
|
49
|
-
self._connection._ensure_async_client_loop()
|
|
50
|
-
# Recreate database reference to ensure it uses the current client
|
|
51
|
-
self._async_db = self._connection.async_client[self._database_name]
|
|
52
|
-
return self._async_db
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def async_collection(self) -> AsyncIOMotorCollection:
|
|
56
|
-
"""Get async collection, ensuring it uses the current event loop"""
|
|
57
|
-
# Always ensure connection's async client is using current loop (for Lambda compatibility)
|
|
58
|
-
self._connection._ensure_async_client_loop()
|
|
59
|
-
# Recreate collection reference to ensure it uses the current client
|
|
60
|
-
self._async_collection = self._connection.async_client[self._database_name][self._collection_name]
|
|
61
|
-
return self._async_collection
|
|
62
33
|
@abstractmethod
|
|
63
34
|
def create_instance(self, data: dict) -> T:
|
|
64
35
|
"""Factory method to create instance from data"""
|
|
65
36
|
pass
|
|
66
37
|
|
|
67
|
-
|
|
68
|
-
async def insert(self, asset: T) -> StrObjectId:
|
|
69
|
-
"""Async insert operation"""
|
|
38
|
+
def insert(self, asset: T) -> StrObjectId:
|
|
70
39
|
if not isinstance(asset, self.type):
|
|
71
40
|
raise ValueError(f"Asset must be of type {self.type.__name__}")
|
|
72
41
|
document = asset.model_dump_json(serializer=SerializerType.DATABASE)
|
|
73
42
|
logger.debug(f"Inserting document: {document}")
|
|
74
|
-
result =
|
|
43
|
+
result = self.collection.insert_one(document)
|
|
75
44
|
if not result.inserted_id:
|
|
76
45
|
raise Exception("Failed to insert document")
|
|
77
46
|
logger.debug(f"Inserted document with id {result.inserted_id}")
|
|
78
47
|
return result.inserted_id
|
|
79
48
|
|
|
80
|
-
|
|
81
|
-
"""Async update operation"""
|
|
49
|
+
def update(self, asset: T) -> StrObjectId:
|
|
82
50
|
logger.debug(f"Updating document with id {asset.id}")
|
|
83
51
|
if not isinstance(asset, self.type):
|
|
84
52
|
raise ValueError(f"Asset must be of type {self.type.__name__}")
|
|
85
53
|
asset.update_now()
|
|
86
54
|
document = asset.model_dump_json(serializer=SerializerType.DATABASE)
|
|
87
|
-
document.pop('_id', None)
|
|
88
|
-
result =
|
|
89
|
-
{"_id": ObjectId(asset.id)},
|
|
90
|
-
{"$set": document}
|
|
91
|
-
)
|
|
55
|
+
document.pop('_id', None) # Still needed
|
|
56
|
+
result = self.collection.update_one({"_id": ObjectId(asset.id)}, {"$set": document})
|
|
92
57
|
if result.matched_count == 0:
|
|
93
58
|
raise NotFoundError(f"No document found with id {asset.id}")
|
|
94
59
|
if result.modified_count == 0:
|
|
95
60
|
logger.debug(f"No changes were made to the document with id {asset.id} probably because the values were the same")
|
|
96
61
|
return asset.id
|
|
97
62
|
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
63
|
+
def get_by_id(self, doc_id: str) -> T:
|
|
64
|
+
logger.debug(f"Getting document with id {doc_id} from collection {self.collection.name} and db {self.db.name}")
|
|
65
|
+
doc = self.collection.find_one({"_id": ObjectId(doc_id)})
|
|
66
|
+
|
|
102
67
|
if doc:
|
|
103
68
|
return self.create_instance(doc)
|
|
104
69
|
else:
|
|
105
|
-
raise NotFoundError(f"No document found with id {doc_id} in collection")
|
|
70
|
+
raise NotFoundError(f"No document found with id {doc_id} in db collection {self.collection.name} and db {self.db.name}")
|
|
106
71
|
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
logger.debug(f"Getting documents from collection with company_id {company_id} and query {query}")
|
|
72
|
+
def get_docs(self, company_id:Optional[StrObjectId], query = {}, limit = 0) -> List[T]:
|
|
73
|
+
logger.debug(f"Getting documents from collection {self.collection.name} with company_id {company_id} and query {query}")
|
|
110
74
|
if company_id:
|
|
111
|
-
query = query.copy()
|
|
75
|
+
query = query.copy() # Create a copy to avoid modifying the original
|
|
112
76
|
query["company_id"] = company_id
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
cursor = cursor.limit(limit)
|
|
116
|
-
docs = await cursor.to_list(length=limit if limit > 0 else None)
|
|
117
|
-
logger.debug(f"Found {len(docs)} documents")
|
|
77
|
+
docs = list(self.collection.find(filter=query).limit(limit))
|
|
78
|
+
logger.debug(f"Found {len(docs)} documents in collection {self.collection.name}")
|
|
118
79
|
return [self.create_instance(doc) for doc in docs]
|
|
119
80
|
|
|
120
|
-
async def delete(self, doc_id: str, deletion_type: DeletionType = DeletionType.LOGICAL) -> StrObjectId:
|
|
121
|
-
"""Delete operation"""
|
|
122
|
-
logger.debug(f"Deleting document with id {doc_id} - deletion type: {deletion_type}")
|
|
123
|
-
if deletion_type == DeletionType.LOGICAL:
|
|
124
|
-
result = await self.async_collection.update_one(
|
|
125
|
-
{"_id": ObjectId(doc_id)},
|
|
126
|
-
{"$set": {"deleted_at": datetime.now(ZoneInfo("UTC")), "updated_at": datetime.now(ZoneInfo("UTC"))}}
|
|
127
|
-
)
|
|
128
|
-
if result.modified_count == 0:
|
|
129
|
-
raise NotFoundError(f"No document found with id {doc_id}")
|
|
130
|
-
return doc_id
|
|
131
|
-
elif deletion_type == DeletionType.PHYSICAL:
|
|
132
|
-
result = await self.async_collection.delete_one({"_id": ObjectId(doc_id)})
|
|
133
|
-
if result.deleted_count == 0:
|
|
134
|
-
raise NotFoundError(f"No document found with id {doc_id}")
|
|
135
|
-
return doc_id
|
|
136
|
-
else:
|
|
137
|
-
raise ValueError(f"Invalid deletion type: {deletion_type}")
|
|
138
|
-
|
|
139
|
-
# Additional methods - keeping these sync as they're less critical
|
|
140
81
|
def get_preview_docs(self, projection = {}, all=True) -> List[P]:
|
|
141
82
|
"""We get the previews of all the documents in the collection for all companies"""
|
|
142
83
|
if not self.preview_type:
|
|
@@ -157,31 +98,18 @@ class ChattyAssetCollectionInterface(Generic[T, P], ABC):
|
|
|
157
98
|
docs = self.collection.find(query)
|
|
158
99
|
return [self.create_instance(doc) for doc in docs]
|
|
159
100
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
object_ids = [ObjectId(id) for id in ids]
|
|
175
|
-
|
|
176
|
-
# Query for all filter criteria with matching IDs
|
|
177
|
-
query = {
|
|
178
|
-
"_id": {"$in": object_ids},
|
|
179
|
-
"deleted_at": None
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
# Use the sync collection directly (inherited from ChattyAssetCollectionInterface)
|
|
183
|
-
docs = await self.async_collection.find(query).to_list(length=None)
|
|
184
|
-
|
|
185
|
-
# Create FilterCriteria instances
|
|
186
|
-
return [self.create_instance(doc) for doc in docs]
|
|
101
|
+
def delete(self, doc_id: str, deletion_type : DeletionType = DeletionType.LOGICAL) -> StrObjectId:
|
|
102
|
+
logger.debug(f"Deleting document with id {doc_id} - deletion type: {deletion_type}")
|
|
103
|
+
if deletion_type == DeletionType.LOGICAL:
|
|
104
|
+
result = self.collection.update_one({"_id": ObjectId(doc_id)}, {"$set": {"deleted_at": datetime.now(ZoneInfo("UTC")), "updated_at": datetime.now(ZoneInfo("UTC"))}})
|
|
105
|
+
if result.modified_count == 0:
|
|
106
|
+
raise NotFoundError(f"No document found with id {doc_id}")
|
|
107
|
+
return doc_id
|
|
108
|
+
elif deletion_type == DeletionType.PHYSICAL:
|
|
109
|
+
result = self.collection.delete_one({"_id": ObjectId(doc_id)})
|
|
110
|
+
if result.deleted_count == 0:
|
|
111
|
+
raise NotFoundError(f"No document found with id {doc_id}")
|
|
112
|
+
return doc_id
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Invalid deletion type: {deletion_type}")
|
|
187
115
|
|