fusesell 1.2.0__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.
Potentially problematic release.
This version of fusesell might be problematic. Click here for more details.
- fusesell-1.2.0.dist-info/METADATA +872 -0
- fusesell-1.2.0.dist-info/RECORD +31 -0
- fusesell-1.2.0.dist-info/WHEEL +5 -0
- fusesell-1.2.0.dist-info/entry_points.txt +2 -0
- fusesell-1.2.0.dist-info/licenses/LICENSE +21 -0
- fusesell-1.2.0.dist-info/top_level.txt +2 -0
- fusesell.py +15 -0
- fusesell_local/__init__.py +37 -0
- fusesell_local/api.py +341 -0
- fusesell_local/cli.py +1450 -0
- fusesell_local/config/__init__.py +11 -0
- fusesell_local/config/prompts.py +245 -0
- fusesell_local/config/settings.py +277 -0
- fusesell_local/pipeline.py +932 -0
- fusesell_local/stages/__init__.py +19 -0
- fusesell_local/stages/base_stage.py +602 -0
- fusesell_local/stages/data_acquisition.py +1820 -0
- fusesell_local/stages/data_preparation.py +1231 -0
- fusesell_local/stages/follow_up.py +1590 -0
- fusesell_local/stages/initial_outreach.py +2337 -0
- fusesell_local/stages/lead_scoring.py +1452 -0
- fusesell_local/tests/test_api.py +65 -0
- fusesell_local/tests/test_cli.py +37 -0
- fusesell_local/utils/__init__.py +15 -0
- fusesell_local/utils/birthday_email_manager.py +467 -0
- fusesell_local/utils/data_manager.py +4050 -0
- fusesell_local/utils/event_scheduler.py +618 -0
- fusesell_local/utils/llm_client.py +283 -0
- fusesell_local/utils/logger.py +203 -0
- fusesell_local/utils/timezone_detector.py +914 -0
- fusesell_local/utils/validators.py +416 -0
fusesell_local/cli.py
ADDED
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FuseSell Local - Command-line interface for local sales automation pipeline
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from .api import (
|
|
13
|
+
build_config as build_pipeline_config,
|
|
14
|
+
configure_logging as configure_pipeline_logging,
|
|
15
|
+
prepare_data_directory,
|
|
16
|
+
validate_config as validate_pipeline_config,
|
|
17
|
+
)
|
|
18
|
+
from .pipeline import FuseSellPipeline
|
|
19
|
+
from .utils.validators import InputValidator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FuseSellCLI:
|
|
23
|
+
"""
|
|
24
|
+
Command-line interface for FuseSell local execution.
|
|
25
|
+
Handles argument parsing, validation, and pipeline orchestration.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""Initialize CLI with argument parser."""
|
|
30
|
+
self.parser = self._setup_argument_parser()
|
|
31
|
+
self.logger = None # Will be initialized after parsing args
|
|
32
|
+
|
|
33
|
+
def _setup_argument_parser(self) -> argparse.ArgumentParser:
|
|
34
|
+
"""
|
|
35
|
+
Set up command-line argument parser with subcommands.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Configured ArgumentParser instance
|
|
39
|
+
"""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
description='FuseSell Local - AI-powered sales automation pipeline',
|
|
42
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
43
|
+
epilog="""
|
|
44
|
+
Examples:
|
|
45
|
+
# PIPELINE EXECUTION
|
|
46
|
+
python fusesell.py pipeline --openai-api-key sk-xxx --org-id rta --org-name "RTA Corp" \\
|
|
47
|
+
--full-input "Seller: RTA Corp, Customer: Example Company, Communication: English" \\
|
|
48
|
+
--input-website "https://example.com"
|
|
49
|
+
|
|
50
|
+
# TEAM MANAGEMENT
|
|
51
|
+
python fusesell.py team create --name "Sales Team A" --description "Primary sales team" \\
|
|
52
|
+
--org-id rta --plan-id plan-123
|
|
53
|
+
python fusesell.py team list --org-id rta
|
|
54
|
+
python fusesell.py team describe team-456
|
|
55
|
+
|
|
56
|
+
# PRODUCT MANAGEMENT
|
|
57
|
+
python fusesell.py product create --name "FuseSell Pro" --description "Advanced sales automation" \\
|
|
58
|
+
--org-id rta --product-data '{"category":"Sales Automation"}'
|
|
59
|
+
python fusesell.py product list --org-id rta
|
|
60
|
+
|
|
61
|
+
# SETTINGS MANAGEMENT
|
|
62
|
+
python fusesell.py settings set team-456 --setting-name product_settings \\
|
|
63
|
+
--value-json '[{"product_id": "prod-123"}]'
|
|
64
|
+
python fusesell.py settings view team-456 --setting-name product_settings
|
|
65
|
+
|
|
66
|
+
# BIRTHDAY EMAIL MANAGEMENT
|
|
67
|
+
python fusesell.py settings birthday configure team-456 --org-id rta \\
|
|
68
|
+
--prompt "Send friendly birthday greetings, max 200 words, UTC+07"
|
|
69
|
+
python fusesell.py settings birthday list-templates --team-id team-456
|
|
70
|
+
python fusesell.py settings birthday view-template birthday_email__team-456
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Add subcommands (optional for backward compatibility)
|
|
75
|
+
subparsers = parser.add_subparsers(
|
|
76
|
+
dest='command', help='Available commands', required=False)
|
|
77
|
+
|
|
78
|
+
# Team management subcommand
|
|
79
|
+
team_parser = subparsers.add_parser('team', help='Manage teams')
|
|
80
|
+
self._add_team_arguments(team_parser)
|
|
81
|
+
|
|
82
|
+
# Product management subcommand
|
|
83
|
+
product_parser = subparsers.add_parser(
|
|
84
|
+
'product', help='Manage products')
|
|
85
|
+
self._add_product_arguments(product_parser)
|
|
86
|
+
|
|
87
|
+
# Settings management subcommand
|
|
88
|
+
settings_parser = subparsers.add_parser(
|
|
89
|
+
'settings', help='Manage team settings')
|
|
90
|
+
self._add_settings_arguments(settings_parser)
|
|
91
|
+
|
|
92
|
+
# Add pipeline arguments directly to main parser for backward compatibility
|
|
93
|
+
self._add_pipeline_arguments(parser)
|
|
94
|
+
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
def _add_pipeline_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
98
|
+
"""Add pipeline-specific arguments."""
|
|
99
|
+
|
|
100
|
+
# Required arguments (for pipeline execution)
|
|
101
|
+
required = parser.add_argument_group(
|
|
102
|
+
'pipeline arguments (required for pipeline execution)')
|
|
103
|
+
required.add_argument(
|
|
104
|
+
'--openai-api-key',
|
|
105
|
+
help='OpenAI API key for LLM processing'
|
|
106
|
+
)
|
|
107
|
+
required.add_argument(
|
|
108
|
+
'--org-id',
|
|
109
|
+
help='Organization ID (seller identifier)'
|
|
110
|
+
)
|
|
111
|
+
required.add_argument(
|
|
112
|
+
'--org-name',
|
|
113
|
+
help='Organization name (seller name)'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Data source fields (at least one required) - matching executor schema
|
|
117
|
+
data_sources = parser.add_argument_group(
|
|
118
|
+
'data sources (at least one required)')
|
|
119
|
+
data_sources.add_argument(
|
|
120
|
+
'--input-website',
|
|
121
|
+
help='The URL of the website. If no URL is provided, the return value will be empty (maps to input_website)'
|
|
122
|
+
)
|
|
123
|
+
data_sources.add_argument(
|
|
124
|
+
'--input-description',
|
|
125
|
+
help='Full information about the customer. Such as name, phone, email, address, taxcode, another info... (maps to input_description)'
|
|
126
|
+
)
|
|
127
|
+
data_sources.add_argument(
|
|
128
|
+
'--input-business-card',
|
|
129
|
+
help='The image URL of the business card. If no image URL is provided, the return value will be empty (maps to input_business_card)'
|
|
130
|
+
)
|
|
131
|
+
data_sources.add_argument(
|
|
132
|
+
'--input-facebook-url',
|
|
133
|
+
help='Valid URL pointing to a personal profile or business page on Facebook (maps to input_facebook_url)'
|
|
134
|
+
)
|
|
135
|
+
data_sources.add_argument(
|
|
136
|
+
'--input-linkedin-url',
|
|
137
|
+
help='Valid URL pointing to a personal profile or business page on LinkedIn (maps to input_linkedin_url)'
|
|
138
|
+
)
|
|
139
|
+
data_sources.add_argument(
|
|
140
|
+
'--input-freetext',
|
|
141
|
+
help='Free text input with customer information (maps to input_freetext from executor schema)'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Required context fields (matching executor schema)
|
|
145
|
+
context_group = parser.add_argument_group(
|
|
146
|
+
'context fields (required for pipeline execution)')
|
|
147
|
+
context_group.add_argument(
|
|
148
|
+
'--full-input',
|
|
149
|
+
help='Full information input. Includes: Seller, Customer and Communication information'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Optional context fields
|
|
153
|
+
optional_context = parser.add_argument_group('optional context')
|
|
154
|
+
optional_context.add_argument(
|
|
155
|
+
'--customer-id',
|
|
156
|
+
help='ID of customer, if not provided, return null'
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Team and project settings
|
|
160
|
+
team_group = parser.add_argument_group('team settings')
|
|
161
|
+
team_group.add_argument(
|
|
162
|
+
'--team-id',
|
|
163
|
+
help='Team ID for team-specific configurations'
|
|
164
|
+
)
|
|
165
|
+
team_group.add_argument(
|
|
166
|
+
'--team-name',
|
|
167
|
+
help='Team name for team-specific configurations'
|
|
168
|
+
)
|
|
169
|
+
team_group.add_argument(
|
|
170
|
+
'--project-code',
|
|
171
|
+
help='Project code for organization'
|
|
172
|
+
)
|
|
173
|
+
team_group.add_argument(
|
|
174
|
+
'--staff-name',
|
|
175
|
+
default='Sales Team',
|
|
176
|
+
help='Staff member name for email signatures (default: Sales Team)'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Processing options
|
|
180
|
+
processing_group = parser.add_argument_group('processing options')
|
|
181
|
+
processing_group.add_argument(
|
|
182
|
+
'--language',
|
|
183
|
+
default='english',
|
|
184
|
+
choices=['english', 'vietnamese', 'spanish', 'french', 'german'],
|
|
185
|
+
help='Language for communication and processing (default: english)'
|
|
186
|
+
)
|
|
187
|
+
processing_group.add_argument(
|
|
188
|
+
'--skip-stages',
|
|
189
|
+
nargs='*',
|
|
190
|
+
choices=['data_acquisition', 'data_preparation',
|
|
191
|
+
'lead_scoring', 'initial_outreach', 'follow_up'],
|
|
192
|
+
help='Stages to skip during execution'
|
|
193
|
+
)
|
|
194
|
+
processing_group.add_argument(
|
|
195
|
+
'--stop-after',
|
|
196
|
+
choices=['data_acquisition', 'data_preparation',
|
|
197
|
+
'lead_scoring', 'initial_outreach', 'follow_up'],
|
|
198
|
+
help='Stop pipeline after specified stage'
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Process continuation and action options (matching server executor schema)
|
|
202
|
+
continuation_group = parser.add_argument_group(
|
|
203
|
+
'process continuation and actions')
|
|
204
|
+
continuation_group.add_argument(
|
|
205
|
+
'--continue-execution',
|
|
206
|
+
help='Continue an existing execution by providing execution ID'
|
|
207
|
+
)
|
|
208
|
+
continuation_group.add_argument(
|
|
209
|
+
'--action',
|
|
210
|
+
choices=['draft_write', 'draft_rewrite', 'send', 'close'],
|
|
211
|
+
default='draft_write',
|
|
212
|
+
help='Action to perform: draft_write (generate new drafts), draft_rewrite (modify existing), send (send approved draft), close (close outreach)'
|
|
213
|
+
)
|
|
214
|
+
continuation_group.add_argument(
|
|
215
|
+
'--selected-draft-id',
|
|
216
|
+
help='ID of existing draft to rewrite or send (required for draft_rewrite and send actions)'
|
|
217
|
+
)
|
|
218
|
+
continuation_group.add_argument(
|
|
219
|
+
'--reason',
|
|
220
|
+
help='Reason for the action (e.g., "customer requested more info", "make tone more casual")'
|
|
221
|
+
)
|
|
222
|
+
continuation_group.add_argument(
|
|
223
|
+
'--recipient-address',
|
|
224
|
+
help='Email address of the recipient (required for send action)'
|
|
225
|
+
)
|
|
226
|
+
continuation_group.add_argument(
|
|
227
|
+
'--recipient-name',
|
|
228
|
+
help='Name of the recipient (optional for send action)'
|
|
229
|
+
)
|
|
230
|
+
continuation_group.add_argument(
|
|
231
|
+
'--interaction-type',
|
|
232
|
+
default='email',
|
|
233
|
+
help='Type of interaction (default: email)'
|
|
234
|
+
)
|
|
235
|
+
continuation_group.add_argument(
|
|
236
|
+
'--send-immediately',
|
|
237
|
+
action='store_true',
|
|
238
|
+
help='Send email immediately instead of scheduling for optimal time'
|
|
239
|
+
)
|
|
240
|
+
continuation_group.add_argument(
|
|
241
|
+
'--customer-timezone',
|
|
242
|
+
help='Customer timezone (e.g., America/New_York, Europe/London). Auto-detected if not provided'
|
|
243
|
+
)
|
|
244
|
+
continuation_group.add_argument(
|
|
245
|
+
'--business-hours-start',
|
|
246
|
+
default='08:00',
|
|
247
|
+
help='Business hours start time in HH:MM format (default: 08:00)'
|
|
248
|
+
)
|
|
249
|
+
continuation_group.add_argument(
|
|
250
|
+
'--business-hours-end',
|
|
251
|
+
default='20:00',
|
|
252
|
+
help='Business hours end time in HH:MM format (default: 20:00)'
|
|
253
|
+
)
|
|
254
|
+
continuation_group.add_argument(
|
|
255
|
+
'--delay-hours',
|
|
256
|
+
type=int,
|
|
257
|
+
default=2,
|
|
258
|
+
help='Default delay in hours before sending (default: 2)'
|
|
259
|
+
)
|
|
260
|
+
continuation_group.add_argument(
|
|
261
|
+
'--human-action-id',
|
|
262
|
+
help='ID of the human action event (for server integration)'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Output and storage options
|
|
266
|
+
output_group = parser.add_argument_group('output options')
|
|
267
|
+
output_group.add_argument(
|
|
268
|
+
'--output-format',
|
|
269
|
+
choices=['json', 'text', 'yaml'],
|
|
270
|
+
default='json',
|
|
271
|
+
help='Output format for results (default: json)'
|
|
272
|
+
)
|
|
273
|
+
output_group.add_argument(
|
|
274
|
+
'--data-dir',
|
|
275
|
+
default='./fusesell_data',
|
|
276
|
+
help='Directory for local data storage (default: ./fusesell_data)'
|
|
277
|
+
)
|
|
278
|
+
output_group.add_argument(
|
|
279
|
+
'--execution-id',
|
|
280
|
+
help='Custom execution ID (auto-generated if not provided)'
|
|
281
|
+
)
|
|
282
|
+
output_group.add_argument(
|
|
283
|
+
'--save-intermediate',
|
|
284
|
+
action='store_true',
|
|
285
|
+
help='Save intermediate results from each stage'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Logging and debugging
|
|
289
|
+
debug_group = parser.add_argument_group('logging and debugging')
|
|
290
|
+
debug_group.add_argument(
|
|
291
|
+
'--log-level',
|
|
292
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
|
293
|
+
default='INFO',
|
|
294
|
+
help='Logging level (default: INFO)'
|
|
295
|
+
)
|
|
296
|
+
debug_group.add_argument(
|
|
297
|
+
'--log-file',
|
|
298
|
+
help='Log file path (logs to console if not specified)'
|
|
299
|
+
)
|
|
300
|
+
debug_group.add_argument(
|
|
301
|
+
'--verbose',
|
|
302
|
+
action='store_true',
|
|
303
|
+
help='Enable verbose output'
|
|
304
|
+
)
|
|
305
|
+
debug_group.add_argument(
|
|
306
|
+
'--dry-run',
|
|
307
|
+
action='store_true',
|
|
308
|
+
help='Validate inputs and show execution plan without running'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Advanced options
|
|
312
|
+
advanced_group = parser.add_argument_group('advanced options')
|
|
313
|
+
advanced_group.add_argument(
|
|
314
|
+
'--llm-model',
|
|
315
|
+
default='gpt-4o-mini',
|
|
316
|
+
help='LLM model to use (default: gpt-4o-mini)'
|
|
317
|
+
)
|
|
318
|
+
advanced_group.add_argument(
|
|
319
|
+
'--llm-base-url',
|
|
320
|
+
help='Custom LLM API base URL'
|
|
321
|
+
)
|
|
322
|
+
advanced_group.add_argument(
|
|
323
|
+
'--temperature',
|
|
324
|
+
type=float,
|
|
325
|
+
default=0.7,
|
|
326
|
+
help='LLM temperature for creativity (0.0-2.0, default: 0.7)'
|
|
327
|
+
)
|
|
328
|
+
advanced_group.add_argument(
|
|
329
|
+
'--max-retries',
|
|
330
|
+
type=int,
|
|
331
|
+
default=3,
|
|
332
|
+
help='Maximum retries for API calls (default: 3)'
|
|
333
|
+
)
|
|
334
|
+
advanced_group.add_argument(
|
|
335
|
+
'--serper-api-key',
|
|
336
|
+
help='Serper API key for enhanced web scraping and company research (optional)'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _add_team_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
340
|
+
"""Add team management arguments."""
|
|
341
|
+
team_subparsers = parser.add_subparsers(
|
|
342
|
+
dest='team_action', help='Team actions')
|
|
343
|
+
|
|
344
|
+
# Team create
|
|
345
|
+
create_parser = team_subparsers.add_parser(
|
|
346
|
+
'create', help='Create a new team')
|
|
347
|
+
create_parser.add_argument('--name', required=True, help='Team name')
|
|
348
|
+
create_parser.add_argument('--description', help='Team description')
|
|
349
|
+
create_parser.add_argument(
|
|
350
|
+
'--org-id', required=True, help='Organization ID')
|
|
351
|
+
create_parser.add_argument(
|
|
352
|
+
'--org-name', required=True, help='Organization name')
|
|
353
|
+
create_parser.add_argument('--plan-id', required=True, help='Plan ID')
|
|
354
|
+
create_parser.add_argument('--plan-name', help='Plan name')
|
|
355
|
+
create_parser.add_argument('--project-code', help='Project code')
|
|
356
|
+
create_parser.add_argument('--avatar', help='Avatar URL')
|
|
357
|
+
|
|
358
|
+
# Team update
|
|
359
|
+
update_parser = team_subparsers.add_parser(
|
|
360
|
+
'update', help='Update an existing team')
|
|
361
|
+
update_parser.add_argument('team_id', help='Team ID to update')
|
|
362
|
+
update_parser.add_argument('--name', help='New team name')
|
|
363
|
+
update_parser.add_argument(
|
|
364
|
+
'--description', help='New team description')
|
|
365
|
+
update_parser.add_argument('--plan-name', help='New plan name')
|
|
366
|
+
update_parser.add_argument('--project-code', help='New project code')
|
|
367
|
+
update_parser.add_argument('--avatar', help='New avatar URL')
|
|
368
|
+
|
|
369
|
+
# Team list
|
|
370
|
+
list_parser = team_subparsers.add_parser('list', help='List teams')
|
|
371
|
+
list_parser.add_argument(
|
|
372
|
+
'--org-id', required=True, help='Organization ID')
|
|
373
|
+
|
|
374
|
+
# Team describe
|
|
375
|
+
describe_parser = team_subparsers.add_parser(
|
|
376
|
+
'describe', help='Show team details')
|
|
377
|
+
describe_parser.add_argument('team_id', help='Team ID to describe')
|
|
378
|
+
|
|
379
|
+
def _add_product_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
380
|
+
"""Add product management arguments."""
|
|
381
|
+
product_subparsers = parser.add_subparsers(
|
|
382
|
+
dest='product_action', help='Product actions')
|
|
383
|
+
|
|
384
|
+
# Product create
|
|
385
|
+
create_parser = product_subparsers.add_parser(
|
|
386
|
+
'create', help='Create a new product')
|
|
387
|
+
create_parser.add_argument(
|
|
388
|
+
'--name', required=True, help='Product name')
|
|
389
|
+
create_parser.add_argument('--description', help='Product description')
|
|
390
|
+
create_parser.add_argument(
|
|
391
|
+
'--org-id', required=True, help='Organization ID')
|
|
392
|
+
create_parser.add_argument(
|
|
393
|
+
'--org-name', required=True, help='Organization name')
|
|
394
|
+
create_parser.add_argument(
|
|
395
|
+
'--product-data', help='Additional product data as JSON')
|
|
396
|
+
create_parser.add_argument('--category', help='Product category')
|
|
397
|
+
create_parser.add_argument('--subcategory', help='Product subcategory')
|
|
398
|
+
create_parser.add_argument('--project-code', help='Project code')
|
|
399
|
+
|
|
400
|
+
# Product update
|
|
401
|
+
update_parser = product_subparsers.add_parser(
|
|
402
|
+
'update', help='Update an existing product')
|
|
403
|
+
update_parser.add_argument('product_id', help='Product ID to update')
|
|
404
|
+
update_parser.add_argument('--name', help='New product name')
|
|
405
|
+
update_parser.add_argument(
|
|
406
|
+
'--description', help='New product description')
|
|
407
|
+
update_parser.add_argument(
|
|
408
|
+
'--product-data', help='Updated product data as JSON')
|
|
409
|
+
update_parser.add_argument('--category', help='New product category')
|
|
410
|
+
update_parser.add_argument(
|
|
411
|
+
'--subcategory', help='New product subcategory')
|
|
412
|
+
|
|
413
|
+
# Product list
|
|
414
|
+
list_parser = product_subparsers.add_parser(
|
|
415
|
+
'list', help='List products')
|
|
416
|
+
list_parser.add_argument(
|
|
417
|
+
'--org-id', required=True, help='Organization ID')
|
|
418
|
+
|
|
419
|
+
def _add_settings_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
420
|
+
"""Add settings management arguments."""
|
|
421
|
+
settings_subparsers = parser.add_subparsers(
|
|
422
|
+
dest='settings_action', help='Settings actions')
|
|
423
|
+
|
|
424
|
+
# Settings set (for simple settings)
|
|
425
|
+
set_parser = settings_subparsers.add_parser(
|
|
426
|
+
'set', help='Set team setting (for simple settings)')
|
|
427
|
+
set_parser.add_argument('team_id', help='Team ID')
|
|
428
|
+
set_parser.add_argument('--setting-name', required=True,
|
|
429
|
+
choices=['gs_team_organization', 'gs_team_rep', 'gs_team_product',
|
|
430
|
+
'gs_team_schedule_time', 'gs_team_auto_interaction',
|
|
431
|
+
'gs_team_followup_schedule_time', 'gs_team_birthday_email'],
|
|
432
|
+
help='Setting name to update')
|
|
433
|
+
set_parser.add_argument(
|
|
434
|
+
'--value-json', required=True, help='Setting value as JSON')
|
|
435
|
+
|
|
436
|
+
# Settings configure (for complex settings like initial_outreach and follow_up)
|
|
437
|
+
configure_parser = settings_subparsers.add_parser(
|
|
438
|
+
'configure', help='Configure complex team settings')
|
|
439
|
+
configure_parser.add_argument('team_id', help='Team ID')
|
|
440
|
+
configure_parser.add_argument('--setting-type', required=True,
|
|
441
|
+
choices=[
|
|
442
|
+
'initial_outreach', 'follow_up'],
|
|
443
|
+
help='Type of setting to configure')
|
|
444
|
+
configure_parser.add_argument('--user-input', required=True,
|
|
445
|
+
help='User instructions, prompt, or guidance for the setting')
|
|
446
|
+
configure_parser.add_argument('--examples-files', nargs='*',
|
|
447
|
+
help='Paths to example email files (optional)')
|
|
448
|
+
configure_parser.add_argument('--template-mode', choices=['ai_enhancement', 'strict_template'],
|
|
449
|
+
default='ai_enhancement',
|
|
450
|
+
help='Template processing mode (default: ai_enhancement)')
|
|
451
|
+
|
|
452
|
+
# Settings view
|
|
453
|
+
view_parser = settings_subparsers.add_parser(
|
|
454
|
+
'view', help='View team setting')
|
|
455
|
+
view_parser.add_argument('team_id', help='Team ID')
|
|
456
|
+
view_parser.add_argument('--setting-name', required=True,
|
|
457
|
+
choices=['gs_team_organization', 'gs_team_rep', 'gs_team_product',
|
|
458
|
+
'gs_team_schedule_time', 'gs_team_initial_outreach', 'gs_team_follow_up',
|
|
459
|
+
'gs_team_auto_interaction', 'gs_team_followup_schedule_time', 'gs_team_birthday_email'],
|
|
460
|
+
help='Setting name to view')
|
|
461
|
+
|
|
462
|
+
# Birthday email management
|
|
463
|
+
birthday_parser = settings_subparsers.add_parser(
|
|
464
|
+
'birthday', help='Manage birthday email settings and templates')
|
|
465
|
+
birthday_subparsers = birthday_parser.add_subparsers(
|
|
466
|
+
dest='birthday_action', help='Birthday email actions')
|
|
467
|
+
|
|
468
|
+
# Birthday configure
|
|
469
|
+
birthday_configure_parser = birthday_subparsers.add_parser(
|
|
470
|
+
'configure', help='Configure birthday email settings')
|
|
471
|
+
birthday_configure_parser.add_argument('team_id', help='Team ID')
|
|
472
|
+
birthday_configure_parser.add_argument('--prompt', required=True,
|
|
473
|
+
help='Birthday email configuration prompt')
|
|
474
|
+
birthday_configure_parser.add_argument('--org-id', required=True,
|
|
475
|
+
help='Organization ID')
|
|
476
|
+
|
|
477
|
+
# Birthday template list
|
|
478
|
+
birthday_list_parser = birthday_subparsers.add_parser(
|
|
479
|
+
'list-templates', help='List birthday email templates')
|
|
480
|
+
birthday_list_parser.add_argument('--team-id', help='Filter by team ID')
|
|
481
|
+
birthday_list_parser.add_argument('--org-id', help='Filter by organization ID')
|
|
482
|
+
|
|
483
|
+
# Birthday template view
|
|
484
|
+
birthday_view_parser = birthday_subparsers.add_parser(
|
|
485
|
+
'view-template', help='View birthday email template')
|
|
486
|
+
birthday_view_parser.add_argument('template_id', help='Template ID to view')
|
|
487
|
+
|
|
488
|
+
def parse_args(self, args: Optional[list] = None) -> argparse.Namespace:
|
|
489
|
+
"""
|
|
490
|
+
Parse command-line arguments.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
args: Optional list of arguments (uses sys.argv if None)
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Parsed arguments namespace
|
|
497
|
+
"""
|
|
498
|
+
return self.parser.parse_args(args)
|
|
499
|
+
|
|
500
|
+
def validate_args(self, args: argparse.Namespace) -> bool:
|
|
501
|
+
"""
|
|
502
|
+
Validate parsed arguments.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
args: Parsed arguments
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
True if arguments are valid, False otherwise
|
|
509
|
+
"""
|
|
510
|
+
validator = InputValidator()
|
|
511
|
+
|
|
512
|
+
# Validate required fields
|
|
513
|
+
if not validator.validate_api_key(args.openai_api_key):
|
|
514
|
+
print("Error: Invalid OpenAI API key format", file=sys.stderr)
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
# Different validation for new vs continuation processes
|
|
518
|
+
if args.continue_execution:
|
|
519
|
+
# Continuation mode - validate continuation parameters
|
|
520
|
+
if not args.action:
|
|
521
|
+
print(
|
|
522
|
+
"Error: --action is required when continuing an execution", file=sys.stderr)
|
|
523
|
+
print(
|
|
524
|
+
"Available actions: draft_write, draft_rewrite, send, close", file=sys.stderr)
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
if args.action in ['draft_rewrite', 'send'] and not args.selected_draft_id:
|
|
528
|
+
print(
|
|
529
|
+
f"Error: --selected-draft-id is required for action '{args.action}'", file=sys.stderr)
|
|
530
|
+
return False
|
|
531
|
+
|
|
532
|
+
if args.action == 'send' and not args.recipient_address:
|
|
533
|
+
print(
|
|
534
|
+
f"Error: --recipient-address is required for action '{args.action}'", file=sys.stderr)
|
|
535
|
+
return False
|
|
536
|
+
else:
|
|
537
|
+
# New process mode - validate data sources (matching executor schema)
|
|
538
|
+
data_sources = [
|
|
539
|
+
args.input_website,
|
|
540
|
+
args.input_description,
|
|
541
|
+
args.input_business_card,
|
|
542
|
+
args.input_linkedin_url,
|
|
543
|
+
args.input_facebook_url,
|
|
544
|
+
args.input_freetext
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
if not any(data_sources):
|
|
548
|
+
print(
|
|
549
|
+
"Error: At least one data source is required for new processes:", file=sys.stderr)
|
|
550
|
+
print(" - Website URL (--input-website)", file=sys.stderr)
|
|
551
|
+
print(" - Customer description (--input-description)",
|
|
552
|
+
file=sys.stderr)
|
|
553
|
+
print(" - Business card URL (--input-business-card)",
|
|
554
|
+
file=sys.stderr)
|
|
555
|
+
print(" - LinkedIn URL (--input-linkedin-url)", file=sys.stderr)
|
|
556
|
+
print(" - Facebook URL (--input-facebook-url)", file=sys.stderr)
|
|
557
|
+
print(" - Free text input (--input-freetext)", file=sys.stderr)
|
|
558
|
+
print("", file=sys.stderr)
|
|
559
|
+
print(
|
|
560
|
+
"To continue an existing process, use --continue-execution with --action", file=sys.stderr)
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
# Validate URLs if provided
|
|
564
|
+
if args.input_website and not validator.validate_url(args.input_website):
|
|
565
|
+
print(
|
|
566
|
+
f"Error: Invalid website URL: {args.input_website}", file=sys.stderr)
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
if args.input_business_card and not validator.validate_url(args.input_business_card):
|
|
570
|
+
print(
|
|
571
|
+
f"Error: Invalid business card URL: {args.input_business_card}", file=sys.stderr)
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
if args.input_linkedin_url and not validator.validate_url(args.input_linkedin_url):
|
|
575
|
+
print(
|
|
576
|
+
f"Error: Invalid LinkedIn URL: {args.input_linkedin_url}", file=sys.stderr)
|
|
577
|
+
return False
|
|
578
|
+
|
|
579
|
+
if args.input_facebook_url and not validator.validate_url(args.input_facebook_url):
|
|
580
|
+
print(
|
|
581
|
+
f"Error: Invalid Facebook URL: {args.input_facebook_url}", file=sys.stderr)
|
|
582
|
+
return False
|
|
583
|
+
|
|
584
|
+
# Validate recipient email if provided
|
|
585
|
+
if hasattr(args, 'recipient_address') and args.recipient_address and not validator.validate_email(args.recipient_address):
|
|
586
|
+
print(
|
|
587
|
+
f"Error: Invalid recipient email: {args.recipient_address}", file=sys.stderr)
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
# Validate temperature range
|
|
591
|
+
if not (0.0 <= args.temperature <= 2.0):
|
|
592
|
+
print(
|
|
593
|
+
f"Error: Temperature must be between 0.0 and 2.0, got: {args.temperature}", file=sys.stderr)
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
def create_config(self, args: argparse.Namespace) -> Dict[str, Any]:
|
|
599
|
+
"""
|
|
600
|
+
Create configuration dictionary from parsed arguments.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
args: Parsed arguments
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Configuration dictionary
|
|
607
|
+
"""
|
|
608
|
+
return build_pipeline_config(args)
|
|
609
|
+
|
|
610
|
+
def setup_logging(self, config: Dict[str, Any]) -> None:
|
|
611
|
+
"""
|
|
612
|
+
Set up logging based on configuration.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
config: Configuration dictionary
|
|
616
|
+
"""
|
|
617
|
+
self.logger = configure_pipeline_logging(config)
|
|
618
|
+
|
|
619
|
+
def validate_configuration(self, config: Dict[str, Any]) -> tuple[bool, list]:
|
|
620
|
+
"""
|
|
621
|
+
Comprehensive configuration validation.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
config: Configuration dictionary to validate
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Tuple of (is_valid, list_of_errors)
|
|
628
|
+
"""
|
|
629
|
+
return validate_pipeline_config(config)
|
|
630
|
+
|
|
631
|
+
def setup_output_directories(self, config: Dict[str, Any]) -> None:
|
|
632
|
+
"""
|
|
633
|
+
Set up output directories based on configuration.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
config: Configuration dictionary
|
|
637
|
+
"""
|
|
638
|
+
try:
|
|
639
|
+
data_dir = prepare_data_directory(config)
|
|
640
|
+
print(f"Output directories set up in: {data_dir}")
|
|
641
|
+
except Exception as e:
|
|
642
|
+
print(
|
|
643
|
+
f"Failed to set up output directories: {str(e)}", file=sys.stderr)
|
|
644
|
+
raise
|
|
645
|
+
|
|
646
|
+
def print_execution_plan(self, config: Dict[str, Any]) -> None:
|
|
647
|
+
"""
|
|
648
|
+
Print execution plan for dry run.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
config: Configuration dictionary
|
|
652
|
+
"""
|
|
653
|
+
print("FuseSell Execution Plan")
|
|
654
|
+
print("=" * 50)
|
|
655
|
+
print(f"Execution ID: {config['execution_id']}")
|
|
656
|
+
print(f"Organization: {config['org_name']} ({config['org_id']})")
|
|
657
|
+
|
|
658
|
+
# Display data sources
|
|
659
|
+
data_sources = []
|
|
660
|
+
if config.get('input_website'):
|
|
661
|
+
data_sources.append(f"Website: {config['input_website']}")
|
|
662
|
+
if config.get('input_description'):
|
|
663
|
+
data_sources.append(
|
|
664
|
+
f"Description: {config['input_description'][:50]}...")
|
|
665
|
+
if config.get('input_business_card'):
|
|
666
|
+
data_sources.append(
|
|
667
|
+
f"Business Card: {config['input_business_card']}")
|
|
668
|
+
if config.get('input_linkedin_url'):
|
|
669
|
+
data_sources.append(f"LinkedIn: {config['input_linkedin_url']}")
|
|
670
|
+
if config.get('input_facebook_url'):
|
|
671
|
+
data_sources.append(f"Facebook: {config['input_facebook_url']}")
|
|
672
|
+
|
|
673
|
+
print(
|
|
674
|
+
f"Data Sources: {'; '.join(data_sources) if data_sources else 'None'}")
|
|
675
|
+
print(f"Language: {config['language']}")
|
|
676
|
+
print(f"Data Directory: {config['data_dir']}")
|
|
677
|
+
print(f"Output Format: {config['output_format']}")
|
|
678
|
+
|
|
679
|
+
if config['team_id']:
|
|
680
|
+
print(f"Team: {config['team_name']} ({config['team_id']})")
|
|
681
|
+
|
|
682
|
+
print("\nPipeline Stages:")
|
|
683
|
+
stages = ['data_acquisition', 'data_preparation',
|
|
684
|
+
'lead_scoring', 'initial_outreach', 'follow_up']
|
|
685
|
+
skip_stages = config.get('skip_stages', [])
|
|
686
|
+
stop_after = config.get('stop_after')
|
|
687
|
+
|
|
688
|
+
for i, stage in enumerate(stages, 1):
|
|
689
|
+
status = "SKIP" if stage in skip_stages else "RUN"
|
|
690
|
+
print(f" {i}. {stage.replace('_', ' ').title()}: {status}")
|
|
691
|
+
|
|
692
|
+
if stop_after == stage:
|
|
693
|
+
print(f" Pipeline will stop after {stage}")
|
|
694
|
+
break
|
|
695
|
+
|
|
696
|
+
print(f"\nConfiguration saved to: {config['data_dir']}/config/")
|
|
697
|
+
print("Run without --dry-run to execute the pipeline.")
|
|
698
|
+
|
|
699
|
+
def format_output(self, results: Dict[str, Any], format_type: str) -> str:
|
|
700
|
+
"""
|
|
701
|
+
Format execution results for output.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
results: Execution results
|
|
705
|
+
format_type: Output format (json, text, yaml)
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Formatted output string
|
|
709
|
+
"""
|
|
710
|
+
if format_type == 'json':
|
|
711
|
+
return json.dumps(results, indent=2, default=str)
|
|
712
|
+
|
|
713
|
+
elif format_type == 'yaml':
|
|
714
|
+
try:
|
|
715
|
+
import yaml
|
|
716
|
+
return yaml.dump(results, default_flow_style=False)
|
|
717
|
+
except ImportError:
|
|
718
|
+
self.logger.warning(
|
|
719
|
+
"PyYAML not installed, falling back to JSON")
|
|
720
|
+
return json.dumps(results, indent=2, default=str)
|
|
721
|
+
|
|
722
|
+
elif format_type == 'text':
|
|
723
|
+
return self._format_text_output(results)
|
|
724
|
+
|
|
725
|
+
else:
|
|
726
|
+
return json.dumps(results, indent=2, default=str)
|
|
727
|
+
|
|
728
|
+
def _format_text_output(self, results: Dict[str, Any]) -> str:
|
|
729
|
+
"""
|
|
730
|
+
Format results as human-readable text.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
results: Execution results
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
Formatted text output
|
|
737
|
+
"""
|
|
738
|
+
output = []
|
|
739
|
+
output.append("FuseSell Execution Results")
|
|
740
|
+
output.append("=" * 50)
|
|
741
|
+
|
|
742
|
+
# Basic info
|
|
743
|
+
output.append(f"Execution ID: {results.get('execution_id', 'N/A')}")
|
|
744
|
+
output.append(f"Status: {results.get('status', 'N/A')}")
|
|
745
|
+
output.append(f"Started: {results.get('started_at', 'N/A')}")
|
|
746
|
+
output.append(f"Completed: {results.get('completed_at', 'N/A')}")
|
|
747
|
+
output.append("")
|
|
748
|
+
|
|
749
|
+
# Stage results
|
|
750
|
+
stage_results = results.get('stage_results', {})
|
|
751
|
+
if stage_results:
|
|
752
|
+
output.append("Stage Results:")
|
|
753
|
+
output.append("-" * 20)
|
|
754
|
+
for stage, result in stage_results.items():
|
|
755
|
+
status = result.get('status', 'unknown')
|
|
756
|
+
output.append(
|
|
757
|
+
f"{stage.replace('_', ' ').title()}: {status.upper()}")
|
|
758
|
+
|
|
759
|
+
if status == 'error' and result.get('error_message'):
|
|
760
|
+
output.append(f" Error: {result['error_message']}")
|
|
761
|
+
|
|
762
|
+
# Customer info
|
|
763
|
+
customer_data = results.get('customer_data', {})
|
|
764
|
+
if customer_data:
|
|
765
|
+
output.append("\nCustomer Information:")
|
|
766
|
+
output.append("-" * 20)
|
|
767
|
+
output.append(
|
|
768
|
+
f"Company: {customer_data.get('company_name', 'N/A')}")
|
|
769
|
+
output.append(f"Industry: {customer_data.get('industry', 'N/A')}")
|
|
770
|
+
output.append(f"Website: {customer_data.get('website', 'N/A')}")
|
|
771
|
+
|
|
772
|
+
# Lead scores
|
|
773
|
+
lead_scores = results.get('lead_scores', [])
|
|
774
|
+
if lead_scores:
|
|
775
|
+
output.append("\nLead Scores:")
|
|
776
|
+
output.append("-" * 20)
|
|
777
|
+
for score in lead_scores:
|
|
778
|
+
output.append(
|
|
779
|
+
f"Product {score.get('product_id', 'N/A')}: {score.get('score', 0)}/100")
|
|
780
|
+
|
|
781
|
+
# Email drafts
|
|
782
|
+
email_drafts = results.get('email_drafts', [])
|
|
783
|
+
if email_drafts:
|
|
784
|
+
output.append("\nEmail Drafts Generated:")
|
|
785
|
+
output.append("-" * 20)
|
|
786
|
+
for draft in email_drafts:
|
|
787
|
+
output.append(f"Subject: {draft.get('subject', 'N/A')}")
|
|
788
|
+
output.append(f"Type: {draft.get('draft_type', 'N/A')}")
|
|
789
|
+
|
|
790
|
+
return "\n".join(output)
|
|
791
|
+
|
|
792
|
+
def run(self, args: Optional[list] = None) -> int:
|
|
793
|
+
"""
|
|
794
|
+
Main execution method.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
args: Optional command-line arguments
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
Exit code (0 for success, 1 for error)
|
|
801
|
+
"""
|
|
802
|
+
try:
|
|
803
|
+
# Parse arguments
|
|
804
|
+
parsed_args = self.parse_args(args)
|
|
805
|
+
|
|
806
|
+
# Handle different commands
|
|
807
|
+
command = getattr(parsed_args, 'command', None)
|
|
808
|
+
|
|
809
|
+
# If no command specified, default to pipeline for backward compatibility
|
|
810
|
+
if command is None:
|
|
811
|
+
# For backward compatibility, treat no subcommand as pipeline
|
|
812
|
+
return self._run_pipeline(parsed_args)
|
|
813
|
+
elif command == 'pipeline':
|
|
814
|
+
return self._run_pipeline(parsed_args)
|
|
815
|
+
elif command == 'team':
|
|
816
|
+
return self._run_team_command(parsed_args)
|
|
817
|
+
elif command == 'product':
|
|
818
|
+
return self._run_product_command(parsed_args)
|
|
819
|
+
elif command == 'settings':
|
|
820
|
+
return self._run_settings_command(parsed_args)
|
|
821
|
+
else:
|
|
822
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
823
|
+
return 1
|
|
824
|
+
|
|
825
|
+
except KeyboardInterrupt:
|
|
826
|
+
print("\nExecution interrupted by user", file=sys.stderr)
|
|
827
|
+
return 1
|
|
828
|
+
except Exception as e:
|
|
829
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
830
|
+
return 1
|
|
831
|
+
|
|
832
|
+
def _run_pipeline(self, args: argparse.Namespace) -> int:
|
|
833
|
+
"""Run the sales automation pipeline."""
|
|
834
|
+
try:
|
|
835
|
+
# Validate required pipeline arguments
|
|
836
|
+
if not args.openai_api_key:
|
|
837
|
+
print(
|
|
838
|
+
"Error: --openai-api-key is required for pipeline execution", file=sys.stderr)
|
|
839
|
+
return 1
|
|
840
|
+
if not args.org_id:
|
|
841
|
+
print("Error: --org-id is required for pipeline execution",
|
|
842
|
+
file=sys.stderr)
|
|
843
|
+
return 1
|
|
844
|
+
if not args.org_name:
|
|
845
|
+
print(
|
|
846
|
+
"Error: --org-name is required for pipeline execution", file=sys.stderr)
|
|
847
|
+
return 1
|
|
848
|
+
if not args.full_input:
|
|
849
|
+
print(
|
|
850
|
+
"Error: --full-input is required for pipeline execution", file=sys.stderr)
|
|
851
|
+
return 1
|
|
852
|
+
|
|
853
|
+
if not self.validate_args(args):
|
|
854
|
+
return 1
|
|
855
|
+
|
|
856
|
+
# Create configuration
|
|
857
|
+
config = self.create_config(args)
|
|
858
|
+
|
|
859
|
+
# Set up output directories first
|
|
860
|
+
self.setup_output_directories(config)
|
|
861
|
+
|
|
862
|
+
# Set up logging (after log file path is configured)
|
|
863
|
+
self.setup_logging(config)
|
|
864
|
+
|
|
865
|
+
# Validate configuration
|
|
866
|
+
config_valid, config_errors = self.validate_configuration(config)
|
|
867
|
+
if not config_valid:
|
|
868
|
+
print("Configuration validation failed:", file=sys.stderr)
|
|
869
|
+
for error in config_errors:
|
|
870
|
+
print(f" - {error}", file=sys.stderr)
|
|
871
|
+
return 1
|
|
872
|
+
|
|
873
|
+
self.logger.info(
|
|
874
|
+
f"Starting FuseSell execution: {config['execution_id']}")
|
|
875
|
+
|
|
876
|
+
# Handle dry run
|
|
877
|
+
if config['dry_run']:
|
|
878
|
+
self.print_execution_plan(config)
|
|
879
|
+
return 0
|
|
880
|
+
|
|
881
|
+
# Initialize and run pipeline
|
|
882
|
+
pipeline = FuseSellPipeline(config)
|
|
883
|
+
results = pipeline.execute()
|
|
884
|
+
|
|
885
|
+
# Format and output results
|
|
886
|
+
formatted_output = self.format_output(
|
|
887
|
+
results, config['output_format'])
|
|
888
|
+
print(formatted_output)
|
|
889
|
+
|
|
890
|
+
# Return appropriate exit code
|
|
891
|
+
if results.get('status') == 'completed':
|
|
892
|
+
self.logger.info("FuseSell execution completed successfully")
|
|
893
|
+
return 0
|
|
894
|
+
else:
|
|
895
|
+
self.logger.error("FuseSell execution failed")
|
|
896
|
+
return 1
|
|
897
|
+
|
|
898
|
+
except Exception as e:
|
|
899
|
+
print(f"Pipeline error: {str(e)}", file=sys.stderr)
|
|
900
|
+
return 1
|
|
901
|
+
|
|
902
|
+
def _run_team_command(self, args: argparse.Namespace) -> int:
|
|
903
|
+
"""Handle team management commands."""
|
|
904
|
+
from .utils.data_manager import LocalDataManager
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
data_manager = LocalDataManager()
|
|
908
|
+
action = getattr(args, 'team_action', None)
|
|
909
|
+
|
|
910
|
+
if action == 'create':
|
|
911
|
+
team_id = f"team_{args.org_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
912
|
+
data_manager.save_team(
|
|
913
|
+
team_id=team_id,
|
|
914
|
+
org_id=args.org_id,
|
|
915
|
+
org_name=args.org_name,
|
|
916
|
+
plan_id=args.plan_id,
|
|
917
|
+
name=args.name,
|
|
918
|
+
description=args.description,
|
|
919
|
+
plan_name=getattr(args, 'plan_name', None),
|
|
920
|
+
project_code=getattr(args, 'project_code', None),
|
|
921
|
+
avatar=getattr(args, 'avatar', None)
|
|
922
|
+
)
|
|
923
|
+
print(f"Team created successfully: {team_id}")
|
|
924
|
+
return 0
|
|
925
|
+
|
|
926
|
+
elif action == 'update':
|
|
927
|
+
success = data_manager.update_team(
|
|
928
|
+
team_id=args.team_id,
|
|
929
|
+
name=getattr(args, 'name', None),
|
|
930
|
+
description=getattr(args, 'description', None),
|
|
931
|
+
plan_name=getattr(args, 'plan_name', None),
|
|
932
|
+
project_code=getattr(args, 'project_code', None),
|
|
933
|
+
avatar=getattr(args, 'avatar', None)
|
|
934
|
+
)
|
|
935
|
+
if success:
|
|
936
|
+
print(f"Team updated successfully: {args.team_id}")
|
|
937
|
+
return 0
|
|
938
|
+
else:
|
|
939
|
+
print(f"Team not found: {args.team_id}", file=sys.stderr)
|
|
940
|
+
return 1
|
|
941
|
+
|
|
942
|
+
elif action == 'list':
|
|
943
|
+
teams = data_manager.list_teams(args.org_id)
|
|
944
|
+
if teams:
|
|
945
|
+
print(f"Teams for organization {args.org_id}:")
|
|
946
|
+
for team in teams:
|
|
947
|
+
print(
|
|
948
|
+
f" {team['team_id']}: {team['name']} - {team.get('description', 'No description')}")
|
|
949
|
+
else:
|
|
950
|
+
print(f"No teams found for organization {args.org_id}")
|
|
951
|
+
return 0
|
|
952
|
+
|
|
953
|
+
elif action == 'describe':
|
|
954
|
+
team = data_manager.get_team(args.team_id)
|
|
955
|
+
if team:
|
|
956
|
+
print(json.dumps(team, indent=2, default=str))
|
|
957
|
+
else:
|
|
958
|
+
print(f"Team not found: {args.team_id}", file=sys.stderr)
|
|
959
|
+
return 1
|
|
960
|
+
return 0
|
|
961
|
+
|
|
962
|
+
else:
|
|
963
|
+
print(f"Unknown team action: {action}", file=sys.stderr)
|
|
964
|
+
return 1
|
|
965
|
+
|
|
966
|
+
except Exception as e:
|
|
967
|
+
print(f"Team command error: {str(e)}", file=sys.stderr)
|
|
968
|
+
return 1
|
|
969
|
+
|
|
970
|
+
def _run_product_command(self, args: argparse.Namespace) -> int:
|
|
971
|
+
"""Handle product management commands."""
|
|
972
|
+
from .utils.data_manager import LocalDataManager
|
|
973
|
+
|
|
974
|
+
try:
|
|
975
|
+
data_manager = LocalDataManager()
|
|
976
|
+
action = getattr(args, 'product_action', None)
|
|
977
|
+
|
|
978
|
+
if action == 'create':
|
|
979
|
+
product_data = {
|
|
980
|
+
'org_id': args.org_id,
|
|
981
|
+
'org_name': args.org_name,
|
|
982
|
+
'productName': args.name,
|
|
983
|
+
'shortDescription': getattr(args, 'description', None),
|
|
984
|
+
'category': getattr(args, 'category', None),
|
|
985
|
+
'subcategory': getattr(args, 'subcategory', None),
|
|
986
|
+
'project_code': getattr(args, 'project_code', None)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
# Parse additional product data if provided
|
|
990
|
+
if hasattr(args, 'product_data') and args.product_data:
|
|
991
|
+
try:
|
|
992
|
+
additional_data = json.loads(args.product_data)
|
|
993
|
+
product_data.update(additional_data)
|
|
994
|
+
except json.JSONDecodeError:
|
|
995
|
+
print("Invalid JSON in --product-data", file=sys.stderr)
|
|
996
|
+
return 1
|
|
997
|
+
|
|
998
|
+
product_id = data_manager.save_product(product_data)
|
|
999
|
+
print(f"Product created successfully: {product_id}")
|
|
1000
|
+
return 0
|
|
1001
|
+
|
|
1002
|
+
elif action == 'update':
|
|
1003
|
+
product_data = {}
|
|
1004
|
+
if hasattr(args, 'name') and args.name:
|
|
1005
|
+
product_data['productName'] = args.name
|
|
1006
|
+
if hasattr(args, 'description') and args.description:
|
|
1007
|
+
product_data['shortDescription'] = args.description
|
|
1008
|
+
if hasattr(args, 'category') and args.category:
|
|
1009
|
+
product_data['category'] = args.category
|
|
1010
|
+
if hasattr(args, 'subcategory') and args.subcategory:
|
|
1011
|
+
product_data['subcategory'] = args.subcategory
|
|
1012
|
+
|
|
1013
|
+
# Parse additional product data if provided
|
|
1014
|
+
if hasattr(args, 'product_data') and args.product_data:
|
|
1015
|
+
try:
|
|
1016
|
+
additional_data = json.loads(args.product_data)
|
|
1017
|
+
product_data.update(additional_data)
|
|
1018
|
+
except json.JSONDecodeError:
|
|
1019
|
+
print("Invalid JSON in --product-data", file=sys.stderr)
|
|
1020
|
+
return 1
|
|
1021
|
+
|
|
1022
|
+
success = data_manager.update_product(
|
|
1023
|
+
args.product_id, product_data)
|
|
1024
|
+
if success:
|
|
1025
|
+
print(f"Product updated successfully: {args.product_id}")
|
|
1026
|
+
return 0
|
|
1027
|
+
else:
|
|
1028
|
+
print(
|
|
1029
|
+
f"Product not found: {args.product_id}", file=sys.stderr)
|
|
1030
|
+
return 1
|
|
1031
|
+
|
|
1032
|
+
elif action == 'list':
|
|
1033
|
+
products = data_manager.get_products_by_org(args.org_id)
|
|
1034
|
+
if products:
|
|
1035
|
+
print(f"Products for organization {args.org_id}:")
|
|
1036
|
+
for product in products:
|
|
1037
|
+
print(
|
|
1038
|
+
f" {product['product_id']}: {product['product_name']} - {product.get('short_description', 'No description')}")
|
|
1039
|
+
else:
|
|
1040
|
+
print(f"No products found for organization {args.org_id}")
|
|
1041
|
+
return 0
|
|
1042
|
+
|
|
1043
|
+
else:
|
|
1044
|
+
print(f"Unknown product action: {action}", file=sys.stderr)
|
|
1045
|
+
return 1
|
|
1046
|
+
|
|
1047
|
+
except Exception as e:
|
|
1048
|
+
print(f"Product command error: {str(e)}", file=sys.stderr)
|
|
1049
|
+
return 1
|
|
1050
|
+
|
|
1051
|
+
def _run_settings_command(self, args: argparse.Namespace) -> int:
|
|
1052
|
+
"""Handle settings management commands."""
|
|
1053
|
+
from .utils.data_manager import LocalDataManager
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
data_manager = LocalDataManager()
|
|
1057
|
+
action = getattr(args, 'settings_action', None)
|
|
1058
|
+
|
|
1059
|
+
if action == 'set':
|
|
1060
|
+
# Parse the JSON value
|
|
1061
|
+
try:
|
|
1062
|
+
value = json.loads(args.value_json)
|
|
1063
|
+
except json.JSONDecodeError:
|
|
1064
|
+
print("Invalid JSON in --value-json", file=sys.stderr)
|
|
1065
|
+
return 1
|
|
1066
|
+
|
|
1067
|
+
# Validate auto interaction settings format
|
|
1068
|
+
if args.setting_name == 'gs_team_auto_interaction':
|
|
1069
|
+
validation_error = self._validate_auto_interaction_settings(value)
|
|
1070
|
+
if validation_error:
|
|
1071
|
+
print(f"Invalid auto interaction settings: {validation_error}", file=sys.stderr)
|
|
1072
|
+
return 1
|
|
1073
|
+
|
|
1074
|
+
# Get existing team settings or create new ones
|
|
1075
|
+
team_settings = data_manager.get_team_settings(args.team_id)
|
|
1076
|
+
if not team_settings:
|
|
1077
|
+
# Get team info to create settings
|
|
1078
|
+
team = data_manager.get_team(args.team_id)
|
|
1079
|
+
if not team:
|
|
1080
|
+
print(
|
|
1081
|
+
f"Team not found: {args.team_id}", file=sys.stderr)
|
|
1082
|
+
return 1
|
|
1083
|
+
|
|
1084
|
+
# Create new settings with all fields initialized
|
|
1085
|
+
settings_kwargs = {
|
|
1086
|
+
'team_id': args.team_id,
|
|
1087
|
+
'org_id': team['org_id'],
|
|
1088
|
+
'plan_id': team['plan_id'],
|
|
1089
|
+
'team_name': team['name'],
|
|
1090
|
+
'gs_team_organization': None,
|
|
1091
|
+
'gs_team_rep': None,
|
|
1092
|
+
'gs_team_product': None,
|
|
1093
|
+
'gs_team_schedule_time': None,
|
|
1094
|
+
'gs_team_initial_outreach': None,
|
|
1095
|
+
'gs_team_follow_up': None,
|
|
1096
|
+
'gs_team_auto_interaction': None,
|
|
1097
|
+
'gs_team_followup_schedule_time': None,
|
|
1098
|
+
'gs_team_birthday_email': None
|
|
1099
|
+
}
|
|
1100
|
+
# Set the specific setting being updated
|
|
1101
|
+
settings_kwargs[args.setting_name] = value
|
|
1102
|
+
else:
|
|
1103
|
+
# Update existing settings - preserve all existing values
|
|
1104
|
+
settings_kwargs = {
|
|
1105
|
+
'team_id': args.team_id,
|
|
1106
|
+
'org_id': team_settings['org_id'],
|
|
1107
|
+
'plan_id': team_settings['plan_id'],
|
|
1108
|
+
'team_name': team_settings.get('team_name', ''),
|
|
1109
|
+
'gs_team_organization': team_settings.get('gs_team_organization'),
|
|
1110
|
+
'gs_team_rep': team_settings.get('gs_team_rep'),
|
|
1111
|
+
'gs_team_product': team_settings.get('gs_team_product'),
|
|
1112
|
+
'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
|
|
1113
|
+
'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
|
|
1114
|
+
'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
|
|
1115
|
+
'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
|
|
1116
|
+
'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
|
|
1117
|
+
'gs_team_birthday_email': team_settings.get('gs_team_birthday_email')
|
|
1118
|
+
}
|
|
1119
|
+
# Update only the specific setting being changed
|
|
1120
|
+
settings_kwargs[args.setting_name] = value
|
|
1121
|
+
|
|
1122
|
+
data_manager.save_team_settings(**settings_kwargs)
|
|
1123
|
+
print(
|
|
1124
|
+
f"Setting '{args.setting_name}' updated for team {args.team_id}")
|
|
1125
|
+
return 0
|
|
1126
|
+
|
|
1127
|
+
elif action == 'configure':
|
|
1128
|
+
# Handle complex settings configuration
|
|
1129
|
+
return self._configure_complex_setting(args, data_manager)
|
|
1130
|
+
|
|
1131
|
+
elif action == 'view':
|
|
1132
|
+
team_settings = data_manager.get_team_settings(args.team_id)
|
|
1133
|
+
if team_settings and args.setting_name in team_settings:
|
|
1134
|
+
setting_value = team_settings[args.setting_name]
|
|
1135
|
+
print(json.dumps(setting_value, indent=2, default=str))
|
|
1136
|
+
else:
|
|
1137
|
+
print(
|
|
1138
|
+
f"Setting '{args.setting_name}' not found for team {args.team_id}", file=sys.stderr)
|
|
1139
|
+
return 1
|
|
1140
|
+
return 0
|
|
1141
|
+
|
|
1142
|
+
elif action == 'birthday':
|
|
1143
|
+
return self._run_birthday_email_command(args, data_manager)
|
|
1144
|
+
|
|
1145
|
+
else:
|
|
1146
|
+
print(f"Unknown settings action: {action}", file=sys.stderr)
|
|
1147
|
+
return 1
|
|
1148
|
+
|
|
1149
|
+
except Exception as e:
|
|
1150
|
+
print(f"Settings command error: {str(e)}", file=sys.stderr)
|
|
1151
|
+
return 1
|
|
1152
|
+
|
|
1153
|
+
def _configure_complex_setting(self, args: argparse.Namespace, data_manager) -> int:
|
|
1154
|
+
"""Configure complex settings like initial_outreach and follow_up."""
|
|
1155
|
+
try:
|
|
1156
|
+
# Determine if user input is a complete prompt or instructions
|
|
1157
|
+
user_input = args.user_input.strip()
|
|
1158
|
+
has_examples = bool(args.examples_files)
|
|
1159
|
+
|
|
1160
|
+
# Process according to the flowchart logic
|
|
1161
|
+
if not has_examples:
|
|
1162
|
+
# Case 1: No Examples
|
|
1163
|
+
setting_value = self._process_no_examples_case(
|
|
1164
|
+
user_input, args.setting_type)
|
|
1165
|
+
else:
|
|
1166
|
+
# Case 2 or 3: With Examples
|
|
1167
|
+
if args.template_mode == 'strict_template':
|
|
1168
|
+
# Case 3: Strict Template Mode
|
|
1169
|
+
setting_value = self._process_strict_template_case(
|
|
1170
|
+
user_input, args.examples_files, args.setting_type)
|
|
1171
|
+
else:
|
|
1172
|
+
# Case 2: AI Enhancement Mode
|
|
1173
|
+
setting_value = self._process_ai_enhancement_case(
|
|
1174
|
+
user_input, args.examples_files, args.setting_type)
|
|
1175
|
+
|
|
1176
|
+
# Get team info for settings update
|
|
1177
|
+
team = data_manager.get_team(args.team_id)
|
|
1178
|
+
if not team:
|
|
1179
|
+
print(f"Team not found: {args.team_id}", file=sys.stderr)
|
|
1180
|
+
return 1
|
|
1181
|
+
|
|
1182
|
+
# Get existing team settings or create new ones
|
|
1183
|
+
team_settings = data_manager.get_team_settings(args.team_id)
|
|
1184
|
+
|
|
1185
|
+
# Determine the setting field name
|
|
1186
|
+
setting_field = f"gs_team_{args.setting_type}"
|
|
1187
|
+
|
|
1188
|
+
if not team_settings:
|
|
1189
|
+
# Create new settings with all fields initialized
|
|
1190
|
+
settings_kwargs = {
|
|
1191
|
+
'team_id': args.team_id,
|
|
1192
|
+
'org_id': team['org_id'],
|
|
1193
|
+
'plan_id': team['plan_id'],
|
|
1194
|
+
'team_name': team['name'],
|
|
1195
|
+
'gs_team_organization': None,
|
|
1196
|
+
'gs_team_rep': None,
|
|
1197
|
+
'gs_team_product': None,
|
|
1198
|
+
'gs_team_schedule_time': None,
|
|
1199
|
+
'gs_team_initial_outreach': None,
|
|
1200
|
+
'gs_team_follow_up': None,
|
|
1201
|
+
'gs_team_auto_interaction': None,
|
|
1202
|
+
'gs_team_followup_schedule_time': None,
|
|
1203
|
+
'gs_team_birthday_email': None
|
|
1204
|
+
}
|
|
1205
|
+
# Set the specific setting being updated
|
|
1206
|
+
settings_kwargs[setting_field] = setting_value
|
|
1207
|
+
else:
|
|
1208
|
+
# Update existing settings - preserve all existing values
|
|
1209
|
+
settings_kwargs = {
|
|
1210
|
+
'team_id': args.team_id,
|
|
1211
|
+
'org_id': team_settings['org_id'],
|
|
1212
|
+
'plan_id': team_settings['plan_id'],
|
|
1213
|
+
'team_name': team_settings.get('team_name', ''),
|
|
1214
|
+
'gs_team_organization': team_settings.get('gs_team_organization'),
|
|
1215
|
+
'gs_team_rep': team_settings.get('gs_team_rep'),
|
|
1216
|
+
'gs_team_product': team_settings.get('gs_team_product'),
|
|
1217
|
+
'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
|
|
1218
|
+
'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
|
|
1219
|
+
'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
|
|
1220
|
+
'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
|
|
1221
|
+
'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
|
|
1222
|
+
'gs_team_birthday_email': team_settings.get('gs_team_birthday_email')
|
|
1223
|
+
}
|
|
1224
|
+
# Update only the specific setting being changed
|
|
1225
|
+
settings_kwargs[setting_field] = setting_value
|
|
1226
|
+
|
|
1227
|
+
data_manager.save_team_settings(**settings_kwargs)
|
|
1228
|
+
print(
|
|
1229
|
+
f"Complex setting '{args.setting_type}' configured for team {args.team_id}")
|
|
1230
|
+
print(
|
|
1231
|
+
f"Configuration: fewshots={setting_value.get('fewshots', False)}, strict_follow={setting_value.get('fewshots_strict_follow', False)}")
|
|
1232
|
+
return 0
|
|
1233
|
+
|
|
1234
|
+
except Exception as e:
|
|
1235
|
+
print(f"Configuration error: {str(e)}", file=sys.stderr)
|
|
1236
|
+
return 1
|
|
1237
|
+
|
|
1238
|
+
def _run_birthday_email_command(self, args: argparse.Namespace, data_manager) -> int:
|
|
1239
|
+
"""Handle birthday email management commands."""
|
|
1240
|
+
from .utils.birthday_email_manager import BirthdayEmailManager
|
|
1241
|
+
|
|
1242
|
+
try:
|
|
1243
|
+
birthday_manager = BirthdayEmailManager(self.create_config(args))
|
|
1244
|
+
birthday_action = getattr(args, 'birthday_action', None)
|
|
1245
|
+
|
|
1246
|
+
if birthday_action == 'configure':
|
|
1247
|
+
# Configure birthday email settings
|
|
1248
|
+
result = birthday_manager.process_birthday_email_settings(
|
|
1249
|
+
team_id=args.team_id,
|
|
1250
|
+
prompt=args.prompt,
|
|
1251
|
+
org_id=args.org_id
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
if result['success']:
|
|
1255
|
+
# Save the generated settings to team settings
|
|
1256
|
+
team = data_manager.get_team(args.team_id)
|
|
1257
|
+
if not team:
|
|
1258
|
+
print(f"Team not found: {args.team_id}", file=sys.stderr)
|
|
1259
|
+
return 1
|
|
1260
|
+
|
|
1261
|
+
# Get existing team settings
|
|
1262
|
+
team_settings = data_manager.get_team_settings(args.team_id)
|
|
1263
|
+
|
|
1264
|
+
if not team_settings:
|
|
1265
|
+
# Create new settings
|
|
1266
|
+
settings_kwargs = {
|
|
1267
|
+
'team_id': args.team_id,
|
|
1268
|
+
'org_id': team['org_id'],
|
|
1269
|
+
'plan_id': team['plan_id'],
|
|
1270
|
+
'team_name': team['name'],
|
|
1271
|
+
'gs_team_organization': None,
|
|
1272
|
+
'gs_team_rep': None,
|
|
1273
|
+
'gs_team_product': None,
|
|
1274
|
+
'gs_team_schedule_time': None,
|
|
1275
|
+
'gs_team_initial_outreach': None,
|
|
1276
|
+
'gs_team_follow_up': None,
|
|
1277
|
+
'gs_team_auto_interaction': None,
|
|
1278
|
+
'gs_team_followup_schedule_time': None,
|
|
1279
|
+
'gs_team_birthday_email': result['settings']
|
|
1280
|
+
}
|
|
1281
|
+
else:
|
|
1282
|
+
# Update existing settings
|
|
1283
|
+
settings_kwargs = {
|
|
1284
|
+
'team_id': args.team_id,
|
|
1285
|
+
'org_id': team_settings['org_id'],
|
|
1286
|
+
'plan_id': team_settings['plan_id'],
|
|
1287
|
+
'team_name': team_settings.get('team_name', ''),
|
|
1288
|
+
'gs_team_organization': team_settings.get('gs_team_organization'),
|
|
1289
|
+
'gs_team_rep': team_settings.get('gs_team_rep'),
|
|
1290
|
+
'gs_team_product': team_settings.get('gs_team_product'),
|
|
1291
|
+
'gs_team_schedule_time': team_settings.get('gs_team_schedule_time'),
|
|
1292
|
+
'gs_team_initial_outreach': team_settings.get('gs_team_initial_outreach'),
|
|
1293
|
+
'gs_team_follow_up': team_settings.get('gs_team_follow_up'),
|
|
1294
|
+
'gs_team_auto_interaction': team_settings.get('gs_team_auto_interaction'),
|
|
1295
|
+
'gs_team_followup_schedule_time': team_settings.get('gs_team_followup_schedule_time'),
|
|
1296
|
+
'gs_team_birthday_email': result['settings']
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
data_manager.save_team_settings(**settings_kwargs)
|
|
1300
|
+
|
|
1301
|
+
print(f"Birthday email settings configured for team {args.team_id}")
|
|
1302
|
+
print(f"Settings: {json.dumps(result['settings'], indent=2)}")
|
|
1303
|
+
|
|
1304
|
+
if result.get('template'):
|
|
1305
|
+
print(f"Template generated: {result['template']['template_id']}")
|
|
1306
|
+
|
|
1307
|
+
return 0
|
|
1308
|
+
else:
|
|
1309
|
+
print(f"Birthday email configuration failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
|
|
1310
|
+
return 1
|
|
1311
|
+
|
|
1312
|
+
elif birthday_action == 'list-templates':
|
|
1313
|
+
templates = birthday_manager.list_birthday_templates(
|
|
1314
|
+
team_id=args.team_id,
|
|
1315
|
+
org_id=args.org_id
|
|
1316
|
+
)
|
|
1317
|
+
|
|
1318
|
+
if templates:
|
|
1319
|
+
print("Birthday Email Templates:")
|
|
1320
|
+
for template in templates:
|
|
1321
|
+
print(f" {template['template_id']}: {template.get('subject', 'No subject')} "
|
|
1322
|
+
f"(Team: {template['team_id']}, Created: {template['created_at']})")
|
|
1323
|
+
else:
|
|
1324
|
+
print("No birthday email templates found.")
|
|
1325
|
+
return 0
|
|
1326
|
+
|
|
1327
|
+
elif birthday_action == 'view-template':
|
|
1328
|
+
template = birthday_manager.get_birthday_template(args.template_id)
|
|
1329
|
+
|
|
1330
|
+
if template:
|
|
1331
|
+
print(json.dumps(template, indent=2, default=str))
|
|
1332
|
+
else:
|
|
1333
|
+
print(f"Template not found: {args.template_id}", file=sys.stderr)
|
|
1334
|
+
return 1
|
|
1335
|
+
return 0
|
|
1336
|
+
|
|
1337
|
+
else:
|
|
1338
|
+
print(f"Unknown birthday email action: {birthday_action}", file=sys.stderr)
|
|
1339
|
+
return 1
|
|
1340
|
+
|
|
1341
|
+
except Exception as e:
|
|
1342
|
+
print(f"Birthday email command error: {str(e)}", file=sys.stderr)
|
|
1343
|
+
return 1
|
|
1344
|
+
|
|
1345
|
+
def _process_no_examples_case(self, user_input: str, setting_type: str = "initial_outreach") -> dict:
|
|
1346
|
+
"""Process Case 1: No Examples - determine if complete prompt or instructions."""
|
|
1347
|
+
# Simple heuristic to determine if it's a complete prompt or instructions
|
|
1348
|
+
is_complete_prompt = (
|
|
1349
|
+
len(user_input) > 100 and
|
|
1350
|
+
('##' in user_input or 'create' in user_input.lower()
|
|
1351
|
+
or 'generate' in user_input.lower())
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
if is_complete_prompt:
|
|
1355
|
+
# Use message directly as prompt
|
|
1356
|
+
prompt = user_input
|
|
1357
|
+
else:
|
|
1358
|
+
# Use message + appropriate default prompt
|
|
1359
|
+
default_prompts = {
|
|
1360
|
+
"initial_outreach": "Create professional initial outreach emails for ##customer_name## on behalf of ##staff_name##.",
|
|
1361
|
+
"follow_up": "Create professional follow-up emails for ##customer_name## on behalf of ##staff_name##. Reference previous interactions and add new value."
|
|
1362
|
+
}
|
|
1363
|
+
default_prompt = default_prompts.get(
|
|
1364
|
+
setting_type, default_prompts["initial_outreach"])
|
|
1365
|
+
prompt = f"{default_prompt} Additional guidance: {user_input}"
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
"fewshots": False,
|
|
1369
|
+
"fewshots_location": [],
|
|
1370
|
+
"fewshots_strict_follow": False,
|
|
1371
|
+
"prompt": prompt,
|
|
1372
|
+
"prompt_in_template": ""
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
def _process_ai_enhancement_case(self, user_input: str, examples_files: list, setting_type: str = "initial_outreach") -> dict:
|
|
1376
|
+
"""Process Case 2: AI Enhancement Mode with examples."""
|
|
1377
|
+
email_type = "initial outreach" if setting_type == "initial_outreach" else "follow-up"
|
|
1378
|
+
return {
|
|
1379
|
+
"fewshots": True,
|
|
1380
|
+
"fewshots_location": examples_files,
|
|
1381
|
+
"fewshots_strict_follow": False,
|
|
1382
|
+
"prompt": f"Create {email_type} emails based on provided examples. Additional guidance: {user_input}",
|
|
1383
|
+
"prompt_in_template": "Use the provided examples as inspiration while incorporating the user guidance for improvements and customization."
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
def _process_strict_template_case(self, user_input: str, examples_files: list, setting_type: str = "initial_outreach") -> dict:
|
|
1387
|
+
"""Process Case 3: Strict Template Mode with exact template following."""
|
|
1388
|
+
return {
|
|
1389
|
+
"fewshots": True,
|
|
1390
|
+
"fewshots_location": examples_files,
|
|
1391
|
+
"fewshots_strict_follow": True,
|
|
1392
|
+
"prompt": f"Use exact templates from examples for {setting_type}. Context: {user_input}",
|
|
1393
|
+
"prompt_in_template": "Mirror the EXACT CONTENT of provided examples with ZERO wording changes. Only replace the recipient to ##customer_name## from company ##company_name##.\n\nNO PLACEHOLDERS OR COMPANY NAMES AS GREETINGS:\n- Do not use [Contact Name], [Company], etc.\n- If recipient name is unclear, use \"Hi\" or \"Hello\" without a name\n- Never use company name as a greeting\n- No hyperlinks/attachments\n- No invented information\n\nReturn 1 JSON object which include these required fields: mail_tone, subject, body, priority_order, approach, product_mention, product_name, message_type, tags"
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
def _validate_auto_interaction_settings(self, value) -> str:
|
|
1397
|
+
"""Validate auto interaction settings format.
|
|
1398
|
+
|
|
1399
|
+
Expected format: [{"from_email": "value", "from_name": "value", "from_number": "value",
|
|
1400
|
+
"tool_type": "Email|Autocall|Notif|SMS", "email_cc": "comma,separated,emails",
|
|
1401
|
+
"email_bcc": "comma,separated,emails"}]
|
|
1402
|
+
|
|
1403
|
+
Returns: Error message if invalid, None if valid
|
|
1404
|
+
"""
|
|
1405
|
+
if not isinstance(value, list):
|
|
1406
|
+
return "Auto interaction settings must be a list"
|
|
1407
|
+
|
|
1408
|
+
if len(value) == 0:
|
|
1409
|
+
return "Auto interaction settings list cannot be empty"
|
|
1410
|
+
|
|
1411
|
+
valid_tool_types = ["Email", "Autocall", "Notif", "SMS"]
|
|
1412
|
+
required_fields = ["from_email", "from_name", "from_number", "tool_type", "email_cc", "email_bcc"]
|
|
1413
|
+
|
|
1414
|
+
for i, item in enumerate(value):
|
|
1415
|
+
if not isinstance(item, dict):
|
|
1416
|
+
return f"Item {i} must be an object/dictionary"
|
|
1417
|
+
|
|
1418
|
+
# Check required fields
|
|
1419
|
+
for field in required_fields:
|
|
1420
|
+
if field not in item:
|
|
1421
|
+
return f"Item {i} missing required field: {field}"
|
|
1422
|
+
|
|
1423
|
+
# Validate tool_type
|
|
1424
|
+
if item["tool_type"] not in valid_tool_types:
|
|
1425
|
+
return f"Item {i} has invalid tool_type '{item['tool_type']}'. Must be one of: {', '.join(valid_tool_types)}"
|
|
1426
|
+
|
|
1427
|
+
# Validate email format (basic check)
|
|
1428
|
+
if item["from_email"] and "@" not in item["from_email"]:
|
|
1429
|
+
return f"Item {i} has invalid from_email format"
|
|
1430
|
+
|
|
1431
|
+
# Validate CC/BCC email lists (basic check)
|
|
1432
|
+
for email_field in ["email_cc", "email_bcc"]:
|
|
1433
|
+
if item[email_field]: # Only validate if not empty
|
|
1434
|
+
emails = [email.strip() for email in item[email_field].split(",")]
|
|
1435
|
+
for email in emails:
|
|
1436
|
+
if email and "@" not in email:
|
|
1437
|
+
return f"Item {i} has invalid email in {email_field}: {email}"
|
|
1438
|
+
|
|
1439
|
+
return None # No errors
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def main():
|
|
1443
|
+
"""Main entry point for command-line execution."""
|
|
1444
|
+
cli = FuseSellCLI()
|
|
1445
|
+
exit_code = cli.run()
|
|
1446
|
+
sys.exit(exit_code)
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
if __name__ == "__main__":
|
|
1450
|
+
main()
|