fusesell 1.3.42__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.
- fusesell-1.3.42.dist-info/METADATA +873 -0
- fusesell-1.3.42.dist-info/RECORD +35 -0
- fusesell-1.3.42.dist-info/WHEEL +5 -0
- fusesell-1.3.42.dist-info/entry_points.txt +2 -0
- fusesell-1.3.42.dist-info/licenses/LICENSE +21 -0
- fusesell-1.3.42.dist-info/top_level.txt +2 -0
- fusesell.py +20 -0
- fusesell_local/__init__.py +37 -0
- fusesell_local/api.py +343 -0
- fusesell_local/cli.py +1480 -0
- fusesell_local/config/__init__.py +11 -0
- fusesell_local/config/default_email_templates.json +34 -0
- fusesell_local/config/default_prompts.json +19 -0
- fusesell_local/config/default_scoring_criteria.json +154 -0
- fusesell_local/config/prompts.py +245 -0
- fusesell_local/config/settings.py +277 -0
- fusesell_local/pipeline.py +978 -0
- fusesell_local/stages/__init__.py +19 -0
- fusesell_local/stages/base_stage.py +603 -0
- fusesell_local/stages/data_acquisition.py +1820 -0
- fusesell_local/stages/data_preparation.py +1238 -0
- fusesell_local/stages/follow_up.py +1728 -0
- fusesell_local/stages/initial_outreach.py +2972 -0
- fusesell_local/stages/lead_scoring.py +1452 -0
- fusesell_local/utils/__init__.py +36 -0
- fusesell_local/utils/agent_context.py +552 -0
- fusesell_local/utils/auto_setup.py +361 -0
- fusesell_local/utils/birthday_email_manager.py +467 -0
- fusesell_local/utils/data_manager.py +4857 -0
- fusesell_local/utils/event_scheduler.py +959 -0
- fusesell_local/utils/llm_client.py +342 -0
- fusesell_local/utils/logger.py +203 -0
- fusesell_local/utils/output_helpers.py +2443 -0
- fusesell_local/utils/timezone_detector.py +914 -0
- fusesell_local/utils/validators.py +436 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-setup and intelligent initialization utilities for FuseSell.
|
|
3
|
+
|
|
4
|
+
This module provides functions for:
|
|
5
|
+
- Auto-initialization of settings with smart defaults (e.g., Gmail email)
|
|
6
|
+
- Settings completion checking
|
|
7
|
+
- Agent context generation and updates
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
14
|
+
|
|
15
|
+
from .data_manager import LocalDataManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("fusesell.auto_setup")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_gmail_email_safe(get_gmail_email_func: Optional[Callable[[], str]] = None) -> Optional[str]:
|
|
22
|
+
"""
|
|
23
|
+
Safely retrieve Gmail email from MCP server.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
get_gmail_email_func: Optional function to retrieve Gmail email.
|
|
27
|
+
If None, returns None.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Gmail email address if available, None otherwise
|
|
31
|
+
"""
|
|
32
|
+
if get_gmail_email_func is None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
email = get_gmail_email_func()
|
|
37
|
+
if email and isinstance(email, str) and email.strip():
|
|
38
|
+
return email.strip()
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.warning(f"Could not retrieve Gmail email: {exc}")
|
|
41
|
+
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_settings_completion(
|
|
46
|
+
manager: LocalDataManager,
|
|
47
|
+
team_id: str
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Check which settings sections are completed for a team.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
manager: LocalDataManager instance
|
|
54
|
+
team_id: Team identifier
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary with completion status for each settings section:
|
|
58
|
+
{
|
|
59
|
+
"team_id": str,
|
|
60
|
+
"auto_interaction_completed": bool,
|
|
61
|
+
"rep_completed": bool,
|
|
62
|
+
"product_completed": bool,
|
|
63
|
+
"organization_completed": bool,
|
|
64
|
+
"follow_up_completed": bool,
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
settings = manager.get_team_settings(team_id)
|
|
68
|
+
|
|
69
|
+
def _is_completed(value: Any) -> bool:
|
|
70
|
+
"""Check if a settings value is considered completed."""
|
|
71
|
+
if value is None:
|
|
72
|
+
return False
|
|
73
|
+
if isinstance(value, str):
|
|
74
|
+
return value.strip() != ""
|
|
75
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
76
|
+
return len(value) > 0
|
|
77
|
+
if isinstance(value, dict):
|
|
78
|
+
return len(value) > 0
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
if not settings:
|
|
82
|
+
return {
|
|
83
|
+
"team_id": team_id,
|
|
84
|
+
"auto_interaction_completed": False,
|
|
85
|
+
"rep_completed": False,
|
|
86
|
+
"product_completed": False,
|
|
87
|
+
"organization_completed": False,
|
|
88
|
+
"follow_up_completed": False,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"team_id": team_id,
|
|
93
|
+
"auto_interaction_completed": _is_completed(settings.get("gs_team_auto_interaction")),
|
|
94
|
+
"rep_completed": _is_completed(settings.get("gs_team_rep")),
|
|
95
|
+
"product_completed": _is_completed(settings.get("gs_team_product")),
|
|
96
|
+
"organization_completed": _is_completed(settings.get("gs_team_organization")),
|
|
97
|
+
"follow_up_completed": _is_completed(settings.get("gs_team_follow_up")),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def auto_initialize_auto_interaction(
|
|
102
|
+
manager: LocalDataManager,
|
|
103
|
+
team_id: str,
|
|
104
|
+
gmail_email: Optional[str] = None,
|
|
105
|
+
get_gmail_email_func: Optional[Callable[[], str]] = None
|
|
106
|
+
) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Auto-initialize auto_interaction settings with Gmail email if:
|
|
109
|
+
1. Gmail MCP is connected (or gmail_email is provided)
|
|
110
|
+
2. auto_interaction is not yet set
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
manager: LocalDataManager instance
|
|
114
|
+
team_id: Team identifier
|
|
115
|
+
gmail_email: Optional pre-fetched Gmail email
|
|
116
|
+
get_gmail_email_func: Optional function to retrieve Gmail email
|
|
117
|
+
(will be called if gmail_email is None)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if auto-initialization was performed, False otherwise
|
|
121
|
+
"""
|
|
122
|
+
# Get Gmail email if not provided
|
|
123
|
+
if gmail_email is None and get_gmail_email_func is not None:
|
|
124
|
+
gmail_email = get_gmail_email_safe(get_gmail_email_func)
|
|
125
|
+
|
|
126
|
+
if not gmail_email:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# Check if already initialized
|
|
130
|
+
settings = manager.get_team_settings(team_id)
|
|
131
|
+
auto_interaction_value = settings.get("gs_team_auto_interaction") if settings else None
|
|
132
|
+
|
|
133
|
+
# Check if auto_interaction is already completed
|
|
134
|
+
if auto_interaction_value:
|
|
135
|
+
if isinstance(auto_interaction_value, list) and len(auto_interaction_value) > 0:
|
|
136
|
+
return False
|
|
137
|
+
if isinstance(auto_interaction_value, dict) and len(auto_interaction_value) > 0:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Create default auto_interaction with Gmail email
|
|
142
|
+
default_auto_interaction = [{
|
|
143
|
+
"from_email": gmail_email,
|
|
144
|
+
"from_name": "",
|
|
145
|
+
"from_number": "",
|
|
146
|
+
"tool_type": "Email",
|
|
147
|
+
"email_cc": "",
|
|
148
|
+
"email_bcc": "",
|
|
149
|
+
}]
|
|
150
|
+
|
|
151
|
+
manager.update_team_settings(
|
|
152
|
+
team_id=team_id,
|
|
153
|
+
gs_team_auto_interaction=default_auto_interaction
|
|
154
|
+
)
|
|
155
|
+
logger.info(f"Auto-initialized auto_interaction with Gmail: {gmail_email}")
|
|
156
|
+
return True
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.error(f"Failed to auto-initialize auto_interaction: {exc}")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def auto_populate_from_email(
|
|
163
|
+
from_email: str,
|
|
164
|
+
gmail_email: Optional[str] = None,
|
|
165
|
+
get_gmail_email_func: Optional[Callable[[], str]] = None
|
|
166
|
+
) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Auto-populate from_email field if empty using Gmail email.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
from_email: Current from_email value
|
|
172
|
+
gmail_email: Optional pre-fetched Gmail email
|
|
173
|
+
get_gmail_email_func: Optional function to retrieve Gmail email
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Original from_email if not empty, otherwise Gmail email if available,
|
|
177
|
+
otherwise empty string
|
|
178
|
+
"""
|
|
179
|
+
# Return existing value if not empty
|
|
180
|
+
if from_email and from_email.strip():
|
|
181
|
+
return from_email.strip()
|
|
182
|
+
|
|
183
|
+
# Get Gmail email if not provided
|
|
184
|
+
if gmail_email is None and get_gmail_email_func is not None:
|
|
185
|
+
gmail_email = get_gmail_email_safe(get_gmail_email_func)
|
|
186
|
+
|
|
187
|
+
if gmail_email:
|
|
188
|
+
logger.info(f"Auto-populated from_email with Gmail: {gmail_email}")
|
|
189
|
+
return gmail_email
|
|
190
|
+
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def generate_agent_context(
|
|
195
|
+
manager: LocalDataManager,
|
|
196
|
+
org_id: str,
|
|
197
|
+
detail_limit: Optional[int] = None
|
|
198
|
+
) -> Dict[str, Any]:
|
|
199
|
+
"""
|
|
200
|
+
Generate comprehensive agent context for the workspace.
|
|
201
|
+
|
|
202
|
+
This function collects and structures all relevant workspace data
|
|
203
|
+
for agent memory/context updates.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
manager: LocalDataManager instance
|
|
207
|
+
org_id: Organization identifier
|
|
208
|
+
detail_limit: Optional limit for detail fields in products/processes
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary containing:
|
|
212
|
+
- workspace_summary: Overview text
|
|
213
|
+
- products: List of active products
|
|
214
|
+
- active_processes: List of active sales processes
|
|
215
|
+
- team_settings: Team configuration
|
|
216
|
+
- statistics: Usage statistics
|
|
217
|
+
- last_updated: Timestamp
|
|
218
|
+
"""
|
|
219
|
+
# Get teams
|
|
220
|
+
teams = manager.list_teams(org_id=org_id, status='active')
|
|
221
|
+
team = teams[0] if teams else None
|
|
222
|
+
|
|
223
|
+
team_id = None
|
|
224
|
+
team_name = org_id or 'Workspace Team'
|
|
225
|
+
team_created_at = 'N/A'
|
|
226
|
+
|
|
227
|
+
if team:
|
|
228
|
+
team_id = team.get('team_id')
|
|
229
|
+
team_name = team.get('name') or team.get('team_name') or team_name
|
|
230
|
+
team_created_at = team.get('created_at', 'N/A')
|
|
231
|
+
|
|
232
|
+
# Get products (include all, track active separately)
|
|
233
|
+
products = manager.search_products(org_id=org_id, status="all") or []
|
|
234
|
+
active_products_count = sum(1 for p in products if (p.get('status') or '').lower() == 'active')
|
|
235
|
+
inactive_products_count = len(products) - active_products_count
|
|
236
|
+
|
|
237
|
+
# Apply detail limit if specified
|
|
238
|
+
if detail_limit is not None and detail_limit > 0:
|
|
239
|
+
for product in products:
|
|
240
|
+
for key in ['short_description', 'long_description', 'category']:
|
|
241
|
+
if key in product and isinstance(product[key], str):
|
|
242
|
+
if len(product[key]) > detail_limit:
|
|
243
|
+
product[key] = product[key][:detail_limit] + '...'
|
|
244
|
+
|
|
245
|
+
# Get settings (fallback to empty when team not yet created)
|
|
246
|
+
settings: Dict[str, Any] = {}
|
|
247
|
+
if team_id:
|
|
248
|
+
settings = manager.get_team_settings(team_id) or {}
|
|
249
|
+
|
|
250
|
+
# Get processes
|
|
251
|
+
all_processes = manager.list_tasks(org_id=org_id, limit=100)
|
|
252
|
+
active_processes = [
|
|
253
|
+
p for p in all_processes
|
|
254
|
+
if p.get('status') in ('running', 'pending', 'in_progress')
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
# Apply detail limit to processes
|
|
258
|
+
if detail_limit is not None and detail_limit > 0:
|
|
259
|
+
for process in active_processes:
|
|
260
|
+
for key in ['customer_name', 'customer_company', 'notes']:
|
|
261
|
+
if key in process and isinstance(process[key], str):
|
|
262
|
+
if len(process[key]) > detail_limit:
|
|
263
|
+
process[key] = process[key][:detail_limit] + '...'
|
|
264
|
+
|
|
265
|
+
# Calculate completion status
|
|
266
|
+
completion_status = check_settings_completion(manager, team_id) if team_id else {
|
|
267
|
+
'completed': 0,
|
|
268
|
+
'total': 0,
|
|
269
|
+
'completed_keys': [],
|
|
270
|
+
'missing_keys': [
|
|
271
|
+
'gs_team_organization',
|
|
272
|
+
'gs_team_rep',
|
|
273
|
+
'gs_team_product',
|
|
274
|
+
'gs_team_auto_interaction',
|
|
275
|
+
'gs_team_initial_outreach',
|
|
276
|
+
'gs_team_follow_up',
|
|
277
|
+
'gs_team_schedule_time',
|
|
278
|
+
'gs_team_followup_schedule_time',
|
|
279
|
+
],
|
|
280
|
+
'auto_interaction_completed': False,
|
|
281
|
+
'has_sales_rep': False,
|
|
282
|
+
'has_products': len(products) > 0,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Build workspace summary
|
|
286
|
+
workspace_summary_parts = [
|
|
287
|
+
f"Team: {team_name}",
|
|
288
|
+
f"Products: {len(products)} total ({active_products_count} active)",
|
|
289
|
+
f"Active Processes: {len(active_processes)}",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
if completion_status.get('auto_interaction_completed'):
|
|
293
|
+
workspace_summary_parts.append("Auto interaction: configured")
|
|
294
|
+
else:
|
|
295
|
+
workspace_summary_parts.append("Auto interaction: not configured")
|
|
296
|
+
|
|
297
|
+
workspace_summary = " | ".join(workspace_summary_parts)
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
'workspace_summary': workspace_summary,
|
|
301
|
+
'workspace_slug': manager.data_dir, # best-effort; flows can override in writer
|
|
302
|
+
'team_id': team_id,
|
|
303
|
+
'team_name': team_name,
|
|
304
|
+
'team_created_at': team_created_at,
|
|
305
|
+
'org_id': org_id,
|
|
306
|
+
'products': products,
|
|
307
|
+
'active_processes': active_processes,
|
|
308
|
+
'team_settings': settings,
|
|
309
|
+
'statistics': {
|
|
310
|
+
'total_products': len(products),
|
|
311
|
+
'active_products': active_products_count,
|
|
312
|
+
'inactive_products': inactive_products_count,
|
|
313
|
+
'active_processes': len(active_processes),
|
|
314
|
+
'total_processes': len(all_processes),
|
|
315
|
+
'settings_completion': completion_status,
|
|
316
|
+
},
|
|
317
|
+
'last_updated': datetime.utcnow().isoformat() + 'Z',
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def should_update_agent_context(
|
|
322
|
+
action: str,
|
|
323
|
+
critical_actions: Optional[List[str]] = None,
|
|
324
|
+
skip_actions: Optional[List[str]] = None
|
|
325
|
+
) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Determine if agent context should be updated based on the action performed.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
action: The action that was performed
|
|
331
|
+
critical_actions: List of actions that trigger context updates
|
|
332
|
+
skip_actions: List of actions that should NOT trigger updates
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
True if agent context should be updated, False otherwise
|
|
336
|
+
"""
|
|
337
|
+
# Default critical actions
|
|
338
|
+
if critical_actions is None:
|
|
339
|
+
critical_actions = [
|
|
340
|
+
'product_create', 'product_update', 'product_delete',
|
|
341
|
+
'product_status_change', 'product_bulk_import',
|
|
342
|
+
'process_create',
|
|
343
|
+
'team_create', 'team_update',
|
|
344
|
+
'gs_organization', 'sales_rep', 'product_team', 'auto_interaction',
|
|
345
|
+
'initial_outreach', 'follow_up', 'schedule_time', 'followup_schedule_time',
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
# Default skip actions
|
|
349
|
+
if skip_actions is None:
|
|
350
|
+
skip_actions = [
|
|
351
|
+
'product_view', 'product_list', 'team_view', 'team_list', 'settings_view',
|
|
352
|
+
'process_query', 'process_status_change', 'draft_list', 'event_list',
|
|
353
|
+
'event_update', 'knowledge_query', 'proposal_generate'
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
# Skip actions take precedence
|
|
357
|
+
if action in skip_actions:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
# Check if action is critical
|
|
361
|
+
return action in critical_actions
|