adloop 0.33.0__tar.gz → 0.33.1__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.
- {adloop-0.33.0 → adloop-0.33.1}/PKG-INFO +1 -1
- {adloop-0.33.0 → adloop-0.33.1}/pyproject.toml +1 -1
- {adloop-0.33.0 → adloop-0.33.1}/pyproject.toml.orig +1 -1
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/adloop.md +1 -1
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/approval.py +69 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/preview.py +5 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/server.py +11 -2
- {adloop-0.33.0 → adloop-0.33.1}/README.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/__main__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/audiences.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/brands.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/currency.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/enums.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/read.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/shopping.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ads/write.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/auth.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/cli.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/config.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/crossref.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/diagnostics.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ga4/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/ga4/write.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gsc/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gtm/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gtm/read.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/gtm/write.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/merchant/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/merchant/read.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/pagespeed.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/client.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/read.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/reddit/write.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/rules_install.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/runtime.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/approval_card.html +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/audit.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/safety/guards.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/tracking.py +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/upload_card.html +0 -0
- {adloop-0.33.0 → adloop-0.33.1}/src/adloop/uploads.py +0 -0
|
@@ -345,7 +345,7 @@ Same draft → preview → `confirm_and_apply` gate; the plan carries `platform:
|
|
|
345
345
|
|
|
346
346
|
**Approval cards (Claude, ChatGPT, VS Code and other apps with MCP Apps):** when the preview comes back with `approval: "card"`, the user sees it as an AdLoop approval card with Test run, Apply and Discard buttons, and approves there. Summarize the change and its warnings, then let the user decide on the card instead of asking for a "yes" in the chat. In those apps `confirm_and_apply(dry_run=false)` is refused with status `APPROVE_ON_CARD` (a dry run still works), because only the click applies; the AI cannot press the button. Do not retry. If the user says the card is not visible, tell them the workspace owner can allow approval in chat (AdLoop Cloud: Settings → Safety; self-hosted: `safety.allow_chat_approval`). After a click the card posts a message such as "Applied on the AdLoop approval card: ..."; continue from there (e.g. remind them to enable a new paused ad).
|
|
347
347
|
|
|
348
|
-
**Confirmation dialogs (apps without cards that support elicitation):** when you call `confirm_and_apply(dry_run=false)` after the user's go-ahead, AdLoop itself asks the user in the app's confirmation dialog before anything changes; irreversible changes need an extra "cannot be undone" tick there. You do not answer that dialog. `NOT_CONFIRMED` means the user declined or closed it: nothing was changed, the preview stays pending, so ask what they want instead of retrying. Apps with neither cards nor dialogs keep the chat flow above.
|
|
348
|
+
**Confirmation dialogs (apps without cards that support elicitation):** when you call `confirm_and_apply(dry_run=false)` after the user's go-ahead, AdLoop itself asks the user in the app's confirmation dialog before anything changes; irreversible changes need an extra "cannot be undone" tick there. You do not answer that dialog. `NOT_CONFIRMED` means the user declined or closed it: nothing was changed, the preview stays pending, so ask what they want instead of retrying. A dry run's `next_step` says when a dialog will follow, so tell the user to look out for it. `CONFIRMATION_UNANSWERED` means the app received the dialog but never returned an answer (some headless agents and IDE setups declare dialogs they cannot show): nothing was changed; ask the user whether a dialog appeared, and if their app cannot show one, relay the message's ways out (approval in chat, or an app with the approval card). Apps with neither cards nor dialogs keep the chat flow above.
|
|
349
349
|
|
|
350
350
|
**Safety behaviors:**
|
|
351
351
|
- New campaigns and RSAs (and every Reddit campaign, ad group and ad) are created as PAUSED — user must explicitly enable them after review.
|
|
@@ -335,6 +335,64 @@ def _not_confirmed(plan: ChangePlan) -> dict:
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
def _dialog_unanswered(plan: ChangePlan) -> dict:
|
|
339
|
+
return {
|
|
340
|
+
"status": "CONFIRMATION_UNANSWERED",
|
|
341
|
+
"plan_id": plan.plan_id,
|
|
342
|
+
"operation": plan.operation,
|
|
343
|
+
"message": (
|
|
344
|
+
"AdLoop asked the user to confirm this change in a dialog of their app, "
|
|
345
|
+
"but the app never returned an answer, so nothing was changed and the "
|
|
346
|
+
"plan stays pending. Ask the user whether a confirmation dialog appeared. "
|
|
347
|
+
"If it did, call confirm_and_apply again and have them answer it. If their "
|
|
348
|
+
"app cannot show dialogs (for example a headless agent or a script), the "
|
|
349
|
+
"workspace owner can allow approval in chat (AdLoop Cloud: Settings -> "
|
|
350
|
+
"Safety -> 'Also allow approval in chat'; self-hosted: "
|
|
351
|
+
"safety.allow_chat_approval: true), or the change can be applied from an "
|
|
352
|
+
"app that shows AdLoop's approval card, such as Claude or ChatGPT. Calling "
|
|
353
|
+
"confirm_and_apply again sends the dialog once more."
|
|
354
|
+
),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _mark_dialog(plan: ChangePlan, open_: bool) -> None:
|
|
359
|
+
"""Record (or clear) that a dialog for this plan awaits an answer."""
|
|
360
|
+
from datetime import datetime, timezone
|
|
361
|
+
|
|
362
|
+
from adloop.safety.preview import store_plan
|
|
363
|
+
|
|
364
|
+
plan.dialog_requested_at = datetime.now(timezone.utc).isoformat() if open_ else ""
|
|
365
|
+
store_plan(plan)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def dialog_expected(plan_id: str) -> bool:
|
|
369
|
+
"""Whether a real apply of this plan will ask the user in a dialog.
|
|
370
|
+
|
|
371
|
+
Used after a successful dry run, so the AI can tell the user to look out
|
|
372
|
+
for it instead of letting an unanswered dialog look like a hang.
|
|
373
|
+
"""
|
|
374
|
+
if _dialog_applies(plan_id) is None:
|
|
375
|
+
return False
|
|
376
|
+
try:
|
|
377
|
+
from fastmcp.server.dependencies import get_context
|
|
378
|
+
|
|
379
|
+
if get_context()._is_modern_protocol():
|
|
380
|
+
return True
|
|
381
|
+
except Exception:
|
|
382
|
+
return False
|
|
383
|
+
from adloop.runtime import deployment_mode
|
|
384
|
+
|
|
385
|
+
# Older protocols get the dialog only self-hosted (see confirm_in_dialog).
|
|
386
|
+
return deployment_mode() != "server"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
DIALOG_NOTE = (
|
|
390
|
+
"The real apply (dry_run=false) asks the user to confirm in a dialog shown by "
|
|
391
|
+
"their app; tell them to look out for it and answer it. If no dialog appears, "
|
|
392
|
+
"the call returns CONFIRMATION_UNANSWERED on the next attempt with the ways out."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
338
396
|
def _dialog_applies(plan_id: str) -> ChangePlan | None:
|
|
339
397
|
"""The plan when a real apply must be confirmed in a dialog, else None."""
|
|
340
398
|
from adloop.runtime import current_config
|
|
@@ -375,6 +433,14 @@ async def confirm_in_dialog(context: Any, plan_id: str) -> Any | None:
|
|
|
375
433
|
if context._is_modern_protocol():
|
|
376
434
|
responses = context.input_responses
|
|
377
435
|
if responses is None:
|
|
436
|
+
if plan.dialog_requested_at:
|
|
437
|
+
# The last dialog for this plan was sent and this is a fresh
|
|
438
|
+
# call, not its answer: the app received the dialog and never
|
|
439
|
+
# returned one. Say so instead of sending another dialog the
|
|
440
|
+
# app will sit on until its timeout. Cleared, so asking again
|
|
441
|
+
# sends a new dialog.
|
|
442
|
+
_mark_dialog(plan, False)
|
|
443
|
+
return ToolResult(structured_content=_dialog_unanswered(plan))
|
|
378
444
|
import mcp_types
|
|
379
445
|
from fastmcp.tools.base import InputRequiredToolResult
|
|
380
446
|
|
|
@@ -385,11 +451,14 @@ async def confirm_in_dialog(context: Any, plan_id: str) -> Any | None:
|
|
|
385
451
|
)
|
|
386
452
|
)
|
|
387
453
|
state = json.dumps({"plan_id": plan.plan_id, "code": approval_code(plan)})
|
|
454
|
+
_mark_dialog(plan, True)
|
|
388
455
|
return InputRequiredToolResult(
|
|
389
456
|
mcp_types.InputRequiredResult(
|
|
390
457
|
input_requests={DIALOG_KEY: request}, request_state=state
|
|
391
458
|
)
|
|
392
459
|
)
|
|
460
|
+
if plan.dialog_requested_at:
|
|
461
|
+
_mark_dialog(plan, False) # answered, whatever the answer is
|
|
393
462
|
try:
|
|
394
463
|
state = json.loads(context.request_state or "{}")
|
|
395
464
|
except ValueError:
|
|
@@ -25,6 +25,11 @@ class ChangePlan:
|
|
|
25
25
|
# Ad platform the plan targets: "" (Google, the default so stored plans
|
|
26
26
|
# from before the field existed still load) or "reddit".
|
|
27
27
|
platform: str = ""
|
|
28
|
+
# When a confirmation dialog for this plan was last sent and not yet
|
|
29
|
+
# answered (ISO timestamp, "" when none is open). An app that receives
|
|
30
|
+
# the dialog but never answers it would otherwise leave every retry
|
|
31
|
+
# waiting until its own timeout; see approval.confirm_in_dialog.
|
|
32
|
+
dialog_requested_at: str = ""
|
|
28
33
|
|
|
29
34
|
# Payload the applier needs but a preview must never show: an upload's rows
|
|
30
35
|
# (the caller id cannot be hashed, so call uploads carry raw phone numbers),
|
|
@@ -4482,6 +4482,11 @@ def confirm_and_apply(
|
|
|
4482
4482
|
only the user's click on the card applies the change. Dry runs still
|
|
4483
4483
|
work. The workspace owner can allow approval in chat as well.
|
|
4484
4484
|
|
|
4485
|
+
Confirmation dialogs: in apps that show dialogs instead, dry_run=false
|
|
4486
|
+
first asks the user in a dialog. A declined dialog returns NOT_CONFIRMED;
|
|
4487
|
+
an app that receives the dialog but never answers it gets
|
|
4488
|
+
CONFIRMATION_UNANSWERED on the next call, with the ways out.
|
|
4489
|
+
|
|
4485
4490
|
Args:
|
|
4486
4491
|
plan_id: The plan_id returned by a prior draft_*, update_*, add_*,
|
|
4487
4492
|
pause/enable/remove or other preview tool call. An unknown or
|
|
@@ -4490,13 +4495,17 @@ def confirm_and_apply(
|
|
|
4490
4495
|
applies the change for real.
|
|
4491
4496
|
"""
|
|
4492
4497
|
from adloop.ads.write import confirm_and_apply as _impl
|
|
4493
|
-
from adloop.safety.approval import chat_apply_refusal
|
|
4498
|
+
from adloop.safety.approval import DIALOG_NOTE, chat_apply_refusal, dialog_expected
|
|
4494
4499
|
|
|
4495
4500
|
if not dry_run:
|
|
4496
4501
|
refusal = chat_apply_refusal(plan_id)
|
|
4497
4502
|
if refusal is not None:
|
|
4498
4503
|
return refusal
|
|
4499
|
-
|
|
4504
|
+
result = _impl(current_config(), plan_id=plan_id, dry_run=dry_run)
|
|
4505
|
+
if (dry_run and isinstance(result, dict) and result.get("status") == "DRY_RUN_SUCCESS"
|
|
4506
|
+
and dialog_expected(plan_id)):
|
|
4507
|
+
result["next_step"] = DIALOG_NOTE
|
|
4508
|
+
return result
|
|
4500
4509
|
|
|
4501
4510
|
|
|
4502
4511
|
@_tool(
|
|
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
|
|
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
|