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.

@@ -0,0 +1,31 @@
1
+ fusesell.py,sha256=dBmhnVtoAk4bhY0fpXnwdo8Jo2g1fQ60SHVUYh2Qru0,365
2
+ fusesell-1.2.0.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
+ fusesell_local/__init__.py,sha256=RNrjXYe65DuxE4cY_R1lTr3UWrPsV7EPz9DHRp62I58,930
4
+ fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
5
+ fusesell_local/cli.py,sha256=l9ZjiRJpYCjFfSLmTvxRs8XhB6TqRPkeqbFZsM2xP5o,64870
6
+ fusesell_local/pipeline.py,sha256=KO5oAIHZ3L_uAZWOszauJyv0QWlsQMIDNGRuwQSxNmQ,39531
7
+ fusesell_local/config/__init__.py,sha256=0ErO7QiSDqKn-LHcjIRdLZzh5QaRTkRsIlwfgpkkDz8,209
8
+ fusesell_local/config/prompts.py,sha256=5O3Y2v3GCi9d9FEyR6Ekc1UXVq2TcZp3Rrspvx4bkac,10164
9
+ fusesell_local/config/settings.py,sha256=rbjGPLQTFFr7DiWrPnZDFaOSNsdEMMYFx6pn7b13xGs,10743
10
+ fusesell_local/stages/__init__.py,sha256=2mAmzcMlVKZdseOR5Jju4PaPdKGsBT1ePqvt5RS3ZfQ,514
11
+ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJA96RUE,22069
12
+ fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
13
+ fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
14
+ fusesell_local/stages/follow_up.py,sha256=2CSen5SHJ5k6KMHXpqoRBb3n3IrcMRdDnuyTsOeuRTA,74625
15
+ fusesell_local/stages/initial_outreach.py,sha256=jox2caveSwI3xIfjn8FGYprkjTbW8YhDNvCzz9wNcBE,107503
16
+ fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
17
+ fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
18
+ fusesell_local/tests/test_cli.py,sha256=iNgU8nDlVrcQM5MpBUTIJ5q3oh2-jgX77hJeaqBxToM,1007
19
+ fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
20
+ fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
21
+ fusesell_local/utils/data_manager.py,sha256=wMP0fGeFVCEVOl6QOu2lXvzGqfW3apn_H3Fl7vy11lE,175558
22
+ fusesell_local/utils/event_scheduler.py,sha256=rjtWwtYQoJP0YwoN1-43t6K9GpLfqRq3c7Fv4papvbI,25725
23
+ fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
24
+ fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
25
+ fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
26
+ fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
27
+ fusesell-1.2.0.dist-info/METADATA,sha256=L7O3gPF5e0-yvIOrVjs8LpvIguZqWQkLRWVLDvTRMLw,36070
28
+ fusesell-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ fusesell-1.2.0.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
30
+ fusesell-1.2.0.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
31
+ fusesell-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fusesell = fusesell_local.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FuseSell Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ fusesell
2
+ fusesell_local
fusesell.py ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Backward-compatible CLI entry point for running the FuseSell pipeline.
4
+
5
+ Delegates to ``fusesell_local.cli.main`` so both the installed console script
6
+ and direct script execution share the same implementation.
7
+ """
8
+
9
+ from fusesell_local.cli import FuseSellCLI, main
10
+
11
+ __all__ = ["FuseSellCLI", "main"]
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -0,0 +1,37 @@
1
+ """
2
+ FuseSell Local - AI-powered sales automation platform for local execution.
3
+
4
+ This package exposes a programmatic API so the pipeline can be embedded in
5
+ workflows and code interpreters without launching the CLI entry point.
6
+ """
7
+
8
+ from .api import (
9
+ ConfigValidationError,
10
+ build_config,
11
+ configure_logging,
12
+ execute_pipeline,
13
+ generate_execution_id,
14
+ prepare_data_directory,
15
+ run_pipeline,
16
+ validate_config,
17
+ )
18
+ from .cli import FuseSellCLI, main as cli_main
19
+ from .pipeline import FuseSellPipeline
20
+
21
+ __all__ = [
22
+ "ConfigValidationError",
23
+ "FuseSellCLI",
24
+ "FuseSellPipeline",
25
+ "build_config",
26
+ "cli_main",
27
+ "configure_logging",
28
+ "execute_pipeline",
29
+ "generate_execution_id",
30
+ "prepare_data_directory",
31
+ "run_pipeline",
32
+ "validate_config",
33
+ ]
34
+
35
+ __version__ = "1.2.0"
36
+ __author__ = "FuseSell Team"
37
+ __description__ = "Local implementation of FuseSell AI sales automation pipeline"
fusesell_local/api.py ADDED
@@ -0,0 +1,341 @@
1
+ """
2
+ Public library interface for FuseSell Local.
3
+
4
+ This module exposes helpers that embed FuseSell in external Python runtimes
5
+ without going through the CLI wrapper.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import uuid
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, Mapping, MutableMapping, Optional, Sequence, Tuple, Union
15
+
16
+ from .pipeline import FuseSellPipeline
17
+ from .utils.logger import setup_logging as _setup_logging
18
+ from .utils.validators import InputValidator
19
+
20
+
21
+ class ConfigValidationError(ValueError):
22
+ """Raised when pipeline configuration fails validation."""
23
+
24
+
25
+ OptionsType = Union[Mapping[str, Any], object]
26
+
27
+
28
+ def generate_execution_id(prefix: str = "fusesell") -> str:
29
+ """
30
+ Generate a unique execution identifier.
31
+
32
+ Args:
33
+ prefix: Optional prefix for the identifier (default ``"fusesell"``).
34
+
35
+ Returns:
36
+ Execution ID string.
37
+ """
38
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
39
+ unique_id = uuid.uuid4().hex[:8]
40
+ return f"{prefix}_{timestamp}_{unique_id}"
41
+
42
+
43
+ def build_config(options: OptionsType) -> Dict[str, Any]:
44
+ """
45
+ Build a pipeline configuration dictionary from a mapping or namespace.
46
+
47
+ Args:
48
+ options: Mapping, dataclass, or argparse namespace containing pipeline options.
49
+
50
+ Returns:
51
+ Normalised configuration dictionary suitable for ``FuseSellPipeline``.
52
+ """
53
+
54
+ def _get(name: str, default: Any = None) -> Any:
55
+ if isinstance(options, Mapping):
56
+ return options.get(name, default)
57
+ return getattr(options, name, default)
58
+
59
+ def _coerce_bool(value: Any, default: bool = False) -> bool:
60
+ if value is None:
61
+ return default
62
+ if isinstance(value, bool):
63
+ return value
64
+ if isinstance(value, str):
65
+ return value.strip().lower() in {"1", "true", "t", "yes", "y", "on"}
66
+ return bool(value)
67
+
68
+ def _ensure_list(value: Any) -> Sequence[str]:
69
+ if value is None:
70
+ return []
71
+ if isinstance(value, (list, tuple, set)):
72
+ return [str(item) for item in value if item]
73
+ if isinstance(value, str):
74
+ return [item.strip() for item in value.split(",") if item.strip()]
75
+ return [str(value)]
76
+
77
+ config: Dict[str, Any] = {
78
+ # API and LLM settings
79
+ "openai_api_key": _get("openai_api_key"),
80
+ "serper_api_key": _get("serper_api_key"),
81
+ "llm_model": _get("llm_model", "gpt-4o-mini"),
82
+ "llm_base_url": _get("llm_base_url"),
83
+ "temperature": float(_get("temperature", 0.7) or 0.7),
84
+ "max_retries": int(_get("max_retries", 3) or 3),
85
+
86
+ # Organisation settings
87
+ "org_id": _get("org_id"),
88
+ "org_name": _get("org_name"),
89
+ "team_id": _get("team_id"),
90
+ "team_name": _get("team_name"),
91
+ "project_code": _get("project_code"),
92
+ "staff_name": _get("staff_name", "Sales Team"),
93
+
94
+ # Data sources
95
+ "input_website": _get("input_website") or "",
96
+ "input_description": _get("input_description") or "",
97
+ "input_business_card": _get("input_business_card") or "",
98
+ "input_linkedin_url": _get("input_linkedin_url") or "",
99
+ "input_facebook_url": _get("input_facebook_url") or "",
100
+ "input_freetext": _get("input_freetext") or "",
101
+
102
+ # Context fields
103
+ "customer_id": _get("customer_id", "null"),
104
+ "full_input": _get("full_input"),
105
+ "language": (_get("language") or "english").lower(),
106
+
107
+ # Processing control
108
+ "skip_stages": list(_ensure_list(_get("skip_stages"))),
109
+ "stop_after": _get("stop_after"),
110
+
111
+ # Continuation / action parameters
112
+ "continue_execution": _get("continue_execution"),
113
+ "action": _get("action", "draft_write"),
114
+ "selected_draft_id": _get("selected_draft_id"),
115
+ "reason": _get("reason") or "",
116
+ "recipient_address": _get("recipient_address"),
117
+ "recipient_name": _get("recipient_name") or "",
118
+ "interaction_type": _get("interaction_type", "email"),
119
+ "human_action_id": _get("human_action_id") or "",
120
+
121
+ # Scheduling preferences
122
+ "send_immediately": _coerce_bool(_get("send_immediately"), False),
123
+ "customer_timezone": _get("customer_timezone") or "",
124
+ "business_hours_start": _get("business_hours_start", "08:00") or "08:00",
125
+ "business_hours_end": _get("business_hours_end", "20:00") or "20:00",
126
+ "delay_hours": int(_get("delay_hours", 2) or 2),
127
+
128
+ # Output and storage settings
129
+ "output_format": (_get("output_format") or "json").lower(),
130
+ "data_dir": _get("data_dir", "./fusesell_data"),
131
+ "execution_id": _get("execution_id") or generate_execution_id(),
132
+ "save_intermediate": _coerce_bool(_get("save_intermediate")),
133
+
134
+ # Logging and diagnostics
135
+ "log_level": (_get("log_level") or "INFO").upper(),
136
+ "log_file": _get("log_file"),
137
+ "verbose": _coerce_bool(_get("verbose")),
138
+ "dry_run": _coerce_bool(_get("dry_run")),
139
+ }
140
+
141
+ return config
142
+
143
+
144
+ def prepare_data_directory(
145
+ config: MutableMapping[str, Any],
146
+ *,
147
+ assign_default_log: bool = True,
148
+ on_create: Optional[Callable[[Path], None]] = None,
149
+ ) -> Path:
150
+ """
151
+ Ensure the data directory structure exists for the given configuration.
152
+
153
+ Args:
154
+ config: Configuration dictionary (mutated in-place when log_file is assigned).
155
+ assign_default_log: When True, write a default log file path if none provided.
156
+ on_create: Optional callback invoked with the created ``Path``.
157
+
158
+ Returns:
159
+ Path to the resolved data directory.
160
+ """
161
+ data_dir = Path(config.get("data_dir") or "./fusesell_data").expanduser()
162
+ directories = [
163
+ data_dir,
164
+ data_dir / "config",
165
+ data_dir / "drafts",
166
+ data_dir / "logs",
167
+ data_dir / "exports",
168
+ ]
169
+
170
+ for directory in directories:
171
+ directory.mkdir(parents=True, exist_ok=True)
172
+
173
+ if assign_default_log and not config.get("log_file"):
174
+ log_filename = f"fusesell_{config['execution_id']}.log"
175
+ config["log_file"] = str((data_dir / "logs" / log_filename).resolve())
176
+
177
+ if on_create:
178
+ on_create(data_dir)
179
+
180
+ return data_dir
181
+
182
+
183
+ def configure_logging(config: Mapping[str, Any]) -> logging.Logger:
184
+ """
185
+ Configure logging for the pipeline execution.
186
+
187
+ Args:
188
+ config: Pipeline configuration dictionary.
189
+
190
+ Returns:
191
+ Configured logger instance.
192
+ """
193
+ return _setup_logging(
194
+ level=config.get("log_level", "INFO"),
195
+ log_file=config.get("log_file"),
196
+ verbose=bool(config.get("verbose", False)),
197
+ )
198
+
199
+
200
+ def validate_config(config: Mapping[str, Any]) -> Tuple[bool, list]:
201
+ """
202
+ Validate pipeline configuration for common issues.
203
+
204
+ Args:
205
+ config: Configuration dictionary to validate.
206
+
207
+ Returns:
208
+ Tuple of ``(is_valid, errors)``.
209
+ """
210
+ errors: list = []
211
+ validator = InputValidator()
212
+ errors.extend(validator.validate_config(dict(config)))
213
+
214
+ if not config.get("full_input"):
215
+ errors.append("Missing required configuration: full_input")
216
+
217
+ data_dir_valid, data_errors = validate_data_directory(config.get("data_dir"))
218
+ if not data_dir_valid:
219
+ errors.extend(data_errors)
220
+
221
+ if config.get("continue_execution"):
222
+ cont_valid, cont_errors = validate_continuation_params(config)
223
+ if not cont_valid:
224
+ errors.extend(cont_errors)
225
+
226
+ return len(errors) == 0, errors
227
+
228
+
229
+ def validate_data_directory(data_dir: Optional[str]) -> Tuple[bool, list]:
230
+ """
231
+ Validate that the configured data directory is writable.
232
+
233
+ Args:
234
+ data_dir: Directory path supplied in configuration.
235
+
236
+ Returns:
237
+ Tuple of ``(is_valid, errors)``.
238
+ """
239
+ errors: list = []
240
+
241
+ try:
242
+ if not data_dir:
243
+ raise ValueError("Data directory is not configured")
244
+
245
+ path = Path(data_dir).expanduser()
246
+ path.mkdir(parents=True, exist_ok=True)
247
+
248
+ test_file = path / ".__fusesell_write_test__"
249
+ test_file.write_text("test")
250
+ test_file.unlink()
251
+ except Exception as exc:
252
+ errors.append(f"Failed to prepare data directory '{data_dir}': {exc}")
253
+
254
+ return len(errors) == 0, errors
255
+
256
+
257
+ def validate_continuation_params(config: Mapping[str, Any]) -> Tuple[bool, list]:
258
+ """
259
+ Validate continuation parameters for follow-up executions.
260
+
261
+ Args:
262
+ config: Configuration dictionary.
263
+
264
+ Returns:
265
+ Tuple of ``(is_valid, errors)``.
266
+ """
267
+ errors: list = []
268
+ action = config.get("action")
269
+
270
+ if not action:
271
+ errors.append("Action is required when continuing execution")
272
+ return False, errors
273
+
274
+ valid_actions = {"draft_write", "draft_rewrite", "send", "close"}
275
+ if action not in valid_actions:
276
+ errors.append(f"Invalid action. Must be one of: {', '.join(sorted(valid_actions))}")
277
+
278
+ if action in {"draft_rewrite", "send"} and not config.get("selected_draft_id"):
279
+ errors.append("selected_draft_id is required for draft_rewrite and send actions")
280
+
281
+ if action == "send" and not config.get("recipient_address"):
282
+ errors.append("recipient_address is required for send actions")
283
+
284
+ return len(errors) == 0, errors
285
+
286
+
287
+ def run_pipeline(config: Mapping[str, Any]) -> Dict[str, Any]:
288
+ """
289
+ Execute the FuseSell pipeline with a prepared configuration.
290
+
291
+ Args:
292
+ config: Validated configuration dictionary.
293
+
294
+ Returns:
295
+ Pipeline execution result dictionary.
296
+ """
297
+ pipeline = FuseSellPipeline(dict(config))
298
+ return pipeline.execute()
299
+
300
+
301
+ def execute_pipeline(
302
+ options: OptionsType,
303
+ *,
304
+ auto_prepare: bool = True,
305
+ auto_configure_logging: bool = True,
306
+ auto_validate: bool = True,
307
+ ) -> Dict[str, Any]:
308
+ """
309
+ High-level helper that builds configuration from options and executes the pipeline.
310
+
311
+ Args:
312
+ options: Mapping or namespace of pipeline options.
313
+ auto_prepare: When True, prepare the data directory structure automatically.
314
+ auto_configure_logging: When True, configure logging before execution.
315
+ auto_validate: When True, validate configuration and raise ``ConfigValidationError`` on failure.
316
+
317
+ Returns:
318
+ Pipeline execution result dictionary.
319
+ """
320
+ config = build_config(options)
321
+
322
+ if auto_prepare:
323
+ prepare_data_directory(config)
324
+
325
+ if auto_configure_logging:
326
+ configure_logging(config)
327
+
328
+ if auto_validate:
329
+ valid, errors = validate_config(config)
330
+ if not valid:
331
+ raise ConfigValidationError("; ".join(errors))
332
+
333
+ if config.get("dry_run"):
334
+ return {
335
+ "status": "dry_run",
336
+ "execution_id": config["execution_id"],
337
+ "config": config,
338
+ }
339
+
340
+ return run_pipeline(config)
341
+