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
|
@@ -7,15 +7,16 @@ independently without loading entire chat documents.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from letschatty.models.analytics.events.base import EventType
|
|
10
|
+
from enum import StrEnum
|
|
10
11
|
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
from zoneinfo import ZoneInfo
|
|
13
14
|
from typing import Optional, ClassVar, List, Dict, Any
|
|
14
|
-
from enum import StrEnum
|
|
15
15
|
from letschatty.models.base_models.chatty_asset_model import CompanyAssetModel, ChattyAssetPreview
|
|
16
16
|
from letschatty.models.utils.types.identifier import StrObjectId
|
|
17
17
|
from .chain_of_thought_in_chat import ChainOfThoughtInChatTrigger
|
|
18
18
|
from .chatty_ai_mode import ChattyAIMode
|
|
19
|
+
from .statuses import DataCollectionStatus, PreQualifyStatus
|
|
19
20
|
import logging
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
@@ -98,6 +99,20 @@ class ChattyAIAgentInChat(CompanyAssetModel):
|
|
|
98
99
|
|
|
99
100
|
events: List[SimplifiedExecutionEvent] = Field(default_factory=list, description="Simplified events for UI visibility")
|
|
100
101
|
|
|
102
|
+
# Data collection status (for agents with pre_qualify.form_fields)
|
|
103
|
+
data_collection_status: Optional[DataCollectionStatus] = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="Status of data collection. None if agent has no form_fields, "
|
|
106
|
+
"otherwise tracks progress of data collection."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Pre-qualification status (for agents with pre_qualify config)
|
|
110
|
+
pre_qualify_status: Optional[PreQualifyStatus] = Field(
|
|
111
|
+
default=None,
|
|
112
|
+
description="Status of pre-qualification. None if agent has no pre_qualify config, "
|
|
113
|
+
"otherwise tracks qualification evaluation."
|
|
114
|
+
)
|
|
115
|
+
|
|
101
116
|
# Preview class
|
|
102
117
|
preview_class: ClassVar[type[ChattyAssetPreview]] = ChattyAssetPreview
|
|
103
118
|
|
|
@@ -106,6 +121,8 @@ class ChattyAIAgentInChat(CompanyAssetModel):
|
|
|
106
121
|
validate_by_alias=True
|
|
107
122
|
)
|
|
108
123
|
|
|
124
|
+
|
|
125
|
+
|
|
109
126
|
@property
|
|
110
127
|
def requires_human_intervention(self) -> bool:
|
|
111
128
|
"""Check if the AI agent requires human intervention"""
|
|
@@ -229,3 +246,78 @@ class ChattyAIAgentInChat(CompanyAssetModel):
|
|
|
229
246
|
self.human_intervention = None
|
|
230
247
|
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
231
248
|
|
|
249
|
+
# Data collection methods
|
|
250
|
+
@property
|
|
251
|
+
def is_data_collection_complete(self) -> bool:
|
|
252
|
+
"""Check if data collection is complete (mandatory or all)"""
|
|
253
|
+
return self.data_collection_status in [
|
|
254
|
+
DataCollectionStatus.MANDATORY_COMPLETED,
|
|
255
|
+
DataCollectionStatus.ALL_COMPLETED
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def is_data_collection_in_progress(self) -> bool:
|
|
260
|
+
"""Check if data collection is still in progress"""
|
|
261
|
+
return self.data_collection_status == DataCollectionStatus.COLLECTING
|
|
262
|
+
|
|
263
|
+
def start_data_collection(self) -> None:
|
|
264
|
+
"""Start data collection (set status to COLLECTING)"""
|
|
265
|
+
self.data_collection_status = DataCollectionStatus.COLLECTING
|
|
266
|
+
self.pre_qualify_status = PreQualifyStatus.PENDING
|
|
267
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
268
|
+
|
|
269
|
+
def complete_mandatory_data_collection(self) -> None:
|
|
270
|
+
"""Mark mandatory fields as completed"""
|
|
271
|
+
self.data_collection_status = DataCollectionStatus.MANDATORY_COMPLETED
|
|
272
|
+
self.pre_qualify_status = PreQualifyStatus.EVALUATING
|
|
273
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
274
|
+
|
|
275
|
+
def complete_all_data_collection(self) -> None:
|
|
276
|
+
"""Mark all fields as completed"""
|
|
277
|
+
self.data_collection_status = DataCollectionStatus.ALL_COMPLETED
|
|
278
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
279
|
+
|
|
280
|
+
def cancel_data_collection(self) -> None:
|
|
281
|
+
"""Cancel data collection"""
|
|
282
|
+
self.data_collection_status = DataCollectionStatus.CANCELLED
|
|
283
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
284
|
+
|
|
285
|
+
# Pre-qualification methods
|
|
286
|
+
@property
|
|
287
|
+
def is_qualified(self) -> bool:
|
|
288
|
+
"""Check if user is qualified"""
|
|
289
|
+
return self.pre_qualify_status == PreQualifyStatus.QUALIFIED
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def is_unqualified(self) -> bool:
|
|
293
|
+
"""Check if user is unqualified"""
|
|
294
|
+
return self.pre_qualify_status == PreQualifyStatus.UNQUALIFIED
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def is_pre_qualify_pending(self) -> bool:
|
|
298
|
+
"""Check if pre-qualification is still pending"""
|
|
299
|
+
return self.pre_qualify_status in [PreQualifyStatus.PENDING, PreQualifyStatus.EVALUATING]
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def has_terminal_pre_qualify_status(self) -> bool:
|
|
303
|
+
"""Check if pre-qualification has reached a terminal status"""
|
|
304
|
+
return self.pre_qualify_status in [
|
|
305
|
+
PreQualifyStatus.QUALIFIED,
|
|
306
|
+
PreQualifyStatus.UNQUALIFIED
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
def mark_as_qualified(self) -> None:
|
|
310
|
+
"""Mark user as qualified (met acceptance criteria)"""
|
|
311
|
+
self.pre_qualify_status = PreQualifyStatus.QUALIFIED
|
|
312
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
313
|
+
|
|
314
|
+
def mark_as_unqualified(self) -> None:
|
|
315
|
+
"""Mark user as unqualified (did NOT meet acceptance criteria)"""
|
|
316
|
+
self.pre_qualify_status = PreQualifyStatus.UNQUALIFIED
|
|
317
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
318
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
319
|
+
|
|
320
|
+
def cancel_data_collection(self) -> None:
|
|
321
|
+
"""Cancel data collection"""
|
|
322
|
+
self.data_collection_status = DataCollectionStatus.CANCELLED
|
|
323
|
+
self.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pre-qualification configuration for AI agents.
|
|
3
|
+
|
|
4
|
+
Defines the data collection, acceptance criteria, and destination actions
|
|
5
|
+
for qualifying/disqualifying users.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from letschatty.models.utils.types.identifier import StrObjectId
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PreQualifyDestination(StrEnum):
|
|
15
|
+
"""
|
|
16
|
+
Destination/action when pre-qualification reaches a terminal state.
|
|
17
|
+
"""
|
|
18
|
+
SUBSCRIBE_TO_LAUNCH = "subscribe_to_launch" # Subscribe to launch + welcome kit
|
|
19
|
+
CALENDAR_SCHEDULER = "calendar_scheduler" # Allow AI agent to schedule meetings
|
|
20
|
+
ESCALATE = "escalate" # Escalate to human
|
|
21
|
+
CUSTOM_MESSAGE = "custom_message" # Send a custom message
|
|
22
|
+
CONTINUE = "continue" # Continue normal AI agent flow
|
|
23
|
+
NONE = "none" # Do nothing
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PreQualifyFormField(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Form field reference with required flag.
|
|
29
|
+
The required flag is specific to this pre-qualify config, not the form field itself.
|
|
30
|
+
"""
|
|
31
|
+
field_key: str = Field(description="The field_key of the FormField")
|
|
32
|
+
required: bool = Field(default=False, description="Whether this field is required for pre-qualification")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PreQualifyConfig(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Configuration for pre-qualification process.
|
|
38
|
+
Embedded in ChattyAIAgent.
|
|
39
|
+
"""
|
|
40
|
+
# Form fields to collect (with required flag per field)
|
|
41
|
+
form_fields: List[PreQualifyFormField] = Field(
|
|
42
|
+
default_factory=list,
|
|
43
|
+
description="List of form fields to collect with their required status"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Acceptance criteria
|
|
47
|
+
acceptance_criteria: str = Field(
|
|
48
|
+
default="",
|
|
49
|
+
description="Description of criteria for AI to evaluate if user qualifies. Empty = no criteria (auto-qualify on mandatory completion)"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# On qualified actions
|
|
53
|
+
on_qualified_destination: PreQualifyDestination = Field(
|
|
54
|
+
default=PreQualifyDestination.CONTINUE,
|
|
55
|
+
description="Action when user qualifies"
|
|
56
|
+
)
|
|
57
|
+
on_qualified_message: Optional[str] = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Custom message to send when user qualifies (if destination is custom_message)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# On unqualified actions
|
|
63
|
+
on_unqualified_destination: PreQualifyDestination = Field(
|
|
64
|
+
default=PreQualifyDestination.NONE,
|
|
65
|
+
description="Action when user does NOT qualify"
|
|
66
|
+
)
|
|
67
|
+
on_unqualified_message: Optional[str] = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description="Custom message to send when user does NOT qualify (if destination is custom_message or escalate)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def has_form_fields(self) -> bool:
|
|
74
|
+
"""Check if pre-qualify has form fields configured"""
|
|
75
|
+
return len(self.form_fields) > 0
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def has_acceptance_criteria(self) -> bool:
|
|
79
|
+
"""Check if acceptance criteria is configured"""
|
|
80
|
+
return bool(self.acceptance_criteria.strip())
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_configured(self) -> bool:
|
|
84
|
+
"""Check if pre-qualify is configured (has form fields)"""
|
|
85
|
+
return self.has_form_fields
|
|
86
|
+
|
|
87
|
+
def get_field_keys(self) -> List[str]:
|
|
88
|
+
"""Get list of all field_keys"""
|
|
89
|
+
return [f.field_key for f in self.form_fields]
|
|
90
|
+
|
|
91
|
+
def get_required_field_keys(self) -> List[str]:
|
|
92
|
+
"""Get list of required field_keys"""
|
|
93
|
+
return [f.field_key for f in self.form_fields if f.required]
|
|
94
|
+
|
|
95
|
+
def get_optional_field_keys(self) -> List[str]:
|
|
96
|
+
"""Get list of optional field_keys"""
|
|
97
|
+
return [f.field_key for f in self.form_fields if not f.required]
|
|
98
|
+
|
|
99
|
+
def is_field_required(self, field_key: str) -> bool:
|
|
100
|
+
"""Check if a specific field is required"""
|
|
101
|
+
for f in self.form_fields:
|
|
102
|
+
if f.field_key == field_key:
|
|
103
|
+
return f.required
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def remove_field(self, field_key: str) -> bool:
|
|
107
|
+
"""Remove a field from the config. Returns True if field was found and removed."""
|
|
108
|
+
original_len = len(self.form_fields)
|
|
109
|
+
self.form_fields = [f for f in self.form_fields if f.field_key != field_key]
|
|
110
|
+
return len(self.form_fields) < original_len
|
|
111
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataCollectionStatus(StrEnum):
|
|
5
|
+
"""
|
|
6
|
+
Status of data collection for the AI agent in this chat.
|
|
7
|
+
Only tracks field collection, not qualification status.
|
|
8
|
+
|
|
9
|
+
- COLLECTING: Still collecting data from the user
|
|
10
|
+
- MANDATORY_COMPLETED: All mandatory fields have been collected
|
|
11
|
+
- ALL_COMPLETED: All fields (mandatory + optional) have been collected
|
|
12
|
+
- CANCELLED: Data collection was cancelled
|
|
13
|
+
"""
|
|
14
|
+
COLLECTING = "collecting"
|
|
15
|
+
MANDATORY_COMPLETED = "mandatory_completed"
|
|
16
|
+
ALL_COMPLETED = "all_completed"
|
|
17
|
+
CANCELLED = "cancelled"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PreQualifyStatus(StrEnum):
|
|
21
|
+
"""
|
|
22
|
+
Status of pre-qualification for the AI agent in this chat.
|
|
23
|
+
Separate from data collection - tracks qualification evaluation.
|
|
24
|
+
|
|
25
|
+
- PENDING: Waiting for data collection to complete
|
|
26
|
+
- EVALUATING: Data collected, evaluating acceptance criteria
|
|
27
|
+
- QUALIFIED: User met acceptance criteria
|
|
28
|
+
- UNQUALIFIED: User did NOT meet acceptance criteria
|
|
29
|
+
"""
|
|
30
|
+
PENDING = "pending"
|
|
31
|
+
EVALUATING = "evaluating"
|
|
32
|
+
QUALIFIED = "qualified"
|
|
33
|
+
UNQUALIFIED = "unqualified"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any, ClassVar, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from ....base_models.chatty_asset_model import CompanyAssetModel
|
|
9
|
+
from ....utils.types.identifier import StrObjectId
|
|
10
|
+
from ....utils.types.serializer_type import SerializerType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AssignmentStrategy(StrEnum):
|
|
14
|
+
"""Assignment strategy types."""
|
|
15
|
+
|
|
16
|
+
ROUND_ROBIN = "ROUND_ROBIN"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AssignmentRoom(CompanyAssetModel):
|
|
20
|
+
"""Scope identifier for assignment configuration, rooms and logs."""
|
|
21
|
+
|
|
22
|
+
COLLECTION: ClassVar[str] = "assignment_room"
|
|
23
|
+
|
|
24
|
+
funnel_id: Optional[StrObjectId] = Field(default=None, alias="funnelId")
|
|
25
|
+
area_id: Optional[StrObjectId] = Field(default=None, alias="areaId")
|
|
26
|
+
strategy: AssignmentStrategy = Field(default=AssignmentStrategy.ROUND_ROBIN)
|
|
27
|
+
state: Dict[str, Any] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
exclude_fields = {
|
|
30
|
+
SerializerType.FRONTEND_ASSET_PREVIEW: {"state"},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(
|
|
34
|
+
validate_by_name=True,
|
|
35
|
+
validate_by_alias=True,
|
|
36
|
+
populate_by_name=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AssignmentConfig(CompanyAssetModel):
|
|
41
|
+
"""Assignment configuration document stored in MongoDB."""
|
|
42
|
+
|
|
43
|
+
COLLECTION: ClassVar[str] = "assignment_config"
|
|
44
|
+
|
|
45
|
+
funnel_id: Optional[StrObjectId] = Field(default=None, alias="funnelId")
|
|
46
|
+
area_id: Optional[StrObjectId] = Field(default=None, alias="areaId")
|
|
47
|
+
strategy: AssignmentStrategy = Field(default=AssignmentStrategy.ROUND_ROBIN)
|
|
48
|
+
params: Dict[str, Any] = Field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(
|
|
51
|
+
validate_by_name=True,
|
|
52
|
+
validate_by_alias=True,
|
|
53
|
+
populate_by_name=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AssignmentLogEntry(CompanyAssetModel):
|
|
58
|
+
"""Immutable record inserted into assignment_log_entry collection."""
|
|
59
|
+
|
|
60
|
+
COLLECTION: ClassVar[str] = "assignment_log_entry"
|
|
61
|
+
|
|
62
|
+
chat_id: StrObjectId = Field(alias="chatId")
|
|
63
|
+
agent_id: StrObjectId = Field(alias="agentId")
|
|
64
|
+
strategy: AssignmentStrategy
|
|
65
|
+
room: AssignmentRoom
|
|
66
|
+
|
|
67
|
+
exclude_fields = {
|
|
68
|
+
SerializerType.FRONTEND_ASSET_PREVIEW: {"room"},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(
|
|
72
|
+
validate_by_name=True,
|
|
73
|
+
validate_by_alias=True,
|
|
74
|
+
populate_by_name=True,
|
|
75
|
+
)
|
|
@@ -14,27 +14,18 @@ class Automation(BaseModel):
|
|
|
14
14
|
chatty_ai_agent_config: Optional[ChattyAIConfigForAutomation] = Field(default=None)
|
|
15
15
|
area: Optional[Area] = Field(default=None)
|
|
16
16
|
agent_id: Optional[StrObjectId] = Field(default=None)
|
|
17
|
-
chain_of_thought
|
|
17
|
+
chain_of_thought: Optional[str] = Field(default=None)
|
|
18
|
+
# Funnel transition automations
|
|
19
|
+
target_funnel_id: Optional[StrObjectId] = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Target funnel to move the chat to (for cross-funnel transitions)"
|
|
22
|
+
)
|
|
23
|
+
target_stage_id: Optional[StrObjectId] = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description="Target stage within the target funnel"
|
|
26
|
+
)
|
|
18
27
|
# client_info: Optional[ClientInfo] = Field(default=None) me gustaría que levante el mail y/o otros atributos
|
|
19
28
|
|
|
20
|
-
@property
|
|
21
|
-
def has_automation(self) -> bool:
|
|
22
|
-
"""
|
|
23
|
-
Check if there's an actual automation defined (tags, products, or flow).
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
bool: True if there's at least one tag, product, or flow defined
|
|
27
|
-
"""
|
|
28
|
-
return (
|
|
29
|
-
len(self.tags) > 0 or
|
|
30
|
-
len(self.products) > 0 or
|
|
31
|
-
len(self.flow) > 0 or
|
|
32
|
-
self.quality_score is not None or
|
|
33
|
-
self.chatty_ai_agent_config is not None or
|
|
34
|
-
self.area is not None or
|
|
35
|
-
self.highlight_description is not None
|
|
36
|
-
)
|
|
37
|
-
|
|
38
29
|
@model_validator(mode='after')
|
|
39
30
|
def check_agent_id(self):
|
|
40
31
|
if self.area == Area.WITH_AGENT and not self.agent_id:
|
|
@@ -4,12 +4,13 @@ from enum import StrEnum
|
|
|
4
4
|
from pydantic_core.core_schema import str_schema
|
|
5
5
|
|
|
6
6
|
from letschatty.models.company.assets.ai_agents_v2.chain_of_thought_in_chat import ChainOfThoughtInChatTrigger
|
|
7
|
+
from letschatty.models.company.assets.ai_agents_v2.statuses import DataCollectionStatus, PreQualifyStatus
|
|
7
8
|
from ...utils.types.identifier import StrObjectId
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from zoneinfo import ZoneInfo
|
|
10
11
|
from bson import ObjectId
|
|
11
12
|
import json
|
|
12
|
-
from typing import Dict, Any, Optional
|
|
13
|
+
from typing import Dict, Any, Optional, List
|
|
13
14
|
from letschatty.models.utils.types.serializer_type import SerializerType
|
|
14
15
|
from letschatty.models.company.assets.ai_agents_v2.chatty_ai_mode import ChattyAIMode
|
|
15
16
|
|
|
@@ -66,7 +67,8 @@ class AssignedAssetToChat(BaseModel):
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
class SaleAssignedToChat(AssignedAssetToChat):
|
|
69
|
-
product_id: StrObjectId = Field(
|
|
70
|
+
product_id: Optional[StrObjectId] = Field(default=None)
|
|
71
|
+
product_ids: List[StrObjectId] = Field(default_factory=list)
|
|
70
72
|
|
|
71
73
|
class ContactPointAssignedToChat(AssignedAssetToChat):
|
|
72
74
|
source_id: StrObjectId = Field(frozen=True)
|
|
@@ -75,6 +77,14 @@ class ChattyAIAgentAssignedToChat(AssignedAssetToChat):
|
|
|
75
77
|
mode: ChattyAIMode = Field(default=ChattyAIMode.OFF)
|
|
76
78
|
requires_human_intervention: bool = Field(default=False)
|
|
77
79
|
is_processing: bool = Field(default=False)
|
|
80
|
+
data_collection_status: Optional[DataCollectionStatus] = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
description="Status of data collection for pre-qualification"
|
|
83
|
+
)
|
|
84
|
+
pre_qualify_status: Optional[PreQualifyStatus] = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="Status of pre-qualification"
|
|
87
|
+
)
|
|
78
88
|
last_call_started_at: Optional[datetime] = Field(default=None, description="The timestamp of the get chat with prompt (the moment n8n started processing the call)")
|
|
79
89
|
trigger_timestamp: Optional[datetime] = Field(default=None, description="The timestamp of the trigger that started the call, if it's a manual trigger, it will be the timestamp of the manual trigger, if it's a follow up, it will be the timestamp of the follow up, if it's a user message, it will be the timestamp of the user message")
|
|
80
90
|
last_call_cot_id: Optional[StrObjectId] = Field(default=None)
|
|
@@ -5,6 +5,8 @@ class CompanyAssetType(StrEnum):
|
|
|
5
5
|
USERS = "users"
|
|
6
6
|
BUSINESS_AREAS = "business_areas"
|
|
7
7
|
FUNNELS = "funnels"
|
|
8
|
+
FUNNEL_STAGES = "funnel_stages"
|
|
9
|
+
FUNNEL_MEMBERS = "funnel_members"
|
|
8
10
|
PRODUCTS = "products"
|
|
9
11
|
SALES = "sales"
|
|
10
12
|
TAGS = "tags"
|
|
@@ -20,6 +22,7 @@ class CompanyAssetType(StrEnum):
|
|
|
20
22
|
WORKFLOWS = "workflows"
|
|
21
23
|
CHATTY_AI_AGENTS = "chatty_ai_agents"
|
|
22
24
|
FILTER_CRITERIA = "filter_criteria"
|
|
25
|
+
FORM_FIELDS = "form_fields"
|
|
23
26
|
|
|
24
27
|
@classmethod
|
|
25
28
|
def get_all(cls) -> List[str]:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .launch import Launch, LaunchStatus
|
|
2
|
+
from .scheduled_communication import LaunchScheduledCommunication
|
|
3
|
+
from .subscription import LaunchSubscription, LaunchSubscriptionStatus
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'Launch',
|
|
7
|
+
'LaunchStatus',
|
|
8
|
+
'LaunchScheduledCommunication',
|
|
9
|
+
'LaunchSubscription',
|
|
10
|
+
'LaunchSubscriptionStatus'
|
|
11
|
+
]
|
|
12
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from letschatty.models.base_models.chatty_asset_model import CompanyAssetModel
|
|
6
|
+
from letschatty.models.utils.types.identifier import StrObjectId
|
|
7
|
+
from .scheduled_communication import LaunchScheduledCommunication
|
|
8
|
+
from .subscription import LaunchSubscription, LaunchSubscriptionStatus
|
|
9
|
+
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LaunchStatus(StrEnum):
|
|
13
|
+
"""Status of the launch"""
|
|
14
|
+
DRAFT = "draft"
|
|
15
|
+
ACTIVE = "active"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
CANCELLED = "cancelled"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Launch(CompanyAssetModel):
|
|
21
|
+
"""
|
|
22
|
+
Launch asset model.
|
|
23
|
+
Represents a product launch event with communications and subscriptions.
|
|
24
|
+
"""
|
|
25
|
+
# Basic info
|
|
26
|
+
name: str = Field(description="Name of the launch")
|
|
27
|
+
product_name: str = Field(description="Name of the product being launched")
|
|
28
|
+
product_description: str = Field(description="Description of the product being launched")
|
|
29
|
+
launch_datetime: datetime = Field(description="Date and time of the launch")
|
|
30
|
+
access_link: str = Field(description="Base link for the launch event")
|
|
31
|
+
status: LaunchStatus = Field(default=LaunchStatus.DRAFT, description="Current status of the launch")
|
|
32
|
+
|
|
33
|
+
# Data Collection & Pre-qualification
|
|
34
|
+
form_fields: List[StrObjectId] = Field(
|
|
35
|
+
default_factory=list,
|
|
36
|
+
description="FormField IDs required before subscription"
|
|
37
|
+
)
|
|
38
|
+
acceptance_criteria: str = Field(
|
|
39
|
+
default="",
|
|
40
|
+
description="Instructions for AI to determine when to subscribe the user"
|
|
41
|
+
)
|
|
42
|
+
enable_tracking: bool = Field(
|
|
43
|
+
default=False,
|
|
44
|
+
description="If True, generate personal trackable links for each subscriber"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Communications
|
|
48
|
+
welcome_kit: Optional[LaunchScheduledCommunication] = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
description="Communication sent immediately upon subscription (delta_minutes=0)"
|
|
51
|
+
)
|
|
52
|
+
pre_launch_communications: List[LaunchScheduledCommunication] = Field(
|
|
53
|
+
default_factory=list,
|
|
54
|
+
description="Communications scheduled before the launch"
|
|
55
|
+
)
|
|
56
|
+
post_launch_attended_communications: List[LaunchScheduledCommunication] = Field(
|
|
57
|
+
default_factory=list,
|
|
58
|
+
description="Communications for attendees after the launch"
|
|
59
|
+
)
|
|
60
|
+
post_launch_missed_communications: List[LaunchScheduledCommunication] = Field(
|
|
61
|
+
default_factory=list,
|
|
62
|
+
description="Communications for those who missed the launch"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Subscriptions (embedded)
|
|
66
|
+
subscriptions: List[LaunchSubscription] = Field(
|
|
67
|
+
default_factory=list,
|
|
68
|
+
description="List of chat subscriptions to this launch"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# AI Configuration
|
|
72
|
+
chat_examples: List[StrObjectId] = Field(
|
|
73
|
+
default_factory=list,
|
|
74
|
+
description="Chat examples for AI message adaptation"
|
|
75
|
+
)
|
|
76
|
+
contexts: List[StrObjectId] = Field(
|
|
77
|
+
default_factory=list,
|
|
78
|
+
description="Specific contexts for the launch"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def all_communications(self) -> List[LaunchScheduledCommunication]:
|
|
83
|
+
"""Get all communications including welcome kit"""
|
|
84
|
+
comms = []
|
|
85
|
+
if self.welcome_kit:
|
|
86
|
+
comms.append(self.welcome_kit)
|
|
87
|
+
comms.extend(self.pre_launch_communications)
|
|
88
|
+
comms.extend(self.post_launch_attended_communications)
|
|
89
|
+
comms.extend(self.post_launch_missed_communications)
|
|
90
|
+
return comms
|
|
91
|
+
|
|
92
|
+
def get_subscription_by_chat_id(self, chat_id: StrObjectId) -> Optional[LaunchSubscription]:
|
|
93
|
+
"""Find a subscription by chat ID"""
|
|
94
|
+
for sub in self.subscriptions:
|
|
95
|
+
if str(sub.chat_id) == str(chat_id):
|
|
96
|
+
return sub
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def is_chat_subscribed(self, chat_id: StrObjectId) -> bool:
|
|
100
|
+
"""Check if a chat is subscribed to this launch"""
|
|
101
|
+
sub = self.get_subscription_by_chat_id(chat_id)
|
|
102
|
+
return sub is not None and sub.status in [
|
|
103
|
+
LaunchSubscriptionStatus.SUBSCRIBED,
|
|
104
|
+
LaunchSubscriptionStatus.ATTENDED
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def get_access_link_for_subscriber(self, chat_id: StrObjectId) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Returns the personal trackable link for a subscriber if tracking is enabled,
|
|
110
|
+
otherwise returns the general access_link.
|
|
111
|
+
"""
|
|
112
|
+
if self.enable_tracking:
|
|
113
|
+
subscription = self.get_subscription_by_chat_id(chat_id)
|
|
114
|
+
if subscription and subscription.personal_access_link:
|
|
115
|
+
return subscription.personal_access_link
|
|
116
|
+
return self.access_link
|
|
117
|
+
|
|
118
|
+
def generate_personal_link(self, chat_id: StrObjectId) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Generates a unique trackable link for a given chat_id.
|
|
121
|
+
Adds a 'subscriber' query parameter to the base access_link.
|
|
122
|
+
"""
|
|
123
|
+
parsed_url = urlparse(self.access_link)
|
|
124
|
+
query_params = parse_qs(parsed_url.query)
|
|
125
|
+
query_params['subscriber'] = [str(chat_id)]
|
|
126
|
+
new_query = urlencode(query_params, doseq=True)
|
|
127
|
+
return urlunparse(parsed_url._replace(query=new_query))
|
|
128
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from letschatty.models.messages.chatty_messages.base.message_draft import MessageDraft
|
|
5
|
+
from letschatty.models.company.assets.ai_agents_v2.ai_agent_message_draft import AIAgentMessageDraft
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LaunchScheduledCommunication(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Scheduled communication for pre/post launch.
|
|
11
|
+
Can be AI-adapted or literal messages.
|
|
12
|
+
"""
|
|
13
|
+
id: str = Field(description="Unique identifier for the communication")
|
|
14
|
+
name: str = Field(description="Name of the communication (e.g., '5 dias antes', '2 horas antes')")
|
|
15
|
+
delta_minutes: int = Field(
|
|
16
|
+
description="Minutes relative to the launch (negative=before, positive=after, 0=welcome kit)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Content - one or the other
|
|
20
|
+
ai_communication: Optional[AIAgentMessageDraft] = Field(
|
|
21
|
+
default=None,
|
|
22
|
+
description="AI adapted communication content and instructions"
|
|
23
|
+
)
|
|
24
|
+
literal_messages: Optional[List[MessageDraft]] = Field(
|
|
25
|
+
default=None,
|
|
26
|
+
description="Literal messages to be sent without AI adaptation"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# State
|
|
30
|
+
sent_at: Optional[datetime] = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description="Timestamp when this communication was sent globally for the launch"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def requires_ai(self) -> bool:
|
|
37
|
+
"""Check if this communication requires AI adaptation"""
|
|
38
|
+
return self.ai_communication is not None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_sent(self) -> bool:
|
|
42
|
+
"""Check if this communication has been sent"""
|
|
43
|
+
return self.sent_at is not None
|
|
44
|
+
|