diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index c49e3ce..a547c08 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -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. diff --git a/docs/screenshots/data-access-dark-1440.png b/docs/screenshots/data-access-dark-1440.png new file mode 100644 index 0000000..8a645c3 Binary files /dev/null and b/docs/screenshots/data-access-dark-1440.png differ diff --git a/docs/screenshots/data-access-dark-390.png b/docs/screenshots/data-access-dark-390.png new file mode 100644 index 0000000..9e2a3e9 Binary files /dev/null and b/docs/screenshots/data-access-dark-390.png differ diff --git a/docs/screenshots/data-access-light-1440.png b/docs/screenshots/data-access-light-1440.png new file mode 100644 index 0000000..0ade36f Binary files /dev/null and b/docs/screenshots/data-access-light-1440.png differ diff --git a/docs/screenshots/data-access-light-390.png b/docs/screenshots/data-access-light-390.png new file mode 100644 index 0000000..e35e255 Binary files /dev/null and b/docs/screenshots/data-access-light-390.png differ diff --git a/docs/screenshots/home-access-dark-1440.png b/docs/screenshots/home-access-dark-1440.png new file mode 100644 index 0000000..fd95279 Binary files /dev/null and b/docs/screenshots/home-access-dark-1440.png differ diff --git a/docs/screenshots/home-access-dark-390.png b/docs/screenshots/home-access-dark-390.png new file mode 100644 index 0000000..471f85d Binary files /dev/null and b/docs/screenshots/home-access-dark-390.png differ diff --git a/docs/screenshots/home-access-light-1440.png b/docs/screenshots/home-access-light-1440.png new file mode 100644 index 0000000..1291c31 Binary files /dev/null and b/docs/screenshots/home-access-light-1440.png differ diff --git a/docs/screenshots/home-access-light-390.png b/docs/screenshots/home-access-light-390.png new file mode 100644 index 0000000..2d0694d Binary files /dev/null and b/docs/screenshots/home-access-light-390.png differ diff --git a/docs/screenshots/home-denials-dark-1440.png b/docs/screenshots/home-denials-dark-1440.png new file mode 100644 index 0000000..fd95279 Binary files /dev/null and b/docs/screenshots/home-denials-dark-1440.png differ diff --git a/docs/screenshots/home-denials-dark-390.png b/docs/screenshots/home-denials-dark-390.png new file mode 100644 index 0000000..471f85d Binary files /dev/null and b/docs/screenshots/home-denials-dark-390.png differ diff --git a/docs/screenshots/home-denials-light-1440.png b/docs/screenshots/home-denials-light-1440.png new file mode 100644 index 0000000..1291c31 Binary files /dev/null and b/docs/screenshots/home-denials-light-1440.png differ diff --git a/docs/screenshots/home-denials-light-390.png b/docs/screenshots/home-denials-light-390.png new file mode 100644 index 0000000..2d0694d Binary files /dev/null and b/docs/screenshots/home-denials-light-390.png differ diff --git a/docs/screenshots/policy-dark-1440.png b/docs/screenshots/policy-dark-1440.png new file mode 100644 index 0000000..3a8869e Binary files /dev/null and b/docs/screenshots/policy-dark-1440.png differ diff --git a/docs/screenshots/policy-dark-390.png b/docs/screenshots/policy-dark-390.png new file mode 100644 index 0000000..7966acf Binary files /dev/null and b/docs/screenshots/policy-dark-390.png differ diff --git a/docs/screenshots/policy-light-1440.png b/docs/screenshots/policy-light-1440.png new file mode 100644 index 0000000..90c1002 Binary files /dev/null and b/docs/screenshots/policy-light-1440.png differ diff --git a/docs/screenshots/policy-light-390.png b/docs/screenshots/policy-light-390.png new file mode 100644 index 0000000..1c3c433 Binary files /dev/null and b/docs/screenshots/policy-light-390.png differ diff --git a/docs/screenshots/profile-dark-1440.png b/docs/screenshots/profile-dark-1440.png new file mode 100644 index 0000000..d001081 Binary files /dev/null and b/docs/screenshots/profile-dark-1440.png differ diff --git a/docs/screenshots/profile-dark-390.png b/docs/screenshots/profile-dark-390.png new file mode 100644 index 0000000..dc2c801 Binary files /dev/null and b/docs/screenshots/profile-dark-390.png differ diff --git a/docs/screenshots/profile-light-1440.png b/docs/screenshots/profile-light-1440.png new file mode 100644 index 0000000..385e90a Binary files /dev/null and b/docs/screenshots/profile-light-1440.png differ diff --git a/docs/screenshots/profile-light-390.png b/docs/screenshots/profile-light-390.png new file mode 100644 index 0000000..33250cf Binary files /dev/null and b/docs/screenshots/profile-light-390.png differ diff --git a/docs/screenshots/skill-dark-1440.png b/docs/screenshots/skill-dark-1440.png new file mode 100644 index 0000000..c193088 Binary files /dev/null and b/docs/screenshots/skill-dark-1440.png differ diff --git a/docs/screenshots/skill-dark-390.png b/docs/screenshots/skill-dark-390.png new file mode 100644 index 0000000..b0a2e6a Binary files /dev/null and b/docs/screenshots/skill-dark-390.png differ diff --git a/docs/screenshots/skill-light-1440.png b/docs/screenshots/skill-light-1440.png new file mode 100644 index 0000000..4d3f688 Binary files /dev/null and b/docs/screenshots/skill-light-1440.png differ diff --git a/docs/screenshots/skill-light-390.png b/docs/screenshots/skill-light-390.png new file mode 100644 index 0000000..31079fc Binary files /dev/null and b/docs/screenshots/skill-light-390.png differ diff --git a/qbit-assistant-provider-anthropic/src/test/java/com/kingsrook/qbits/assistant/provider/anthropic/demo/AssistantDemoServer.java b/qbit-assistant-provider-anthropic/src/test/java/com/kingsrook/qbits/assistant/provider/anthropic/demo/AssistantDemoServer.java index ced4e6c..e95553e 100644 --- a/qbit-assistant-provider-anthropic/src/test/java/com/kingsrook/qbits/assistant/provider/anthropic/demo/AssistantDemoServer.java +++ b/qbit-assistant-provider-anthropic/src/test/java/com/kingsrook/qbits/assistant/provider/anthropic/demo/AssistantDemoServer.java @@ -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"); diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/api/AssistantRouteProvider.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/api/AssistantRouteProvider.java index 34a843c..16ff446 100644 --- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/api/AssistantRouteProvider.java +++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/api/AssistantRouteProvider.java @@ -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) -> diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/home/AssistantAccessWidget.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/home/AssistantAccessWidget.java index 21bd06a..7880ea8 100644 --- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/home/AssistantAccessWidget.java +++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/home/AssistantAccessWidget.java @@ -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 tables = runtime.getAccessExplainer().explain(runtime.getUiApiGateway(), QContext.getQSession().getUuid(), - runtime.getPolicyService().current(false).tools(), table, prompt); + List 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, "

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.

"))); + } 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())))); diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/tools/UiApiToolbox.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/tools/UiApiToolbox.java index 30eb37a..6e1e6aa 100644 --- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/tools/UiApiToolbox.java +++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/tools/UiApiToolbox.java @@ -382,6 +382,13 @@ public class UiApiToolbox } Map 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);