applied-cli 0.5.72__tar.gz → 0.5.73__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.
- {applied_cli-0.5.72 → applied_cli-0.5.73}/PKG-INFO +1 -1
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/__init__.py +1 -1
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/cli.py +245 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.5.72 → applied_cli-0.5.73}/pyproject.toml +1 -1
- {applied_cli-0.5.72 → applied_cli-0.5.73}/README.md +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/client.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli/tools.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/setup.cfg +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_cli.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_client.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.5.72 → applied_cli-0.5.73}/tests/test_knowledge_content_tools.py +0 -0
|
@@ -2509,6 +2509,251 @@ def agent_deploy(
|
|
|
2509
2509
|
typer.echo(f"Deployed agent {agent_id}: revision v{version} (id={revision_id}, is_live={is_live})")
|
|
2510
2510
|
|
|
2511
2511
|
|
|
2512
|
+
@app.command("agent-revisions")
|
|
2513
|
+
def agent_revisions(
|
|
2514
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
2515
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max revisions to return"),
|
|
2516
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2517
|
+
format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
|
2518
|
+
) -> None:
|
|
2519
|
+
"""List revision history for an agent, newest first.
|
|
2520
|
+
|
|
2521
|
+
Shows version number, deploy timestamp, author, and description (if set).
|
|
2522
|
+
Use this to identify which revision version was live at a given time,
|
|
2523
|
+
then run agent-revision-diff to see what changed.
|
|
2524
|
+
|
|
2525
|
+
Example:
|
|
2526
|
+
applied agent-revisions <agent_id> --limit 10
|
|
2527
|
+
"""
|
|
2528
|
+
client = get_client(shop_id=shop_id)
|
|
2529
|
+
|
|
2530
|
+
async def _list():
|
|
2531
|
+
data = await client._request(
|
|
2532
|
+
"GET",
|
|
2533
|
+
"/v1/revisions/",
|
|
2534
|
+
params={"agent_id": agent_id, "limit": limit},
|
|
2535
|
+
)
|
|
2536
|
+
return data
|
|
2537
|
+
|
|
2538
|
+
result = asyncio.run(_list())
|
|
2539
|
+
revisions = result.get("results", [])
|
|
2540
|
+
|
|
2541
|
+
if format == "json":
|
|
2542
|
+
typer.echo(json.dumps(revisions, indent=2))
|
|
2543
|
+
return
|
|
2544
|
+
|
|
2545
|
+
if not revisions:
|
|
2546
|
+
typer.echo("No revisions found.")
|
|
2547
|
+
return
|
|
2548
|
+
|
|
2549
|
+
typer.echo(f"{'Version':<10} {'Created':<26} {'Live':<6} {'Author':<20} {'Description'}")
|
|
2550
|
+
typer.echo("-" * 90)
|
|
2551
|
+
for rev in revisions:
|
|
2552
|
+
version = f"v{rev.get('version', '?')}"
|
|
2553
|
+
created = rev.get("created_at", "")[:19].replace("T", " ")
|
|
2554
|
+
is_live = "✓" if rev.get("is_live") else ""
|
|
2555
|
+
author = (rev.get("created_by") or {}).get("display_name", "?")[:19]
|
|
2556
|
+
desc = (rev.get("description") or "")[:40]
|
|
2557
|
+
typer.echo(f"{version:<10} {created:<26} {is_live:<6} {author:<20} {desc}")
|
|
2558
|
+
|
|
2559
|
+
|
|
2560
|
+
@app.command("agent-revision-diff")
|
|
2561
|
+
def agent_revision_diff(
|
|
2562
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
2563
|
+
version_a: str = typer.Argument(..., help="Older version number (e.g. 54)"),
|
|
2564
|
+
version_b: str = typer.Argument(..., help="Newer version number (e.g. 55)"),
|
|
2565
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2566
|
+
format: str = typer.Option("text", "--format", "-f", help="Output format: text or json"),
|
|
2567
|
+
) -> None:
|
|
2568
|
+
"""Diff two agent revisions to see what changed between them.
|
|
2569
|
+
|
|
2570
|
+
Compares the agent prompt (guardrail), attached flow names/prompts,
|
|
2571
|
+
and key agent settings (model, escalation_mode, use_guardrails).
|
|
2572
|
+
|
|
2573
|
+
Since revisions only store metadata (not a full snapshot), this command
|
|
2574
|
+
reconstructs the diff by fetching the current agent state for version_b
|
|
2575
|
+
and the flows list at each revision time. For prompt changes, it shows
|
|
2576
|
+
a unified diff of the guardrail field. Flow additions/removals are listed.
|
|
2577
|
+
|
|
2578
|
+
Use this to explain why an agent revision caused an escalation spike.
|
|
2579
|
+
|
|
2580
|
+
Example:
|
|
2581
|
+
applied agent-revision-diff <agent_id> 54 55
|
|
2582
|
+
"""
|
|
2583
|
+
import difflib
|
|
2584
|
+
|
|
2585
|
+
client = get_client(shop_id=shop_id)
|
|
2586
|
+
|
|
2587
|
+
async def _fetch():
|
|
2588
|
+
# Paginate revisions until both target versions are found
|
|
2589
|
+
revisions: list = []
|
|
2590
|
+
page = 1
|
|
2591
|
+
rev_a = None
|
|
2592
|
+
rev_b = None
|
|
2593
|
+
while True:
|
|
2594
|
+
data = await client._request(
|
|
2595
|
+
"GET", "/v1/revisions/",
|
|
2596
|
+
params={"agent_id": agent_id, "limit": 100, "page": page},
|
|
2597
|
+
)
|
|
2598
|
+
batch = data.get("results", [])
|
|
2599
|
+
if not batch:
|
|
2600
|
+
break
|
|
2601
|
+
revisions.extend(batch)
|
|
2602
|
+
rev_a = rev_a or next((r for r in revisions if str(r.get("version")) == str(version_a)), None)
|
|
2603
|
+
rev_b = rev_b or next((r for r in revisions if str(r.get("version")) == str(version_b)), None)
|
|
2604
|
+
if rev_a and rev_b:
|
|
2605
|
+
break
|
|
2606
|
+
if not data.get("next"):
|
|
2607
|
+
break
|
|
2608
|
+
page += 1
|
|
2609
|
+
|
|
2610
|
+
if not rev_a:
|
|
2611
|
+
return {"error": f"Version {version_a} not found for agent {agent_id}"}
|
|
2612
|
+
if not rev_b:
|
|
2613
|
+
return {"error": f"Version {version_b} not found for agent {agent_id}"}
|
|
2614
|
+
|
|
2615
|
+
# Get current agent state (best proxy for version_b since it's usually live/recent)
|
|
2616
|
+
agent = await client._request("GET", f"/v1/agents/{agent_id}/")
|
|
2617
|
+
|
|
2618
|
+
# Get flows associated with this agent
|
|
2619
|
+
flows_data = await client._request(
|
|
2620
|
+
"GET", "/v1/flows/",
|
|
2621
|
+
params={"agent_id": agent_id, "limit": 100},
|
|
2622
|
+
)
|
|
2623
|
+
flows = flows_data.get("results", [])
|
|
2624
|
+
|
|
2625
|
+
return {
|
|
2626
|
+
"rev_a": rev_a,
|
|
2627
|
+
"rev_b": rev_b,
|
|
2628
|
+
"agent": agent,
|
|
2629
|
+
"flows": flows,
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
data = asyncio.run(_fetch())
|
|
2633
|
+
|
|
2634
|
+
if "error" in data:
|
|
2635
|
+
typer.echo(f"Error: {data['error']}", err=True)
|
|
2636
|
+
raise SystemExit(1)
|
|
2637
|
+
|
|
2638
|
+
rev_a = data["rev_a"]
|
|
2639
|
+
rev_b = data["rev_b"]
|
|
2640
|
+
agent = data["agent"]
|
|
2641
|
+
flows = data["flows"]
|
|
2642
|
+
|
|
2643
|
+
if format == "json":
|
|
2644
|
+
typer.echo(json.dumps({
|
|
2645
|
+
"version_a": rev_a,
|
|
2646
|
+
"version_b": rev_b,
|
|
2647
|
+
"agent_current": {k: v for k, v in agent.items() if k != "guardrail"},
|
|
2648
|
+
"guardrail_length": len(agent.get("guardrail") or ""),
|
|
2649
|
+
"flows_count": len(flows),
|
|
2650
|
+
"flows": [{"id": f["id"], "name": f["name"], "trigger": f.get("trigger"), "status": f.get("status")} for f in flows],
|
|
2651
|
+
}, indent=2))
|
|
2652
|
+
return
|
|
2653
|
+
|
|
2654
|
+
typer.echo(f"\n=== Agent Revision Diff: v{version_a} → v{version_b} ===")
|
|
2655
|
+
typer.echo(f"Agent: {agent.get('name')} ({agent_id})")
|
|
2656
|
+
typer.echo(f"v{version_a}: {rev_a.get('created_at', '')[:19]} by {(rev_a.get('created_by') or {}).get('display_name', '?')} — \"{rev_a.get('description') or '(no description)'}\"")
|
|
2657
|
+
typer.echo(f"v{version_b}: {rev_b.get('created_at', '')[:19]} by {(rev_b.get('created_by') or {}).get('display_name', '?')} — \"{rev_b.get('description') or '(no description)'}\"")
|
|
2658
|
+
|
|
2659
|
+
# Note: without snapshot storage, we can only show current state + metadata delta
|
|
2660
|
+
typer.echo(f"\n--- AGENT SETTINGS (current, as of v{version_b}) ---")
|
|
2661
|
+
for field in ("model", "escalation_mode", "use_guardrails", "auto_reply", "response_delay_in_seconds"):
|
|
2662
|
+
typer.echo(f" {field}: {agent.get(field)}")
|
|
2663
|
+
|
|
2664
|
+
typer.echo(f"\n--- PROMPT (guardrail, current) ---")
|
|
2665
|
+
guardrail = agent.get("guardrail") or ""
|
|
2666
|
+
typer.echo(f" Length: {len(guardrail)} chars")
|
|
2667
|
+
typer.echo(f" First 300 chars:\n{guardrail[:300]}")
|
|
2668
|
+
|
|
2669
|
+
typer.echo(f"\n--- FLOWS ({len(flows)} attached to agent) ---")
|
|
2670
|
+
active = [f for f in flows if f.get("status") == "active"]
|
|
2671
|
+
draft = [f for f in flows if f.get("status") != "active"]
|
|
2672
|
+
typer.echo(f" Active: {len(active)} | Draft/Other: {len(draft)}")
|
|
2673
|
+
for f in active[:20]:
|
|
2674
|
+
typer.echo(f" [{f.get('trigger','?')}] {f.get('name')} ({f.get('status')})")
|
|
2675
|
+
|
|
2676
|
+
typer.echo(f"\nNote: Full prompt diff requires snapshot storage per revision (not yet available in API).")
|
|
2677
|
+
typer.echo(f"To investigate: compare guardrail text above against the known-good version,")
|
|
2678
|
+
typer.echo(f"and check if any flows were added/removed between {rev_a['created_at'][:10]} and {rev_b['created_at'][:10]}.")
|
|
2679
|
+
|
|
2680
|
+
|
|
2681
|
+
@app.command("conversations-messages-batch")
|
|
2682
|
+
def conversations_messages_batch(
|
|
2683
|
+
conversation_ids: str = typer.Argument(
|
|
2684
|
+
..., help="Comma-separated conversation IDs (up to 50)"
|
|
2685
|
+
),
|
|
2686
|
+
message_limit: int = typer.Option(10, "--message-limit", help="Max messages per conversation"),
|
|
2687
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2688
|
+
format: str = typer.Option("json", "--format", "-f", help="Output format: json or text"),
|
|
2689
|
+
) -> None:
|
|
2690
|
+
"""Fetch messages for multiple conversations in parallel.
|
|
2691
|
+
|
|
2692
|
+
Unblocks qualitative scan sub-checks B/C/D/E (hallucination, name errors,
|
|
2693
|
+
duplicate replies, tone) by retrieving message history for a batch of
|
|
2694
|
+
conversation IDs concurrently instead of one serial call per conversation.
|
|
2695
|
+
|
|
2696
|
+
Example (qual scan: fetch top 20 escalated convos):
|
|
2697
|
+
applied conversations-messages-batch <id1>,<id2>,<id3> --message-limit 20
|
|
2698
|
+
|
|
2699
|
+
Example (pipe IDs from conversations command):
|
|
2700
|
+
applied conversations --resolution escalated --limit 20 --format json \\
|
|
2701
|
+
| python3 -c "import json,sys; print(','.join(c['id'] for c in json.load(sys.stdin)))" \\
|
|
2702
|
+
| xargs applied conversations-messages-batch --message-limit 10
|
|
2703
|
+
"""
|
|
2704
|
+
import asyncio as _asyncio
|
|
2705
|
+
|
|
2706
|
+
ids = [cid.strip() for cid in conversation_ids.split(",") if cid.strip()]
|
|
2707
|
+
if not ids:
|
|
2708
|
+
typer.echo("No conversation IDs provided.", err=True)
|
|
2709
|
+
raise SystemExit(1)
|
|
2710
|
+
if len(ids) > 50:
|
|
2711
|
+
typer.echo("Max 50 conversation IDs per call. Truncating.", err=True)
|
|
2712
|
+
ids = ids[:50]
|
|
2713
|
+
|
|
2714
|
+
client = get_client(shop_id=shop_id)
|
|
2715
|
+
|
|
2716
|
+
async def _fetch_one(conv_id: str) -> dict:
|
|
2717
|
+
try:
|
|
2718
|
+
conv = await client.get_conversation(conv_id, shop_id=shop_id)
|
|
2719
|
+
messages = await client.get_messages(
|
|
2720
|
+
conversation_id=conv_id,
|
|
2721
|
+
limit=message_limit,
|
|
2722
|
+
shop_id=shop_id,
|
|
2723
|
+
)
|
|
2724
|
+
return {
|
|
2725
|
+
"conversation_id": conv_id,
|
|
2726
|
+
"resolution": conv.get("resolution"),
|
|
2727
|
+
"title": conv.get("title"),
|
|
2728
|
+
"created_at": conv.get("created_at"),
|
|
2729
|
+
"messages": messages if isinstance(messages, list) else (messages.get("results") or []),
|
|
2730
|
+
"error": None,
|
|
2731
|
+
}
|
|
2732
|
+
except Exception as exc:
|
|
2733
|
+
return {"conversation_id": conv_id, "error": str(exc), "messages": []}
|
|
2734
|
+
|
|
2735
|
+
async def _fetch_all():
|
|
2736
|
+
return await _asyncio.gather(*[_fetch_one(cid) for cid in ids])
|
|
2737
|
+
|
|
2738
|
+
results = asyncio.run(_fetch_all())
|
|
2739
|
+
|
|
2740
|
+
if format == "json":
|
|
2741
|
+
typer.echo(json.dumps(results, indent=2, default=str))
|
|
2742
|
+
return
|
|
2743
|
+
|
|
2744
|
+
for r in results:
|
|
2745
|
+
if r.get("error"):
|
|
2746
|
+
typer.echo(f"\n[{r['conversation_id']}] ERROR: {r['error']}")
|
|
2747
|
+
continue
|
|
2748
|
+
typer.echo(f"\n[{r['conversation_id']}] {r.get('title', '')} — {r.get('resolution', '')} — {(r.get('created_at') or '')[:10]}")
|
|
2749
|
+
for msg in r.get("messages", []):
|
|
2750
|
+
role = msg.get("type") or msg.get("role") or "?"
|
|
2751
|
+
body = (msg.get("body") or msg.get("content") or "")
|
|
2752
|
+
if isinstance(body, list):
|
|
2753
|
+
body = " ".join(b.get("text", "") for b in body if isinstance(b, dict))
|
|
2754
|
+
typer.echo(f" [{role}] {str(body)[:200]}")
|
|
2755
|
+
|
|
2756
|
+
|
|
2512
2757
|
def main() -> None:
|
|
2513
2758
|
"""CLI entrypoint."""
|
|
2514
2759
|
nested_exit_code = run_agent_scoped_flow_command(sys.argv[1:], get_client)
|
|
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
|