autotouch-cli 0.2.59__tar.gz → 0.2.61__tar.gz
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.
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/PKG-INFO +1 -1
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/columns.py +16 -1
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/validation.py +62 -6
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/data/CLI_REFERENCE.md +15 -1
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/data/cli-manifest.json +3 -3
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/templates.py +6 -6
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/PKG-INFO +1 -1
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/provider_registry.py +79 -13
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/pyproject.toml +1 -1
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/MANIFEST.in +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/README.md +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/cli.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/cli_contracts.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/__init__.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/auth.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/cells.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/jobs.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/leads.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/linkedin.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/prompts.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/rows.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/search.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/sequences.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/tables.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/tasks.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/webhooks.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/commands/workspace_secrets.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/__init__.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/auth.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/config.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/csv_import.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/http.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/io.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/output.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/polling.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/core/run.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/exceptions.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/mongo_status.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/parser.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/parser_groups.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli/sequence_support.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/SOURCES.txt +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/__init__.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/linkedin_contract.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/autotouch_shared/search_contract.py +0 -0
- {autotouch_cli-0.2.59 → autotouch_cli-0.2.61}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.61
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Project-URL: Homepage, https://app.autotouch.ai
|
|
6
6
|
Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
|
|
@@ -379,7 +379,22 @@ def register_columns_subcommands(
|
|
|
379
379
|
add_api_common_arguments(pcl)
|
|
380
380
|
pcl.set_defaults(func=handlers["list"])
|
|
381
381
|
|
|
382
|
-
pcc = col_sub.add_parser(
|
|
382
|
+
pcc = col_sub.add_parser(
|
|
383
|
+
"create",
|
|
384
|
+
help="Create a column",
|
|
385
|
+
description=(
|
|
386
|
+
"Create a column from a ColumnCreate payload.\n"
|
|
387
|
+
"For llm_enrichment, there are two valid authoring paths:\n"
|
|
388
|
+
"- generated path: send config.instructions and let the API compile config.advancedPrompt\n"
|
|
389
|
+
"- manual path: send config.advancedPrompt directly when you want exact prompt control"
|
|
390
|
+
),
|
|
391
|
+
epilog=(
|
|
392
|
+
"Recommended flow for llm_enrichment:\n\n"
|
|
393
|
+
"autotouch columns recipe --type llm_enrichment --output human\n\n"
|
|
394
|
+
"The runnable prompt is always config.advancedPrompt before execution."
|
|
395
|
+
),
|
|
396
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
397
|
+
)
|
|
383
398
|
pcc.add_argument("--table-id", required=True)
|
|
384
399
|
pcc.add_argument("--data-json", help="ColumnCreate payload JSON")
|
|
385
400
|
pcc.add_argument("--data-file", help="ColumnCreate payload file path")
|
|
@@ -55,8 +55,18 @@ def extract_add_to_crm_field_mappings(payload: Any) -> Optional[Dict[str, Any]]:
|
|
|
55
55
|
return {}
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def inspect_add_to_crm_field_mappings(
|
|
59
|
-
|
|
58
|
+
def inspect_add_to_crm_field_mappings(
|
|
59
|
+
field_mappings: Dict[str, Any],
|
|
60
|
+
*,
|
|
61
|
+
require_company_domain: bool = True,
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
raw_mode = str(field_mappings.get("mode") or "").strip().lower()
|
|
64
|
+
if raw_mode == "array":
|
|
65
|
+
mode = "array"
|
|
66
|
+
elif raw_mode == "multi":
|
|
67
|
+
mode = "multi"
|
|
68
|
+
else:
|
|
69
|
+
mode = "single"
|
|
60
70
|
required_missing: List[str] = []
|
|
61
71
|
optional_missing: List[str] = []
|
|
62
72
|
|
|
@@ -75,12 +85,20 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
|
|
|
75
85
|
return True
|
|
76
86
|
return False
|
|
77
87
|
|
|
88
|
+
if not require_company_domain and mode != "single":
|
|
89
|
+
required_missing.append("singleModeOnlyWhenCompanyDomainOptional")
|
|
90
|
+
return {
|
|
91
|
+
"mode": mode,
|
|
92
|
+
"required_missing": required_missing,
|
|
93
|
+
"optional_missing": optional_missing,
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
if mode == "multi":
|
|
79
97
|
common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
|
|
80
98
|
leads = field_mappings.get("leads") if isinstance(field_mappings.get("leads"), list) else []
|
|
81
99
|
|
|
82
100
|
common_domain = str((common or {}).get("companyDomain") or field_mappings.get("companyDomain") or "").strip()
|
|
83
|
-
if not common_domain:
|
|
101
|
+
if require_company_domain and not common_domain:
|
|
84
102
|
required_missing.append("common.companyDomain")
|
|
85
103
|
|
|
86
104
|
has_lead_identity = False
|
|
@@ -95,10 +113,24 @@ def inspect_add_to_crm_field_mappings(field_mappings: Dict[str, Any]) -> Dict[st
|
|
|
95
113
|
optional_missing.append(f"leads[{idx}].{slot}")
|
|
96
114
|
if not has_lead_identity:
|
|
97
115
|
required_missing.append("leads[].identityFields")
|
|
116
|
+
elif mode == "array":
|
|
117
|
+
common = field_mappings.get("common") if isinstance(field_mappings.get("common"), dict) else {}
|
|
118
|
+
template = field_mappings.get("template") if isinstance(field_mappings.get("template"), dict) else {}
|
|
119
|
+
if require_company_domain and not str((common or {}).get("companyDomain") or "").strip():
|
|
120
|
+
required_missing.append("common.companyDomain")
|
|
121
|
+
if not str(field_mappings.get("sourceColumn") or "").strip():
|
|
122
|
+
required_missing.append("sourceColumn")
|
|
123
|
+
if not _has_identity_mapping(template):
|
|
124
|
+
required_missing.append("template.identityFields")
|
|
98
125
|
else:
|
|
99
126
|
company_domain_key = str(field_mappings.get("companyDomain") or "").strip()
|
|
100
|
-
if not company_domain_key:
|
|
127
|
+
if require_company_domain and not company_domain_key:
|
|
101
128
|
required_missing.append("companyDomain")
|
|
129
|
+
if require_company_domain:
|
|
130
|
+
if not _has_identity_mapping(field_mappings):
|
|
131
|
+
required_missing.append("identityFields")
|
|
132
|
+
elif not str(field_mappings.get("linkedinUrl") or "").strip():
|
|
133
|
+
required_missing.append("linkedinUrl")
|
|
102
134
|
if not _has_identity_mapping(field_mappings):
|
|
103
135
|
required_missing.append("identityFields")
|
|
104
136
|
for slot in ["firstName", "lastName", "title", "companyName"]:
|
|
@@ -116,6 +148,7 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
|
|
|
116
148
|
field_mappings = extract_add_to_crm_field_mappings(payload)
|
|
117
149
|
if field_mappings is None:
|
|
118
150
|
return
|
|
151
|
+
config = payload.get("config") if isinstance(payload.get("config"), dict) else {}
|
|
119
152
|
|
|
120
153
|
kind_value = str(payload.get("kind") or "").strip().lower()
|
|
121
154
|
if kind_value != "enrichment":
|
|
@@ -130,10 +163,24 @@ def validate_add_to_crm_create_payload(payload: Dict[str, Any]) -> None:
|
|
|
130
163
|
"Tip: run `autotouch columns recipe --type add_to_crm`."
|
|
131
164
|
)
|
|
132
165
|
|
|
133
|
-
|
|
166
|
+
require_company_domain = config.get("requireCompanyDomain", True)
|
|
167
|
+
if not isinstance(require_company_domain, bool):
|
|
168
|
+
raise AutotouchInputError(
|
|
169
|
+
"add_to_crm config.requireCompanyDomain must be a boolean when provided."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
issues = inspect_add_to_crm_field_mappings(
|
|
173
|
+
field_mappings,
|
|
174
|
+
require_company_domain=require_company_domain,
|
|
175
|
+
)
|
|
134
176
|
required_missing = issues.get("required_missing") or []
|
|
135
177
|
optional_missing = issues.get("optional_missing") or []
|
|
136
178
|
if required_missing:
|
|
179
|
+
if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
|
|
180
|
+
raise AutotouchInputError(
|
|
181
|
+
"add_to_crm with config.requireCompanyDomain=false only supports single-mode fieldMappings. "
|
|
182
|
+
"Tip: map config.fieldMappings.linkedinUrl and keep mode='single' for LinkedIn-First Sync to Leads."
|
|
183
|
+
)
|
|
137
184
|
missing = ", ".join(str(v) for v in required_missing)
|
|
138
185
|
raise AutotouchInputError(
|
|
139
186
|
f"add_to_crm fieldMappings missing required keys: {missing}. "
|
|
@@ -301,11 +348,20 @@ def run_precheck_for_add_to_crm(
|
|
|
301
348
|
if field_mappings is None:
|
|
302
349
|
return
|
|
303
350
|
|
|
304
|
-
|
|
351
|
+
config = column.get("config") if isinstance(column.get("config"), dict) else {}
|
|
352
|
+
require_company_domain = config.get("requireCompanyDomain", True)
|
|
353
|
+
issues = inspect_add_to_crm_field_mappings(
|
|
354
|
+
field_mappings,
|
|
355
|
+
require_company_domain=bool(require_company_domain) if isinstance(require_company_domain, bool) else True,
|
|
356
|
+
)
|
|
305
357
|
required_missing = issues.get("required_missing") or []
|
|
306
358
|
optional_missing = issues.get("optional_missing") or []
|
|
307
359
|
|
|
308
360
|
if required_missing:
|
|
361
|
+
if "singleModeOnlyWhenCompanyDomainOptional" in required_missing:
|
|
362
|
+
raise AutotouchInputError(
|
|
363
|
+
"add_to_crm column is misconfigured: config.requireCompanyDomain=false only supports single-mode fieldMappings for LinkedIn-First Sync to Leads."
|
|
364
|
+
)
|
|
309
365
|
missing = ", ".join(str(v) for v in required_missing)
|
|
310
366
|
raise AutotouchInputError(
|
|
311
367
|
f"add_to_crm column is misconfigured (missing required mappings: {missing}). "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Autotouch CLI Reference
|
|
2
2
|
|
|
3
|
-
Generated from the installed parser for `autotouch-cli` `0.2.
|
|
3
|
+
Generated from the installed parser for `autotouch-cli` `0.2.61`.
|
|
4
4
|
Manifest schema version: `2`.
|
|
5
5
|
|
|
6
6
|
## Output Modes
|
|
@@ -653,6 +653,11 @@ Column operations
|
|
|
653
653
|
|
|
654
654
|
Create a column
|
|
655
655
|
|
|
656
|
+
Create a column from a ColumnCreate payload.
|
|
657
|
+
For llm_enrichment, there are two valid authoring paths:
|
|
658
|
+
- generated path: send config.instructions and let the API compile config.advancedPrompt
|
|
659
|
+
- manual path: send config.advancedPrompt directly when you want exact prompt control
|
|
660
|
+
|
|
656
661
|
- Auth: `developer_key_or_user_session`
|
|
657
662
|
- Stability: `stable`
|
|
658
663
|
- Destructive: `no`
|
|
@@ -669,6 +674,15 @@ Create a column
|
|
|
669
674
|
[--output {json,ndjson,human}] [--compact]
|
|
670
675
|
[--select SELECT |
|
|
671
676
|
--json-pointer JSON_POINTER] [--verbose]`
|
|
677
|
+
- Notes:
|
|
678
|
+
|
|
679
|
+
```text
|
|
680
|
+
Recommended flow for llm_enrichment:
|
|
681
|
+
|
|
682
|
+
autotouch columns recipe --type llm_enrichment --output human
|
|
683
|
+
|
|
684
|
+
The runnable prompt is always config.advancedPrompt before execution.
|
|
685
|
+
```
|
|
672
686
|
- Options:
|
|
673
687
|
- `--table-id` (required; kind=string)
|
|
674
688
|
- `--data-json` (kind=json; input=json): ColumnCreate payload JSON
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
2
|
+
"version": "0.2.61",
|
|
3
3
|
"manifest_schema_version": 2,
|
|
4
4
|
"entry_points": {
|
|
5
5
|
"autotouch": "autotouch_cli.cli:main",
|
|
@@ -14777,8 +14777,8 @@
|
|
|
14777
14777
|
"aliases": [],
|
|
14778
14778
|
"group": "columns",
|
|
14779
14779
|
"help": "Create a column",
|
|
14780
|
-
"description":
|
|
14781
|
-
"notes":
|
|
14780
|
+
"description": "Create a column from a ColumnCreate payload.\nFor llm_enrichment, there are two valid authoring paths:\n- generated path: send config.instructions and let the API compile config.advancedPrompt\n- manual path: send config.advancedPrompt directly when you want exact prompt control",
|
|
14781
|
+
"notes": "Recommended flow for llm_enrichment:\n\nautotouch columns recipe --type llm_enrichment --output human\n\nThe runnable prompt is always config.advancedPrompt before execution.",
|
|
14782
14782
|
"required_flags": [
|
|
14783
14783
|
"--table-id"
|
|
14784
14784
|
],
|
|
@@ -35,11 +35,11 @@ COLUMN_RECIPE_BASE_TYPES = tuple(recipe_types())
|
|
|
35
35
|
COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
|
|
36
36
|
"add_to_leads": {
|
|
37
37
|
"canonical": "add_to_crm",
|
|
38
|
-
"key": "
|
|
39
|
-
"label": "
|
|
38
|
+
"key": "sync_to_leads",
|
|
39
|
+
"label": "Sync to Leads",
|
|
40
40
|
"notes": [
|
|
41
|
-
"CLI alias for the canonical add_to_crm provider recipe.",
|
|
42
|
-
"
|
|
41
|
+
"Legacy CLI alias for the canonical add_to_crm provider recipe.",
|
|
42
|
+
"Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
|
|
43
43
|
],
|
|
44
44
|
},
|
|
45
45
|
"sync_to_leads": {
|
|
@@ -47,8 +47,8 @@ COLUMN_RECIPE_ALIASES: Dict[str, Dict[str, Any]] = {
|
|
|
47
47
|
"key": "sync_to_leads",
|
|
48
48
|
"label": "Sync to Leads",
|
|
49
49
|
"notes": [
|
|
50
|
-
"CLI alias for the canonical add_to_crm provider recipe.",
|
|
51
|
-
"
|
|
50
|
+
"Preferred CLI alias for the canonical add_to_crm provider recipe.",
|
|
51
|
+
"Emits the standard Sync to Leads payload. Internal provider/config names remain add_to_crm.",
|
|
52
52
|
],
|
|
53
53
|
},
|
|
54
54
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.61
|
|
4
4
|
Summary: Autotouch Smart Table CLI
|
|
5
5
|
Project-URL: Homepage, https://app.autotouch.ai
|
|
6
6
|
Project-URL: Documentation, https://github.com/nicolonic/autotouch_main/tree/main/docs/research-table/reference
|
|
@@ -158,6 +158,14 @@ def _mapping_text(value: Any) -> str:
|
|
|
158
158
|
return _text(value)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def _llm_advanced_prompt_text(config: Dict[str, Any]) -> str:
|
|
162
|
+
return (
|
|
163
|
+
_text(config.get("advancedPrompt"))
|
|
164
|
+
or _text(config.get("advanced_prompt"))
|
|
165
|
+
or _text(config.get("prompt"))
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
161
169
|
def _llm_provider_values() -> Tuple[str, ...]:
|
|
162
170
|
return ("llm", "gemini", "xai", "openai", "anthropic", "groq")
|
|
163
171
|
|
|
@@ -378,13 +386,14 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
378
386
|
},
|
|
379
387
|
},
|
|
380
388
|
recipe_notes=(
|
|
381
|
-
"
|
|
389
|
+
"The runnable prompt is always config.advancedPrompt before execution.",
|
|
390
|
+
"Generated path: provide instructions and let the API compile config.advancedPrompt for you.",
|
|
391
|
+
"Manual path: write config.advancedPrompt directly when you want exact control over the final prompt.",
|
|
382
392
|
"When the platform generates the runnable prompt from instructions, keep useAutoSchema=true unless you intentionally want to own the schema yourself.",
|
|
383
393
|
"Do not send user_schema/response_schema in that default generated-prompt path; prompt/schema drift is possible if you later change the generated prompt but keep a frozen custom schema.",
|
|
384
394
|
"Basic is for extraction, scoring, or classification using fields already in the row. Agent is for tasks that may need outside lookup or web research.",
|
|
385
395
|
"Create/update always materializes the runnable prompt into config.advancedPrompt before execution.",
|
|
386
396
|
"Generated agent-mode prompts always include company/requester context.",
|
|
387
|
-
"Basic mode stays manual-prompt-first; write the prompt directly from the user goal/preferences.",
|
|
388
397
|
"Runtime only injects values for placeholders the prompt explicitly references.",
|
|
389
398
|
"Manual/basic prompts should use explicit placeholders like {{company_name}}, {{domain}}, or {{user_value_proposition}} when those values are needed.",
|
|
390
399
|
"Agent mode is JSON-oriented.",
|
|
@@ -746,20 +755,21 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
746
755
|
),
|
|
747
756
|
ResearchTableProviderContract(
|
|
748
757
|
key="add_to_crm",
|
|
749
|
-
display_name="
|
|
758
|
+
display_name="Sync to Leads",
|
|
750
759
|
kind="enrichment",
|
|
751
760
|
behavior_class="action_enrichment",
|
|
752
761
|
provider_aliases=("add_to_crm",),
|
|
753
762
|
recipe_type="add_to_crm",
|
|
754
763
|
recipe_payload={
|
|
755
|
-
"key": "
|
|
756
|
-
"label": "
|
|
764
|
+
"key": "sync_to_leads",
|
|
765
|
+
"label": "Sync to Leads",
|
|
757
766
|
"kind": "enrichment",
|
|
758
767
|
"dataType": "json",
|
|
759
768
|
"origin": "manual",
|
|
760
769
|
"autoRun": "onSourceUpdate",
|
|
761
770
|
"config": {
|
|
762
771
|
"provider": "add_to_crm",
|
|
772
|
+
"requireCompanyDomain": True,
|
|
763
773
|
"leadSource": "research_table_export",
|
|
764
774
|
"fieldMappings": {
|
|
765
775
|
"mode": "single",
|
|
@@ -786,18 +796,19 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
786
796
|
},
|
|
787
797
|
recipe_notes=(
|
|
788
798
|
"companyDomain is required; LinkedIn is optional.",
|
|
799
|
+
"Set `config.requireCompanyDomain=false` only for single-mode LinkedIn-first syncs. In that relaxed mode, `linkedinUrl` becomes required and multi/array mode stays unsupported.",
|
|
789
800
|
"Include at least one usable hard-identity mapping such as LinkedIn, email, or phone.",
|
|
790
801
|
"If companyDomain is missing in the table, derive or enrich that domain column first.",
|
|
791
802
|
"Primitive source columns map directly. If a mapped source column stores structured JSON, send an object with column + path, for example {\"column\": \"work_email\", \"path\": \"response\"} or {\"column\": \"mobile_phone\", \"path\": \"mobile_number\"}.",
|
|
792
803
|
"For firstName and lastName, prefer authoritative structured source fields when available, for example {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"first_name\"} and {\"column\": \"linkedin_profile_lookup_raw\", \"path\": \"last_name\"}. Avoid mapping a combined full-name column directly unless that is an intentional fallback and you accept imperfect CRM data.",
|
|
793
|
-
"
|
|
794
|
-
"Creating add_to_crm after upstream email/phone/lead columns already finished does not replay those older source updates; run
|
|
804
|
+
"Sync to Leads is non-billable.",
|
|
805
|
+
"Creating Sync to Leads (`add_to_crm`) after upstream email/phone/lead columns already finished does not replay those older source updates; run Sync to Leads explicitly or rerun the upstream source column.",
|
|
795
806
|
),
|
|
796
807
|
column_type={
|
|
797
808
|
"type": "add_to_crm",
|
|
798
809
|
"kind": "enrichment",
|
|
799
810
|
"runnable": True,
|
|
800
|
-
"description": "
|
|
811
|
+
"description": "Sync eligible mapped rows into Leads from research-table data.",
|
|
801
812
|
"requirements": {
|
|
802
813
|
"config_provider": "add_to_crm",
|
|
803
814
|
"field_mappings": "required",
|
|
@@ -806,7 +817,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
806
817
|
"billing": {
|
|
807
818
|
"billable": False,
|
|
808
819
|
"credits": 0,
|
|
809
|
-
"notes": "
|
|
820
|
+
"notes": "Sync to Leads does not consume credits.",
|
|
810
821
|
},
|
|
811
822
|
},
|
|
812
823
|
setup_contract={
|
|
@@ -971,14 +982,14 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
971
982
|
"config": {
|
|
972
983
|
"provider": "add_to_sequence",
|
|
973
984
|
"sequenceId": "<SEQUENCE_ID>",
|
|
974
|
-
"sourceLeadColumn": "
|
|
985
|
+
"sourceLeadColumn": "sync_to_leads",
|
|
975
986
|
},
|
|
976
987
|
},
|
|
977
988
|
recipe_notes=(
|
|
978
|
-
"sourceLeadColumn should reference the source column key that stores lead IDs (for example
|
|
989
|
+
"sourceLeadColumn should reference the source column key that stores lead IDs (for example sync_to_leads), not the provider name add_to_crm.",
|
|
979
990
|
"Set sequenceId to the target workflow sequence ID.",
|
|
980
991
|
"The target sequence must already be ACTIVE for real enrollment.",
|
|
981
|
-
"Common tail order after contact rows or lead IDs exist is email_finder -> add_to_crm -> create/activate sequence -> add_to_sequence.",
|
|
992
|
+
"Common tail order after contact rows or lead IDs exist is email_finder -> Sync to Leads (`add_to_crm`) -> create/activate sequence -> add_to_sequence.",
|
|
982
993
|
"Creating add_to_sequence after lead IDs already exist does not replay those older updates; run add_to_sequence explicitly or rerun the lead-id source column.",
|
|
983
994
|
),
|
|
984
995
|
column_type={
|
|
@@ -1355,6 +1366,38 @@ def validate_column_provider_contract(
|
|
|
1355
1366
|
)
|
|
1356
1367
|
return contract
|
|
1357
1368
|
|
|
1369
|
+
if contract.key == "llm":
|
|
1370
|
+
prompt_source = _normalize(cfg.get("promptSource") or cfg.get("prompt_source"))
|
|
1371
|
+
instructions = _text(cfg.get("instructions"))
|
|
1372
|
+
advanced_prompt = _llm_advanced_prompt_text(cfg)
|
|
1373
|
+
|
|
1374
|
+
if prompt_source == "generated":
|
|
1375
|
+
if not instructions:
|
|
1376
|
+
raise ProviderContractValidationError(
|
|
1377
|
+
"missing_required_field",
|
|
1378
|
+
"Generated LLM prompt mode requires config.instructions.",
|
|
1379
|
+
hint="Use instructions for shorthand/high-level intent, or switch to config.promptSource='manual' and write config.advancedPrompt directly.",
|
|
1380
|
+
)
|
|
1381
|
+
return contract
|
|
1382
|
+
|
|
1383
|
+
if prompt_source == "manual":
|
|
1384
|
+
if not advanced_prompt:
|
|
1385
|
+
raise ProviderContractValidationError(
|
|
1386
|
+
"missing_required_field",
|
|
1387
|
+
"Manual LLM prompt mode requires config.advancedPrompt.",
|
|
1388
|
+
hint="Write config.advancedPrompt directly, or remove config.promptSource and supply config.instructions for the generated path.",
|
|
1389
|
+
)
|
|
1390
|
+
return contract
|
|
1391
|
+
|
|
1392
|
+
if instructions or advanced_prompt:
|
|
1393
|
+
return contract
|
|
1394
|
+
|
|
1395
|
+
raise ProviderContractValidationError(
|
|
1396
|
+
"missing_required_field",
|
|
1397
|
+
"LLM enrichment columns require either config.instructions or config.advancedPrompt.",
|
|
1398
|
+
hint="Use instructions for the generated path or write config.advancedPrompt directly for the manual path.",
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1358
1401
|
if contract.key == "add_to_sequence":
|
|
1359
1402
|
if not _text(cfg.get("sequenceId")):
|
|
1360
1403
|
raise ProviderContractValidationError(
|
|
@@ -1366,7 +1409,7 @@ def validate_column_provider_contract(
|
|
|
1366
1409
|
raise ProviderContractValidationError(
|
|
1367
1410
|
"missing_required_field",
|
|
1368
1411
|
"add_to_sequence requires config.sourceLeadColumn.",
|
|
1369
|
-
hint="Set sourceLeadColumn to the column key that stores lead IDs, usually
|
|
1412
|
+
hint="Set sourceLeadColumn to the column key that stores lead IDs, usually sync_to_leads.",
|
|
1370
1413
|
)
|
|
1371
1414
|
return contract
|
|
1372
1415
|
|
|
@@ -1378,6 +1421,29 @@ def validate_column_provider_contract(
|
|
|
1378
1421
|
"add_to_crm requires config.fieldMappings.",
|
|
1379
1422
|
hint="Use `autotouch columns recipe --type add_to_crm`.",
|
|
1380
1423
|
)
|
|
1424
|
+
require_company_domain = cfg.get("requireCompanyDomain", True)
|
|
1425
|
+
if not isinstance(require_company_domain, bool):
|
|
1426
|
+
raise ProviderContractValidationError(
|
|
1427
|
+
"invalid_field",
|
|
1428
|
+
"add_to_crm config.requireCompanyDomain must be a boolean when provided.",
|
|
1429
|
+
hint="Use true for the default strict mode or false for single-mode LinkedIn-first sync.",
|
|
1430
|
+
detail={"requireCompanyDomain": require_company_domain},
|
|
1431
|
+
)
|
|
1432
|
+
if require_company_domain is False:
|
|
1433
|
+
mode = _normalize(field_mappings.get("mode")) or "single"
|
|
1434
|
+
if mode in {"multi", "array"}:
|
|
1435
|
+
raise ProviderContractValidationError(
|
|
1436
|
+
"invalid_field",
|
|
1437
|
+
"add_to_crm config.requireCompanyDomain=false only supports single-mode fieldMappings.",
|
|
1438
|
+
hint="Use single-mode sync_to_leads with config.fieldMappings.linkedinUrl when companyDomain is optional.",
|
|
1439
|
+
detail={"mode": field_mappings.get("mode")},
|
|
1440
|
+
)
|
|
1441
|
+
if not _mapping_text(field_mappings.get("linkedinUrl")):
|
|
1442
|
+
raise ProviderContractValidationError(
|
|
1443
|
+
"missing_required_field",
|
|
1444
|
+
"add_to_crm with config.requireCompanyDomain=false requires config.fieldMappings.linkedinUrl.",
|
|
1445
|
+
hint="Map a LinkedIn profile URL column when using LinkedIn-first sync_to_leads.",
|
|
1446
|
+
)
|
|
1381
1447
|
return contract
|
|
1382
1448
|
|
|
1383
1449
|
if contract.key == "sync_to_table":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|