@josephyan/qingflow-app-builder-mcp 0.2.0-beta.5 → 0.2.0-beta.6
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.
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.6 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -305,8 +305,30 @@ class AiBuilderFacade:
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
308
|
-
|
|
308
|
+
try:
|
|
309
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
310
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
311
|
+
api_error = _coerce_api_error(error)
|
|
312
|
+
return _failed_from_api_error(
|
|
313
|
+
"APP_READ_FAILED",
|
|
314
|
+
api_error,
|
|
315
|
+
normalized_args={"app_key": app_key},
|
|
316
|
+
details={"app_key": app_key},
|
|
317
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
|
|
318
|
+
)
|
|
319
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
320
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
309
321
|
parsed = state["parsed"]
|
|
322
|
+
verification_hints = _build_verification_hints(
|
|
323
|
+
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
324
|
+
fields=parsed["fields"],
|
|
325
|
+
layout=parsed["layout"],
|
|
326
|
+
views=_summarize_views(views),
|
|
327
|
+
)
|
|
328
|
+
if views_unavailable:
|
|
329
|
+
verification_hints.append("views_read_unavailable")
|
|
330
|
+
if workflow_unavailable:
|
|
331
|
+
verification_hints.append("workflow_read_unavailable")
|
|
310
332
|
response = AppReadSummaryResponse(
|
|
311
333
|
app_key=app_key,
|
|
312
334
|
title=state["base"].get("formTitle"),
|
|
@@ -314,14 +336,9 @@ class AiBuilderFacade:
|
|
|
314
336
|
publish_status=state["base"].get("appPublishStatus"),
|
|
315
337
|
field_count=len(parsed["fields"]),
|
|
316
338
|
layout_section_count=len(parsed["layout"].get("sections", [])),
|
|
317
|
-
view_count=len(_summarize_views(
|
|
318
|
-
workflow_enabled=bool(
|
|
319
|
-
verification_hints=
|
|
320
|
-
tag_ids=_coerce_int_list(state["base"].get("tagIds")),
|
|
321
|
-
fields=parsed["fields"],
|
|
322
|
-
layout=parsed["layout"],
|
|
323
|
-
views=_summarize_views(state["views"]),
|
|
324
|
-
),
|
|
339
|
+
view_count=len(_summarize_views(views)),
|
|
340
|
+
workflow_enabled=bool(workflow),
|
|
341
|
+
verification_hints=verification_hints,
|
|
325
342
|
)
|
|
326
343
|
return {
|
|
327
344
|
"status": "success",
|
|
@@ -340,7 +357,17 @@ class AiBuilderFacade:
|
|
|
340
357
|
}
|
|
341
358
|
|
|
342
359
|
def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
|
|
343
|
-
|
|
360
|
+
try:
|
|
361
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
362
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
363
|
+
api_error = _coerce_api_error(error)
|
|
364
|
+
return _failed_from_api_error(
|
|
365
|
+
"FIELDS_READ_FAILED",
|
|
366
|
+
api_error,
|
|
367
|
+
normalized_args={"app_key": app_key},
|
|
368
|
+
details={"app_key": app_key},
|
|
369
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
370
|
+
)
|
|
344
371
|
parsed = state["parsed"]
|
|
345
372
|
response = AppFieldsReadResponse(
|
|
346
373
|
app_key=app_key,
|
|
@@ -374,7 +401,17 @@ class AiBuilderFacade:
|
|
|
374
401
|
}
|
|
375
402
|
|
|
376
403
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
377
|
-
|
|
404
|
+
try:
|
|
405
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
406
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
407
|
+
api_error = _coerce_api_error(error)
|
|
408
|
+
return _failed_from_api_error(
|
|
409
|
+
"LAYOUT_READ_FAILED",
|
|
410
|
+
api_error,
|
|
411
|
+
normalized_args={"app_key": app_key},
|
|
412
|
+
details={"app_key": app_key},
|
|
413
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
414
|
+
)
|
|
378
415
|
parsed = state["parsed"]
|
|
379
416
|
layout = parsed["layout"]
|
|
380
417
|
response = AppLayoutReadResponse(
|
|
@@ -400,10 +437,20 @@ class AiBuilderFacade:
|
|
|
400
437
|
}
|
|
401
438
|
|
|
402
439
|
def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
403
|
-
|
|
440
|
+
try:
|
|
441
|
+
views, _ = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
442
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
443
|
+
api_error = _coerce_api_error(error)
|
|
444
|
+
return _failed_from_api_error(
|
|
445
|
+
"VIEWS_READ_FAILED",
|
|
446
|
+
api_error,
|
|
447
|
+
normalized_args={"app_key": app_key},
|
|
448
|
+
details={"app_key": app_key},
|
|
449
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
450
|
+
)
|
|
404
451
|
response = AppViewsReadResponse(
|
|
405
452
|
app_key=app_key,
|
|
406
|
-
views=_summarize_views(
|
|
453
|
+
views=_summarize_views(views),
|
|
407
454
|
)
|
|
408
455
|
return {
|
|
409
456
|
"status": "success",
|
|
@@ -422,11 +469,21 @@ class AiBuilderFacade:
|
|
|
422
469
|
}
|
|
423
470
|
|
|
424
471
|
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
425
|
-
|
|
472
|
+
try:
|
|
473
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
474
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
475
|
+
api_error = _coerce_api_error(error)
|
|
476
|
+
return _failed_from_api_error(
|
|
477
|
+
"FLOW_READ_FAILED",
|
|
478
|
+
api_error,
|
|
479
|
+
normalized_args={"app_key": app_key},
|
|
480
|
+
details={"app_key": app_key},
|
|
481
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
482
|
+
)
|
|
426
483
|
response = AppFlowReadResponse(
|
|
427
484
|
app_key=app_key,
|
|
428
|
-
enabled=bool(
|
|
429
|
-
nodes=_summarize_workflow_nodes(
|
|
485
|
+
enabled=bool(workflow),
|
|
486
|
+
nodes=_summarize_workflow_nodes(workflow),
|
|
430
487
|
transitions=[],
|
|
431
488
|
)
|
|
432
489
|
return {
|
|
@@ -441,7 +498,7 @@ class AiBuilderFacade:
|
|
|
441
498
|
"request_id": None,
|
|
442
499
|
"suggested_next_call": None,
|
|
443
500
|
"noop": False,
|
|
444
|
-
"verification": {"app_exists": True},
|
|
501
|
+
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
445
502
|
**response.model_dump(mode="json"),
|
|
446
503
|
}
|
|
447
504
|
|
|
@@ -459,7 +516,11 @@ class AiBuilderFacade:
|
|
|
459
516
|
return target
|
|
460
517
|
current_fields: list[dict[str, Any]] = []
|
|
461
518
|
if not bool(target.get("would_create")):
|
|
462
|
-
|
|
519
|
+
fields_result = self.app_read_fields(profile=profile, app_key=str(target["app_key"]))
|
|
520
|
+
if fields_result.get("status") == "failed":
|
|
521
|
+
fields_result.setdefault("normalized_args", normalized_args)
|
|
522
|
+
return fields_result
|
|
523
|
+
current_fields = fields_result.get("fields", [])
|
|
463
524
|
current_by_name = {str(field.get("name") or ""): field for field in current_fields}
|
|
464
525
|
blocking_issues: list[dict[str, Any]] = []
|
|
465
526
|
preview_added: list[str] = []
|
|
@@ -517,8 +578,12 @@ class AiBuilderFacade:
|
|
|
517
578
|
|
|
518
579
|
def app_layout_plan(self, *, profile: str, request: LayoutPlanRequest) -> JSONObject:
|
|
519
580
|
read_fields = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
581
|
+
if read_fields.get("status") == "failed":
|
|
582
|
+
return read_fields
|
|
520
583
|
current_names = [str(field.get("name") or "") for field in read_fields.get("fields", []) if field.get("name")]
|
|
521
584
|
current_layout = self.app_read_layout_summary(profile=profile, app_key=request.app_key)
|
|
585
|
+
if current_layout.get("status") == "failed":
|
|
586
|
+
return current_layout
|
|
522
587
|
requested_sections = [section.model_dump(mode="json") for section in request.sections]
|
|
523
588
|
if request.preset is not None:
|
|
524
589
|
requested_sections = _build_layout_preset_sections(preset=request.preset, field_names=current_names)
|
|
@@ -581,7 +646,10 @@ class AiBuilderFacade:
|
|
|
581
646
|
transitions = [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions]
|
|
582
647
|
if request.preset is not None:
|
|
583
648
|
nodes, transitions = _build_flow_preset(request.preset)
|
|
584
|
-
|
|
649
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
650
|
+
if fields_result.get("status") == "failed":
|
|
651
|
+
return fields_result
|
|
652
|
+
current_fields = fields_result.get("fields", [])
|
|
585
653
|
status_field_present = _infer_status_field_id(current_fields) is not None
|
|
586
654
|
node_types = {str(node.get("type") or "") for node in nodes}
|
|
587
655
|
if ("approve" in node_types or request.preset in {FlowPreset.basic_approval, FlowPreset.basic_fill_then_approve}) and not status_field_present:
|
|
@@ -651,7 +719,10 @@ class AiBuilderFacade:
|
|
|
651
719
|
}
|
|
652
720
|
|
|
653
721
|
def app_views_plan(self, *, profile: str, request: ViewsPlanRequest) -> JSONObject:
|
|
654
|
-
|
|
722
|
+
fields_result = self.app_read_fields(profile=profile, app_key=request.app_key)
|
|
723
|
+
if fields_result.get("status") == "failed":
|
|
724
|
+
return fields_result
|
|
725
|
+
current_fields = fields_result.get("fields", [])
|
|
655
726
|
field_names = {str(field.get("name") or "") for field in current_fields}
|
|
656
727
|
upsert_views = [view.model_dump(mode="json") for view in request.upsert_views]
|
|
657
728
|
if request.preset is not None:
|
|
@@ -996,7 +1067,17 @@ class AiBuilderFacade:
|
|
|
996
1067
|
"sections": [section.model_dump(mode="json") for section in sections],
|
|
997
1068
|
"publish": publish,
|
|
998
1069
|
}
|
|
999
|
-
|
|
1070
|
+
try:
|
|
1071
|
+
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1072
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1073
|
+
api_error = _coerce_api_error(error)
|
|
1074
|
+
return _failed_from_api_error(
|
|
1075
|
+
"LAYOUT_READ_FAILED",
|
|
1076
|
+
api_error,
|
|
1077
|
+
normalized_args=normalized_args,
|
|
1078
|
+
details={"app_key": app_key},
|
|
1079
|
+
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1080
|
+
)
|
|
1000
1081
|
parsed = _parse_schema(schema_result)
|
|
1001
1082
|
current_fields = parsed["fields"]
|
|
1002
1083
|
fields_by_name = {field["name"]: field for field in current_fields}
|
|
@@ -1127,7 +1208,32 @@ class AiBuilderFacade:
|
|
|
1127
1208
|
},
|
|
1128
1209
|
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1129
1210
|
)
|
|
1130
|
-
verified = self.
|
|
1211
|
+
verified = self.app_read_layout_summary(profile=profile, app_key=app_key)
|
|
1212
|
+
if verified.get("status") == "failed":
|
|
1213
|
+
response = {
|
|
1214
|
+
"status": "partial_success",
|
|
1215
|
+
"error_code": "LAYOUT_READBACK_PENDING",
|
|
1216
|
+
"recoverable": True,
|
|
1217
|
+
"message": "applied app layout; layout readback pending",
|
|
1218
|
+
"normalized_args": normalized_args,
|
|
1219
|
+
"missing_fields": [],
|
|
1220
|
+
"allowed_values": {"modes": ["merge", "replace"]},
|
|
1221
|
+
"details": {},
|
|
1222
|
+
"request_id": verified.get("request_id"),
|
|
1223
|
+
"suggested_next_call": {"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1224
|
+
"noop": False,
|
|
1225
|
+
"verification": {"layout_verified": False, "layout_read_unavailable": True},
|
|
1226
|
+
"app_key": app_key,
|
|
1227
|
+
"layout_diff": {
|
|
1228
|
+
"mode": mode.value,
|
|
1229
|
+
"replaced": mode == LayoutApplyMode.replace,
|
|
1230
|
+
"merged": mode == LayoutApplyMode.merge,
|
|
1231
|
+
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1232
|
+
"fallback_applied": fallback_applied,
|
|
1233
|
+
},
|
|
1234
|
+
"verified": False,
|
|
1235
|
+
}
|
|
1236
|
+
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1131
1237
|
response = {
|
|
1132
1238
|
"status": "partial_success" if fallback_applied else "success",
|
|
1133
1239
|
"error_code": None,
|
|
@@ -1140,7 +1246,7 @@ class AiBuilderFacade:
|
|
|
1140
1246
|
"request_id": None,
|
|
1141
1247
|
"suggested_next_call": None,
|
|
1142
1248
|
"noop": False,
|
|
1143
|
-
"verification": {"layout_verified": verified["
|
|
1249
|
+
"verification": {"layout_verified": verified["sections"] == applied_layout.get("sections", [])},
|
|
1144
1250
|
"app_key": app_key,
|
|
1145
1251
|
"layout_diff": {
|
|
1146
1252
|
"mode": mode.value,
|
|
@@ -1149,7 +1255,7 @@ class AiBuilderFacade:
|
|
|
1149
1255
|
"auto_added_fields": merged["auto_added_fields"] if mode == LayoutApplyMode.merge else [],
|
|
1150
1256
|
"fallback_applied": fallback_applied,
|
|
1151
1257
|
},
|
|
1152
|
-
"verified": verified["
|
|
1258
|
+
"verified": verified["sections"] == applied_layout.get("sections", []),
|
|
1153
1259
|
}
|
|
1154
1260
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1155
1261
|
|
|
@@ -1178,8 +1284,18 @@ class AiBuilderFacade:
|
|
|
1178
1284
|
allowed_values={"modes": ["replace"]},
|
|
1179
1285
|
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}},
|
|
1180
1286
|
)
|
|
1181
|
-
|
|
1182
|
-
|
|
1287
|
+
try:
|
|
1288
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1289
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1290
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1291
|
+
api_error = _coerce_api_error(error)
|
|
1292
|
+
return _failed_from_api_error(
|
|
1293
|
+
"FLOW_READ_FAILED",
|
|
1294
|
+
api_error,
|
|
1295
|
+
normalized_args=normalized_args,
|
|
1296
|
+
details={"app_key": app_key},
|
|
1297
|
+
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1298
|
+
)
|
|
1183
1299
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
1184
1300
|
workflow_spec = _build_public_workflow_spec(nodes=nodes, transitions=transitions)
|
|
1185
1301
|
if workflow_spec.get("status") == "failed":
|
|
@@ -1188,7 +1304,8 @@ class AiBuilderFacade:
|
|
|
1188
1304
|
workflow_spec["suggested_next_call"] = {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1189
1305
|
return workflow_spec
|
|
1190
1306
|
desired_node_count = len([node for node in nodes if node.get("type") != "end"])
|
|
1191
|
-
|
|
1307
|
+
current_workflow, _workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1308
|
+
current_node_count = len(_summarize_workflow_nodes(current_workflow))
|
|
1192
1309
|
if current_node_count == desired_node_count and desired_node_count > 0:
|
|
1193
1310
|
# Lightweight idempotency check for repeat submissions of same simple graph.
|
|
1194
1311
|
pass
|
|
@@ -1232,12 +1349,12 @@ class AiBuilderFacade:
|
|
|
1232
1349
|
failed["normalized_args"] = normalized_args
|
|
1233
1350
|
failed["suggested_next_call"] = failed.get("suggested_next_call") or {"tool_name": "app_flow_plan", "arguments": {"profile": profile, **normalized_args}}
|
|
1234
1351
|
return failed
|
|
1235
|
-
verified_nodes = self.
|
|
1352
|
+
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1236
1353
|
response = {
|
|
1237
|
-
"status": "success",
|
|
1354
|
+
"status": "success" if bool(verified_nodes) or not verified_nodes_unavailable else "partial_success",
|
|
1238
1355
|
"error_code": None,
|
|
1239
|
-
"recoverable":
|
|
1240
|
-
"message": "applied workflow patch",
|
|
1356
|
+
"recoverable": bool(verified_nodes_unavailable),
|
|
1357
|
+
"message": "applied workflow patch" if not verified_nodes_unavailable else "applied workflow patch; flow readback pending",
|
|
1241
1358
|
"normalized_args": normalized_args,
|
|
1242
1359
|
"missing_fields": [],
|
|
1243
1360
|
"allowed_values": {"modes": ["replace"]},
|
|
@@ -1245,11 +1362,14 @@ class AiBuilderFacade:
|
|
|
1245
1362
|
"request_id": None,
|
|
1246
1363
|
"suggested_next_call": None,
|
|
1247
1364
|
"noop": False,
|
|
1248
|
-
"verification": {"workflow_verified": bool(verified_nodes)},
|
|
1365
|
+
"verification": {"workflow_verified": bool(verified_nodes), "workflow_read_unavailable": verified_nodes_unavailable},
|
|
1249
1366
|
"app_key": app_key,
|
|
1250
1367
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
1251
1368
|
"verified": bool(verified_nodes),
|
|
1252
1369
|
}
|
|
1370
|
+
if verified_nodes_unavailable:
|
|
1371
|
+
response["error_code"] = "FLOW_READBACK_PENDING"
|
|
1372
|
+
response["suggested_next_call"] = {"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}}
|
|
1253
1373
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1254
1374
|
|
|
1255
1375
|
def app_views_apply(
|
|
@@ -1286,9 +1406,20 @@ class AiBuilderFacade:
|
|
|
1286
1406
|
"verified": True,
|
|
1287
1407
|
}
|
|
1288
1408
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1409
|
+
try:
|
|
1410
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1411
|
+
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1412
|
+
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
1413
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1414
|
+
api_error = _coerce_api_error(error)
|
|
1415
|
+
return _failed_from_api_error(
|
|
1416
|
+
"VIEWS_READ_FAILED",
|
|
1417
|
+
api_error,
|
|
1418
|
+
normalized_args=normalized_args,
|
|
1419
|
+
details={"app_key": app_key},
|
|
1420
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1421
|
+
)
|
|
1422
|
+
existing_views = existing_views or []
|
|
1292
1423
|
existing_by_name = {}
|
|
1293
1424
|
for view in existing_views if isinstance(existing_views, list) else []:
|
|
1294
1425
|
if not isinstance(view, dict):
|
|
@@ -1401,22 +1532,37 @@ class AiBuilderFacade:
|
|
|
1401
1532
|
"arguments": {"profile": profile, "app_key": app_key},
|
|
1402
1533
|
},
|
|
1403
1534
|
)
|
|
1404
|
-
|
|
1405
|
-
|
|
1535
|
+
try:
|
|
1536
|
+
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1537
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1538
|
+
api_error = _coerce_api_error(error)
|
|
1539
|
+
return _failed_from_api_error(
|
|
1540
|
+
"VIEWS_READ_FAILED",
|
|
1541
|
+
api_error,
|
|
1542
|
+
normalized_args=normalized_args,
|
|
1543
|
+
details={"app_key": app_key},
|
|
1544
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1545
|
+
)
|
|
1546
|
+
verified_names = {
|
|
1547
|
+
item.get("viewgraphName") or item.get("viewName") or item.get("title")
|
|
1548
|
+
for item in (verified_view_result or [])
|
|
1549
|
+
if isinstance(item, dict)
|
|
1550
|
+
}
|
|
1551
|
+
verified = (not verified_views_unavailable) and all(name in verified_names for name in created + updated) and all(name not in verified_names for name in removed)
|
|
1406
1552
|
noop = not created and not updated and not removed
|
|
1407
1553
|
response = {
|
|
1408
1554
|
"status": "success" if verified else "partial_success",
|
|
1409
|
-
"error_code": None,
|
|
1410
|
-
"recoverable":
|
|
1411
|
-
"message": "applied view patch",
|
|
1555
|
+
"error_code": None if not verified_views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1556
|
+
"recoverable": bool(verified_views_unavailable),
|
|
1557
|
+
"message": "applied view patch" if not verified_views_unavailable else "applied view patch; views readback pending",
|
|
1412
1558
|
"normalized_args": normalized_args,
|
|
1413
1559
|
"missing_fields": [],
|
|
1414
1560
|
"allowed_values": {"view_types": ["table", "card", "board"]},
|
|
1415
1561
|
"details": {},
|
|
1416
1562
|
"request_id": None,
|
|
1417
|
-
"suggested_next_call": None,
|
|
1563
|
+
"suggested_next_call": None if not verified_views_unavailable else {"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1418
1564
|
"noop": noop,
|
|
1419
|
-
"verification": {"views_verified": verified},
|
|
1565
|
+
"verification": {"views_verified": verified, "views_read_unavailable": verified_views_unavailable},
|
|
1420
1566
|
"app_key": app_key,
|
|
1421
1567
|
"views_diff": {"created": created, "updated": updated, "removed": removed},
|
|
1422
1568
|
"verified": verified,
|
|
@@ -1431,12 +1577,33 @@ class AiBuilderFacade:
|
|
|
1431
1577
|
expected_package_tag_id: int | None = None,
|
|
1432
1578
|
) -> JSONObject:
|
|
1433
1579
|
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
1434
|
-
|
|
1580
|
+
try:
|
|
1581
|
+
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1582
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1583
|
+
api_error = _coerce_api_error(error)
|
|
1584
|
+
return _failed_from_api_error(
|
|
1585
|
+
"APP_READ_FAILED",
|
|
1586
|
+
api_error,
|
|
1587
|
+
normalized_args=normalized_args,
|
|
1588
|
+
details={"app_key": app_key},
|
|
1589
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1590
|
+
)
|
|
1435
1591
|
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
1436
1592
|
already_published = bool(base_before.get("appPublishStatus") in {1, 2})
|
|
1437
1593
|
package_already_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_before
|
|
1438
|
-
|
|
1439
|
-
|
|
1594
|
+
try:
|
|
1595
|
+
views_before, views_before_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1596
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1597
|
+
api_error = _coerce_api_error(error)
|
|
1598
|
+
return _failed_from_api_error(
|
|
1599
|
+
"VIEWS_READ_FAILED",
|
|
1600
|
+
api_error,
|
|
1601
|
+
normalized_args=normalized_args,
|
|
1602
|
+
details={"app_key": app_key},
|
|
1603
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1604
|
+
)
|
|
1605
|
+
views_before = views_before or []
|
|
1606
|
+
if already_published and package_already_attached is not False and isinstance(views_before, list) and not views_before_unavailable:
|
|
1440
1607
|
return {
|
|
1441
1608
|
"status": "success",
|
|
1442
1609
|
"error_code": None,
|
|
@@ -1471,17 +1638,38 @@ class AiBuilderFacade:
|
|
|
1471
1638
|
details={"app_key": app_key, "edit_version_no": edit_version_no},
|
|
1472
1639
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1473
1640
|
)
|
|
1474
|
-
|
|
1641
|
+
try:
|
|
1642
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1643
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1644
|
+
api_error = _coerce_api_error(error)
|
|
1645
|
+
return _failed_from_api_error(
|
|
1646
|
+
"APP_READ_FAILED",
|
|
1647
|
+
api_error,
|
|
1648
|
+
normalized_args=normalized_args,
|
|
1649
|
+
details={"app_key": app_key},
|
|
1650
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1651
|
+
)
|
|
1475
1652
|
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
1476
1653
|
package_attached = None if not expected_package_tag_id else expected_package_tag_id in tag_ids_after
|
|
1477
|
-
|
|
1478
|
-
|
|
1654
|
+
try:
|
|
1655
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1656
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1657
|
+
api_error = _coerce_api_error(error)
|
|
1658
|
+
return _failed_from_api_error(
|
|
1659
|
+
"VIEWS_READ_FAILED",
|
|
1660
|
+
api_error,
|
|
1661
|
+
normalized_args=normalized_args,
|
|
1662
|
+
details={"app_key": app_key},
|
|
1663
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1664
|
+
)
|
|
1665
|
+
views = views or []
|
|
1666
|
+
views_ok = isinstance(views, list) and not views_unavailable
|
|
1479
1667
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
1480
1668
|
return {
|
|
1481
1669
|
"status": "success" if verified else "partial_success",
|
|
1482
|
-
"error_code": None,
|
|
1483
|
-
"recoverable":
|
|
1484
|
-
"message": "published and verified app",
|
|
1670
|
+
"error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
|
|
1671
|
+
"recoverable": bool(views_unavailable),
|
|
1672
|
+
"message": "published and verified app" if not views_unavailable else "published app; views readback pending",
|
|
1485
1673
|
"normalized_args": normalized_args,
|
|
1486
1674
|
"missing_fields": [],
|
|
1487
1675
|
"allowed_values": {},
|
|
@@ -1494,7 +1682,7 @@ class AiBuilderFacade:
|
|
|
1494
1682
|
"arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
|
|
1495
1683
|
},
|
|
1496
1684
|
"noop": False,
|
|
1497
|
-
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok},
|
|
1685
|
+
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
1498
1686
|
"app_key": app_key,
|
|
1499
1687
|
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
1500
1688
|
"package_attached": package_attached,
|
|
@@ -1558,21 +1746,59 @@ class AiBuilderFacade:
|
|
|
1558
1746
|
response["suggested_next_call"] = publish_result.get("suggested_next_call")
|
|
1559
1747
|
return response
|
|
1560
1748
|
|
|
1561
|
-
def
|
|
1749
|
+
def _load_base_schema_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1562
1750
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
1563
1751
|
schema_result, schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
1564
|
-
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1565
|
-
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1566
1752
|
base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
|
|
1567
1753
|
return {
|
|
1568
1754
|
"base": base_result,
|
|
1569
1755
|
"schema": schema_result,
|
|
1570
1756
|
"parsed": _parse_schema(schema_result),
|
|
1571
|
-
"views": views.get("result"),
|
|
1572
|
-
"workflow": workflow.get("result"),
|
|
1573
1757
|
"schema_source": schema_source,
|
|
1574
1758
|
}
|
|
1575
1759
|
|
|
1760
|
+
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
1761
|
+
try:
|
|
1762
|
+
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
1763
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1764
|
+
api_error = _coerce_api_error(error)
|
|
1765
|
+
if api_error.http_status == 404:
|
|
1766
|
+
try:
|
|
1767
|
+
legacy_views = self.views.view_list(profile=profile, app_key=app_key)
|
|
1768
|
+
except (QingflowApiError, RuntimeError) as legacy_error:
|
|
1769
|
+
legacy_api_error = _coerce_api_error(legacy_error)
|
|
1770
|
+
if tolerate_404 and legacy_api_error.http_status == 404:
|
|
1771
|
+
return [], True
|
|
1772
|
+
raise
|
|
1773
|
+
legacy_result = legacy_views.get("result")
|
|
1774
|
+
if _is_view_collection_shape(legacy_result):
|
|
1775
|
+
return _normalize_view_collection(legacy_result), False
|
|
1776
|
+
if tolerate_404:
|
|
1777
|
+
return [], True
|
|
1778
|
+
raise error
|
|
1779
|
+
raise
|
|
1780
|
+
return _normalize_view_collection(views.get("result")), False
|
|
1781
|
+
|
|
1782
|
+
def _load_workflow_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
1783
|
+
try:
|
|
1784
|
+
workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
1785
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1786
|
+
api_error = _coerce_api_error(error)
|
|
1787
|
+
if tolerate_404 and api_error.http_status == 404:
|
|
1788
|
+
return [], True
|
|
1789
|
+
raise
|
|
1790
|
+
return workflow.get("result"), False
|
|
1791
|
+
|
|
1792
|
+
def _load_app_state(self, *, profile: str, app_key: str) -> dict[str, Any]:
|
|
1793
|
+
state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
1794
|
+
views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1795
|
+
workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
1796
|
+
state["views"] = views
|
|
1797
|
+
state["workflow"] = workflow
|
|
1798
|
+
state["views_unavailable"] = views_unavailable
|
|
1799
|
+
state["workflow_unavailable"] = workflow_unavailable
|
|
1800
|
+
return state
|
|
1801
|
+
|
|
1576
1802
|
def _read_schema_with_fallback(self, *, profile: str, app_key: str) -> tuple[dict[str, Any], str]:
|
|
1577
1803
|
attempts = (
|
|
1578
1804
|
("draft", True),
|
|
@@ -1806,18 +2032,30 @@ def _failed_from_api_error(
|
|
|
1806
2032
|
suggested_next_call: JSONObject | None = None,
|
|
1807
2033
|
recoverable: bool = True,
|
|
1808
2034
|
) -> JSONObject:
|
|
2035
|
+
public_message = _public_error_message(error_code, error)
|
|
2036
|
+
public_http_status = None if error.http_status == 404 else error.http_status
|
|
2037
|
+
merged_details = dict(details or {})
|
|
2038
|
+
if error.http_status is not None or error.backend_code is not None:
|
|
2039
|
+
merged_details.setdefault(
|
|
2040
|
+
"transport_error",
|
|
2041
|
+
{
|
|
2042
|
+
"http_status": error.http_status,
|
|
2043
|
+
"backend_code": error.backend_code,
|
|
2044
|
+
"category": error.category,
|
|
2045
|
+
},
|
|
2046
|
+
)
|
|
1809
2047
|
return _failed(
|
|
1810
2048
|
error_code,
|
|
1811
|
-
|
|
2049
|
+
public_message,
|
|
1812
2050
|
recoverable=recoverable,
|
|
1813
2051
|
normalized_args=normalized_args,
|
|
1814
2052
|
missing_fields=missing_fields,
|
|
1815
2053
|
allowed_values=allowed_values,
|
|
1816
|
-
details=
|
|
2054
|
+
details=merged_details,
|
|
1817
2055
|
suggested_next_call=suggested_next_call,
|
|
1818
2056
|
request_id=error.request_id,
|
|
1819
2057
|
backend_code=error.backend_code,
|
|
1820
|
-
http_status=
|
|
2058
|
+
http_status=public_http_status,
|
|
1821
2059
|
)
|
|
1822
2060
|
|
|
1823
2061
|
|
|
@@ -1856,6 +2094,27 @@ def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
|
1856
2094
|
return QingflowApiError(category="runtime", message=str(error))
|
|
1857
2095
|
|
|
1858
2096
|
|
|
2097
|
+
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
2098
|
+
if error.http_status != 404:
|
|
2099
|
+
return error.message
|
|
2100
|
+
mapping = {
|
|
2101
|
+
"APP_READ_FAILED": "app base or schema is unavailable in the current route",
|
|
2102
|
+
"FIELDS_READ_FAILED": "app fields are unavailable in the current route",
|
|
2103
|
+
"LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
|
|
2104
|
+
"VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
|
|
2105
|
+
"FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
|
|
2106
|
+
"SCHEMA_READBACK_FAILED": "schema was written but schema readback is unavailable in the current route",
|
|
2107
|
+
"CREATE_APP_ROUTE_NOT_FOUND": "create app route is unavailable in the current workspace route",
|
|
2108
|
+
"APP_CREATE_READBACK_FAILED": "app was created but base readback is unavailable in the current route",
|
|
2109
|
+
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
2110
|
+
"PUBLISH_FAILED": "publish route is unavailable in the current route",
|
|
2111
|
+
"VIEW_APPLY_FAILED": "view resource rejected the operation or is unavailable in the current route",
|
|
2112
|
+
"LAYOUT_APPLY_FAILED": "layout resource rejected the operation or is unavailable in the current route",
|
|
2113
|
+
"SCHEMA_APPLY_FAILED": "schema resource rejected the operation or is unavailable in the current route",
|
|
2114
|
+
}
|
|
2115
|
+
return mapping.get(error_code, "requested builder resource is unavailable in the current route")
|
|
2116
|
+
|
|
2117
|
+
|
|
1859
2118
|
def _coerce_positive_int(value: Any) -> int | None:
|
|
1860
2119
|
if isinstance(value, bool) or value is None:
|
|
1861
2120
|
return None
|
|
@@ -1883,6 +2142,25 @@ def _coerce_int_list(values: Any) -> list[int]:
|
|
|
1883
2142
|
return result
|
|
1884
2143
|
|
|
1885
2144
|
|
|
2145
|
+
def _normalize_view_collection(values: Any) -> list[dict[str, Any]]:
|
|
2146
|
+
if isinstance(values, list):
|
|
2147
|
+
return [item for item in values if isinstance(item, dict)]
|
|
2148
|
+
if isinstance(values, dict):
|
|
2149
|
+
for key in ("list", "viewList", "views", "result"):
|
|
2150
|
+
candidate = values.get(key)
|
|
2151
|
+
if isinstance(candidate, list):
|
|
2152
|
+
return [item for item in candidate if isinstance(item, dict)]
|
|
2153
|
+
return []
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
def _is_view_collection_shape(values: Any) -> bool:
|
|
2157
|
+
if isinstance(values, list):
|
|
2158
|
+
return True
|
|
2159
|
+
if isinstance(values, dict):
|
|
2160
|
+
return any(isinstance(values.get(key), list) for key in ("list", "viewList", "views", "result"))
|
|
2161
|
+
return False
|
|
2162
|
+
|
|
2163
|
+
|
|
1886
2164
|
def _empty_schema_result(title: str) -> dict[str, Any]:
|
|
1887
2165
|
return {
|
|
1888
2166
|
"formTitle": title,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from pydantic import ValidationError
|
|
4
6
|
|
|
5
7
|
from ..config import DEFAULT_PROFILE
|
|
8
|
+
from ..errors import QingflowApiError
|
|
6
9
|
from ..json_types import JSONObject
|
|
7
10
|
from ..builder_facade.models import (
|
|
8
11
|
FieldPatch,
|
|
@@ -240,31 +243,85 @@ class AiBuilderTools(ToolBase):
|
|
|
240
243
|
)
|
|
241
244
|
|
|
242
245
|
def package_list(self, *, profile: str, trial_status: str = "all") -> JSONObject:
|
|
243
|
-
|
|
246
|
+
normalized_args = {"trial_status": trial_status}
|
|
247
|
+
return _safe_tool_call(
|
|
248
|
+
lambda: self._facade.package_list(profile=profile, trial_status=trial_status),
|
|
249
|
+
error_code="PACKAGE_LIST_FAILED",
|
|
250
|
+
normalized_args=normalized_args,
|
|
251
|
+
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": trial_status}},
|
|
252
|
+
)
|
|
244
253
|
|
|
245
254
|
def package_resolve(self, *, profile: str, package_name: str) -> JSONObject:
|
|
246
|
-
|
|
255
|
+
normalized_args = {"package_name": package_name}
|
|
256
|
+
return _safe_tool_call(
|
|
257
|
+
lambda: self._facade.package_resolve(profile=profile, package_name=package_name),
|
|
258
|
+
error_code="PACKAGE_RESOLVE_FAILED",
|
|
259
|
+
normalized_args=normalized_args,
|
|
260
|
+
suggested_next_call={"tool_name": "package_resolve", "arguments": {"profile": profile, "package_name": package_name}},
|
|
261
|
+
)
|
|
247
262
|
|
|
248
263
|
def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str = "") -> JSONObject:
|
|
249
|
-
|
|
264
|
+
normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
|
|
265
|
+
return _safe_tool_call(
|
|
266
|
+
lambda: self._facade.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
|
|
267
|
+
error_code="PACKAGE_ATTACH_FAILED",
|
|
268
|
+
normalized_args=normalized_args,
|
|
269
|
+
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, **normalized_args}},
|
|
270
|
+
)
|
|
250
271
|
|
|
251
272
|
def app_resolve(self, *, profile: str, app_key: str = "", app_name: str = "", package_tag_id: int | None = None) -> JSONObject:
|
|
252
|
-
|
|
273
|
+
normalized_args = {"app_key": app_key, "app_name": app_name, "package_tag_id": package_tag_id}
|
|
274
|
+
return _safe_tool_call(
|
|
275
|
+
lambda: self._facade.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id),
|
|
276
|
+
error_code="APP_RESOLVE_FAILED",
|
|
277
|
+
normalized_args=normalized_args,
|
|
278
|
+
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
|
|
279
|
+
)
|
|
253
280
|
|
|
254
281
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
255
|
-
|
|
282
|
+
normalized_args = {"app_key": app_key}
|
|
283
|
+
return _safe_tool_call(
|
|
284
|
+
lambda: self._facade.app_read_summary(profile=profile, app_key=app_key),
|
|
285
|
+
error_code="APP_READ_FAILED",
|
|
286
|
+
normalized_args=normalized_args,
|
|
287
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
288
|
+
)
|
|
256
289
|
|
|
257
290
|
def app_read_fields(self, *, profile: str, app_key: str) -> JSONObject:
|
|
258
|
-
|
|
291
|
+
normalized_args = {"app_key": app_key}
|
|
292
|
+
return _safe_tool_call(
|
|
293
|
+
lambda: self._facade.app_read_fields(profile=profile, app_key=app_key),
|
|
294
|
+
error_code="FIELDS_READ_FAILED",
|
|
295
|
+
normalized_args=normalized_args,
|
|
296
|
+
suggested_next_call={"tool_name": "app_read_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
297
|
+
)
|
|
259
298
|
|
|
260
299
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
261
|
-
|
|
300
|
+
normalized_args = {"app_key": app_key}
|
|
301
|
+
return _safe_tool_call(
|
|
302
|
+
lambda: self._facade.app_read_layout_summary(profile=profile, app_key=app_key),
|
|
303
|
+
error_code="LAYOUT_READ_FAILED",
|
|
304
|
+
normalized_args=normalized_args,
|
|
305
|
+
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
306
|
+
)
|
|
262
307
|
|
|
263
308
|
def app_read_views_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
264
|
-
|
|
309
|
+
normalized_args = {"app_key": app_key}
|
|
310
|
+
return _safe_tool_call(
|
|
311
|
+
lambda: self._facade.app_read_views_summary(profile=profile, app_key=app_key),
|
|
312
|
+
error_code="VIEWS_READ_FAILED",
|
|
313
|
+
normalized_args=normalized_args,
|
|
314
|
+
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
315
|
+
)
|
|
265
316
|
|
|
266
317
|
def app_read_flow_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
267
|
-
|
|
318
|
+
normalized_args = {"app_key": app_key}
|
|
319
|
+
return _safe_tool_call(
|
|
320
|
+
lambda: self._facade.app_read_flow_summary(profile=profile, app_key=app_key),
|
|
321
|
+
error_code="FLOW_READ_FAILED",
|
|
322
|
+
normalized_args=normalized_args,
|
|
323
|
+
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
324
|
+
)
|
|
268
325
|
|
|
269
326
|
def app_schema_plan(
|
|
270
327
|
self,
|
|
@@ -307,7 +364,12 @@ class AiBuilderTools(ToolBase):
|
|
|
307
364
|
},
|
|
308
365
|
},
|
|
309
366
|
)
|
|
310
|
-
return
|
|
367
|
+
return _safe_tool_call(
|
|
368
|
+
lambda: self._facade.app_schema_plan(profile=profile, request=request),
|
|
369
|
+
error_code="SCHEMA_PLAN_FAILED",
|
|
370
|
+
normalized_args=request.model_dump(mode="json"),
|
|
371
|
+
suggested_next_call={"tool_name": "app_schema_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
372
|
+
)
|
|
311
373
|
|
|
312
374
|
def app_layout_plan(
|
|
313
375
|
self,
|
|
@@ -341,7 +403,12 @@ class AiBuilderTools(ToolBase):
|
|
|
341
403
|
},
|
|
342
404
|
},
|
|
343
405
|
)
|
|
344
|
-
return
|
|
406
|
+
return _safe_tool_call(
|
|
407
|
+
lambda: self._facade.app_layout_plan(profile=profile, request=request),
|
|
408
|
+
error_code="LAYOUT_PLAN_FAILED",
|
|
409
|
+
normalized_args=request.model_dump(mode="json"),
|
|
410
|
+
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
411
|
+
)
|
|
345
412
|
|
|
346
413
|
def app_flow_plan(
|
|
347
414
|
self,
|
|
@@ -378,7 +445,12 @@ class AiBuilderTools(ToolBase):
|
|
|
378
445
|
},
|
|
379
446
|
},
|
|
380
447
|
)
|
|
381
|
-
return
|
|
448
|
+
return _safe_tool_call(
|
|
449
|
+
lambda: self._facade.app_flow_plan(profile=profile, request=request),
|
|
450
|
+
error_code="FLOW_PLAN_FAILED",
|
|
451
|
+
normalized_args=request.model_dump(mode="json"),
|
|
452
|
+
suggested_next_call={"tool_name": "app_flow_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
453
|
+
)
|
|
382
454
|
|
|
383
455
|
def app_views_plan(
|
|
384
456
|
self,
|
|
@@ -412,7 +484,12 @@ class AiBuilderTools(ToolBase):
|
|
|
412
484
|
},
|
|
413
485
|
},
|
|
414
486
|
)
|
|
415
|
-
return
|
|
487
|
+
return _safe_tool_call(
|
|
488
|
+
lambda: self._facade.app_views_plan(profile=profile, request=request),
|
|
489
|
+
error_code="VIEWS_PLAN_FAILED",
|
|
490
|
+
normalized_args=request.model_dump(mode="json"),
|
|
491
|
+
suggested_next_call={"tool_name": "app_views_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
|
|
492
|
+
)
|
|
416
493
|
|
|
417
494
|
def app_schema_apply(
|
|
418
495
|
self,
|
|
@@ -450,16 +527,31 @@ class AiBuilderTools(ToolBase):
|
|
|
450
527
|
},
|
|
451
528
|
},
|
|
452
529
|
)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
530
|
+
normalized_args = {
|
|
531
|
+
"app_key": app_key,
|
|
532
|
+
"package_tag_id": package_tag_id,
|
|
533
|
+
"app_name": effective_app_name,
|
|
534
|
+
"create_if_missing": create_if_missing,
|
|
535
|
+
"publish": publish,
|
|
536
|
+
"add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
|
|
537
|
+
"update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
|
|
538
|
+
"remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
|
|
539
|
+
}
|
|
540
|
+
return _safe_tool_call(
|
|
541
|
+
lambda: self._facade.app_schema_apply(
|
|
542
|
+
profile=profile,
|
|
543
|
+
app_key=app_key,
|
|
544
|
+
package_tag_id=package_tag_id,
|
|
545
|
+
app_name=effective_app_name,
|
|
546
|
+
create_if_missing=create_if_missing,
|
|
547
|
+
publish=publish,
|
|
548
|
+
add_fields=parsed_add,
|
|
549
|
+
update_fields=parsed_update,
|
|
550
|
+
remove_fields=parsed_remove,
|
|
551
|
+
),
|
|
552
|
+
error_code="SCHEMA_APPLY_FAILED",
|
|
553
|
+
normalized_args=normalized_args,
|
|
554
|
+
suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
463
555
|
)
|
|
464
556
|
|
|
465
557
|
def app_layout_apply(self, *, profile: str, app_key: str, mode: str = "merge", publish: bool = True, sections: list[JSONObject]) -> JSONObject:
|
|
@@ -495,7 +587,18 @@ class AiBuilderTools(ToolBase):
|
|
|
495
587
|
},
|
|
496
588
|
},
|
|
497
589
|
)
|
|
498
|
-
|
|
590
|
+
normalized_args = {
|
|
591
|
+
"app_key": app_key,
|
|
592
|
+
"mode": parsed_mode.value,
|
|
593
|
+
"publish": publish,
|
|
594
|
+
"sections": [section.model_dump(mode="json") for section in parsed_sections],
|
|
595
|
+
}
|
|
596
|
+
return _safe_tool_call(
|
|
597
|
+
lambda: self._facade.app_layout_apply(profile=profile, app_key=app_key, mode=parsed_mode, publish=publish, sections=parsed_sections),
|
|
598
|
+
error_code="LAYOUT_APPLY_FAILED",
|
|
599
|
+
normalized_args=normalized_args,
|
|
600
|
+
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
601
|
+
)
|
|
499
602
|
|
|
500
603
|
def app_flow_apply(
|
|
501
604
|
self,
|
|
@@ -531,13 +634,25 @@ class AiBuilderTools(ToolBase):
|
|
|
531
634
|
},
|
|
532
635
|
},
|
|
533
636
|
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
637
|
+
normalized_args = {
|
|
638
|
+
"app_key": request.app_key,
|
|
639
|
+
"mode": request.mode,
|
|
640
|
+
"publish": publish,
|
|
641
|
+
"nodes": [node.model_dump(mode="json") for node in request.nodes],
|
|
642
|
+
"transitions": [transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
643
|
+
}
|
|
644
|
+
return _safe_tool_call(
|
|
645
|
+
lambda: self._facade.app_flow_apply(
|
|
646
|
+
profile=profile,
|
|
647
|
+
app_key=request.app_key,
|
|
648
|
+
mode=request.mode,
|
|
649
|
+
publish=publish,
|
|
650
|
+
nodes=[node.model_dump(mode="json") for node in request.nodes],
|
|
651
|
+
transitions=[transition.model_dump(mode="json", by_alias=True) for transition in request.transitions],
|
|
652
|
+
),
|
|
653
|
+
error_code="FLOW_APPLY_FAILED",
|
|
654
|
+
normalized_args=normalized_args,
|
|
655
|
+
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
541
656
|
)
|
|
542
657
|
|
|
543
658
|
def app_views_apply(
|
|
@@ -564,10 +679,27 @@ class AiBuilderTools(ToolBase):
|
|
|
564
679
|
},
|
|
565
680
|
},
|
|
566
681
|
)
|
|
567
|
-
|
|
682
|
+
normalized_args = {
|
|
683
|
+
"app_key": app_key,
|
|
684
|
+
"publish": publish,
|
|
685
|
+
"upsert_views": [view.model_dump(mode="json") for view in parsed_views],
|
|
686
|
+
"remove_views": list(remove_views),
|
|
687
|
+
}
|
|
688
|
+
return _safe_tool_call(
|
|
689
|
+
lambda: self._facade.app_views_apply(profile=profile, app_key=app_key, publish=publish, upsert_views=parsed_views, remove_views=remove_views),
|
|
690
|
+
error_code="VIEWS_APPLY_FAILED",
|
|
691
|
+
normalized_args=normalized_args,
|
|
692
|
+
suggested_next_call={"tool_name": "app_views_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
693
|
+
)
|
|
568
694
|
|
|
569
695
|
def app_publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None = None) -> JSONObject:
|
|
570
|
-
|
|
696
|
+
normalized_args = {"app_key": app_key, "expected_package_tag_id": expected_package_tag_id}
|
|
697
|
+
return _safe_tool_call(
|
|
698
|
+
lambda: self._facade.app_publish_verify(profile=profile, app_key=app_key, expected_package_tag_id=expected_package_tag_id),
|
|
699
|
+
error_code="PUBLISH_VERIFY_FAILED",
|
|
700
|
+
normalized_args=normalized_args,
|
|
701
|
+
suggested_next_call={"tool_name": "app_publish_verify", "arguments": {"profile": profile, **normalized_args}},
|
|
702
|
+
)
|
|
571
703
|
|
|
572
704
|
|
|
573
705
|
def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None = None) -> JSONObject:
|
|
@@ -587,3 +719,86 @@ def _validation_failure(detail: str, *, suggested_next_call: JSONObject | None =
|
|
|
587
719
|
"noop": False,
|
|
588
720
|
"verification": {},
|
|
589
721
|
}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _safe_tool_call(
|
|
725
|
+
call,
|
|
726
|
+
*,
|
|
727
|
+
error_code: str,
|
|
728
|
+
normalized_args: JSONObject,
|
|
729
|
+
suggested_next_call: JSONObject | None,
|
|
730
|
+
) -> JSONObject:
|
|
731
|
+
try:
|
|
732
|
+
return call()
|
|
733
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
734
|
+
api_error = _coerce_api_error(error)
|
|
735
|
+
public_http_status = None if api_error.http_status == 404 else api_error.http_status
|
|
736
|
+
return {
|
|
737
|
+
"status": "failed",
|
|
738
|
+
"error_code": error_code,
|
|
739
|
+
"recoverable": True,
|
|
740
|
+
"message": _public_error_message(error_code, api_error),
|
|
741
|
+
"normalized_args": normalized_args,
|
|
742
|
+
"missing_fields": [],
|
|
743
|
+
"allowed_values": {},
|
|
744
|
+
"details": {
|
|
745
|
+
"transport_error": {
|
|
746
|
+
"http_status": api_error.http_status,
|
|
747
|
+
"backend_code": api_error.backend_code,
|
|
748
|
+
"category": api_error.category,
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
"suggested_next_call": suggested_next_call,
|
|
752
|
+
"request_id": api_error.request_id,
|
|
753
|
+
"backend_code": api_error.backend_code,
|
|
754
|
+
"http_status": public_http_status,
|
|
755
|
+
"noop": False,
|
|
756
|
+
"verification": {},
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _coerce_api_error(error: Exception) -> QingflowApiError:
|
|
761
|
+
if isinstance(error, QingflowApiError):
|
|
762
|
+
return error
|
|
763
|
+
if isinstance(error, RuntimeError):
|
|
764
|
+
try:
|
|
765
|
+
payload = json.loads(str(error))
|
|
766
|
+
except json.JSONDecodeError:
|
|
767
|
+
payload = None
|
|
768
|
+
if isinstance(payload, dict) and payload.get("category") and payload.get("message"):
|
|
769
|
+
details = payload.get("details")
|
|
770
|
+
return QingflowApiError(
|
|
771
|
+
category=str(payload.get("category")),
|
|
772
|
+
message=str(payload.get("message")),
|
|
773
|
+
backend_code=payload.get("backend_code"),
|
|
774
|
+
request_id=payload.get("request_id"),
|
|
775
|
+
http_status=payload.get("http_status"),
|
|
776
|
+
details=details if isinstance(details, dict) else None,
|
|
777
|
+
)
|
|
778
|
+
return QingflowApiError(category="runtime", message=str(error))
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
782
|
+
if error.http_status != 404:
|
|
783
|
+
return error.message
|
|
784
|
+
mapping = {
|
|
785
|
+
"PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
|
|
786
|
+
"PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
|
|
787
|
+
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
788
|
+
"APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
|
|
789
|
+
"APP_READ_FAILED": "app base or schema is unavailable in the current route",
|
|
790
|
+
"FIELDS_READ_FAILED": "app fields are unavailable in the current route",
|
|
791
|
+
"LAYOUT_READ_FAILED": "layout resource is unavailable for this app in the current route",
|
|
792
|
+
"VIEWS_READ_FAILED": "views resource is unavailable for this app in the current route",
|
|
793
|
+
"FLOW_READ_FAILED": "workflow resource is unavailable for this app in the current route",
|
|
794
|
+
"SCHEMA_PLAN_FAILED": "schema planning could not load the required app state in the current route",
|
|
795
|
+
"LAYOUT_PLAN_FAILED": "layout planning could not load the required app state in the current route",
|
|
796
|
+
"FLOW_PLAN_FAILED": "flow planning could not load the required app state in the current route",
|
|
797
|
+
"VIEWS_PLAN_FAILED": "views planning could not load the required app state in the current route",
|
|
798
|
+
"SCHEMA_APPLY_FAILED": "schema apply could not complete because the app route or readback is unavailable",
|
|
799
|
+
"LAYOUT_APPLY_FAILED": "layout apply could not complete because the layout route or readback is unavailable",
|
|
800
|
+
"FLOW_APPLY_FAILED": "flow apply could not complete because the workflow route or readback is unavailable",
|
|
801
|
+
"VIEWS_APPLY_FAILED": "views apply could not complete because the views route or readback is unavailable",
|
|
802
|
+
"PUBLISH_VERIFY_FAILED": "publish verification is unavailable in the current route",
|
|
803
|
+
}
|
|
804
|
+
return mapping.get(error_code, "requested builder resource is unavailable in the current route")
|