fusesell 1.3.2__py3-none-any.whl → 1.3.4__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -22,7 +22,6 @@ License-File: LICENSE
22
22
  Requires-Dist: openai>=1.0.0
23
23
  Requires-Dist: requests>=2.25.0
24
24
  Requires-Dist: beautifulsoup4>=4.9.0
25
- Requires-Dist: lxml>=4.6.0
26
25
  Requires-Dist: python-dateutil>=2.8.0
27
26
  Requires-Dist: pytz>=2021.1
28
27
  Provides-Extra: ocr
@@ -1,7 +1,7 @@
1
1
  fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
2
- fusesell-1.3.2.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
- fusesell_local/__init__.py,sha256=tXr5H0DMQau3o2A4S5BUBYFUA9AGfHAmeZdHNIF78xA,967
4
- fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
2
+ fusesell-1.3.4.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
+ fusesell_local/__init__.py,sha256=Q6pGa5zMldeiz9JEKEbO3rcIJphGt8qmZBl03G7wMMM,966
4
+ fusesell_local/api.py,sha256=SABxRr0TsB1Xfhl_OlL2IiMKYLG0jxbsaBKVNsMh-Qo,10972
5
5
  fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
6
6
  fusesell_local/pipeline.py,sha256=RMF_kgwNEc1ka8-CDJyzIOTSo8PGtR_zPKAgRevhlNo,39913
7
7
  fusesell_local/config/__init__.py,sha256=0ErO7QiSDqKn-LHcjIRdLZzh5QaRTkRsIlwfgpkkDz8,209
@@ -15,21 +15,21 @@ fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0
15
15
  fusesell_local/stages/initial_outreach.py,sha256=98KMaGP_aFkCV4K8j8HgURmNEgbVTYZSvXfLOlXX3Mc,127216
16
16
  fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
17
17
  fusesell_local/tests/conftest.py,sha256=TWUtlP6cNPVOYkTPz-j9BzS_KnXdPWy8D-ObPLHvXYs,366
18
- fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
18
+ fusesell_local/tests/test_api.py,sha256=vXlNaIDxqTTIFHRHs5zdUAPrxEleyoNxVOeyGuNgoQo,2304
19
19
  fusesell_local/tests/test_cli.py,sha256=iNgU8nDlVrcQM5MpBUTIJ5q3oh2-jgX77hJeaqBxToM,1007
20
20
  fusesell_local/tests/test_data_manager_products.py,sha256=g8EUSxTqdg18VifzhuOtDDywiMYzwOWFADny5Vntc28,4691
21
21
  fusesell_local/tests/test_data_manager_sales_process.py,sha256=NbwxQ9oBKCZfrkRQYxzHHQ08F7epqPUsyeGz_vm3kf8,4447
22
22
  fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd470SSwRHJAvpHln9KO4,4578
23
- fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
23
+ fusesell_local/utils/__init__.py,sha256=onCrMaFAr_RepjhilcATvo2VBsSwglbaDSS7M5UVNQ0,374
24
24
  fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
25
25
  fusesell_local/utils/data_manager.py,sha256=FHW9nvLXDgf-HYNFwxZlegZp0OgB3altszW6INIgyLM,188910
26
- fusesell_local/utils/event_scheduler.py,sha256=TDk1v19cNgLhn2aJriQfpvZnwBcRpOWyHLDvkefW110,39834
27
- fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
26
+ fusesell_local/utils/event_scheduler.py,sha256=9hKobxMwHipZCn5ZyPZr7wrH3a2StMHdEOXKfQvoIE8,39977
27
+ fusesell_local/utils/llm_client.py,sha256=eNfbZBcyawBvZGLDPuyeectwBUVi5fjeD5GeLkEqWXI,12271
28
28
  fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
29
29
  fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
30
30
  fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
31
- fusesell-1.3.2.dist-info/METADATA,sha256=GbUAPyqmrD5-9N3jaNnlAVQUNVG0AiRbNDvWOZDAaKo,35074
32
- fusesell-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- fusesell-1.3.2.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
- fusesell-1.3.2.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
- fusesell-1.3.2.dist-info/RECORD,,
31
+ fusesell-1.3.4.dist-info/METADATA,sha256=Dt8C1y9fo4rpPwZElI9-l-Qckkm62o7smXKlibjn8gQ,35046
32
+ fusesell-1.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ fusesell-1.3.4.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
+ fusesell-1.3.4.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
+ fusesell-1.3.4.dist-info/RECORD,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.3.2"
35
+ __version__ = "1.3.3"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
fusesell_local/api.py CHANGED
@@ -16,6 +16,7 @@ from typing import Any, Callable, Dict, Mapping, MutableMapping, Optional, Seque
16
16
  from .pipeline import FuseSellPipeline
17
17
  from .utils.logger import setup_logging as _setup_logging
18
18
  from .utils.validators import InputValidator
19
+ from .utils.llm_client import normalize_llm_base_url
19
20
 
20
21
 
21
22
  class ConfigValidationError(ValueError):
@@ -138,6 +139,8 @@ def build_config(options: OptionsType) -> Dict[str, Any]:
138
139
  "dry_run": _coerce_bool(_get("dry_run")),
139
140
  }
140
141
 
142
+ config["llm_base_url"] = normalize_llm_base_url(config.get("llm_base_url"))
143
+
141
144
  return config
142
145
 
143
146
 
@@ -338,4 +341,3 @@ def execute_pipeline(
338
341
  }
339
342
 
340
343
  return run_pipeline(config)
341
-
@@ -24,20 +24,37 @@ def base_options(**overrides):
24
24
  return options
25
25
 
26
26
 
27
- def test_build_config_generates_defaults():
28
- config = build_config(base_options())
29
-
30
- assert config["execution_id"].startswith("fusesell_")
31
- assert config["output_format"] == "json"
32
- assert config["skip_stages"] == []
33
- assert config["send_immediately"] is False
34
-
35
-
36
- def test_validate_config_detects_missing_sources():
37
- config = build_config(
38
- base_options(
39
- input_description="",
40
- input_website="",
27
+ def test_build_config_generates_defaults():
28
+ config = build_config(base_options())
29
+
30
+ assert config["execution_id"].startswith("fusesell_")
31
+ assert config["output_format"] == "json"
32
+ assert config["skip_stages"] == []
33
+ assert config["send_immediately"] is False
34
+
35
+
36
+ def test_build_config_normalizes_llm_base_url():
37
+ config = build_config(
38
+ base_options(
39
+ llm_base_url="https://custom-llm.example.com",
40
+ )
41
+ )
42
+
43
+ assert config["llm_base_url"] == "https://custom-llm.example.com/v1"
44
+
45
+
46
+ def test_build_config_preserves_azure_base_url():
47
+ azure_url = "https://rtx-openai.openai.azure.com/openai/deployments/gpt4"
48
+ config = build_config(base_options(llm_base_url=azure_url))
49
+
50
+ assert config["llm_base_url"] == azure_url
51
+
52
+
53
+ def test_validate_config_detects_missing_sources():
54
+ config = build_config(
55
+ base_options(
56
+ input_description="",
57
+ input_website="",
41
58
  input_freetext="",
42
59
  )
43
60
  )
@@ -2,14 +2,15 @@
2
2
  FuseSell Utilities - Common utilities and helper functions
3
3
  """
4
4
 
5
- from .data_manager import LocalDataManager
6
- from .llm_client import LLMClient
7
- from .validators import InputValidator
8
- from .logger import setup_logging
9
-
10
- __all__ = [
11
- 'LocalDataManager',
12
- 'LLMClient',
13
- 'InputValidator',
14
- 'setup_logging'
15
- ]
5
+ from .data_manager import LocalDataManager
6
+ from .llm_client import LLMClient, normalize_llm_base_url
7
+ from .validators import InputValidator
8
+ from .logger import setup_logging
9
+
10
+ __all__ = [
11
+ 'LocalDataManager',
12
+ 'LLMClient',
13
+ 'normalize_llm_base_url',
14
+ 'InputValidator',
15
+ 'setup_logging'
16
+ ]
@@ -183,17 +183,17 @@ class EventScheduler:
183
183
  ISO 8601 formatted string.
184
184
  """
185
185
  if isinstance(value, datetime):
186
- return value.isoformat()
186
+ return value.replace(second=0, microsecond=0).isoformat()
187
187
  if value is None:
188
- return datetime.utcnow().isoformat()
188
+ return datetime.utcnow().replace(second=0, microsecond=0).isoformat()
189
189
 
190
190
  value_str = str(value).strip()
191
191
  if not value_str:
192
- return datetime.utcnow().isoformat()
192
+ return datetime.utcnow().replace(second=0, microsecond=0).isoformat()
193
193
 
194
194
  try:
195
195
  parsed = datetime.fromisoformat(value_str)
196
- return parsed.isoformat()
196
+ return parsed.replace(second=0, microsecond=0).isoformat()
197
197
  except ValueError:
198
198
  return value_str
199
199
 
@@ -283,7 +283,7 @@ class EventScheduler:
283
283
  customextra.setdefault('recipient_name', recipient_name)
284
284
  customextra.setdefault('draft_id', draft_id)
285
285
  customextra.setdefault('customer_timezone', customer_timezone)
286
- customextra.setdefault('scheduled_time_utc', send_time.isoformat())
286
+ customextra.setdefault('scheduled_time_utc', self._format_datetime(send_time))
287
287
 
288
288
  if team_id and 'team_id' not in customextra:
289
289
  customextra['team_id'] = team_id
@@ -2,17 +2,74 @@
2
2
  LLM Client for OpenAI API integration
3
3
  """
4
4
 
5
- try:
6
- import openai
7
- OPENAI_AVAILABLE = True
8
- except ImportError:
9
- OPENAI_AVAILABLE = False
10
- openai = None
11
-
12
- from typing import Dict, Any, List, Optional
13
- import logging
14
- import time
15
- import json
5
+ try:
6
+ import openai
7
+ OPENAI_AVAILABLE = True
8
+ except ImportError:
9
+ OPENAI_AVAILABLE = False
10
+ openai = None
11
+
12
+ from typing import Dict, Any, List, Optional
13
+ import logging
14
+ import time
15
+ import json
16
+ from urllib.parse import urlsplit, urlunsplit
17
+
18
+
19
+ def normalize_llm_base_url(base_url: Optional[str], provider: Optional[str] = None) -> Optional[str]:
20
+ """
21
+ Ensure LLM base URLs point to the OpenAI-compatible /v1 endpoint unless they already target
22
+ Azure deployment paths that do not expect the suffix.
23
+
24
+ Args:
25
+ base_url: User-provided base URL.
26
+ provider: Optional provider hint (e.g., 'azure-openai').
27
+
28
+ Returns:
29
+ Normalized base URL with `/v1` appended when needed, or ``None`` if input is empty.
30
+ """
31
+ if not base_url:
32
+ return None
33
+
34
+ normalized = base_url.strip()
35
+ if not normalized:
36
+ return None
37
+
38
+ provider_hint = (provider or "").lower()
39
+ if provider_hint.startswith("azure") or "openai.azure.com" in normalized.lower():
40
+ return normalized.rstrip("/")
41
+
42
+ try:
43
+ parsed = urlsplit(normalized)
44
+ except ValueError:
45
+ parsed = None
46
+
47
+ if parsed and parsed.scheme and parsed.netloc:
48
+ path = parsed.path.rstrip("/")
49
+ segments = [segment for segment in path.split("/") if segment]
50
+
51
+ if not segments:
52
+ new_path = "/v1"
53
+ elif segments[-1] in {"v1", "v1beta"} or "v1" in segments or "deployments" in segments:
54
+ new_path = "/" + "/".join(segments)
55
+ else:
56
+ new_path = f"{path}/v1" if path else "/v1"
57
+
58
+ rebuilt = urlunsplit(
59
+ (
60
+ parsed.scheme,
61
+ parsed.netloc,
62
+ new_path,
63
+ parsed.query,
64
+ parsed.fragment,
65
+ )
66
+ )
67
+ return rebuilt.rstrip("/")
68
+
69
+ stripped = normalized.rstrip("/")
70
+ if stripped.endswith("/v1") or "/v1/" in stripped:
71
+ return stripped
72
+ return f"{stripped}/v1"
16
73
 
17
74
 
18
75
  class LLMClient:
@@ -35,13 +92,15 @@ class LLMClient:
35
92
 
36
93
  self.api_key = api_key
37
94
  self.model = model
38
- self.logger = logging.getLogger("fusesell.llm_client")
39
-
40
- # Initialize OpenAI client
41
- if base_url:
42
- self.client = openai.OpenAI(api_key=api_key, base_url=base_url)
43
- else:
44
- self.client = openai.OpenAI(api_key=api_key)
95
+ self.logger = logging.getLogger("fusesell.llm_client")
96
+
97
+ normalized_base_url = normalize_llm_base_url(base_url)
98
+
99
+ # Initialize OpenAI client
100
+ if normalized_base_url:
101
+ self.client = openai.OpenAI(api_key=api_key, base_url=normalized_base_url)
102
+ else:
103
+ self.client = openai.OpenAI(api_key=api_key)
45
104
 
46
105
  def chat_completion(
47
106
  self,
@@ -280,4 +339,4 @@ Response:"""
280
339
  return len(response) > 0
281
340
  except Exception as e:
282
341
  self.logger.error(f"API key validation failed: {str(e)}")
283
- return False
342
+ return False