autotouch-cli 0.2.61__tar.gz → 0.2.63__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.61 → autotouch_cli-0.2.63}/PKG-INFO +6 -1
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/README.md +5 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/cli.py +40 -0
- autotouch_cli-0.2.63/autotouch_cli/commands/agents.py +465 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/parser.py +21 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/PKG-INFO +6 -1
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/SOURCES.txt +1 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_shared/provider_registry.py +10 -10
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/pyproject.toml +1 -1
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/MANIFEST.in +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/__init__.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/cli_contracts.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/__init__.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/auth.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/cells.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/columns.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/jobs.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/leads.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/linkedin.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/prompts.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/rows.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/search.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/sequences.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/tables.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/tasks.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/webhooks.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/commands/workspace_secrets.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/__init__.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/auth.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/config.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/csv_import.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/http.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/io.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/output.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/polling.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/run.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/core/validation.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/data/CLI_REFERENCE.md +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/data/cli-manifest.json +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/exceptions.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/mongo_status.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/parser_groups.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/sequence_support.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli/templates.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/dependency_links.txt +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/entry_points.txt +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/requires.txt +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_cli.egg-info/top_level.txt +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_shared/__init__.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_shared/linkedin_contract.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/autotouch_shared/search_contract.py +0 -0
- {autotouch_cli-0.2.61 → autotouch_cli-0.2.63}/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.63
|
|
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,11 @@ For `llm_enrichment` in `agent` mode, the recommended path is:
|
|
|
158
158
|
|
|
159
159
|
Only send `user_schema` / `response_schema` when you intentionally want to override the generated schema and keep it aligned yourself. The installed recipe surface at `autotouch columns recipe --type llm_enrichment` follows this contract.
|
|
160
160
|
|
|
161
|
+
Schema ownership rules:
|
|
162
|
+
- Accepted generated schemas and explicit user schemas are the saved output contract.
|
|
163
|
+
- Row execution must not add fields, rename fields, or replace a valid locked schema.
|
|
164
|
+
- Agent-mode evidence/scored state decides which values may fill the schema; the finalizer formats those values and schema validation gates persistence.
|
|
165
|
+
|
|
161
166
|
Prompt variables in authored prompts support nested JSON access:
|
|
162
167
|
- Use flat row variables like `{{company_name}}` for scalar columns.
|
|
163
168
|
- Use dotted placeholders like `{{linkedin_lookup.linkedin_url}}` when the source column stores JSON or stringified JSON.
|
|
@@ -133,6 +133,11 @@ For `llm_enrichment` in `agent` mode, the recommended path is:
|
|
|
133
133
|
|
|
134
134
|
Only send `user_schema` / `response_schema` when you intentionally want to override the generated schema and keep it aligned yourself. The installed recipe surface at `autotouch columns recipe --type llm_enrichment` follows this contract.
|
|
135
135
|
|
|
136
|
+
Schema ownership rules:
|
|
137
|
+
- Accepted generated schemas and explicit user schemas are the saved output contract.
|
|
138
|
+
- Row execution must not add fields, rename fields, or replace a valid locked schema.
|
|
139
|
+
- Agent-mode evidence/scored state decides which values may fill the schema; the finalizer formats those values and schema validation gates persistence.
|
|
140
|
+
|
|
136
141
|
Prompt variables in authored prompts support nested JSON access:
|
|
137
142
|
- Use flat row variables like `{{company_name}}` for scalar columns.
|
|
138
143
|
- Use dotted placeholders like `{{linkedin_lookup.linkedin_url}}` when the source column stores JSON or stringified JSON.
|
|
@@ -125,6 +125,21 @@ from autotouch_cli.commands.search import (
|
|
|
125
125
|
cmd_search_reviews as cmd_search_reviews_impl,
|
|
126
126
|
cmd_search_similar_companies as cmd_search_similar_companies_impl,
|
|
127
127
|
)
|
|
128
|
+
from autotouch_cli.commands.agents import (
|
|
129
|
+
AgentCommandRuntime as AgentCommandHandlerRuntime,
|
|
130
|
+
cmd_agents_list as cmd_agents_list_impl,
|
|
131
|
+
cmd_agents_get as cmd_agents_get_impl,
|
|
132
|
+
cmd_agents_create as cmd_agents_create_impl,
|
|
133
|
+
cmd_agents_update as cmd_agents_update_impl,
|
|
134
|
+
cmd_agents_delete as cmd_agents_delete_impl,
|
|
135
|
+
cmd_agents_run as cmd_agents_run_impl,
|
|
136
|
+
cmd_agents_runs as cmd_agents_runs_impl,
|
|
137
|
+
cmd_agents_watch as cmd_agents_watch_impl,
|
|
138
|
+
cmd_agents_generate_persona as cmd_agents_generate_persona_impl,
|
|
139
|
+
cmd_agents_generate_topics as cmd_agents_generate_topics_impl,
|
|
140
|
+
cmd_agents_generate_job_signal as cmd_agents_generate_job_signal_impl,
|
|
141
|
+
cmd_agents_signals as cmd_agents_signals_impl,
|
|
142
|
+
)
|
|
128
143
|
from autotouch_cli.commands.sequences import (
|
|
129
144
|
SequenceCommandRuntime as SequenceCommandHandlerRuntime,
|
|
130
145
|
cmd_sequences_clone as cmd_sequences_clone_impl,
|
|
@@ -1164,6 +1179,17 @@ def _sequence_command_runtime() -> SequenceCommandHandlerRuntime:
|
|
|
1164
1179
|
)
|
|
1165
1180
|
|
|
1166
1181
|
|
|
1182
|
+
def _agent_command_runtime() -> AgentCommandHandlerRuntime:
|
|
1183
|
+
return AgentCommandHandlerRuntime(
|
|
1184
|
+
resolve_token=_resolve_token,
|
|
1185
|
+
request_api=_request_api,
|
|
1186
|
+
print_json=lambda data, compact=False: _print_json(data, compact=compact),
|
|
1187
|
+
load_json_input=_load_json_input,
|
|
1188
|
+
api_url=_api_url,
|
|
1189
|
+
default_timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
|
|
1167
1193
|
def _column_command_runtime() -> ColumnCommandHandlerRuntime:
|
|
1168
1194
|
return ColumnCommandHandlerRuntime(
|
|
1169
1195
|
resolve_token=_resolve_token,
|
|
@@ -1420,6 +1446,20 @@ _register("columns_run_next", cmd_columns_run_next_impl, _column_command_runtime
|
|
|
1420
1446
|
_register("columns_stop", cmd_columns_stop_impl, _column_command_runtime)
|
|
1421
1447
|
_register("columns_estimate", cmd_columns_estimate_impl, _column_command_runtime)
|
|
1422
1448
|
|
|
1449
|
+
# -- Agent commands --
|
|
1450
|
+
_register("agents_list", cmd_agents_list_impl, _agent_command_runtime)
|
|
1451
|
+
_register("agents_get", cmd_agents_get_impl, _agent_command_runtime)
|
|
1452
|
+
_register("agents_create", cmd_agents_create_impl, _agent_command_runtime)
|
|
1453
|
+
_register("agents_update", cmd_agents_update_impl, _agent_command_runtime)
|
|
1454
|
+
_register("agents_delete", cmd_agents_delete_impl, _agent_command_runtime)
|
|
1455
|
+
_register("agents_run", cmd_agents_run_impl, _agent_command_runtime)
|
|
1456
|
+
_register("agents_runs", cmd_agents_runs_impl, _agent_command_runtime)
|
|
1457
|
+
_register("agents_watch", cmd_agents_watch_impl, _agent_command_runtime)
|
|
1458
|
+
_register("agents_generate_persona", cmd_agents_generate_persona_impl, _agent_command_runtime)
|
|
1459
|
+
_register("agents_generate_topics", cmd_agents_generate_topics_impl, _agent_command_runtime)
|
|
1460
|
+
_register("agents_generate_job_signal", cmd_agents_generate_job_signal_impl, _agent_command_runtime)
|
|
1461
|
+
_register("agents_signals", cmd_agents_signals_impl, _agent_command_runtime)
|
|
1462
|
+
|
|
1423
1463
|
# -- Sequence commands --
|
|
1424
1464
|
_register("sequences_list", cmd_sequences_list_impl, _sequence_command_runtime)
|
|
1425
1465
|
_register("sequences_get", cmd_sequences_get_impl, _sequence_command_runtime)
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Scheduled agent CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable, Dict, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class AgentCommandRuntime:
|
|
15
|
+
resolve_token: Callable[[Optional[str], bool], Optional[str]]
|
|
16
|
+
request_api: Callable[..., Any]
|
|
17
|
+
print_json: Callable[[Any, bool], None]
|
|
18
|
+
load_json_input: Callable[..., Any]
|
|
19
|
+
api_url: Callable[[Optional[str]], str]
|
|
20
|
+
default_timeout_seconds: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# CRUD
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def cmd_agents_list(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
28
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
29
|
+
data = runtime.request_api(
|
|
30
|
+
"GET", "/api/agents",
|
|
31
|
+
base_url=args.base_url, token=token,
|
|
32
|
+
use_x_api_key=args.use_x_api_key,
|
|
33
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
34
|
+
)
|
|
35
|
+
runtime.print_json(data, args.compact)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cmd_agents_get(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
39
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
40
|
+
data = runtime.request_api(
|
|
41
|
+
"GET", f"/api/agents/{args.agent_id}",
|
|
42
|
+
base_url=args.base_url, token=token,
|
|
43
|
+
use_x_api_key=args.use_x_api_key,
|
|
44
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
45
|
+
)
|
|
46
|
+
runtime.print_json(data, args.compact)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_agents_create(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
50
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
51
|
+
|
|
52
|
+
if getattr(args, "from_file", None):
|
|
53
|
+
payload = runtime.load_json_input(args.from_file)
|
|
54
|
+
else:
|
|
55
|
+
payload: Dict[str, Any] = {}
|
|
56
|
+
if args.name:
|
|
57
|
+
payload["name"] = args.name
|
|
58
|
+
if args.target:
|
|
59
|
+
payload["targetType"] = args.target
|
|
60
|
+
if args.assigned_user:
|
|
61
|
+
payload["assignedUserId"] = args.assigned_user
|
|
62
|
+
if args.schedule_time:
|
|
63
|
+
tz = getattr(args, "schedule_tz", None) or "UTC"
|
|
64
|
+
payload["schedule"] = {"timezone": tz, "timeOfDay": args.schedule_time}
|
|
65
|
+
if getattr(args, "auto_find_email", False):
|
|
66
|
+
payload["autoFindEmail"] = True
|
|
67
|
+
if getattr(args, "auto_find_phone", False):
|
|
68
|
+
payload["autoFindPhone"] = True
|
|
69
|
+
if getattr(args, "auto_broaden", False):
|
|
70
|
+
payload["autoBroadenSearch"] = True
|
|
71
|
+
status = getattr(args, "status", None)
|
|
72
|
+
if status:
|
|
73
|
+
payload["status"] = status
|
|
74
|
+
|
|
75
|
+
# AI generation: generate persona, signals, and create in one shot
|
|
76
|
+
if getattr(args, "generate", False) and "personaConfig" not in payload:
|
|
77
|
+
target = payload.get("targetType", "LEADS")
|
|
78
|
+
sys.stderr.write(f"Generating persona for {target}...\n")
|
|
79
|
+
persona_data = runtime.request_api(
|
|
80
|
+
"POST", "/api/agents/generate-persona",
|
|
81
|
+
base_url=args.base_url, token=token,
|
|
82
|
+
use_x_api_key=args.use_x_api_key,
|
|
83
|
+
json={"targetType": target},
|
|
84
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
85
|
+
)
|
|
86
|
+
persona = persona_data.get("persona") or persona_data.get("personaConfig") or {}
|
|
87
|
+
payload["personaConfig"] = persona
|
|
88
|
+
sys.stderr.write(f" Persona generated: {json.dumps(persona, ensure_ascii=False)[:200]}...\n")
|
|
89
|
+
|
|
90
|
+
# Generate topics
|
|
91
|
+
sys.stderr.write("Generating topic suggestions...\n")
|
|
92
|
+
topic_data = runtime.request_api(
|
|
93
|
+
"POST", "/api/agents/generate-signal-topics",
|
|
94
|
+
base_url=args.base_url, token=token,
|
|
95
|
+
use_x_api_key=args.use_x_api_key,
|
|
96
|
+
json={"targetType": target, "personaConfig": persona},
|
|
97
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
98
|
+
)
|
|
99
|
+
topics = topic_data.get("topics") or []
|
|
100
|
+
sys.stderr.write(f" Topics: {topics}\n")
|
|
101
|
+
|
|
102
|
+
# Generate job signal
|
|
103
|
+
sys.stderr.write("Generating job signal config...\n")
|
|
104
|
+
job_data = runtime.request_api(
|
|
105
|
+
"POST", "/api/agents/generate-job-signal",
|
|
106
|
+
base_url=args.base_url, token=token,
|
|
107
|
+
use_x_api_key=args.use_x_api_key,
|
|
108
|
+
json={"targetType": target, "personaConfig": persona},
|
|
109
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
110
|
+
)
|
|
111
|
+
job_titles = job_data.get("jobTitles") or []
|
|
112
|
+
job_keywords = job_data.get("keywords") or []
|
|
113
|
+
sys.stderr.write(f" Job titles: {job_titles}\n")
|
|
114
|
+
|
|
115
|
+
# Assemble signals
|
|
116
|
+
signals = []
|
|
117
|
+
if topics:
|
|
118
|
+
signals.append({
|
|
119
|
+
"key": "linkedin-topic-post-engagement",
|
|
120
|
+
"config": {"topics": [{"id": "", "label": t, "trackingMode": "all"} for t in topics[:5]]},
|
|
121
|
+
})
|
|
122
|
+
if job_titles or job_keywords:
|
|
123
|
+
signals.append({
|
|
124
|
+
"key": "hiring-signals",
|
|
125
|
+
"config": {"jobTitles": job_titles, "keywords": job_keywords, "remoteOnly": False, "lookbackWindow": "7d"},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
# News scan: derive buying trigger topics from persona
|
|
129
|
+
news_topics = ["raised funding", "new executive hire"]
|
|
130
|
+
industries = (persona.get("industries") or [])[:2]
|
|
131
|
+
if industries:
|
|
132
|
+
news_topics.append(f"expansion {industries[0]}")
|
|
133
|
+
signals.append({
|
|
134
|
+
"key": "news-scan",
|
|
135
|
+
"config": {"topics": news_topics, "lookback": "24h"},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if signals:
|
|
139
|
+
payload["selectedSignals"] = signals
|
|
140
|
+
|
|
141
|
+
data = runtime.request_api(
|
|
142
|
+
"POST", "/api/agents",
|
|
143
|
+
base_url=args.base_url, token=token,
|
|
144
|
+
use_x_api_key=args.use_x_api_key,
|
|
145
|
+
json=payload,
|
|
146
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
147
|
+
)
|
|
148
|
+
runtime.print_json(data, args.compact)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_agents_update(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
152
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
153
|
+
|
|
154
|
+
if getattr(args, "from_file", None):
|
|
155
|
+
payload = runtime.load_json_input(args.from_file)
|
|
156
|
+
else:
|
|
157
|
+
payload: Dict[str, Any] = {}
|
|
158
|
+
if getattr(args, "name", None):
|
|
159
|
+
payload["name"] = args.name
|
|
160
|
+
if getattr(args, "status", None):
|
|
161
|
+
payload["status"] = args.status
|
|
162
|
+
if getattr(args, "auto_find_email", None) is not None:
|
|
163
|
+
payload["autoFindEmail"] = args.auto_find_email
|
|
164
|
+
if getattr(args, "auto_find_phone", None) is not None:
|
|
165
|
+
payload["autoFindPhone"] = args.auto_find_phone
|
|
166
|
+
if getattr(args, "auto_broaden", None) is not None:
|
|
167
|
+
payload["autoBroadenSearch"] = args.auto_broaden
|
|
168
|
+
|
|
169
|
+
data = runtime.request_api(
|
|
170
|
+
"PATCH", f"/api/agents/{args.agent_id}",
|
|
171
|
+
base_url=args.base_url, token=token,
|
|
172
|
+
use_x_api_key=args.use_x_api_key,
|
|
173
|
+
json=payload,
|
|
174
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
175
|
+
)
|
|
176
|
+
runtime.print_json(data, args.compact)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_agents_delete(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
180
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
181
|
+
data = runtime.request_api(
|
|
182
|
+
"PATCH", f"/api/agents/{args.agent_id}",
|
|
183
|
+
base_url=args.base_url, token=token,
|
|
184
|
+
use_x_api_key=args.use_x_api_key,
|
|
185
|
+
json={"status": "DRAFT"},
|
|
186
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
187
|
+
)
|
|
188
|
+
sys.stderr.write(f"Agent {args.agent_id} deleted.\n")
|
|
189
|
+
runtime.print_json(data, args.compact)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Run management
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def cmd_agents_run(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
197
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
198
|
+
data = runtime.request_api(
|
|
199
|
+
"POST", f"/api/agents/{args.agent_id}/run-now",
|
|
200
|
+
base_url=args.base_url, token=token,
|
|
201
|
+
use_x_api_key=args.use_x_api_key,
|
|
202
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
203
|
+
)
|
|
204
|
+
runtime.print_json(data, args.compact)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def cmd_agents_runs(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
208
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
209
|
+
params = {"limit": max(1, int(getattr(args, "limit", 10) or 10))}
|
|
210
|
+
data = runtime.request_api(
|
|
211
|
+
"GET", f"/api/agents/{args.agent_id}/runs",
|
|
212
|
+
base_url=args.base_url, token=token,
|
|
213
|
+
use_x_api_key=args.use_x_api_key,
|
|
214
|
+
params=params,
|
|
215
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
216
|
+
)
|
|
217
|
+
runtime.print_json(data, args.compact)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cmd_agents_watch(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
221
|
+
"""Trigger a run and poll until complete."""
|
|
222
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
223
|
+
|
|
224
|
+
# Trigger
|
|
225
|
+
run_data = runtime.request_api(
|
|
226
|
+
"POST", f"/api/agents/{args.agent_id}/run-now",
|
|
227
|
+
base_url=args.base_url, token=token,
|
|
228
|
+
use_x_api_key=args.use_x_api_key,
|
|
229
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
230
|
+
)
|
|
231
|
+
run_id = run_data.get("id", "")
|
|
232
|
+
sys.stderr.write(f"Run {run_id} queued. Watching...\n")
|
|
233
|
+
|
|
234
|
+
interval = max(5, int(getattr(args, "interval", 10) or 10))
|
|
235
|
+
max_wait = max(60, int(getattr(args, "max_wait", 1800) or 1800))
|
|
236
|
+
elapsed = 0
|
|
237
|
+
|
|
238
|
+
while elapsed < max_wait:
|
|
239
|
+
time.sleep(interval)
|
|
240
|
+
elapsed += interval
|
|
241
|
+
|
|
242
|
+
runs_data = runtime.request_api(
|
|
243
|
+
"GET", f"/api/agents/{args.agent_id}/runs",
|
|
244
|
+
base_url=args.base_url, token=token,
|
|
245
|
+
use_x_api_key=args.use_x_api_key,
|
|
246
|
+
params={"limit": 1},
|
|
247
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
248
|
+
)
|
|
249
|
+
runs = runs_data.get("runs", [])
|
|
250
|
+
if not runs:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
latest = runs[0]
|
|
254
|
+
status = latest.get("status", "")
|
|
255
|
+
leads = latest.get("resultCounts", {}).get("leads", 0)
|
|
256
|
+
sys.stderr.write(f" [{elapsed}s] {status} | leads={leads}\n")
|
|
257
|
+
|
|
258
|
+
if status in ("SUCCEEDED", "FAILED"):
|
|
259
|
+
runtime.print_json(latest, args.compact)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
sys.stderr.write(f"Timed out after {max_wait}s. Run may still be in progress.\n")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# AI generation
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def cmd_agents_generate_persona(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
270
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
271
|
+
payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
|
|
272
|
+
|
|
273
|
+
current = getattr(args, "current_config", None)
|
|
274
|
+
if current:
|
|
275
|
+
payload["currentPersonaConfig"] = runtime.load_json_input(current)
|
|
276
|
+
|
|
277
|
+
data = runtime.request_api(
|
|
278
|
+
"POST", "/api/agents/generate-persona",
|
|
279
|
+
base_url=args.base_url, token=token,
|
|
280
|
+
use_x_api_key=args.use_x_api_key,
|
|
281
|
+
json=payload,
|
|
282
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
283
|
+
)
|
|
284
|
+
runtime.print_json(data, args.compact)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_agents_generate_topics(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
288
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
289
|
+
payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
|
|
290
|
+
|
|
291
|
+
persona = getattr(args, "persona", None)
|
|
292
|
+
if persona:
|
|
293
|
+
payload["personaConfig"] = runtime.load_json_input(persona)
|
|
294
|
+
|
|
295
|
+
current = getattr(args, "current_topics", None)
|
|
296
|
+
if current:
|
|
297
|
+
payload["currentTopics"] = [t.strip() for t in current.split(",") if t.strip()]
|
|
298
|
+
|
|
299
|
+
data = runtime.request_api(
|
|
300
|
+
"POST", "/api/agents/generate-signal-topics",
|
|
301
|
+
base_url=args.base_url, token=token,
|
|
302
|
+
use_x_api_key=args.use_x_api_key,
|
|
303
|
+
json=payload,
|
|
304
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
305
|
+
)
|
|
306
|
+
runtime.print_json(data, args.compact)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def cmd_agents_generate_job_signal(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
310
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
311
|
+
payload: Dict[str, Any] = {"targetType": getattr(args, "target", "LEADS") or "LEADS"}
|
|
312
|
+
|
|
313
|
+
persona = getattr(args, "persona", None)
|
|
314
|
+
if persona:
|
|
315
|
+
payload["personaConfig"] = runtime.load_json_input(persona)
|
|
316
|
+
|
|
317
|
+
current_titles = getattr(args, "current_titles", None)
|
|
318
|
+
if current_titles:
|
|
319
|
+
payload["currentJobTitles"] = [t.strip() for t in current_titles.split(",") if t.strip()]
|
|
320
|
+
|
|
321
|
+
current_keywords = getattr(args, "current_keywords", None)
|
|
322
|
+
if current_keywords:
|
|
323
|
+
payload["currentKeywords"] = [k.strip() for k in current_keywords.split(",") if k.strip()]
|
|
324
|
+
|
|
325
|
+
data = runtime.request_api(
|
|
326
|
+
"POST", "/api/agents/generate-job-signal",
|
|
327
|
+
base_url=args.base_url, token=token,
|
|
328
|
+
use_x_api_key=args.use_x_api_key,
|
|
329
|
+
json=payload,
|
|
330
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
331
|
+
)
|
|
332
|
+
runtime.print_json(data, args.compact)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Skill packs / signals
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
def cmd_agents_signals(args: argparse.Namespace, *, runtime: AgentCommandRuntime) -> None:
|
|
340
|
+
token = runtime.resolve_token(args.token, required=True)
|
|
341
|
+
data = runtime.request_api(
|
|
342
|
+
"GET", "/api/agents/skill-packs",
|
|
343
|
+
base_url=args.base_url, token=token,
|
|
344
|
+
use_x_api_key=args.use_x_api_key,
|
|
345
|
+
timeout=args.timeout, verbose=args.verbose,
|
|
346
|
+
)
|
|
347
|
+
runtime.print_json(data, args.compact)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# Parser registration
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
def register_agents_subcommands(
|
|
355
|
+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
356
|
+
*,
|
|
357
|
+
add_api_common_arguments: Callable[[argparse.ArgumentParser], None],
|
|
358
|
+
handlers: Dict[str, Callable[[argparse.Namespace], None]],
|
|
359
|
+
) -> None:
|
|
360
|
+
pa = subparsers.add_parser("agents", help="Scheduled agent management")
|
|
361
|
+
agents_sub = pa.add_subparsers(dest="agents_cmd", required=True)
|
|
362
|
+
|
|
363
|
+
# -- list --
|
|
364
|
+
pal = agents_sub.add_parser("list", help="List all agents")
|
|
365
|
+
add_api_common_arguments(pal)
|
|
366
|
+
pal.set_defaults(func=handlers["list"])
|
|
367
|
+
|
|
368
|
+
# -- get --
|
|
369
|
+
pag = agents_sub.add_parser("get", help="Get one agent by id")
|
|
370
|
+
pag.add_argument("agent_id", help="Agent ID")
|
|
371
|
+
add_api_common_arguments(pag)
|
|
372
|
+
pag.set_defaults(func=handlers["get"])
|
|
373
|
+
|
|
374
|
+
# -- create --
|
|
375
|
+
pac = agents_sub.add_parser("create", help="Create a new agent")
|
|
376
|
+
pac.add_argument("--name", help="Agent name")
|
|
377
|
+
pac.add_argument("--target", choices=["LEADS", "COMPANIES"], help="Target type")
|
|
378
|
+
pac.add_argument("--assigned-user", dest="assigned_user", help="Assigned user ID")
|
|
379
|
+
pac.add_argument("--schedule-time", dest="schedule_time", help="Time of day (HH:MM)")
|
|
380
|
+
pac.add_argument("--schedule-tz", dest="schedule_tz", help="Timezone (default: UTC)")
|
|
381
|
+
pac.add_argument("--auto-find-email", dest="auto_find_email", action="store_true", default=False, help="Enable auto email finder")
|
|
382
|
+
pac.add_argument("--auto-find-phone", dest="auto_find_phone", action="store_true", default=False, help="Enable auto phone finder")
|
|
383
|
+
pac.add_argument("--auto-broaden", dest="auto_broaden", action="store_true", default=False, help="Enable auto broaden search")
|
|
384
|
+
pac.add_argument("--status", choices=["DRAFT", "ACTIVE", "PAUSED"], help="Initial status")
|
|
385
|
+
pac.add_argument("--from-file", dest="from_file", help="Path to JSON payload file")
|
|
386
|
+
pac.add_argument("--generate", action="store_true", help="Auto-generate persona, topics, and job signal config")
|
|
387
|
+
add_api_common_arguments(pac)
|
|
388
|
+
pac.set_defaults(func=handlers["create"])
|
|
389
|
+
|
|
390
|
+
# -- update --
|
|
391
|
+
pau = agents_sub.add_parser("update", help="Update an existing agent")
|
|
392
|
+
pau.add_argument("agent_id", help="Agent ID")
|
|
393
|
+
pau.add_argument("--name", help="New agent name")
|
|
394
|
+
pau.add_argument("--status", choices=["DRAFT", "ACTIVE", "PAUSED"], help="New status")
|
|
395
|
+
pau.add_argument("--auto-find-email", dest="auto_find_email", type=_bool_arg, default=None, help="Enable/disable auto email finder")
|
|
396
|
+
pau.add_argument("--auto-find-phone", dest="auto_find_phone", type=_bool_arg, default=None, help="Enable/disable auto phone finder")
|
|
397
|
+
pau.add_argument("--auto-broaden", dest="auto_broaden", type=_bool_arg, default=None, help="Enable/disable auto broaden search")
|
|
398
|
+
pau.add_argument("--from-file", dest="from_file", help="Path to JSON patch payload file")
|
|
399
|
+
add_api_common_arguments(pau)
|
|
400
|
+
pau.set_defaults(func=handlers["update"])
|
|
401
|
+
|
|
402
|
+
# -- delete --
|
|
403
|
+
pad = agents_sub.add_parser("delete", help="Delete an agent (sets status to DRAFT)")
|
|
404
|
+
pad.add_argument("agent_id", help="Agent ID")
|
|
405
|
+
add_api_common_arguments(pad)
|
|
406
|
+
pad.set_defaults(func=handlers["delete"])
|
|
407
|
+
|
|
408
|
+
# -- run --
|
|
409
|
+
par = agents_sub.add_parser("run", help="Trigger an immediate run")
|
|
410
|
+
par.add_argument("agent_id", help="Agent ID")
|
|
411
|
+
add_api_common_arguments(par)
|
|
412
|
+
par.set_defaults(func=handlers["run"])
|
|
413
|
+
|
|
414
|
+
# -- runs --
|
|
415
|
+
pars = agents_sub.add_parser("runs", help="List recent runs for an agent")
|
|
416
|
+
pars.add_argument("agent_id", help="Agent ID")
|
|
417
|
+
pars.add_argument("--limit", type=int, default=10, help="Max runs to return (default: 10)")
|
|
418
|
+
add_api_common_arguments(pars)
|
|
419
|
+
pars.set_defaults(func=handlers["runs"])
|
|
420
|
+
|
|
421
|
+
# -- watch --
|
|
422
|
+
paw = agents_sub.add_parser("watch", help="Trigger a run and poll until complete")
|
|
423
|
+
paw.add_argument("agent_id", help="Agent ID")
|
|
424
|
+
paw.add_argument("--interval", type=int, default=10, help="Poll interval in seconds (default: 10)")
|
|
425
|
+
paw.add_argument("--max-wait", dest="max_wait", type=int, default=1800, help="Max wait in seconds (default: 1800)")
|
|
426
|
+
add_api_common_arguments(paw)
|
|
427
|
+
paw.set_defaults(func=handlers["watch"])
|
|
428
|
+
|
|
429
|
+
# -- generate-persona --
|
|
430
|
+
pagp = agents_sub.add_parser("generate-persona", help="Generate ICP persona config via AI")
|
|
431
|
+
pagp.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
|
|
432
|
+
pagp.add_argument("--current-config", dest="current_config", help="Path to current persona config JSON")
|
|
433
|
+
add_api_common_arguments(pagp)
|
|
434
|
+
pagp.set_defaults(func=handlers["generate_persona"])
|
|
435
|
+
|
|
436
|
+
# -- generate-topics --
|
|
437
|
+
pagt = agents_sub.add_parser("generate-topics", help="Generate signal topic suggestions via AI")
|
|
438
|
+
pagt.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
|
|
439
|
+
pagt.add_argument("--persona", help="Path to persona config JSON")
|
|
440
|
+
pagt.add_argument("--current-topics", dest="current_topics", help="Comma-separated current topics")
|
|
441
|
+
add_api_common_arguments(pagt)
|
|
442
|
+
pagt.set_defaults(func=handlers["generate_topics"])
|
|
443
|
+
|
|
444
|
+
# -- generate-job-signal --
|
|
445
|
+
pagj = agents_sub.add_parser("generate-job-signal", help="Generate job signal config via AI")
|
|
446
|
+
pagj.add_argument("--target", choices=["LEADS", "COMPANIES"], default="LEADS", help="Target type")
|
|
447
|
+
pagj.add_argument("--persona", help="Path to persona config JSON")
|
|
448
|
+
pagj.add_argument("--current-titles", dest="current_titles", help="Comma-separated current job titles")
|
|
449
|
+
pagj.add_argument("--current-keywords", dest="current_keywords", help="Comma-separated current keywords")
|
|
450
|
+
add_api_common_arguments(pagj)
|
|
451
|
+
pagj.set_defaults(func=handlers["generate_job_signal"])
|
|
452
|
+
|
|
453
|
+
# -- signals --
|
|
454
|
+
pass_ = agents_sub.add_parser("signals", help="List available signal skill packs")
|
|
455
|
+
add_api_common_arguments(pass_)
|
|
456
|
+
pass_.set_defaults(func=handlers["signals"])
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _bool_arg(v: str) -> bool:
|
|
460
|
+
"""Parse boolean CLI argument values."""
|
|
461
|
+
if v.lower() in ("true", "1", "yes"):
|
|
462
|
+
return True
|
|
463
|
+
if v.lower() in ("false", "0", "no"):
|
|
464
|
+
return False
|
|
465
|
+
raise argparse.ArgumentTypeError(f"Expected true/false, got: {v}")
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import argparse
|
|
10
10
|
from typing import Any, Callable, Dict, List, Optional
|
|
11
11
|
|
|
12
|
+
from autotouch_cli.commands.agents import register_agents_subcommands
|
|
12
13
|
from autotouch_cli.commands.auth import register_auth_subcommands
|
|
13
14
|
from autotouch_cli.commands.cells import register_cells_subcommands
|
|
14
15
|
from autotouch_cli.commands.columns import register_columns_subcommands
|
|
@@ -756,6 +757,26 @@ def build_parser(
|
|
|
756
757
|
recipe_types=LINKEDIN_RECIPE_TYPES,
|
|
757
758
|
)
|
|
758
759
|
|
|
760
|
+
# agents
|
|
761
|
+
register_agents_subcommands(
|
|
762
|
+
sub,
|
|
763
|
+
add_api_common_arguments=add_api,
|
|
764
|
+
handlers={
|
|
765
|
+
"list": commands["agents_list"],
|
|
766
|
+
"get": commands["agents_get"],
|
|
767
|
+
"create": commands["agents_create"],
|
|
768
|
+
"update": commands["agents_update"],
|
|
769
|
+
"delete": commands["agents_delete"],
|
|
770
|
+
"run": commands["agents_run"],
|
|
771
|
+
"runs": commands["agents_runs"],
|
|
772
|
+
"watch": commands["agents_watch"],
|
|
773
|
+
"generate_persona": commands["agents_generate_persona"],
|
|
774
|
+
"generate_topics": commands["agents_generate_topics"],
|
|
775
|
+
"generate_job_signal": commands["agents_generate_job_signal"],
|
|
776
|
+
"signals": commands["agents_signals"],
|
|
777
|
+
},
|
|
778
|
+
)
|
|
779
|
+
|
|
759
780
|
# Backward-compatible aliases
|
|
760
781
|
# run
|
|
761
782
|
palias_run = sub.add_parser("run", help="Alias for: columns run")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autotouch-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.63
|
|
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,11 @@ For `llm_enrichment` in `agent` mode, the recommended path is:
|
|
|
158
158
|
|
|
159
159
|
Only send `user_schema` / `response_schema` when you intentionally want to override the generated schema and keep it aligned yourself. The installed recipe surface at `autotouch columns recipe --type llm_enrichment` follows this contract.
|
|
160
160
|
|
|
161
|
+
Schema ownership rules:
|
|
162
|
+
- Accepted generated schemas and explicit user schemas are the saved output contract.
|
|
163
|
+
- Row execution must not add fields, rename fields, or replace a valid locked schema.
|
|
164
|
+
- Agent-mode evidence/scored state decides which values may fill the schema; the finalizer formats those values and schema validation gates persistence.
|
|
165
|
+
|
|
161
166
|
Prompt variables in authored prompts support nested JSON access:
|
|
162
167
|
- Use flat row variables like `{{company_name}}` for scalar columns.
|
|
163
168
|
- Use dotted placeholders like `{{linkedin_lookup.linkedin_url}}` when the source column stores JSON or stringified JSON.
|
|
@@ -17,6 +17,7 @@ autotouch_cli.egg-info/entry_points.txt
|
|
|
17
17
|
autotouch_cli.egg-info/requires.txt
|
|
18
18
|
autotouch_cli.egg-info/top_level.txt
|
|
19
19
|
autotouch_cli/commands/__init__.py
|
|
20
|
+
autotouch_cli/commands/agents.py
|
|
20
21
|
autotouch_cli/commands/auth.py
|
|
21
22
|
autotouch_cli/commands/cells.py
|
|
22
23
|
autotouch_cli/commands/columns.py
|
|
@@ -219,7 +219,7 @@ _FORMATTER_POLICY = AutoRunExecutionPolicy(
|
|
|
219
219
|
|
|
220
220
|
_LLM_POLICY = AutoRunExecutionPolicy(
|
|
221
221
|
name="llm",
|
|
222
|
-
orchestrator_provider="
|
|
222
|
+
orchestrator_provider="llm",
|
|
223
223
|
paid=True,
|
|
224
224
|
force_rerun=False,
|
|
225
225
|
respect_existing_done=True,
|
|
@@ -795,10 +795,10 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
795
795
|
},
|
|
796
796
|
},
|
|
797
797
|
recipe_notes=(
|
|
798
|
-
"
|
|
798
|
+
"Strict mode requires `companyDomain`; LinkedIn is optional.",
|
|
799
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.",
|
|
800
|
-
"
|
|
801
|
-
"If companyDomain is missing in the table, derive or enrich that domain column first.",
|
|
800
|
+
"Strict single-mode sync can use either a usable hard-identity mapping (LinkedIn, email, or phone) or mapped `firstName` / `lastName` values that create a provisional lead.",
|
|
801
|
+
"If `companyDomain` is missing in the table, derive or enrich that domain column first, or explicitly opt into `config.requireCompanyDomain=false` for LinkedIn-first sync.",
|
|
802
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\"}.",
|
|
803
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.",
|
|
804
804
|
"Sync to Leads is non-billable.",
|
|
@@ -812,7 +812,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
812
812
|
"requirements": {
|
|
813
813
|
"config_provider": "add_to_crm",
|
|
814
814
|
"field_mappings": "required",
|
|
815
|
-
"eligibility": "company domain plus
|
|
815
|
+
"eligibility": "strict mode requires company domain plus either a usable hard identity signal (LinkedIn, email, or phone) or, in single mode, mapped firstName/lastName; LinkedIn-first mode requires linkedinUrl when companyDomain is optional",
|
|
816
816
|
},
|
|
817
817
|
"billing": {
|
|
818
818
|
"billable": False,
|
|
@@ -824,7 +824,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
824
824
|
"primary_provider_value": "add_to_crm",
|
|
825
825
|
"field_mappings_field": "config.fieldMappings",
|
|
826
826
|
"source_columns_field": "config.sourceColumns",
|
|
827
|
-
"eligibility": "company domain plus
|
|
827
|
+
"eligibility": "strict mode requires company domain plus either a usable hard identity signal (LinkedIn, email, or phone) or, in single mode, mapped firstName/lastName; LinkedIn-first mode requires linkedinUrl when companyDomain is optional",
|
|
828
828
|
"replay_behavior": "source updates only; explicit run required to backfill older upstream results",
|
|
829
829
|
"structured_source_mapping": {
|
|
830
830
|
"rule": "primitive columns map directly; structured columns use {column, path}",
|
|
@@ -839,7 +839,7 @@ _PROVIDER_CONTRACTS: Tuple[ResearchTableProviderContract, ...] = (
|
|
|
839
839
|
composition_contract={
|
|
840
840
|
"consumes": {
|
|
841
841
|
"kind": "crm_export_inputs",
|
|
842
|
-
"shape": "mapped source row values including company domain plus
|
|
842
|
+
"shape": "mapped source row values including company domain plus hard identity or, in single mode, provisional firstName/lastName fallback; structured source columns use explicit {column, path} mappings to identify the exact field",
|
|
843
843
|
},
|
|
844
844
|
"produces": {
|
|
845
845
|
"kind": "lead_ids",
|
|
@@ -1590,7 +1590,7 @@ def orchestrated_runtime_provider_keys(*, include_formatter: bool = False) -> Tu
|
|
|
1590
1590
|
providers.append(policy.orchestrator_provider)
|
|
1591
1591
|
continue
|
|
1592
1592
|
if contract.key == "llm":
|
|
1593
|
-
providers.extend(["gemini", "xai"])
|
|
1593
|
+
providers.extend(["llm", "gemini", "xai"])
|
|
1594
1594
|
continue
|
|
1595
1595
|
providers.append(policy.orchestrator_provider)
|
|
1596
1596
|
return tuple(dict.fromkeys(provider for provider in providers if provider))
|
|
@@ -1627,9 +1627,9 @@ def resolve_runtime_orchestrator_provider(
|
|
|
1627
1627
|
return None
|
|
1628
1628
|
if contract.key == "llm":
|
|
1629
1629
|
runtime_provider = _normalize((config or {}).get("provider"))
|
|
1630
|
-
if runtime_provider in {"gemini", "xai"}:
|
|
1630
|
+
if runtime_provider in {"llm", "gemini", "xai"}:
|
|
1631
1631
|
return runtime_provider
|
|
1632
|
-
if runtime_provider
|
|
1632
|
+
if not runtime_provider:
|
|
1633
1633
|
return contract.execution_policy.orchestrator_provider
|
|
1634
1634
|
return None
|
|
1635
1635
|
return contract.execution_policy.orchestrator_provider
|
|
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
|
|
File without changes
|