applied-cli 0.5.71__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.71 → applied_cli-0.5.73}/PKG-INFO +11 -1
- {applied_cli-0.5.71 → applied_cli-0.5.73}/README.md +10 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/__init__.py +1 -1
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/cli.py +530 -2
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/client.py +34 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/tools.py +850 -5
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/PKG-INFO +11 -1
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/SOURCES.txt +2 -1
- {applied_cli-0.5.71 → applied_cli-0.5.73}/pyproject.toml +1 -1
- applied_cli-0.5.73/tests/test_cli.py +564 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_client.py +148 -0
- applied_cli-0.5.73/tests/test_knowledge_content_tools.py +383 -0
- applied_cli-0.5.71/tests/test_cli.py +0 -96
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/setup.cfg +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.71 → applied_cli-0.5.73}/tests/test_flow_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.73
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -53,6 +53,16 @@ applied tickets --status open
|
|
|
53
53
|
|
|
54
54
|
# Knowledge base
|
|
55
55
|
applied knowledge --type qa --search "refund"
|
|
56
|
+
applied knowledge <id> --format json
|
|
57
|
+
applied knowledge --source "https://help.example.com" --format csv
|
|
58
|
+
applied knowledge-diff --source "https://help.example.com"
|
|
59
|
+
applied content list --source "https://help.example.com"
|
|
60
|
+
applied content-update <content_id> --text "Corrected source text"
|
|
61
|
+
applied content-resync <content_id> --wait
|
|
62
|
+
applied knowledge-pin <id>
|
|
63
|
+
applied knowledge-link <response_id> --content <content_id>
|
|
64
|
+
applied knowledge-protect <id>
|
|
65
|
+
applied knowledge-unprotect <id>
|
|
56
66
|
|
|
57
67
|
# Taxonomy
|
|
58
68
|
applied taxonomy --type topics
|
|
@@ -28,6 +28,16 @@ applied tickets --status open
|
|
|
28
28
|
|
|
29
29
|
# Knowledge base
|
|
30
30
|
applied knowledge --type qa --search "refund"
|
|
31
|
+
applied knowledge <id> --format json
|
|
32
|
+
applied knowledge --source "https://help.example.com" --format csv
|
|
33
|
+
applied knowledge-diff --source "https://help.example.com"
|
|
34
|
+
applied content list --source "https://help.example.com"
|
|
35
|
+
applied content-update <content_id> --text "Corrected source text"
|
|
36
|
+
applied content-resync <content_id> --wait
|
|
37
|
+
applied knowledge-pin <id>
|
|
38
|
+
applied knowledge-link <response_id> --content <content_id>
|
|
39
|
+
applied knowledge-protect <id>
|
|
40
|
+
applied knowledge-unprotect <id>
|
|
31
41
|
|
|
32
42
|
# Taxonomy
|
|
33
43
|
applied taxonomy --type topics
|
|
@@ -22,6 +22,8 @@ app = typer.Typer(
|
|
|
22
22
|
help="Applied Labs CLI - manage your AI agents and conversations.",
|
|
23
23
|
no_args_is_help=True,
|
|
24
24
|
)
|
|
25
|
+
content_app = typer.Typer(help="Inspect synced content items.")
|
|
26
|
+
app.add_typer(content_app, name="content")
|
|
25
27
|
|
|
26
28
|
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
27
29
|
DEFAULT_CLIENT_URL = "https://appliedlabs.ai"
|
|
@@ -938,27 +940,174 @@ def ticket_update(
|
|
|
938
940
|
|
|
939
941
|
@app.command()
|
|
940
942
|
def knowledge(
|
|
943
|
+
id: str = typer.Argument(None, help="Knowledge item ID to fetch"),
|
|
941
944
|
type: str = typer.Option(
|
|
942
945
|
None, "--type", "-t", help="Filter by type: qa, context, escalate, exact"
|
|
943
946
|
),
|
|
944
947
|
search: str = typer.Option(None, "--search", "-s", help="Search query"),
|
|
948
|
+
linked_to: str = typer.Option(
|
|
949
|
+
None, "--linked-to", help="Filter by linked content ID"
|
|
950
|
+
),
|
|
951
|
+
source: str = typer.Option(
|
|
952
|
+
None, "--source", help="Filter by linked content source URL/domain"
|
|
953
|
+
),
|
|
954
|
+
has_content: bool | None = typer.Option(
|
|
955
|
+
None,
|
|
956
|
+
"--has-content/--no-content",
|
|
957
|
+
help="Filter content-linked vs standalone manual entries",
|
|
958
|
+
),
|
|
945
959
|
limit: int = typer.Option(100, "--limit", "-l", help="Results per page"),
|
|
946
960
|
all_pages: bool = typer.Option(
|
|
947
961
|
True, "--all/--no-all", help="Fetch all pages (default: True)"
|
|
948
962
|
),
|
|
963
|
+
full: bool = typer.Option(False, "--full", help="Include full audit fields in CSV"),
|
|
949
964
|
format: str = typer.Option(
|
|
950
965
|
"csv", "--format", "-f", help="Output format: csv or json"
|
|
951
966
|
),
|
|
967
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
952
968
|
) -> None:
|
|
953
|
-
"""List knowledge base items."""
|
|
954
|
-
client = get_client()
|
|
969
|
+
"""List knowledge base items or fetch one item by ID."""
|
|
970
|
+
client = get_client(shop_id=shop_id)
|
|
971
|
+
if id:
|
|
972
|
+
item_format = "json" if format == "json" else "text"
|
|
973
|
+
result = asyncio.run(tools.knowledge_get(client, id, output_format=item_format))
|
|
974
|
+
typer.echo(result)
|
|
975
|
+
return
|
|
976
|
+
|
|
955
977
|
result = asyncio.run(
|
|
956
978
|
tools.knowledge_list(
|
|
957
979
|
client,
|
|
958
980
|
kb_type=type,
|
|
959
981
|
search=search,
|
|
982
|
+
linked_to=linked_to,
|
|
983
|
+
source=source,
|
|
984
|
+
has_content=has_content,
|
|
985
|
+
limit=limit,
|
|
986
|
+
fetch_all=all_pages,
|
|
987
|
+
full=full,
|
|
988
|
+
output_format=format,
|
|
989
|
+
)
|
|
990
|
+
)
|
|
991
|
+
typer.echo(result)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@app.command("knowledge-diff")
|
|
995
|
+
def knowledge_diff(
|
|
996
|
+
source: str = typer.Option(..., "--source", help="Source URL/domain to audit"),
|
|
997
|
+
limit: int = typer.Option(100, "--limit", "-l", help="Results per API page"),
|
|
998
|
+
format: str = typer.Option(
|
|
999
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1000
|
+
),
|
|
1001
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1002
|
+
) -> None:
|
|
1003
|
+
"""Diff content-linked knowledge against current source pages."""
|
|
1004
|
+
client = get_client(shop_id=shop_id)
|
|
1005
|
+
result = asyncio.run(
|
|
1006
|
+
tools.knowledge_diff(
|
|
1007
|
+
client,
|
|
1008
|
+
source=source,
|
|
1009
|
+
limit=limit,
|
|
1010
|
+
output_format=format,
|
|
1011
|
+
)
|
|
1012
|
+
)
|
|
1013
|
+
typer.echo(result)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
# -----------------------------------------------------------------------------
|
|
1017
|
+
# Content commands
|
|
1018
|
+
# -----------------------------------------------------------------------------
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@content_app.command("list")
|
|
1022
|
+
def content_list_cmd(
|
|
1023
|
+
type: str = typer.Option(
|
|
1024
|
+
None, "--type", "-t", help="Filter by type: Article, Product, Document, Site"
|
|
1025
|
+
),
|
|
1026
|
+
status: str = typer.Option(None, "--status", "-s", help="Filter by status"),
|
|
1027
|
+
search: str = typer.Option(None, "--search", help="Search query"),
|
|
1028
|
+
source: str = typer.Option(None, "--source", help="Filter by source URL/domain"),
|
|
1029
|
+
limit: int = typer.Option(100, "--limit", "-l", help="Results per page"),
|
|
1030
|
+
all_pages: bool = typer.Option(
|
|
1031
|
+
True, "--all/--no-all", help="Fetch all pages (default: True)"
|
|
1032
|
+
),
|
|
1033
|
+
full: bool = typer.Option(False, "--full", help="Include sync audit fields in CSV"),
|
|
1034
|
+
format: str = typer.Option(
|
|
1035
|
+
"csv", "--format", "-f", help="Output format: csv or json"
|
|
1036
|
+
),
|
|
1037
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1038
|
+
) -> None:
|
|
1039
|
+
"""List content items imported into the knowledge system."""
|
|
1040
|
+
client = get_client(shop_id=shop_id)
|
|
1041
|
+
result = asyncio.run(
|
|
1042
|
+
tools.content_list(
|
|
1043
|
+
client,
|
|
1044
|
+
content_type=type,
|
|
1045
|
+
status=status,
|
|
1046
|
+
search=search,
|
|
1047
|
+
source=source,
|
|
960
1048
|
limit=limit,
|
|
961
1049
|
fetch_all=all_pages,
|
|
1050
|
+
full=full,
|
|
1051
|
+
output_format=format,
|
|
1052
|
+
)
|
|
1053
|
+
)
|
|
1054
|
+
typer.echo(result)
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
@app.command("content-resync")
|
|
1058
|
+
def content_resync(
|
|
1059
|
+
id: str = typer.Argument(..., help="Content item ID"),
|
|
1060
|
+
wait: bool = typer.Option(
|
|
1061
|
+
False, "--wait", help="Poll until the content item is no longer syncing"
|
|
1062
|
+
),
|
|
1063
|
+
poll_interval: float = typer.Option(
|
|
1064
|
+
2.0, "--poll-interval", help="Seconds between --wait polls"
|
|
1065
|
+
),
|
|
1066
|
+
timeout: float = typer.Option(
|
|
1067
|
+
300.0, "--timeout", help="Maximum seconds to wait for sync completion"
|
|
1068
|
+
),
|
|
1069
|
+
format: str = typer.Option(
|
|
1070
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1071
|
+
),
|
|
1072
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1073
|
+
) -> None:
|
|
1074
|
+
"""Trigger a resync for one content item."""
|
|
1075
|
+
client = get_client(shop_id=shop_id)
|
|
1076
|
+
result = asyncio.run(
|
|
1077
|
+
tools.content_resync(
|
|
1078
|
+
client,
|
|
1079
|
+
id,
|
|
1080
|
+
wait=wait,
|
|
1081
|
+
poll_interval=poll_interval,
|
|
1082
|
+
timeout=timeout,
|
|
1083
|
+
output_format=format,
|
|
1084
|
+
)
|
|
1085
|
+
)
|
|
1086
|
+
typer.echo(result)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
@app.command("content-update")
|
|
1090
|
+
def content_update(
|
|
1091
|
+
id: str = typer.Argument(..., help="Content item ID"),
|
|
1092
|
+
text: str = typer.Option(None, "--text", help="Override source text"),
|
|
1093
|
+
title: str = typer.Option(None, "--title", help="Content title"),
|
|
1094
|
+
description: str = typer.Option(None, "--description", help="Content description"),
|
|
1095
|
+
status: str = typer.Option(None, "--status", help="Content status"),
|
|
1096
|
+
format: str = typer.Option(
|
|
1097
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
1098
|
+
),
|
|
1099
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1100
|
+
) -> None:
|
|
1101
|
+
"""Update a content item before resyncing linked responses."""
|
|
1102
|
+
client = get_client(shop_id=shop_id)
|
|
1103
|
+
result = asyncio.run(
|
|
1104
|
+
tools.content_update(
|
|
1105
|
+
client,
|
|
1106
|
+
id,
|
|
1107
|
+
text=text,
|
|
1108
|
+
title=title,
|
|
1109
|
+
description=description,
|
|
1110
|
+
status=status,
|
|
962
1111
|
output_format=format,
|
|
963
1112
|
)
|
|
964
1113
|
)
|
|
@@ -1923,6 +2072,9 @@ def knowledge_create(
|
|
|
1923
2072
|
),
|
|
1924
2073
|
question: str = typer.Option(..., "--question", "-q", help="Trigger text / question"),
|
|
1925
2074
|
answer: str = typer.Option(..., "--answer", "-a", help="Response text / answer"),
|
|
2075
|
+
chunk_text: str = typer.Option(
|
|
2076
|
+
None, "--chunk-text", help="Raw retrieval chunk text"
|
|
2077
|
+
),
|
|
1926
2078
|
guardrail: str = typer.Option("", "--guardrail", help="Guardrail instructions"),
|
|
1927
2079
|
active: bool = typer.Option(True, "--active/--inactive", help="Whether the item is active"),
|
|
1928
2080
|
agent_ids: str = typer.Option(
|
|
@@ -1941,6 +2093,7 @@ def knowledge_create(
|
|
|
1941
2093
|
kb_type=kb_type,
|
|
1942
2094
|
question=question,
|
|
1943
2095
|
answer=answer,
|
|
2096
|
+
chunk_text=chunk_text,
|
|
1944
2097
|
guardrail=guardrail,
|
|
1945
2098
|
active=active,
|
|
1946
2099
|
agent_ids=parsed_agent_ids,
|
|
@@ -1963,6 +2116,11 @@ def knowledge_update(
|
|
|
1963
2116
|
),
|
|
1964
2117
|
label_id: str = typer.Option(None, "--label-id", help="Topic label UUID"),
|
|
1965
2118
|
flow_id: str = typer.Option(None, "--flow-id", help="Flow UUID to associate with"),
|
|
2119
|
+
pin: bool = typer.Option(
|
|
2120
|
+
False,
|
|
2121
|
+
"--pin",
|
|
2122
|
+
help="Mark the response as manually managed while updating",
|
|
2123
|
+
),
|
|
1966
2124
|
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
1967
2125
|
) -> None:
|
|
1968
2126
|
"""Update a knowledge base item."""
|
|
@@ -1982,6 +2140,95 @@ def knowledge_update(
|
|
|
1982
2140
|
agent_ids=parsed_agent_ids,
|
|
1983
2141
|
label_id=label_id,
|
|
1984
2142
|
flow_id=flow_id,
|
|
2143
|
+
pin=pin,
|
|
2144
|
+
)
|
|
2145
|
+
)
|
|
2146
|
+
typer.echo(result)
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
@app.command("knowledge-pin")
|
|
2150
|
+
def knowledge_pin(
|
|
2151
|
+
id: str = typer.Argument(..., help="Knowledge item ID"),
|
|
2152
|
+
unpin: bool = typer.Option(False, "--unpin", help="Remove the pin/manual flag"),
|
|
2153
|
+
format: str = typer.Option(
|
|
2154
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
2155
|
+
),
|
|
2156
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2157
|
+
) -> None:
|
|
2158
|
+
"""Pin a knowledge item so manual edits can survive content resyncs."""
|
|
2159
|
+
client = get_client(shop_id=shop_id)
|
|
2160
|
+
result = asyncio.run(
|
|
2161
|
+
tools.knowledge_pin(
|
|
2162
|
+
client,
|
|
2163
|
+
id,
|
|
2164
|
+
pinned=not unpin,
|
|
2165
|
+
output_format=format,
|
|
2166
|
+
)
|
|
2167
|
+
)
|
|
2168
|
+
typer.echo(result)
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
@app.command("knowledge-link")
|
|
2172
|
+
def knowledge_link(
|
|
2173
|
+
id: str = typer.Argument(..., help="Knowledge item ID"),
|
|
2174
|
+
content_id: str = typer.Option(
|
|
2175
|
+
..., "--content", help="Content item ID to link to"
|
|
2176
|
+
),
|
|
2177
|
+
format: str = typer.Option(
|
|
2178
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
2179
|
+
),
|
|
2180
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2181
|
+
) -> None:
|
|
2182
|
+
"""Link a knowledge item to a content item so sync can manage it."""
|
|
2183
|
+
client = get_client(shop_id=shop_id)
|
|
2184
|
+
result = asyncio.run(
|
|
2185
|
+
tools.knowledge_link(
|
|
2186
|
+
client,
|
|
2187
|
+
id,
|
|
2188
|
+
content_id=content_id,
|
|
2189
|
+
output_format=format,
|
|
2190
|
+
)
|
|
2191
|
+
)
|
|
2192
|
+
typer.echo(result)
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
@app.command("knowledge-protect")
|
|
2196
|
+
def knowledge_protect(
|
|
2197
|
+
id: str = typer.Argument(..., help="Knowledge item ID"),
|
|
2198
|
+
format: str = typer.Option(
|
|
2199
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
2200
|
+
),
|
|
2201
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2202
|
+
) -> None:
|
|
2203
|
+
"""Disable autogeneration on the linked content for a knowledge item."""
|
|
2204
|
+
client = get_client(shop_id=shop_id)
|
|
2205
|
+
result = asyncio.run(
|
|
2206
|
+
tools.knowledge_protect(
|
|
2207
|
+
client,
|
|
2208
|
+
id,
|
|
2209
|
+
protected=True,
|
|
2210
|
+
output_format=format,
|
|
2211
|
+
)
|
|
2212
|
+
)
|
|
2213
|
+
typer.echo(result)
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
@app.command("knowledge-unprotect")
|
|
2217
|
+
def knowledge_unprotect(
|
|
2218
|
+
id: str = typer.Argument(..., help="Knowledge item ID"),
|
|
2219
|
+
format: str = typer.Option(
|
|
2220
|
+
"text", "--format", "-f", help="Output format: text or json"
|
|
2221
|
+
),
|
|
2222
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2223
|
+
) -> None:
|
|
2224
|
+
"""Re-enable autogeneration on the linked content for a knowledge item."""
|
|
2225
|
+
client = get_client(shop_id=shop_id)
|
|
2226
|
+
result = asyncio.run(
|
|
2227
|
+
tools.knowledge_protect(
|
|
2228
|
+
client,
|
|
2229
|
+
id,
|
|
2230
|
+
protected=False,
|
|
2231
|
+
output_format=format,
|
|
1985
2232
|
)
|
|
1986
2233
|
)
|
|
1987
2234
|
typer.echo(result)
|
|
@@ -2226,6 +2473,287 @@ def product_update(
|
|
|
2226
2473
|
typer.echo(result)
|
|
2227
2474
|
|
|
2228
2475
|
|
|
2476
|
+
@app.command("agent-deploy")
|
|
2477
|
+
def agent_deploy(
|
|
2478
|
+
agent_ids: list[str] = typer.Argument(..., help="One or more Agent IDs to deploy"),
|
|
2479
|
+
description: str = typer.Option("", "--description", "-d", help="Optional revision description"),
|
|
2480
|
+
shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
|
|
2481
|
+
) -> None:
|
|
2482
|
+
"""Deploy the current configuration for one or more agents as new live revisions.
|
|
2483
|
+
|
|
2484
|
+
Creates a new revision from each agent's current state and immediately sets it live,
|
|
2485
|
+
so new conversations use the latest changes.
|
|
2486
|
+
|
|
2487
|
+
Pass multiple agent IDs to deploy several agents at once:
|
|
2488
|
+
applied agent-deploy <id1> <id2> <id3> --description "what changed"
|
|
2489
|
+
"""
|
|
2490
|
+
client = get_client(shop_id=shop_id)
|
|
2491
|
+
|
|
2492
|
+
async def _deploy_one(agent_id: str):
|
|
2493
|
+
return await client._request(
|
|
2494
|
+
"POST",
|
|
2495
|
+
"/v1/revisions/",
|
|
2496
|
+
body={"agent_id": agent_id, "deploy": True, "description": description},
|
|
2497
|
+
)
|
|
2498
|
+
|
|
2499
|
+
async def _deploy_all():
|
|
2500
|
+
import asyncio as _asyncio
|
|
2501
|
+
return await _asyncio.gather(*[_deploy_one(aid) for aid in agent_ids])
|
|
2502
|
+
|
|
2503
|
+
results = asyncio.run(_deploy_all())
|
|
2504
|
+
for result in results:
|
|
2505
|
+
version = result.get("version", "?")
|
|
2506
|
+
revision_id = result.get("id", "?")
|
|
2507
|
+
is_live = result.get("is_live", False)
|
|
2508
|
+
agent_id = result.get("agent_id", "?")
|
|
2509
|
+
typer.echo(f"Deployed agent {agent_id}: revision v{version} (id={revision_id}, is_live={is_live})")
|
|
2510
|
+
|
|
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
|
+
|
|
2229
2757
|
def main() -> None:
|
|
2230
2758
|
"""CLI entrypoint."""
|
|
2231
2759
|
nested_exit_code = run_agent_scoped_flow_command(sys.argv[1:], get_client)
|