pipefy-cli 0.3.0a1__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.
- pipefy_cli/__init__.py +7 -0
- pipefy_cli/_docs.py +3 -0
- pipefy_cli/auth.py +225 -0
- pipefy_cli/commands/__init__.py +5 -0
- pipefy_cli/commands/_common.py +359 -0
- pipefy_cli/commands/agent.py +358 -0
- pipefy_cli/commands/ai_automation.py +318 -0
- pipefy_cli/commands/attachment.py +122 -0
- pipefy_cli/commands/audit.py +63 -0
- pipefy_cli/commands/auth.py +519 -0
- pipefy_cli/commands/automation.py +529 -0
- pipefy_cli/commands/card.py +439 -0
- pipefy_cli/commands/email.py +169 -0
- pipefy_cli/commands/export.py +61 -0
- pipefy_cli/commands/field.py +113 -0
- pipefy_cli/commands/field_condition.py +201 -0
- pipefy_cli/commands/graphql.py +110 -0
- pipefy_cli/commands/introspect.py +110 -0
- pipefy_cli/commands/label.py +109 -0
- pipefy_cli/commands/member.py +126 -0
- pipefy_cli/commands/org.py +60 -0
- pipefy_cli/commands/phase.py +264 -0
- pipefy_cli/commands/pipe.py +167 -0
- pipefy_cli/commands/portal.py +871 -0
- pipefy_cli/commands/record.py +219 -0
- pipefy_cli/commands/relation.py +173 -0
- pipefy_cli/commands/report_org.py +241 -0
- pipefy_cli/commands/report_pipe.py +290 -0
- pipefy_cli/commands/table.py +151 -0
- pipefy_cli/commands/usage.py +100 -0
- pipefy_cli/commands/webhook.py +141 -0
- pipefy_cli/main.py +132 -0
- pipefy_cli/output/__init__.py +8 -0
- pipefy_cli/output/json_renderer.py +24 -0
- pipefy_cli/output/rich_renderer.py +81 -0
- pipefy_cli/settings.py +66 -0
- pipefy_cli-0.3.0a1.dist-info/METADATA +104 -0
- pipefy_cli-0.3.0a1.dist-info/RECORD +40 -0
- pipefy_cli-0.3.0a1.dist-info/WHEEL +4 -0
- pipefy_cli-0.3.0a1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""AI Agent operations (list, CRUD, logs, behavior validation)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import (
|
|
9
|
+
CreateAiAgentInput,
|
|
10
|
+
PipefyClient,
|
|
11
|
+
UpdateAiAgentInput,
|
|
12
|
+
)
|
|
13
|
+
from pipefy_sdk.ai_preflight import validate_ai_agent_behaviors_sdk
|
|
14
|
+
from pipefy_sdk.behavior_placeholders import (
|
|
15
|
+
expand_behaviors_placeholders,
|
|
16
|
+
normalize_pipefy_ai_instruction_tokens,
|
|
17
|
+
)
|
|
18
|
+
from pydantic import ValidationError
|
|
19
|
+
|
|
20
|
+
from pipefy_cli.commands._common import (
|
|
21
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
22
|
+
confirm_destructive,
|
|
23
|
+
parse_json_value,
|
|
24
|
+
resource_id_argument,
|
|
25
|
+
run_cli_command,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
agent_app = typer.Typer(help="AI Agents (repo-scoped).", no_args_is_help=True)
|
|
29
|
+
logs_app = typer.Typer(help="AI Agent execution logs.", no_args_is_help=True)
|
|
30
|
+
agent_app.add_typer(logs_app, name="logs")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_behaviors_json(raw: str) -> list[dict[str, Any]]:
|
|
34
|
+
parsed = parse_json_value(raw, "--behaviors")
|
|
35
|
+
if not isinstance(parsed, list):
|
|
36
|
+
raise typer.BadParameter("--behaviors must be a JSON array")
|
|
37
|
+
out = [b for b in parsed if isinstance(b, dict)]
|
|
38
|
+
if not out:
|
|
39
|
+
raise typer.BadParameter("--behaviors must contain at least one object")
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _raise_if_preflight_blocks(payload: dict[str, Any]) -> None:
|
|
44
|
+
if not payload.get("success"):
|
|
45
|
+
msg = payload.get("error") or payload.get("message") or "Validation failed"
|
|
46
|
+
raise typer.BadParameter(str(msg))
|
|
47
|
+
if not payload.get("valid"):
|
|
48
|
+
problems = payload.get("problems") or []
|
|
49
|
+
text = "\n".join(f" - {p}" for p in problems) if problems else "(no details)"
|
|
50
|
+
raise typer.BadParameter(f"validate-behaviors failed:\n{text}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@agent_app.command("list")
|
|
54
|
+
def agent_list(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
repo: str = typer.Option(..., "--repo", help="Pipe UUID (repoUuid)."),
|
|
57
|
+
json_out: bool = typer.Option(
|
|
58
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
59
|
+
),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""List AI agents for a pipe (``get_ai_agents``)."""
|
|
62
|
+
|
|
63
|
+
async def factory(client: PipefyClient):
|
|
64
|
+
agents = await client.get_ai_agents(repo)
|
|
65
|
+
return {"success": True, "agents": agents}
|
|
66
|
+
|
|
67
|
+
run_cli_command(ctx, json_out, factory)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@agent_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
71
|
+
def agent_get(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
uuid: str = resource_id_argument(help="Agent UUID."),
|
|
74
|
+
json_out: bool = typer.Option(
|
|
75
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
76
|
+
),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Fetch one AI agent (``get_ai_agent``)."""
|
|
79
|
+
|
|
80
|
+
async def factory(client: PipefyClient):
|
|
81
|
+
agent = await client.get_ai_agent(uuid)
|
|
82
|
+
return {"success": True, "agent": agent}
|
|
83
|
+
|
|
84
|
+
run_cli_command(ctx, json_out, factory)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@agent_app.command("create")
|
|
88
|
+
def agent_create(
|
|
89
|
+
ctx: typer.Context,
|
|
90
|
+
repo_uuid: str = typer.Option(..., "--repo-uuid", help="Pipe UUID."),
|
|
91
|
+
name: str = typer.Option(..., "--name", "-n", help="Agent display name."),
|
|
92
|
+
instruction: str = typer.Option(
|
|
93
|
+
...,
|
|
94
|
+
"--instruction",
|
|
95
|
+
help="Agent-level instruction (token-normalized).",
|
|
96
|
+
),
|
|
97
|
+
behaviors: str = typer.Option(
|
|
98
|
+
...,
|
|
99
|
+
"--behaviors",
|
|
100
|
+
help="JSON array of behavior objects.",
|
|
101
|
+
),
|
|
102
|
+
pipe: str = typer.Option(
|
|
103
|
+
...,
|
|
104
|
+
"--pipe",
|
|
105
|
+
help="Numeric pipe id for validate-behaviors pre-flight.",
|
|
106
|
+
),
|
|
107
|
+
data_sources: str | None = typer.Option(
|
|
108
|
+
None,
|
|
109
|
+
"--data-sources",
|
|
110
|
+
help="Optional JSON array of knowledge-source id strings.",
|
|
111
|
+
),
|
|
112
|
+
strict_unknown: bool = typer.Option(
|
|
113
|
+
True,
|
|
114
|
+
"--strict/--no-strict",
|
|
115
|
+
help=(
|
|
116
|
+
"Pre-flight strictness: when --strict (default), unknown actionType values "
|
|
117
|
+
"block; with --no-strict they become warnings only."
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
json_out: bool = typer.Option(
|
|
121
|
+
False, "--json", "-j", help="Print machine-readable JSON to stdout."
|
|
122
|
+
),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Create and configure an AI agent (``create_ai_agent`` + ``update_ai_agent`` chain)."""
|
|
125
|
+
behavior_list = _parse_behaviors_json(behaviors)
|
|
126
|
+
ds_raw = parse_json_value(data_sources, "--data-sources") if data_sources else None
|
|
127
|
+
data_source_ids: list[str] = []
|
|
128
|
+
if ds_raw is not None:
|
|
129
|
+
if not isinstance(ds_raw, list) or not all(isinstance(x, str) for x in ds_raw):
|
|
130
|
+
raise typer.BadParameter("--data-sources must be a JSON array of strings")
|
|
131
|
+
data_source_ids = list(ds_raw)
|
|
132
|
+
|
|
133
|
+
inst = normalize_pipefy_ai_instruction_tokens(instruction.strip())
|
|
134
|
+
try:
|
|
135
|
+
expanded = expand_behaviors_placeholders(behavior_list)
|
|
136
|
+
validated = CreateAiAgentInput(
|
|
137
|
+
name=name.strip(),
|
|
138
|
+
repo_uuid=repo_uuid.strip(),
|
|
139
|
+
instruction=inst,
|
|
140
|
+
behaviors=expanded,
|
|
141
|
+
data_source_ids=data_source_ids,
|
|
142
|
+
)
|
|
143
|
+
except ValidationError as exc:
|
|
144
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
145
|
+
|
|
146
|
+
async def factory(client: PipefyClient):
|
|
147
|
+
pre = await validate_ai_agent_behaviors_sdk(
|
|
148
|
+
client,
|
|
149
|
+
pipe.strip(),
|
|
150
|
+
[b.model_dump(by_alias=True) for b in validated.behaviors],
|
|
151
|
+
strict_unknown_action_types=strict_unknown,
|
|
152
|
+
)
|
|
153
|
+
_raise_if_preflight_blocks(pre)
|
|
154
|
+
create_result = await client.create_ai_agent(validated)
|
|
155
|
+
agent_uuid = create_result["agent_uuid"]
|
|
156
|
+
update_input = UpdateAiAgentInput(
|
|
157
|
+
uuid=agent_uuid,
|
|
158
|
+
name=validated.name,
|
|
159
|
+
repo_uuid=validated.repo_uuid,
|
|
160
|
+
instruction=validated.instruction,
|
|
161
|
+
behaviors=validated.behaviors,
|
|
162
|
+
data_source_ids=validated.data_source_ids,
|
|
163
|
+
)
|
|
164
|
+
await client.update_ai_agent(update_input)
|
|
165
|
+
out: dict[str, Any] = {
|
|
166
|
+
"success": True,
|
|
167
|
+
"agent_uuid": agent_uuid,
|
|
168
|
+
"message": f"Created agent {agent_uuid}",
|
|
169
|
+
}
|
|
170
|
+
if pre.get("warnings"):
|
|
171
|
+
out["preflight"] = pre
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
run_cli_command(ctx, json_out, factory)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@agent_app.command("update")
|
|
178
|
+
def agent_update(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
uuid: str = typer.Option(..., "--uuid", help="Agent UUID."),
|
|
181
|
+
name: str = typer.Option(..., "--name", "-n", help="Agent display name."),
|
|
182
|
+
repo_uuid: str = typer.Option(..., "--repo-uuid", help="Pipe UUID."),
|
|
183
|
+
instruction: str = typer.Option(
|
|
184
|
+
..., "--instruction", help="Agent-level instruction."
|
|
185
|
+
),
|
|
186
|
+
behaviors: str = typer.Option(
|
|
187
|
+
..., "--behaviors", help="JSON array of behavior objects."
|
|
188
|
+
),
|
|
189
|
+
pipe: str = typer.Option(
|
|
190
|
+
...,
|
|
191
|
+
"--pipe",
|
|
192
|
+
help="Numeric pipe id for validate-behaviors pre-flight.",
|
|
193
|
+
),
|
|
194
|
+
data_sources: str | None = typer.Option(
|
|
195
|
+
None,
|
|
196
|
+
"--data-sources",
|
|
197
|
+
help="Optional JSON array of knowledge-source id strings.",
|
|
198
|
+
),
|
|
199
|
+
strict_unknown: bool = typer.Option(
|
|
200
|
+
True,
|
|
201
|
+
"--strict/--no-strict",
|
|
202
|
+
help=(
|
|
203
|
+
"Pre-flight strictness: --strict (default) blocks on unknown actionType "
|
|
204
|
+
"values; --no-strict converts them to warnings."
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Replace AI agent configuration (``update_ai_agent``)."""
|
|
210
|
+
behavior_list = _parse_behaviors_json(behaviors)
|
|
211
|
+
ds_raw = parse_json_value(data_sources, "--data-sources") if data_sources else None
|
|
212
|
+
data_source_ids: list[str] = []
|
|
213
|
+
if ds_raw is not None:
|
|
214
|
+
if not isinstance(ds_raw, list) or not all(isinstance(x, str) for x in ds_raw):
|
|
215
|
+
raise typer.BadParameter("--data-sources must be a JSON array of strings")
|
|
216
|
+
data_source_ids = list(ds_raw)
|
|
217
|
+
|
|
218
|
+
inst = normalize_pipefy_ai_instruction_tokens(instruction.strip())
|
|
219
|
+
try:
|
|
220
|
+
expanded = expand_behaviors_placeholders(behavior_list)
|
|
221
|
+
validated = UpdateAiAgentInput(
|
|
222
|
+
uuid=uuid.strip(),
|
|
223
|
+
name=name.strip(),
|
|
224
|
+
repo_uuid=repo_uuid.strip(),
|
|
225
|
+
instruction=inst,
|
|
226
|
+
behaviors=expanded,
|
|
227
|
+
data_source_ids=data_source_ids,
|
|
228
|
+
)
|
|
229
|
+
except ValidationError as exc:
|
|
230
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
231
|
+
|
|
232
|
+
async def factory(client: PipefyClient):
|
|
233
|
+
pre = await validate_ai_agent_behaviors_sdk(
|
|
234
|
+
client,
|
|
235
|
+
pipe.strip(),
|
|
236
|
+
[b.model_dump(by_alias=True) for b in validated.behaviors],
|
|
237
|
+
strict_unknown_action_types=strict_unknown,
|
|
238
|
+
)
|
|
239
|
+
_raise_if_preflight_blocks(pre)
|
|
240
|
+
result = await client.update_ai_agent(validated)
|
|
241
|
+
if pre.get("warnings"):
|
|
242
|
+
return {**result, "preflight": pre}
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
run_cli_command(ctx, json_out, factory)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@agent_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
249
|
+
def agent_delete(
|
|
250
|
+
ctx: typer.Context,
|
|
251
|
+
uuid: str = resource_id_argument(help="Agent UUID."),
|
|
252
|
+
yes: bool = typer.Option(
|
|
253
|
+
False, "--yes", "-y", help="Skip interactive confirmation."
|
|
254
|
+
),
|
|
255
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Delete an AI agent (``delete_ai_agent``)."""
|
|
258
|
+
confirm_destructive(yes=yes, description=f"AI agent (UUID: {uuid})", verb="delete")
|
|
259
|
+
|
|
260
|
+
async def factory(client: PipefyClient):
|
|
261
|
+
return await client.delete_ai_agent(uuid)
|
|
262
|
+
|
|
263
|
+
run_cli_command(ctx, json_out, factory)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@agent_app.command("toggle", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
267
|
+
def agent_toggle(
|
|
268
|
+
ctx: typer.Context,
|
|
269
|
+
uuid: str = resource_id_argument(help="Agent UUID."),
|
|
270
|
+
active: bool = typer.Option(
|
|
271
|
+
True,
|
|
272
|
+
"--active/--inactive",
|
|
273
|
+
help="Enable or disable the agent.",
|
|
274
|
+
),
|
|
275
|
+
yes: bool = typer.Option(
|
|
276
|
+
False, "--yes", "-y", help="Skip confirmation when disabling."
|
|
277
|
+
),
|
|
278
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Enable or disable an AI agent (``toggle_ai_agent_status``)."""
|
|
281
|
+
if not active:
|
|
282
|
+
confirm_destructive(
|
|
283
|
+
yes=yes,
|
|
284
|
+
description=f"AI agent (UUID: {uuid})",
|
|
285
|
+
verb="disable",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def factory(client: PipefyClient):
|
|
289
|
+
return await client.toggle_ai_agent_status(uuid, active=active)
|
|
290
|
+
|
|
291
|
+
run_cli_command(ctx, json_out, factory)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@logs_app.command("list")
|
|
295
|
+
def agent_logs_list(
|
|
296
|
+
ctx: typer.Context,
|
|
297
|
+
repo: str = typer.Option(..., "--repo", help="Pipe UUID."),
|
|
298
|
+
first: int = typer.Option(30, "--first", help="Page size."),
|
|
299
|
+
after: str | None = typer.Option(None, "--after", help="Pagination cursor."),
|
|
300
|
+
status: str | None = typer.Option(
|
|
301
|
+
None, "--status", help="processing|failed|success"
|
|
302
|
+
),
|
|
303
|
+
search: str | None = typer.Option(None, "--search", help="Free-text search."),
|
|
304
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
305
|
+
) -> None:
|
|
306
|
+
"""List AI agent execution logs (``get_ai_agent_logs``)."""
|
|
307
|
+
|
|
308
|
+
async def factory(client: PipefyClient):
|
|
309
|
+
return await client.get_ai_agent_logs(
|
|
310
|
+
repo, first=first, after=after, status=status, search_term=search
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
run_cli_command(ctx, json_out, factory)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@logs_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
317
|
+
def agent_logs_get(
|
|
318
|
+
ctx: typer.Context,
|
|
319
|
+
log_id: str = resource_id_argument(help="Log UUID."),
|
|
320
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Fetch one AI agent log entry (``get_ai_agent_log_details``)."""
|
|
323
|
+
|
|
324
|
+
async def factory(client: PipefyClient):
|
|
325
|
+
return await client.get_ai_agent_log_details(log_id)
|
|
326
|
+
|
|
327
|
+
run_cli_command(ctx, json_out, factory)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@agent_app.command("validate-behaviors")
|
|
331
|
+
def agent_validate_behaviors(
|
|
332
|
+
ctx: typer.Context,
|
|
333
|
+
pipe: str = typer.Option(..., "--pipe", help="Numeric pipe id."),
|
|
334
|
+
behaviors: str = typer.Option(
|
|
335
|
+
..., "--behaviors", help="JSON array of behavior objects."
|
|
336
|
+
),
|
|
337
|
+
strict_unknown: bool = typer.Option(
|
|
338
|
+
True,
|
|
339
|
+
"--strict/--no-strict",
|
|
340
|
+
help=(
|
|
341
|
+
"Pre-flight strictness: --strict (default) reports unknown actionType "
|
|
342
|
+
"values as problems; --no-strict reports them as warnings only."
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Dry-run behavior validation (``validate_ai_agent_behaviors``)."""
|
|
348
|
+
behavior_list = _parse_behaviors_json(behaviors)
|
|
349
|
+
|
|
350
|
+
async def factory(client: PipefyClient):
|
|
351
|
+
return await validate_ai_agent_behaviors_sdk(
|
|
352
|
+
client,
|
|
353
|
+
pipe.strip(),
|
|
354
|
+
behavior_list,
|
|
355
|
+
strict_unknown_action_types=strict_unknown,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
run_cli_command(ctx, json_out, factory)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""AI Automations (generate_with_ai) via Internal API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pipefy_sdk import (
|
|
9
|
+
CreateAiAutomationInput,
|
|
10
|
+
PipefyClient,
|
|
11
|
+
UpdateAiAutomationInput,
|
|
12
|
+
)
|
|
13
|
+
from pipefy_sdk.ai_preflight import (
|
|
14
|
+
filter_ai_automation_summaries,
|
|
15
|
+
validate_ai_automation_prompt_sdk,
|
|
16
|
+
)
|
|
17
|
+
from pydantic import ValidationError
|
|
18
|
+
|
|
19
|
+
from pipefy_cli.commands._common import (
|
|
20
|
+
ID_POSITIONAL_CONTEXT_SETTINGS,
|
|
21
|
+
confirm_destructive,
|
|
22
|
+
parse_json_object,
|
|
23
|
+
parse_json_value,
|
|
24
|
+
resource_id_argument,
|
|
25
|
+
run_cli_command,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ai_automation_app = typer.Typer(
|
|
29
|
+
help="AI Automations (generate_with_ai).",
|
|
30
|
+
no_args_is_help=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _raise_if_prompt_preflight_blocks(payload: dict[str, Any]) -> None:
|
|
35
|
+
if not payload.get("success"):
|
|
36
|
+
raise typer.BadParameter(str(payload.get("error") or "validate-prompt failed"))
|
|
37
|
+
if not payload.get("valid"):
|
|
38
|
+
problems = payload.get("problems") or []
|
|
39
|
+
text = "\n".join(f" - {p}" for p in problems) if problems else "(no details)"
|
|
40
|
+
raise typer.BadParameter(f"validate-prompt failed:\n{text}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_field_ids(raw: str) -> list[str]:
|
|
44
|
+
parsed = parse_json_value(raw, "--field-ids")
|
|
45
|
+
if not isinstance(parsed, list) or not all(isinstance(x, str) for x in parsed):
|
|
46
|
+
raise typer.BadParameter("--field-ids must be a JSON array of strings")
|
|
47
|
+
return list(parsed)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _extract_existing_prompt_and_field_ids(
|
|
51
|
+
row: dict[str, Any],
|
|
52
|
+
) -> tuple[str | None, list[str] | None]:
|
|
53
|
+
"""Return ``(prompt, field_ids)`` from a ``generate_with_ai`` automation row.
|
|
54
|
+
|
|
55
|
+
Returns ``(None, None)`` for non-AI rows or rows missing the ``aiParams`` block;
|
|
56
|
+
callers must surface that as a clear user error.
|
|
57
|
+
"""
|
|
58
|
+
action_params = row.get("action_params") or row.get("actionParams") or {}
|
|
59
|
+
ai_params = action_params.get("aiParams") or action_params.get("ai_params") or {}
|
|
60
|
+
prompt = ai_params.get("value")
|
|
61
|
+
field_ids = ai_params.get("fieldIds") or ai_params.get("field_ids")
|
|
62
|
+
if not isinstance(prompt, str):
|
|
63
|
+
prompt = None
|
|
64
|
+
if isinstance(field_ids, list) and all(isinstance(x, str) for x in field_ids):
|
|
65
|
+
return prompt, list(field_ids)
|
|
66
|
+
return prompt, None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@ai_automation_app.command("list")
|
|
70
|
+
def ai_automation_list(
|
|
71
|
+
ctx: typer.Context,
|
|
72
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
73
|
+
organization: str | None = typer.Option(
|
|
74
|
+
None, "--organization", "--org", help="Optional organization id."
|
|
75
|
+
),
|
|
76
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""List AI automations for a pipe (``get_ai_automations`` / filtered ``get_automations``)."""
|
|
79
|
+
|
|
80
|
+
async def factory(client: PipefyClient):
|
|
81
|
+
rows = await client.get_automations(
|
|
82
|
+
organization_id=organization,
|
|
83
|
+
pipe_id=pipe,
|
|
84
|
+
)
|
|
85
|
+
filtered = filter_ai_automation_summaries(rows or [])
|
|
86
|
+
return {"success": True, "data": filtered, "message": "AI automations listed."}
|
|
87
|
+
|
|
88
|
+
run_cli_command(ctx, json_out, factory)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@ai_automation_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
92
|
+
def ai_automation_get(
|
|
93
|
+
ctx: typer.Context,
|
|
94
|
+
automation_id: str = resource_id_argument(help="Automation rule id."),
|
|
95
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Load one automation row (``get_ai_automation`` / ``get_automation``)."""
|
|
98
|
+
|
|
99
|
+
async def factory(client: PipefyClient):
|
|
100
|
+
row = await client.get_automation(automation_id)
|
|
101
|
+
if row is None:
|
|
102
|
+
return {
|
|
103
|
+
"success": False,
|
|
104
|
+
"message": "No automation found for the given ID.",
|
|
105
|
+
}
|
|
106
|
+
return {"success": True, "message": "AI automation retrieved.", "data": row}
|
|
107
|
+
|
|
108
|
+
run_cli_command(ctx, json_out, factory)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@ai_automation_app.command("validate-prompt")
|
|
112
|
+
def ai_automation_validate_prompt(
|
|
113
|
+
ctx: typer.Context,
|
|
114
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
115
|
+
prompt: str = typer.Option(
|
|
116
|
+
..., "--prompt", help="Prompt with %{internal_id} tokens."
|
|
117
|
+
),
|
|
118
|
+
field_ids: str = typer.Option(
|
|
119
|
+
..., "--field-ids", help="JSON array of output field internal ids."
|
|
120
|
+
),
|
|
121
|
+
event_id: str | None = typer.Option(
|
|
122
|
+
None,
|
|
123
|
+
"--event-id",
|
|
124
|
+
help="Optional trigger id to validate against the pipe catalog.",
|
|
125
|
+
),
|
|
126
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Pre-flight prompt validation (``validate_ai_automation_prompt``)."""
|
|
129
|
+
fids = _parse_field_ids(field_ids)
|
|
130
|
+
|
|
131
|
+
async def factory(client: PipefyClient):
|
|
132
|
+
return await validate_ai_automation_prompt_sdk(
|
|
133
|
+
client, pipe.strip(), prompt, fids, event_id
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
run_cli_command(ctx, json_out, factory)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@ai_automation_app.command("create")
|
|
140
|
+
def ai_automation_create(
|
|
141
|
+
ctx: typer.Context,
|
|
142
|
+
pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
|
|
143
|
+
name: str = typer.Option(..., "--name", "-n", help="Automation name."),
|
|
144
|
+
event_id: str = typer.Option(..., "--event-id", help="Trigger event id."),
|
|
145
|
+
prompt: str = typer.Option(
|
|
146
|
+
..., "--prompt", help="AI prompt with %{internal_id} refs."
|
|
147
|
+
),
|
|
148
|
+
field_ids: str = typer.Option(
|
|
149
|
+
..., "--field-ids", help="JSON array of output field ids."
|
|
150
|
+
),
|
|
151
|
+
action_repo: str | None = typer.Option(
|
|
152
|
+
None, "--action-repo", help="Optional action repo id (defaults to --pipe)."
|
|
153
|
+
),
|
|
154
|
+
skills_ids: str | None = typer.Option(
|
|
155
|
+
None, "--skills-ids", help="Optional JSON array of skill id strings."
|
|
156
|
+
),
|
|
157
|
+
event_params: str | None = typer.Option(
|
|
158
|
+
None, "--event-params", help="Optional JSON object."
|
|
159
|
+
),
|
|
160
|
+
condition: str | None = typer.Option(
|
|
161
|
+
None, "--condition", help="Optional JSON object (omit for default placeholder)."
|
|
162
|
+
),
|
|
163
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Create an AI automation (``create_ai_automation``)."""
|
|
166
|
+
fids = _parse_field_ids(field_ids)
|
|
167
|
+
ep = parse_json_object(event_params, "--event-params")
|
|
168
|
+
cond = parse_json_object(condition, "--condition")
|
|
169
|
+
skills_raw = parse_json_value(skills_ids, "--skills-ids") if skills_ids else None
|
|
170
|
+
skills: list[str] = []
|
|
171
|
+
if skills_raw is not None:
|
|
172
|
+
if not isinstance(skills_raw, list) or not all(
|
|
173
|
+
isinstance(x, str) for x in skills_raw
|
|
174
|
+
):
|
|
175
|
+
raise typer.BadParameter("--skills-ids must be a JSON array of strings")
|
|
176
|
+
skills = list(skills_raw)
|
|
177
|
+
|
|
178
|
+
async def factory(client: PipefyClient):
|
|
179
|
+
pre = await validate_ai_automation_prompt_sdk(
|
|
180
|
+
client, pipe.strip(), prompt, fids, event_id
|
|
181
|
+
)
|
|
182
|
+
_raise_if_prompt_preflight_blocks(pre)
|
|
183
|
+
try:
|
|
184
|
+
kwargs: dict[str, Any] = {
|
|
185
|
+
"name": name.strip(),
|
|
186
|
+
"event_id": event_id.strip(),
|
|
187
|
+
"pipe_id": pipe.strip(),
|
|
188
|
+
"action_repo_id": action_repo.strip() if action_repo else None,
|
|
189
|
+
"prompt": prompt,
|
|
190
|
+
"field_ids": fids,
|
|
191
|
+
"skills_ids": skills,
|
|
192
|
+
}
|
|
193
|
+
if ep is not None:
|
|
194
|
+
kwargs["event_params"] = ep
|
|
195
|
+
if cond is not None:
|
|
196
|
+
kwargs["condition"] = cond
|
|
197
|
+
inp = CreateAiAutomationInput(**kwargs)
|
|
198
|
+
except ValidationError as exc:
|
|
199
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
200
|
+
return await client.create_ai_automation(inp)
|
|
201
|
+
|
|
202
|
+
run_cli_command(ctx, json_out, factory)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@ai_automation_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
206
|
+
def ai_automation_update(
|
|
207
|
+
ctx: typer.Context,
|
|
208
|
+
automation_id: str = resource_id_argument(help="Automation rule id."),
|
|
209
|
+
pipe: str = typer.Option(
|
|
210
|
+
...,
|
|
211
|
+
"--pipe",
|
|
212
|
+
help="Pipe id for validate-prompt pre-flight.",
|
|
213
|
+
),
|
|
214
|
+
prompt: str | None = typer.Option(
|
|
215
|
+
None,
|
|
216
|
+
"--prompt",
|
|
217
|
+
help="New prompt; omit to keep the current value (re-used in pre-flight).",
|
|
218
|
+
),
|
|
219
|
+
field_ids: str | None = typer.Option(
|
|
220
|
+
None,
|
|
221
|
+
"--field-ids",
|
|
222
|
+
help=(
|
|
223
|
+
"JSON array of output field ids; omit to keep the current values "
|
|
224
|
+
"(re-used in pre-flight)."
|
|
225
|
+
),
|
|
226
|
+
),
|
|
227
|
+
name: str | None = typer.Option(None, "--name", "-n"),
|
|
228
|
+
patch_active: bool | None = typer.Option(
|
|
229
|
+
None,
|
|
230
|
+
"--patch-active/--no-patch-active",
|
|
231
|
+
help="Toggle the enabled flag; omit both flags to leave unchanged.",
|
|
232
|
+
),
|
|
233
|
+
skills_ids: str | None = typer.Option(None, "--skills-ids"),
|
|
234
|
+
event_params: str | None = typer.Option(None, "--event-params"),
|
|
235
|
+
condition: str | None = typer.Option(None, "--condition"),
|
|
236
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Update an AI automation (``update_ai_automation``).
|
|
239
|
+
|
|
240
|
+
``--prompt`` and ``--field-ids`` are optional: when omitted the CLI reads
|
|
241
|
+
the current values from the existing automation and re-uses them for the
|
|
242
|
+
``validate_ai_automation_prompt`` pre-flight. Only fields you pass
|
|
243
|
+
explicitly are sent in the patch.
|
|
244
|
+
"""
|
|
245
|
+
fids: list[str] | None = _parse_field_ids(field_ids) if field_ids else None
|
|
246
|
+
ep = parse_json_object(event_params, "--event-params")
|
|
247
|
+
cond = parse_json_object(condition, "--condition")
|
|
248
|
+
skills_raw = parse_json_value(skills_ids, "--skills-ids") if skills_ids else None
|
|
249
|
+
skills: list[str] | None = None
|
|
250
|
+
if skills_raw is not None:
|
|
251
|
+
if not isinstance(skills_raw, list) or not all(
|
|
252
|
+
isinstance(x, str) for x in skills_raw
|
|
253
|
+
):
|
|
254
|
+
raise typer.BadParameter("--skills-ids must be a JSON array of strings")
|
|
255
|
+
skills = list(skills_raw)
|
|
256
|
+
|
|
257
|
+
async def factory(client: PipefyClient):
|
|
258
|
+
row = await client.get_automation(automation_id)
|
|
259
|
+
if row is None:
|
|
260
|
+
return {"success": False, "message": "Automation not found."}
|
|
261
|
+
existing_prompt, existing_fids = _extract_existing_prompt_and_field_ids(row)
|
|
262
|
+
effective_prompt = prompt if prompt is not None else existing_prompt
|
|
263
|
+
effective_fids = fids if fids is not None else existing_fids
|
|
264
|
+
if not effective_prompt or not effective_fids:
|
|
265
|
+
raise typer.BadParameter(
|
|
266
|
+
"Could not infer current prompt / field_ids from the existing automation. "
|
|
267
|
+
"Pass --prompt and --field-ids explicitly."
|
|
268
|
+
)
|
|
269
|
+
ev = str(row.get("event_id") or row.get("eventId") or "")
|
|
270
|
+
pre = await validate_ai_automation_prompt_sdk(
|
|
271
|
+
client,
|
|
272
|
+
pipe.strip(),
|
|
273
|
+
effective_prompt,
|
|
274
|
+
effective_fids,
|
|
275
|
+
ev or None,
|
|
276
|
+
)
|
|
277
|
+
_raise_if_prompt_preflight_blocks(pre)
|
|
278
|
+
try:
|
|
279
|
+
kwargs_u: dict[str, Any] = {
|
|
280
|
+
"automation_id": str(automation_id).strip(),
|
|
281
|
+
"name": name.strip() if name else None,
|
|
282
|
+
"active": patch_active,
|
|
283
|
+
"skills_ids": skills,
|
|
284
|
+
}
|
|
285
|
+
if prompt is not None:
|
|
286
|
+
kwargs_u["prompt"] = prompt
|
|
287
|
+
if fids is not None:
|
|
288
|
+
kwargs_u["field_ids"] = fids
|
|
289
|
+
if ep is not None:
|
|
290
|
+
kwargs_u["event_params"] = ep
|
|
291
|
+
if cond is not None:
|
|
292
|
+
kwargs_u["condition"] = cond
|
|
293
|
+
inp = UpdateAiAutomationInput(**kwargs_u)
|
|
294
|
+
except ValidationError as exc:
|
|
295
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
296
|
+
return await client.update_ai_automation(inp)
|
|
297
|
+
|
|
298
|
+
run_cli_command(ctx, json_out, factory)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@ai_automation_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
|
|
302
|
+
def ai_automation_delete(
|
|
303
|
+
ctx: typer.Context,
|
|
304
|
+
automation_id: str = resource_id_argument(help="Automation rule id."),
|
|
305
|
+
yes: bool = typer.Option(
|
|
306
|
+
False, "--yes", "-y", help="Skip interactive confirmation."
|
|
307
|
+
),
|
|
308
|
+
json_out: bool = typer.Option(False, "--json", "-j"),
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Delete an AI automation (``delete_ai_automation`` / ``delete_automation``)."""
|
|
311
|
+
confirm_destructive(
|
|
312
|
+
yes=yes, description=f"AI automation {automation_id}", verb="delete"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def factory(client: PipefyClient):
|
|
316
|
+
return await client.delete_automation(automation_id)
|
|
317
|
+
|
|
318
|
+
run_cli_command(ctx, json_out, factory)
|