fix: log denied tool calls against their table; demo persona and data access; screenshots #16

Merged
KofTwentyTwo merged 2 commits from feature/demo-screens into develop 2026-10-13 19:29:40 +00:00
29 changed files with 67 additions and 2 deletions

No files matched your search

+10
View File
@@ -10,3 +10,13 @@ Light and dark, 390 and 1440 px:
- `provider-not-set-*`: a provider whose key variable is not set.
- `provider-edit-*`: the edit form.
- `prices-*`: the model price table.
0.1.0-RC.6 additions, taken 2026-10-13 with the same demo server and the Next UI 1.0.0-RC.17 jar. The demo seeds a profile ("Ada"), two skills, and data access rows (page READ_WRITE, media NONE). Its tools run through the `/qqq/v1` UI API.
Light and dark, 390 and 1440 px:
- `home-denials-*`: the home's "Recent denials" card. The analyst's `count_records` on Blog Post got HTTP 403 from `POST /table/{tableName}/count`.
- `home-access-*`: the "Data access: what the assistant can do for you" widget (Explain access), with each table's access and each tool allowed or denied, with the reason.
- `data-access-*`: the Data access table.
- `profile-*`: a profile (persona), with its personality, its scope and its Skills child list.
- `skill-*`: a skill: its instructions and when it is used.
- `policy-*`: the policy, with its "Tools and data" section.
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -129,6 +129,22 @@ public class AssistantDemoServer extends AbstractQQQApplication
"cacheWrite1hUsdPerMTok", BigDecimal.valueOf(p.getCacheWrite1hUsdPerMTok()), "notes", "Anthropic list price, 2026-10"));
}
////////////////////////////////////////////////////////////////////
// persona, a skill, and data access (page READ_WRITE, media NONE) //
////////////////////////////////////////////////////////////////////
Integer ada = insert("assistant_profile", Map.of("name", "default", "displayName", "Ada", "icon", "school", "isDefault", true,
"personality", "Friendly and brief. Use plain words and short lists.", "greeting", "Hi, I'm Ada. Ask me about your posts and pages.",
"starterPrompts", "How many published posts are there?\nWhat changed on page 12 this week?\nWhich posts have no excerpt?"));
insert("assistant_profile", Map.of("name", "analysts", "displayName", "Ada (analytics)", "icon", "insights", "permissionName", "analytics_daily_stat.read",
"personality", "Lead with the number, then one sentence of context."));
Integer style = insert("assistant_skill", Map.of("name", "blog-style-guide", "description", "House style for titles and excerpts",
"instructions", "## Blog style\n- Titles: sentence case, at most 60 characters.\n- Excerpts: one sentence, no links.", "triggerKeywords", "title, excerpt, headline"));
insert("assistant_skill", Map.of("name", "seo-basics", "description", "Meta description rules", "instructions", "Meta descriptions are 120 to 155 characters.",
"triggerKeywords", "seo, meta description", "enabled", true));
insert("assistant_profile_skill", Map.of("profileId", ada, "skillId", style));
insert("assistant_data_access", Map.of("objectType", "TABLE", "objectName", "page", "mode", "READ_WRITE", "notes", "Editors may let the assistant draft page changes (confirmed in the panel)."));
insert("assistant_data_access", Map.of("objectType", "TABLE", "objectName", "media", "mode", "NONE", "notes", "Not needed for content questions."));
api.thenStream("text-with-thinking.sse", 0);
ConnectionTest.run(config.getRuntime(), config.getRuntime().getStore().getProvider(production), "admin-1");
ConnectionTest.run(config.getRuntime(), config.getRuntime().getStore().getProvider(staging), "admin-1");
@@ -105,6 +105,26 @@ public class AssistantRouteProvider implements QJavalinRouteProviderInterface
@Override
public void acceptJavalinConfig(JavalinConfig config)
{
///////////////////////////////////////////////////////////////////////
// learn this server's local port from the first UI request (e.g. //
// the metaData load), so tools and Explain access can loop back to //
// /qqq/v1 before any assistant route has been called //
///////////////////////////////////////////////////////////////////////
config.routes.before("/qqq/v1/*", ctx ->
{
AssistantRuntime runtime = AssistantRuntime.of(qInstance);
if(runtime != null && runtime.uiApiBaseUrl() == null)
{
try
{
runtime.learnLocalPort(ctx.req().getLocalPort());
}
catch(RuntimeException e)
{
LOG.debug("Could not read the local port", e);
}
}
});
config.routes.get(BASE_PATH + "/config", ctx -> json(ctx, false, (api, body) -> api.config()));
config.routes.get(BASE_PATH + "/conversations", ctx -> json(ctx, false, (api, body) -> api.listConversations(ctx.queryParam("status"))));
config.routes.post(BASE_PATH + "/conversations", ctx -> json(ctx, true, (api, body) ->
@@ -159,8 +159,20 @@ public final class AssistantAccessWidget
}
String table = input.getQueryParams() == null ? null : input.getQueryParams().get("table");
String prompt = input.getQueryParams() == null ? null : input.getQueryParams().get("prompt");
List<AccessExplainer.TableAccess> tables = runtime.getAccessExplainer().explain(runtime.getUiApiGateway(), QContext.getQSession().getUuid(),
runtime.getPolicyService().current(false).tools(), table, prompt);
List<AccessExplainer.TableAccess> tables;
try
{
tables = runtime.getAccessExplainer().explain(runtime.getUiApiGateway(), QContext.getQSession().getUuid(), runtime.getPolicyService().current(false).tools(), table, prompt);
}
catch(QException e)
{
////////////////////////////////////////////////////////////////////
// the UI API is not reachable (no assistant request has told the //
// server its port yet, or the call failed): say so, don't guess //
////////////////////////////////////////////////////////////////////
return (new RenderWidgetOutput(new RawHTML(null, "<p style=\"opacity:.75;font-size:.9rem;margin:0;\">Access could not be computed from the UI API right now ("
+ AssistantHomeView.esc(e.getMessage()) + "). Open the assistant panel once, or set uiApiBaseUrl, then refresh.</p>")));
}
String user = QContext.getQSession().getUser() == null ? "you" : StringUtils.hasContent(QContext.getQSession().getUser().getFullName())
? QContext.getQSession().getUser().getFullName() : QContext.getQSession().getUser().getIdReference();
return (new RenderWidgetOutput(new RawHTML(null, AssistantAccessWidget.render(tables, user, runtime.getConfig().getTablePathPrefix()))));
@@ -382,6 +382,13 @@ public class UiApiToolbox
}
Map<String, Object> in = input == null ? Map.of() : input;
if(in.get("table") instanceof String table && StringUtils.hasContent(table))
{
///////////////////////////////////////////////////////////////
// known up front, so a denial is logged against its table //
///////////////////////////////////////////////////////////////
result.setTableName(table.strip());
}
switch(toolName)
{
case LIST_TABLES -> listTables(context, outcome);