fix: log denied tool calls against their table; demo persona and data access; screenshots #16
No files matched your search
@@ -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.
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
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);
|
||||
|
||||