letschatty 0.4.341__py3-none-any.whl → 0.4.342__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 +29 -2
- letschatty/models/ai_microservices/lambda_events.py +137 -28
- letschatty/models/ai_microservices/lambda_invokation_types.py +3 -1
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +3 -1
- letschatty/models/analytics/events/__init__.py +3 -2
- letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +71 -0
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +13 -69
- letschatty/models/analytics/events/company_based_events/asset_events.py +2 -9
- letschatty/models/analytics/events/event_type_to_classes.py +3 -6
- letschatty/models/analytics/events/event_types.py +50 -9
- letschatty/models/chat/chat.py +2 -13
- letschatty/models/chat/chat_with_assets.py +1 -6
- letschatty/models/chat/client.py +2 -0
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +33 -365
- letschatty/models/company/__init__.py +1 -2
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +4 -0
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
- letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +1 -0
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +14 -2
- letschatty/models/company/assets/automation.py +19 -10
- letschatty/models/company/assets/chat_assets.py +2 -3
- letschatty/models/company/assets/company_assets.py +0 -2
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/data_base/collection_interface.py +101 -29
- letschatty/models/data_base/mongo_connection.py +92 -9
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +2 -4
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +3 -4
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +14 -1
- letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +5 -2
- letschatty/services/chat/chat_service.py +11 -47
- letschatty/services/chatty_assets/__init__.py +12 -0
- letschatty/services/chatty_assets/asset_service.py +190 -13
- letschatty/services/chatty_assets/assets_collections.py +137 -0
- letschatty/services/chatty_assets/base_container.py +3 -2
- letschatty/services/chatty_assets/base_container_with_collection.py +35 -26
- letschatty/services/chatty_assets/collections/__init__.py +38 -0
- letschatty/services/chatty_assets/collections/ai_agent_collection.py +19 -0
- letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +32 -0
- letschatty/services/chatty_assets/collections/ai_component_collection.py +21 -0
- letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +30 -0
- letschatty/services/chatty_assets/collections/chat_collection.py +21 -0
- letschatty/services/chatty_assets/collections/contact_point_collection.py +21 -0
- letschatty/services/chatty_assets/collections/fast_answer_collection.py +21 -0
- letschatty/services/chatty_assets/collections/filter_criteria_collection.py +18 -0
- letschatty/services/chatty_assets/collections/flow_collection.py +20 -0
- letschatty/services/chatty_assets/collections/product_collection.py +20 -0
- letschatty/services/chatty_assets/collections/sale_collection.py +20 -0
- letschatty/services/chatty_assets/collections/source_collection.py +21 -0
- letschatty/services/chatty_assets/collections/tag_collection.py +19 -0
- letschatty/services/chatty_assets/collections/topic_collection.py +21 -0
- letschatty/services/chatty_assets/collections/user_collection.py +20 -0
- letschatty/services/chatty_assets/example_usage.py +44 -0
- letschatty/services/chatty_assets/services/__init__.py +37 -0
- letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +73 -0
- letschatty/services/chatty_assets/services/ai_agent_service.py +23 -0
- letschatty/services/chatty_assets/services/chain_of_thought_service.py +70 -0
- letschatty/services/chatty_assets/services/chat_service.py +25 -0
- letschatty/services/chatty_assets/services/contact_point_service.py +29 -0
- letschatty/services/chatty_assets/services/fast_answer_service.py +32 -0
- letschatty/services/chatty_assets/services/filter_criteria_service.py +30 -0
- letschatty/services/chatty_assets/services/flow_service.py +25 -0
- letschatty/services/chatty_assets/services/product_service.py +30 -0
- letschatty/services/chatty_assets/services/sale_service.py +25 -0
- letschatty/services/chatty_assets/services/source_service.py +28 -0
- letschatty/services/chatty_assets/services/tag_service.py +32 -0
- letschatty/services/chatty_assets/services/topic_service.py +31 -0
- letschatty/services/chatty_assets/services/user_service.py +32 -0
- letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +11 -0
- letschatty/services/events/__init__.py +6 -0
- letschatty/services/events/events_manager.py +218 -1
- letschatty/services/factories/analytics/ai_agent_event_factory.py +161 -0
- letschatty/services/factories/analytics/events_factory.py +66 -6
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +25 -8
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +6 -4
- letschatty/services/validators/analytics_validator.py +0 -11
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/METADATA +1 -1
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/RECORD +82 -46
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/LICENSE +0 -0
- {letschatty-0.4.341.dist-info → letschatty-0.4.342.dist-info}/WHEEL +0 -0
letschatty/models/chat/chat.py
CHANGED
|
@@ -19,7 +19,6 @@ from ..utils.custom_exceptions.custom_exceptions import MissingAIAgentForSmartFo
|
|
|
19
19
|
from .time_left import TimeLeft
|
|
20
20
|
from ..company.conversation_topic import TopicTimelineEntry
|
|
21
21
|
from ..company.form_field import CollectedData
|
|
22
|
-
from ..company.CRM.funnel import ActiveFunnel
|
|
23
22
|
import json
|
|
24
23
|
import logging
|
|
25
24
|
logger = logging.getLogger(__name__)
|
|
@@ -48,7 +47,6 @@ class Chat(CompanyAssetModel):
|
|
|
48
47
|
chatty_ai_agent: Optional[ChattyAIAgentAssignedToChat] = Field(default=None, description="The id of the chatty ai agent that might or might not be assigned to the chat")
|
|
49
48
|
suggested_messages: List[MessageDraft] = Field(default_factory=list)
|
|
50
49
|
topics_timeline: List[TopicTimelineEntry] = Field(default_factory=list, description="Timeline of conversation topics throughout the conversation")
|
|
51
|
-
active_funnel: Optional[ActiveFunnel] = Field(default=None, description="Current active funnel for this chat")
|
|
52
50
|
model_config = ConfigDict(extra="ignore")
|
|
53
51
|
|
|
54
52
|
@property
|
|
@@ -269,17 +267,7 @@ class Chat(CompanyAssetModel):
|
|
|
269
267
|
@property
|
|
270
268
|
def bought_product_ids(self) -> List[StrObjectId]:
|
|
271
269
|
"""Get all sale ids in the chat"""
|
|
272
|
-
|
|
273
|
-
for sale in self.sales:
|
|
274
|
-
sale_product_ids = getattr(sale, "product_ids", None)
|
|
275
|
-
if sale_product_ids:
|
|
276
|
-
for product_id in sale_product_ids:
|
|
277
|
-
if product_id not in product_ids:
|
|
278
|
-
product_ids.append(product_id)
|
|
279
|
-
elif sale.product_id:
|
|
280
|
-
if sale.product_id not in product_ids:
|
|
281
|
-
product_ids.append(sale.product_id)
|
|
282
|
-
return product_ids
|
|
270
|
+
return [sale.product_id for sale in self.sales]
|
|
283
271
|
|
|
284
272
|
@property
|
|
285
273
|
def products(self) -> List[AssignedAssetToChat]:
|
|
@@ -376,3 +364,4 @@ class Chat(CompanyAssetModel):
|
|
|
376
364
|
dump["last_message"] = self.last_message.model_dump_json(serializer=SerializerType.DATABASE) if self.last_message else None
|
|
377
365
|
dump["flow_states"] = [flow_state.model_dump_json(serializer=SerializerType.DATABASE) for flow_state in self.flow_states]
|
|
378
366
|
return dump
|
|
367
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
|
|
1
2
|
from .chat import Chat
|
|
2
3
|
from letschatty.models.company.assets import TagPreview, FlowPreview, ContactPoint, Sale, ChattyAIAgentPreview, ProductPreview
|
|
3
|
-
from letschatty.models.company.CRM.funnel import ActiveFunnel
|
|
4
4
|
|
|
5
5
|
from letschatty.models.utils.types.serializer_type import SerializerType
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -15,8 +15,3 @@ class ChatWithAssets(BaseModel):
|
|
|
15
15
|
contact_points: List[ContactPoint]
|
|
16
16
|
flows_links_states: List[FlowPreview]
|
|
17
17
|
chatty_ai_agent: Optional[ChattyAIAgentPreview]
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def active_funnel(self) -> Optional[ActiveFunnel]:
|
|
21
|
-
"""Convenience property to access the chat's active funnel"""
|
|
22
|
-
return self.chat.active_funnel
|
letschatty/models/chat/client.py
CHANGED
|
@@ -5,6 +5,7 @@ from ..utils.types.identifier import StrObjectId
|
|
|
5
5
|
from .quality_scoring import QualityScore
|
|
6
6
|
from .highlight import Highlight
|
|
7
7
|
from ..company.assets.chat_assets import AssignedAssetToChat, SaleAssignedToChat, ContactPointAssignedToChat
|
|
8
|
+
from ..company.CRM.funnel import ClientFunnel
|
|
8
9
|
from ..utils.types.serializer_type import SerializerType
|
|
9
10
|
|
|
10
11
|
class Client(CompanyAssetModel):
|
|
@@ -22,6 +23,7 @@ class Client(CompanyAssetModel):
|
|
|
22
23
|
contact_points: List[ContactPointAssignedToChat] = Field(default=list())
|
|
23
24
|
business_area: Optional[StrObjectId] = Field(default=None, description="It's a business related area, that works as a queue for the chats")
|
|
24
25
|
external_id: Optional[str] = Field(default=None)
|
|
26
|
+
funnels : List[ClientFunnel] = Field(default=list())
|
|
25
27
|
exclude_fields = {
|
|
26
28
|
SerializerType.FRONTEND: {"products", "tags", "sales", "contact_points", "highlights"}
|
|
27
29
|
}
|
|
@@ -30,7 +30,7 @@ class ContinuousConversation(ChattyAssetModel):
|
|
|
30
30
|
template_message_waid: Optional[str] = None
|
|
31
31
|
status: Optional[ContinuousConversationStatus] = Field(default=ContinuousConversationStatus.CREATED)
|
|
32
32
|
active: bool = Field(default=True)
|
|
33
|
-
expires_at: datetime = Field(
|
|
33
|
+
expires_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")) + timedelta(days=10))
|
|
34
34
|
messages: List[MessageDraft]
|
|
35
35
|
creator_id: StrObjectId
|
|
36
36
|
forced_send: bool = Field(default=False)
|
|
@@ -1,401 +1,69 @@
|
|
|
1
|
-
from ...base_models import CompanyAssetModel
|
|
2
|
-
from typing import List
|
|
1
|
+
from ...base_models import CompanyAssetModel
|
|
2
|
+
from typing import List
|
|
3
3
|
from ...utils.types.identifier import StrObjectId
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
5
|
from enum import StrEnum
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
7
8
|
from zoneinfo import ZoneInfo
|
|
8
9
|
from ...utils.types.executor_types import ExecutorType
|
|
9
|
-
from ..assets.automation import Automation
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# ============================================================================
|
|
13
|
-
# Enums
|
|
14
|
-
# ============================================================================
|
|
15
10
|
|
|
16
11
|
class FunnelStatus(StrEnum):
|
|
17
|
-
"""Status of a chat within a funnel"""
|
|
18
12
|
IN_PROGRESS = "in_progress"
|
|
19
13
|
COMPLETED = "completed"
|
|
20
14
|
ABANDONED = "abandoned"
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
class FunnelMemberRole(StrEnum):
|
|
24
|
-
"""Role of a user within a funnel"""
|
|
25
|
-
ADMIN = "admin" # Full control over funnel settings, stages, and members
|
|
26
|
-
EDITOR = "editor" # Can move chats between stages, view all chats
|
|
27
|
-
VIEWER = "viewer" # Read-only access to funnel and chats
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# ============================================================================
|
|
31
|
-
# Embedded Models (BaseModel)
|
|
32
|
-
# ============================================================================
|
|
33
|
-
|
|
34
16
|
class StageTransition(BaseModel):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
to_stage_id: StrObjectId = Field(description="The stage the chat moved to")
|
|
44
|
-
transitioned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
45
|
-
executor_type: ExecutorType = Field(description="Type of executor that triggered the transition")
|
|
46
|
-
executor_id: StrObjectId = Field(description="ID of the executor that triggered the transition")
|
|
47
|
-
time_in_previous_stage_seconds: Optional[int] = Field(
|
|
48
|
-
default=None,
|
|
49
|
-
description="Time spent in the previous stage before this transition"
|
|
50
|
-
)
|
|
51
|
-
from_stage_order: Optional[int] = Field(
|
|
52
|
-
default=None,
|
|
53
|
-
description="Order of the from_stage at transition time (for detecting regressions)"
|
|
54
|
-
)
|
|
55
|
-
to_stage_order: Optional[int] = Field(
|
|
56
|
-
default=None,
|
|
57
|
-
description="Order of the to_stage at transition time"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def is_regression(self) -> bool:
|
|
62
|
-
"""Check if this transition moved backwards in the funnel"""
|
|
63
|
-
if self.from_stage_order is None or self.to_stage_order is None:
|
|
64
|
-
return False
|
|
65
|
-
return self.to_stage_order < self.from_stage_order
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def is_entry(self) -> bool:
|
|
69
|
-
"""Check if this is an entry transition (no previous stage)"""
|
|
70
|
-
return self.from_stage_id is None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class ActiveFunnel(BaseModel):
|
|
74
|
-
"""
|
|
75
|
-
Lightweight funnel state embedded in Chat for fast filtering and display.
|
|
76
|
-
|
|
77
|
-
The full history (stage_transitions, time metrics) is stored in the
|
|
78
|
-
separate chat_funnels collection via ChatFunnel.
|
|
79
|
-
"""
|
|
80
|
-
chat_funnel_id: StrObjectId = Field(
|
|
81
|
-
description="Reference to the full ChatFunnel document in chat_funnels collection"
|
|
82
|
-
)
|
|
83
|
-
funnel_id: StrObjectId = Field(description="The funnel the chat is in")
|
|
84
|
-
funnel_name: str = Field(description="Denormalized funnel name for display")
|
|
85
|
-
current_stage_id: StrObjectId = Field(description="Current stage ID")
|
|
86
|
-
current_stage_name: str = Field(description="Denormalized stage name for display")
|
|
87
|
-
entered_current_stage_at: datetime = Field(
|
|
88
|
-
default_factory=lambda: datetime.now(ZoneInfo("UTC")),
|
|
89
|
-
description="When the chat entered the current stage"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def time_in_current_stage_seconds(self) -> int:
|
|
94
|
-
"""Calculate time spent in the current stage"""
|
|
95
|
-
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
96
|
-
|
|
97
|
-
def update_stage(self, stage_id: StrObjectId, stage_name: str) -> None:
|
|
98
|
-
"""Update the current stage (called when transitioning)"""
|
|
99
|
-
self.current_stage_id = stage_id
|
|
100
|
-
self.current_stage_name = stage_name
|
|
101
|
-
self.entered_current_stage_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# ============================================================================
|
|
105
|
-
# Preview Classes - For efficient listing
|
|
106
|
-
# ============================================================================
|
|
107
|
-
|
|
108
|
-
class FunnelPreview(ChattyAssetPreview):
|
|
109
|
-
"""Preview of a Funnel for listing without full data"""
|
|
110
|
-
is_active: bool = Field(default=True)
|
|
111
|
-
|
|
112
|
-
@classmethod
|
|
113
|
-
def get_projection(cls) -> dict[str, Any]:
|
|
114
|
-
base = super().get_projection()
|
|
115
|
-
base["is_active"] = 1
|
|
116
|
-
return base
|
|
117
|
-
|
|
118
|
-
@classmethod
|
|
119
|
-
def from_asset(cls, asset: 'Funnel') -> 'FunnelPreview':
|
|
120
|
-
return cls(
|
|
121
|
-
_id=asset.id,
|
|
122
|
-
name=asset.name,
|
|
123
|
-
company_id=asset.company_id,
|
|
124
|
-
created_at=asset.created_at,
|
|
125
|
-
deleted_at=asset.deleted_at,
|
|
126
|
-
is_active=asset.is_active
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class FunnelStagePreview(ChattyAssetPreview):
|
|
131
|
-
"""Preview of a FunnelStage for listing"""
|
|
17
|
+
from_stage_id: Optional[StrObjectId] = None
|
|
18
|
+
to_stage_id: StrObjectId
|
|
19
|
+
transitioned_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
|
|
20
|
+
executor_type: ExecutorType
|
|
21
|
+
executor_id: StrObjectId
|
|
22
|
+
time_in_previous_stage_seconds: Optional[int] = None
|
|
23
|
+
|
|
24
|
+
class ClientFunnel(CompanyAssetModel):
|
|
132
25
|
funnel_id: StrObjectId
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
base.update({
|
|
141
|
-
"funnel_id": 1,
|
|
142
|
-
"color": 1,
|
|
143
|
-
"order": 1,
|
|
144
|
-
"is_exit_stage": 1
|
|
145
|
-
})
|
|
146
|
-
return base
|
|
147
|
-
|
|
148
|
-
@classmethod
|
|
149
|
-
def from_asset(cls, asset: 'FunnelStage') -> 'FunnelStagePreview':
|
|
150
|
-
return cls(
|
|
151
|
-
_id=asset.id,
|
|
152
|
-
name=asset.name,
|
|
153
|
-
company_id=asset.company_id,
|
|
154
|
-
created_at=asset.created_at,
|
|
155
|
-
deleted_at=asset.deleted_at,
|
|
156
|
-
funnel_id=asset.funnel_id,
|
|
157
|
-
color=asset.color,
|
|
158
|
-
order=asset.order,
|
|
159
|
-
is_exit_stage=asset.is_exit_stage
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# ============================================================================
|
|
164
|
-
# Company Assets (CompanyAssetModel) - Stored in separate collections
|
|
165
|
-
# ============================================================================
|
|
166
|
-
|
|
167
|
-
class Funnel(CompanyAssetModel):
|
|
168
|
-
"""
|
|
169
|
-
A funnel represents a pipeline/process for managing chats (e.g., sales funnel, support pipeline).
|
|
170
|
-
Companies can create multiple funnels to organize their chat workflows.
|
|
171
|
-
"""
|
|
172
|
-
name: str = Field(description="Name of the funnel")
|
|
173
|
-
description: Optional[str] = Field(default=None, description="Description of the funnel's purpose")
|
|
174
|
-
created_by: StrObjectId = Field(description="User ID who created the funnel")
|
|
175
|
-
is_active: bool = Field(default=True, description="Whether the funnel is active or archived")
|
|
176
|
-
|
|
177
|
-
preview_class: ClassVar[type[FunnelPreview]] = FunnelPreview
|
|
178
|
-
|
|
179
|
-
model_config = ConfigDict(
|
|
180
|
-
validate_by_name=True,
|
|
181
|
-
validate_by_alias=True
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class FunnelStage(CompanyAssetModel):
|
|
186
|
-
"""
|
|
187
|
-
A stage within a funnel. Stages are ordered and can have automations
|
|
188
|
-
that execute when a chat enters the stage.
|
|
189
|
-
"""
|
|
190
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this stage belongs to")
|
|
191
|
-
name: str = Field(description="Name of the stage")
|
|
192
|
-
description: Optional[str] = Field(default=None, description="Description of the stage")
|
|
193
|
-
color: str = Field(default="#808080", description="Hex color for the stage (e.g., '#FFAA00')")
|
|
194
|
-
order: int = Field(ge=0, description="Position of the stage in the funnel (0-indexed)")
|
|
195
|
-
inflexion_conversion_point: bool = Field(
|
|
196
|
-
default=False,
|
|
197
|
-
description="If true, prioritized for chat assignment and sends notifications in automatic mode"
|
|
198
|
-
)
|
|
199
|
-
is_exit_stage: bool = Field(
|
|
200
|
-
default=False,
|
|
201
|
-
description="If true, moving to this stage completes the funnel for the chat"
|
|
202
|
-
)
|
|
203
|
-
automations: Automation = Field(
|
|
204
|
-
default_factory=Automation,
|
|
205
|
-
description="Automations to execute when a chat enters this stage"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
preview_class: ClassVar[type[FunnelStagePreview]] = FunnelStagePreview
|
|
209
|
-
|
|
210
|
-
model_config = ConfigDict(
|
|
211
|
-
validate_by_name=True,
|
|
212
|
-
validate_by_alias=True
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class FunnelMember(CompanyAssetModel):
|
|
217
|
-
"""
|
|
218
|
-
A user's membership in a funnel with their assigned role.
|
|
219
|
-
Determines what actions the user can perform within the funnel.
|
|
220
|
-
"""
|
|
221
|
-
name: str = Field(default="Funnel Member", description="Name for asset compatibility")
|
|
222
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel this membership belongs to")
|
|
223
|
-
user_id: StrObjectId = Field(frozen=True, description="The user who is a member")
|
|
224
|
-
role: FunnelMemberRole = Field(description="The user's role within the funnel")
|
|
225
|
-
|
|
226
|
-
model_config = ConfigDict(
|
|
227
|
-
validate_by_name=True,
|
|
228
|
-
validate_by_alias=True
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
@property
|
|
232
|
-
def can_edit(self) -> bool:
|
|
233
|
-
"""Check if the member can edit (move chats, etc.)"""
|
|
234
|
-
return self.role in (FunnelMemberRole.ADMIN, FunnelMemberRole.EDITOR)
|
|
235
|
-
|
|
236
|
-
@property
|
|
237
|
-
def can_admin(self) -> bool:
|
|
238
|
-
"""Check if the member can administer the funnel"""
|
|
239
|
-
return self.role == FunnelMemberRole.ADMIN
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# ============================================================================
|
|
243
|
-
# ChatFunnel - Stored in separate chat_funnels collection
|
|
244
|
-
# This is a ChattyAssetModel (not CompanyAssetModel) because it's a chat history
|
|
245
|
-
# record rather than a company-level asset.
|
|
246
|
-
# ============================================================================
|
|
247
|
-
|
|
248
|
-
class ChatFunnel(ChattyAssetModel):
|
|
249
|
-
"""
|
|
250
|
-
Full record of a chat's journey through a funnel.
|
|
251
|
-
Stored in a separate 'chat_funnels' collection, NOT embedded in chat.
|
|
252
|
-
|
|
253
|
-
This extends ChattyAssetModel (not CompanyAssetModel) because it's a
|
|
254
|
-
chat-level record tracking funnel history, not a company-owned asset.
|
|
255
|
-
|
|
256
|
-
A new ChatFunnel is created each time a chat enters a funnel.
|
|
257
|
-
If a chat completes a funnel and enters it again, a NEW ChatFunnel is created.
|
|
258
|
-
|
|
259
|
-
The Chat document stores a lightweight ActiveFunnel for fast
|
|
260
|
-
filtering and display, with a reference to this full record.
|
|
261
|
-
"""
|
|
262
|
-
company_id: StrObjectId = Field(frozen=True, description="Company for multi-tenant isolation")
|
|
263
|
-
chat_id: StrObjectId = Field(frozen=True, description="The chat this funnel record belongs to")
|
|
264
|
-
funnel_id: StrObjectId = Field(frozen=True, description="The funnel the chat entered")
|
|
265
|
-
status: FunnelStatus = Field(default=FunnelStatus.IN_PROGRESS)
|
|
266
|
-
started_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
267
|
-
completed_at: Optional[datetime] = Field(default=None)
|
|
268
|
-
abandoned_at: Optional[datetime] = Field(default=None)
|
|
269
|
-
current_stage_id: Optional[StrObjectId] = Field(
|
|
270
|
-
default=None,
|
|
271
|
-
description="Current stage ID (None if funnel is completed/abandoned)"
|
|
272
|
-
)
|
|
273
|
-
entered_current_stage_at: Optional[datetime] = Field(
|
|
274
|
-
default=None,
|
|
275
|
-
description="When the chat entered the current stage"
|
|
276
|
-
)
|
|
277
|
-
stage_transitions: List[StageTransition] = Field(
|
|
278
|
-
default_factory=list,
|
|
279
|
-
description="Complete history of all stage transitions"
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
model_config = ConfigDict(
|
|
283
|
-
validate_by_name=True,
|
|
284
|
-
validate_by_alias=True
|
|
285
|
-
)
|
|
26
|
+
status: FunnelStatus
|
|
27
|
+
started_at: datetime = Field(default=datetime.now(ZoneInfo("UTC")))
|
|
28
|
+
completed_at: Optional[datetime] = None
|
|
29
|
+
abandoned_at: Optional[datetime] = None
|
|
30
|
+
current_stage_id: Optional[StrObjectId] = None
|
|
31
|
+
entered_current_stage_at: datetime
|
|
32
|
+
stage_transitions: List[StageTransition] = Field(default_factory=list)
|
|
286
33
|
|
|
287
34
|
@property
|
|
288
35
|
def is_completed(self) -> bool:
|
|
289
|
-
|
|
290
|
-
return self.status == FunnelStatus.COMPLETED and self.completed_at is not None
|
|
36
|
+
return self.completed_at is not None
|
|
291
37
|
|
|
292
38
|
@property
|
|
293
39
|
def is_abandoned(self) -> bool:
|
|
294
|
-
|
|
295
|
-
return self.status == FunnelStatus.ABANDONED and self.abandoned_at is not None
|
|
296
|
-
|
|
297
|
-
@property
|
|
298
|
-
def is_active(self) -> bool:
|
|
299
|
-
"""Check if the chat is still active in the funnel"""
|
|
300
|
-
return self.status == FunnelStatus.IN_PROGRESS
|
|
40
|
+
return self.abandoned_at is not None
|
|
301
41
|
|
|
302
42
|
@property
|
|
303
43
|
def time_in_funnel_seconds(self) -> int:
|
|
304
|
-
"""Calculate total time spent in the funnel"""
|
|
305
44
|
end_time = self.completed_at or self.abandoned_at or datetime.now(tz=ZoneInfo("UTC"))
|
|
306
45
|
return int((end_time - self.started_at).total_seconds())
|
|
307
46
|
|
|
308
47
|
@property
|
|
309
48
|
def time_in_current_stage_seconds(self) -> Optional[int]:
|
|
310
|
-
|
|
311
|
-
if not self.entered_current_stage_at or not self.current_stage_id:
|
|
49
|
+
if not self.entered_current_stage_at:
|
|
312
50
|
return None
|
|
313
51
|
return int((datetime.now(tz=ZoneInfo("UTC")) - self.entered_current_stage_at).total_seconds())
|
|
314
52
|
|
|
315
53
|
@property
|
|
316
54
|
def unique_stages_visited(self) -> int:
|
|
317
|
-
"""Count of unique stages the chat has visited"""
|
|
318
55
|
return len(set(t.to_stage_id for t in self.stage_transitions))
|
|
319
56
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def regression_count(self) -> int:
|
|
327
|
-
"""Count of times the chat moved backwards in the funnel"""
|
|
328
|
-
return sum(1 for t in self.stage_transitions if t.is_regression)
|
|
329
|
-
|
|
330
|
-
@property
|
|
331
|
-
def last_transition(self) -> Optional[StageTransition]:
|
|
332
|
-
"""Get the most recent stage transition"""
|
|
333
|
-
return self.stage_transitions[-1] if self.stage_transitions else None
|
|
334
|
-
|
|
335
|
-
def record_transition(
|
|
336
|
-
self,
|
|
337
|
-
to_stage_id: StrObjectId,
|
|
338
|
-
executor_type: ExecutorType,
|
|
339
|
-
executor_id: StrObjectId,
|
|
340
|
-
from_stage_order: Optional[int] = None,
|
|
341
|
-
to_stage_order: Optional[int] = None
|
|
342
|
-
) -> StageTransition:
|
|
343
|
-
"""
|
|
344
|
-
Record a transition to a new stage.
|
|
345
|
-
Returns the created StageTransition.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
to_stage_id: The stage to transition to
|
|
349
|
-
executor_type: Who/what triggered the transition
|
|
350
|
-
executor_id: ID of the executor
|
|
351
|
-
from_stage_order: Order of current stage (for regression detection)
|
|
352
|
-
to_stage_order: Order of target stage (for regression detection)
|
|
353
|
-
"""
|
|
354
|
-
time_in_previous = self.time_in_current_stage_seconds
|
|
355
|
-
|
|
356
|
-
transition = StageTransition(
|
|
357
|
-
from_stage_id=self.current_stage_id,
|
|
358
|
-
to_stage_id=to_stage_id,
|
|
359
|
-
executor_type=executor_type,
|
|
360
|
-
executor_id=executor_id,
|
|
361
|
-
time_in_previous_stage_seconds=time_in_previous,
|
|
362
|
-
from_stage_order=from_stage_order,
|
|
363
|
-
to_stage_order=to_stage_order
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
self.stage_transitions.append(transition)
|
|
367
|
-
self.current_stage_id = to_stage_id
|
|
368
|
-
self.entered_current_stage_at = transition.transitioned_at
|
|
369
|
-
|
|
370
|
-
return transition
|
|
371
|
-
|
|
372
|
-
def complete(self) -> None:
|
|
373
|
-
"""Mark the funnel as completed"""
|
|
374
|
-
self.status = FunnelStatus.COMPLETED
|
|
375
|
-
self.completed_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
57
|
+
class FunnelStage(CompanyAssetModel):
|
|
58
|
+
name: str
|
|
59
|
+
description: str
|
|
60
|
+
index: int
|
|
61
|
+
inflexion_conversion_point: bool = Field(default=False, description="If true, it'll be prioritized for chat assignment and will send notifications in automatic mode to all agents.")
|
|
62
|
+
workflow_ids: List[StrObjectId] = Field(default_factory=list)
|
|
376
63
|
|
|
377
|
-
def abandon(self) -> None:
|
|
378
|
-
"""Mark the funnel as abandoned"""
|
|
379
|
-
self.status = FunnelStatus.ABANDONED
|
|
380
|
-
self.abandoned_at = datetime.now(tz=ZoneInfo("UTC"))
|
|
381
64
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
Args:
|
|
388
|
-
funnel_name: Name of the funnel (for denormalization)
|
|
389
|
-
stage_name: Name of the current stage (for denormalization)
|
|
390
|
-
"""
|
|
391
|
-
if not self.current_stage_id or not self.entered_current_stage_at:
|
|
392
|
-
raise ValueError("Cannot create ActiveFunnel without current stage")
|
|
393
|
-
|
|
394
|
-
return ActiveFunnel(
|
|
395
|
-
chat_funnel_id=self.id,
|
|
396
|
-
funnel_id=self.funnel_id,
|
|
397
|
-
funnel_name=funnel_name,
|
|
398
|
-
current_stage_id=self.current_stage_id,
|
|
399
|
-
current_stage_name=stage_name,
|
|
400
|
-
entered_current_stage_at=self.entered_current_stage_at
|
|
401
|
-
)
|
|
65
|
+
class Funnel(CompanyAssetModel):
|
|
66
|
+
name: str
|
|
67
|
+
description: str
|
|
68
|
+
stages: List[FunnelStage]
|
|
69
|
+
assignment_priority: int = Field(ge=0, le=10, description="Priority for chat assignment, between funnels")
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
from .assets import *
|
|
2
2
|
from .empresa import EmpresaModel
|
|
3
|
-
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
4
|
-
from .CRM.funnel import Funnel, FunnelPreview, ChatFunnel, StageTransition, FunnelStatus
|
|
3
|
+
from .form_field import FormField, FormFieldPreview, CollectedData, SystemFormFields
|
|
@@ -55,7 +55,7 @@ class SmartFollowUpDecision(BaseModel):
|
|
|
55
55
|
SmartFollowUpDecisionAction.POSTPONE_DELTA_TIME]:
|
|
56
56
|
# Postpone actions don't require messages
|
|
57
57
|
if self.messages is not None and len(self.messages) > 0:
|
|
58
|
-
raise ValueError("Messages are not allowed when action is postpone
|
|
58
|
+
raise ValueError("Messages are not allowed when action is postpone")
|
|
59
59
|
else:
|
|
60
60
|
raise ValueError(f"Invalid action: {self.action}")
|
|
61
61
|
|
|
@@ -92,6 +92,10 @@ class ChattyAIAgentInChat(CompanyAssetModel):
|
|
|
92
92
|
default=None,
|
|
93
93
|
description="If the trigger is a user message, this will be the id of the incoming message"
|
|
94
94
|
)
|
|
95
|
+
last_reset_message_id: Optional[str] = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="Last reset control trigger message id handled for this chat"
|
|
98
|
+
)
|
|
95
99
|
|
|
96
100
|
# Assignment metadata
|
|
97
101
|
assigned_at: datetime = Field(default_factory=lambda: datetime.now(ZoneInfo("UTC")))
|
|
@@ -16,7 +16,7 @@ class ChattyAIMode(StrEnum):
|
|
|
16
16
|
return {
|
|
17
17
|
cls.AUTONOMOUS: "El agente de IA tendrá la autonomía de conversar en tiempo real con el usuario, respetando sus instrucciones y las reglas establecidas.",
|
|
18
18
|
cls.SUGGESTIONS: "El agente de IA sugerirá sólo hará sugerencias de respuestas y seguimientos, pero no enviará mensajes ni interactuará de forma directa con el usuario.",
|
|
19
|
-
cls.COPILOT: "El agente de IA
|
|
19
|
+
cls.COPILOT: "El agente de IA responderá automáticamente cuando tenga alta confianza y la consulta esté dentro del alcance; si no, sugerirá o escalará para revisión humana.",
|
|
20
20
|
cls.OFF: "El agente de IA estará inactivo. No responderá al usuario ni hará sugerencias."
|
|
21
21
|
}[mode]
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ class ChattyAIMode(StrEnum):
|
|
|
26
26
|
mode_description = {
|
|
27
27
|
cls.AUTONOMOUS: "Only answer based on the context and rules provided. Do not improvise or make up information. If you can't handle the question, escalate to a human.",
|
|
28
28
|
cls.SUGGESTIONS: "You're only going to be making suggestions, all your messages will be reviewd by a human and you should add the reasoning to your chain of thought. If the user message is not worth answering, you can use the 'skip' action in your output. If the user message is worth answering, you NEED to use the 'suggest' action in your output. ",
|
|
29
|
-
cls.COPILOT: "You're in
|
|
29
|
+
cls.COPILOT: "You're in COPILOT mode. Use your confidence to decide the action: send when the request is within scope and you are confident, suggest when you're unsure or a human touch is needed (complex negotiation, frustration, high-value, or missing info), and escalate if it's required by control triggers. Always include a confidence score (0-100).",
|
|
30
30
|
cls.OFF: ""
|
|
31
31
|
}[mode]
|
|
32
32
|
return intro + "\n\n" + mode_description
|
|
@@ -25,6 +25,7 @@ class GetChatWithPromptResponse(BaseModel):
|
|
|
25
25
|
"messages": self.messages,
|
|
26
26
|
"n8n_agent_type": self.chatty_ai_agent.n8n_workspace_agent_type.value if self.chatty_ai_agent else None,
|
|
27
27
|
"n8n_agent_type_parameters": self.chatty_ai_agent.n8n_workspace_agent_type_parameteres.model_dump() if self.chatty_ai_agent else None,
|
|
28
|
+
"ai_agent_id": self.chatty_ai_agent.id if self.chatty_ai_agent else None,
|
|
28
29
|
"phone_number": self.chat.client.get_waid() if self.chat else None,
|
|
29
30
|
"chain_of_thought_id": self.chain_of_thought_id,
|
|
30
31
|
"trigger_id": self.trigger_id
|
|
@@ -5,11 +5,17 @@ Defines the data collection, acceptance criteria, and destination actions
|
|
|
5
5
|
for qualifying/disqualifying users.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from letschatty.models.company.assets.automation import Automation
|
|
8
9
|
from pydantic import BaseModel, Field
|
|
9
10
|
from typing import Optional, List
|
|
10
11
|
from enum import StrEnum
|
|
11
12
|
from letschatty.models.utils.types.identifier import StrObjectId
|
|
12
13
|
|
|
14
|
+
class PostToExternalApiConfig(BaseModel):
|
|
15
|
+
"""Placeholder provisorio"""
|
|
16
|
+
url: str = Field(description="The URL to post to")
|
|
17
|
+
method: str = Field(description="The HTTP method to use")
|
|
18
|
+
api_key: str
|
|
13
19
|
|
|
14
20
|
class PreQualifyDestination(StrEnum):
|
|
15
21
|
"""
|
|
@@ -19,8 +25,11 @@ class PreQualifyDestination(StrEnum):
|
|
|
19
25
|
CALENDAR_SCHEDULER = "calendar_scheduler" # Allow AI agent to schedule meetings
|
|
20
26
|
ESCALATE = "escalate" # Escalate to human
|
|
21
27
|
CUSTOM_MESSAGE = "custom_message" # Send a custom message
|
|
28
|
+
AUTO_ASSIGN_HUMAN_AGENT = "auto_assign_human_agent" #
|
|
22
29
|
CONTINUE = "continue" # Continue normal AI agent flow
|
|
23
|
-
NONE = "none" # Do nothing
|
|
30
|
+
NONE = "none" # Do nothing (no-op)
|
|
31
|
+
ARCHIVE = "archive"
|
|
32
|
+
POST_TO_EXTERNAL_API = "post_to_external_api"
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
class PreQualifyFormField(BaseModel):
|
|
@@ -68,6 +77,10 @@ class PreQualifyConfig(BaseModel):
|
|
|
68
77
|
default=None,
|
|
69
78
|
description="Custom message to send when user does NOT qualify (if destination is custom_message or escalate)"
|
|
70
79
|
)
|
|
80
|
+
post_to_external_api : Optional[PostToExternalApiConfig] = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
description="Configuration for posting to an external API"
|
|
83
|
+
)
|
|
71
84
|
|
|
72
85
|
@property
|
|
73
86
|
def has_form_fields(self) -> bool:
|
|
@@ -108,4 +121,3 @@ class PreQualifyConfig(BaseModel):
|
|
|
108
121
|
original_len = len(self.form_fields)
|
|
109
122
|
self.form_fields = [f for f in self.form_fields if f.field_key != field_key]
|
|
110
123
|
return len(self.form_fields) < original_len
|
|
111
|
-
|
|
@@ -14,18 +14,27 @@ 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: 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
|
-
)
|
|
17
|
+
chain_of_thought : Optional[str] = Field(default=None)
|
|
27
18
|
# client_info: Optional[ClientInfo] = Field(default=None) me gustaría que levante el mail y/o otros atributos
|
|
28
19
|
|
|
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
|
+
|
|
29
38
|
@model_validator(mode='after')
|
|
30
39
|
def check_agent_id(self):
|
|
31
40
|
if self.area == Area.WITH_AGENT and not self.agent_id:
|