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.
Files changed (92) hide show
  1. letschatty/models/ai_microservices/__init__.py +3 -3
  2. letschatty/models/ai_microservices/expected_output.py +35 -1
  3. letschatty/models/ai_microservices/lambda_events.py +85 -45
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +6 -3
  5. letschatty/models/analytics/events/__init__.py +2 -3
  6. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  7. letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
  8. letschatty/models/analytics/events/event_type_to_classes.py +6 -3
  9. letschatty/models/analytics/events/event_types.py +13 -50
  10. letschatty/models/chat/chat.py +14 -2
  11. letschatty/models/chat/chat_with_assets.py +6 -1
  12. letschatty/models/chat/client.py +0 -2
  13. letschatty/models/chat/continuous_conversation.py +1 -1
  14. letschatty/models/company/CRM/funnel.py +365 -33
  15. letschatty/models/company/__init__.py +3 -1
  16. letschatty/models/company/assets/ai_agents_v2/ai_agent_message_draft.py +58 -0
  17. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  18. letschatty/models/company/assets/ai_agents_v2/chain_of_thought_in_chat.py +5 -3
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent.py +46 -2
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +93 -1
  21. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +111 -0
  22. letschatty/models/company/assets/ai_agents_v2/statuses.py +33 -0
  23. letschatty/models/company/assets/assignment/__init__.py +14 -0
  24. letschatty/models/company/assets/assignment/assignment_assets.py +75 -0
  25. letschatty/models/company/assets/automation.py +10 -19
  26. letschatty/models/company/assets/chat_assets.py +12 -2
  27. letschatty/models/company/assets/company_assets.py +3 -0
  28. letschatty/models/company/assets/launch/__init__.py +12 -0
  29. letschatty/models/company/assets/launch/launch.py +128 -0
  30. letschatty/models/company/assets/launch/scheduled_communication.py +44 -0
  31. letschatty/models/company/assets/launch/subscription.py +63 -0
  32. letschatty/models/company/assets/sale.py +3 -3
  33. letschatty/models/company/assets/users/user.py +5 -1
  34. letschatty/models/company/company_messaging_settgins.py +2 -1
  35. letschatty/models/company/form_field.py +182 -12
  36. letschatty/models/data_base/collection_interface.py +29 -101
  37. letschatty/models/data_base/mongo_connection.py +9 -92
  38. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
  39. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
  40. letschatty/models/utils/custom_exceptions/custom_exceptions.py +24 -13
  41. letschatty/services/ai_agents/smart_follow_up_service_v2.py +10 -0
  42. letschatty/services/chat/chat_service.py +79 -14
  43. letschatty/services/chatty_assets/__init__.py +0 -12
  44. letschatty/services/chatty_assets/asset_service.py +13 -190
  45. letschatty/services/chatty_assets/base_container.py +2 -3
  46. letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
  47. letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
  48. letschatty/services/events/events_manager.py +1 -218
  49. letschatty/services/factories/analytics/events_factory.py +6 -66
  50. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -23
  51. letschatty/services/users/user_factory.py +14 -8
  52. letschatty/services/validators/analytics_validator.py +11 -0
  53. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/METADATA +1 -1
  54. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/RECORD +56 -83
  55. {letschatty-0.4.280.dist-info → letschatty-0.4.343.dist-info}/WHEEL +1 -1
  56. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
  57. letschatty/services/chatty_assets/assets_collections.py +0 -137
  58. letschatty/services/chatty_assets/collections/__init__.py +0 -38
  59. letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
  60. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
  61. letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
  62. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
  63. letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
  64. letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
  65. letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
  66. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
  67. letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
  68. letschatty/services/chatty_assets/collections/product_collection.py +0 -20
  69. letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
  70. letschatty/services/chatty_assets/collections/source_collection.py +0 -21
  71. letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
  72. letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
  73. letschatty/services/chatty_assets/collections/user_collection.py +0 -20
  74. letschatty/services/chatty_assets/example_usage.py +0 -44
  75. letschatty/services/chatty_assets/services/__init__.py +0 -37
  76. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
  77. letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
  78. letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
  79. letschatty/services/chatty_assets/services/chat_service.py +0 -25
  80. letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
  81. letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
  82. letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
  83. letschatty/services/chatty_assets/services/flow_service.py +0 -25
  84. letschatty/services/chatty_assets/services/product_service.py +0 -30
  85. letschatty/services/chatty_assets/services/sale_service.py +0 -25
  86. letschatty/services/chatty_assets/services/source_service.py +0 -28
  87. letschatty/services/chatty_assets/services/tag_service.py +0 -32
  88. letschatty/services/chatty_assets/services/topic_service.py +0 -31
  89. letschatty/services/chatty_assets/services/user_service.py +0 -32
  90. letschatty/services/events/__init__.py +0 -6
  91. letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
  92. {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 FormField(BaseModel):
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
- Simple configuration for data collection fields.
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
- field_name: str = Field(description="Unique identifier for the field (e.g., 'email', 'budget')")
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
- required: bool = Field(
39
+ is_system_field: bool = Field(
19
40
  default=False,
20
- description="Whether this field is required"
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
- "field_name": "email",
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
- "field_name": "budget",
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
- "required": False
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(default=None, description="Customer's DNI/ID number")
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
- # All methods are now async-only for better performance
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 = await self.async_collection.insert_one(document)
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
- async def update(self, asset: T) -> StrObjectId:
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 = await self.async_collection.update_one(
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
- async def get_by_id(self, doc_id: str) -> T:
99
- """Get by ID operation"""
100
- logger.debug(f"Getting document with id {doc_id} from collection {self.async_collection.name}")
101
- doc = await self.async_collection.find_one({"_id": ObjectId(doc_id)})
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
- async def get_docs(self, company_id: Optional[StrObjectId], query={}, limit=0) -> List[T]:
108
- """Get multiple documents operation"""
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
- cursor = self.async_collection.find(filter=query)
114
- if limit:
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
- async def get_by_ids(self, ids: List[StrObjectId]) -> List[T]:
161
- """
162
- Get multiple assets by their IDs in a single query.
163
-
164
- Args:
165
- ids: List of asset IDs
166
-
167
- Returns:
168
- List of assets objects
169
- """
170
- if not ids:
171
- return []
172
-
173
- # Convert string IDs to ObjectIds
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