Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dd92de98e
|
||
|
|
a9ff632750
|
No files matched your search
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
|
||||
@@ -29,7 +29,7 @@ Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed def
|
||||
|
||||
## Modules
|
||||
|
||||
- `qbit-assistant-provider-api`: standalone provider SPI/value types and `ModelPricing`, all exclusively in `com.kingsrook.qbits.assistant.spi`; only Jackson annotations in production. Provider validation takes unresolved `ProviderSettings`, not an entity.
|
||||
- `qbit-assistant-provider-api`: standalone provider SPI/value types and `engine.ModelPricing` (original package names); only Jackson annotations in production. Provider validation takes unresolved `ProviderSettings`, not an entity.
|
||||
|
||||
- `qbit-assistant` (`com.kingsrook.qbits.assistant`):
|
||||
- `AssistantQBitProducer`, `AssistantQBitConfig` (with `AssistantModelRouting` and `AssistantBudgets`), `AssistantRuntime`, `AssistantSecurity`.
|
||||
|
||||
+17
-15
@@ -1,17 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased - Starter defaults
|
||||
### Changed: W1 core Preferences (F13 / #29)
|
||||
- Replace the bespoke assistant preferences operations with the core `assistant`
|
||||
section at `/qqq/v1/preferences/assistant`, backed by the existing visible table.
|
||||
User ownership, partial updates, policy validation, read-only explanations and
|
||||
explicit assistant-channel denial apply to the section.
|
||||
- Keep effective defaults on `/assistant/config`; preserve Read-only/Write tools,
|
||||
low-risk auto-confirm, develop's core invoker, provider routing and audit context.
|
||||
- Require per-task approval and Write-mode policy for scheduled writes; gate bulk
|
||||
write definitions and calls by mode. Preserve default-off audit history on core develop.
|
||||
- Target QQQ 4.1.0-RC.8; use the local PREFS snapshot override until publication.
|
||||
|
||||
- Every seed changeset's rollback is empty: a seeded row cannot be told apart from a host's own row with the same name, and Liquibase also rolls back changesets it marked ran, so a name-based undo would delete host data.
|
||||
|
||||
- Fix the defaults changelog: every precondition is a portable `sql-check` (the formatted-SQL `table-exists` form failed to parse); the policy, budget and prices are seeded only into empty tables and the profile only when no default exists, so an existing host never changes on deploy. The test now runs the changelog through Liquibase 4.33, including an already configured host and a missing table.
|
||||
- The Setup card prices the default model the way calls are costed (registry, price rows, configured prices); "Add rows for every table" reads fresh data-access rows.
|
||||
|
||||
- Ship `db/liquibase/qbit-assistant/assistant-defaults.changelog.sql`: insert-only starter rows for the policy, a default profile, four starter skills offered through it, a monthly instance budget (warn, downgrade, stop), current Anthropic prices and a disabled example scheduled task.
|
||||
- Add the Setup card to the Assistant home: every configuration table with its state (set up, needs setup, using defaults, optional) and the next step.
|
||||
- Add the `assistant_data_access` table action "Add rows for every table", which writes the default access out as editable rows.
|
||||
- Add `help/qbit-assistant.yaml` (QQQ help-bundle format) for the app and every assistant table, saying what each table is for and how its rows get created; applied by `AssistantHelp` until core help bundles ship in a release.
|
||||
- Guardrail tests: every table is used at runtime, seeded or explained, and has empty-state help; the changelog seeds a fresh schema and the seeded rows change the assistant's behavior.
|
||||
|
||||
## Unreleased - X1/Y1
|
||||
|
||||
@@ -33,14 +32,11 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch scheduled-result email delivery to qbit-email 0.1.0-RC.2's published `com.kingsrook.qbits.email.api` package, preserving templates, categories, idempotency keys, linked records and status mapping.
|
||||
- Add optional qbit-email 0.1.0-RC.1 delivery of a scheduled task's result (`AssistantEmail.sendScheduledResult`): template `assistant_scheduled_result`, link-only secure message for results above PUBLIC using core's `DataClassification`, owner-recipient hook, opt-in and `emailExternalApproved` gates. Scheduled-task wiring remains a separate integration step.
|
||||
|
||||
## Unreleased - Provider API extraction (#30)
|
||||
|
||||
- **BREAKING:** move all 23 provider API types, including `ModelPricing`, to `com.kingsrook.qbits.assistant.spi`. Recompile consumers with the new imports; see UPGRADE.md. Enforce exclusive package ownership and API dependency boundaries with ArchUnit.
|
||||
|
||||
- Extract provider SPI/value types and `ModelPricing` into `qbit-assistant-provider-api`, now exclusively in `com.kingsrook.qbits.assistant.spi`. Its only production dependency is Jackson annotations.
|
||||
- Extract provider SPI/value types and `engine.ModelPricing` into `qbit-assistant-provider-api`, preserving package names. Its only production dependency is Jackson annotations.
|
||||
- Move Anthropic, OpenAI, Bedrock and Vertex main dependencies to the API; retain runtime fixtures in test scope. Pgvector still needs runtime RAG interfaces.
|
||||
- Provider-specific validation now takes unresolved `ProviderSettings` instead of `AssistantProvider`; add an ArchUnit boundary and standalone coverage gates.
|
||||
## Unreleased - P-g
|
||||
@@ -122,6 +118,12 @@ Phase 0/1 of the QQQ Assistant: read-only Q&A and navigation. Requires QQQ 4.1 w
|
||||
- the AI tool kernel (`qqq-backend-core` `actions.ai`, QRun-IO/qqq#1078)
|
||||
- the developer-panel contract (QRun-IO/qqq#1076).
|
||||
|
||||
### Added (0.1.0-RC.7): write mode (owner decision 2026-10-13)
|
||||
- Per-conversation **Read-only** (default) or **Write** mode: `writeMode` on conversations (PATCH, create), the write tools exist only in Write mode, and a write tool called in Read-only mode is refused before any request (`mode:READ_ONLY`).
|
||||
- Policy: `writeModeAllowed`, `writeModePermission` (role), `autoConfirmAllowed`. Data access: `autoConfirmOk` per table.
|
||||
- `assistant_user_preference` (own rows): default mode of new conversations, auto-confirm of low-risk writes (single-record insert/update on auto-confirm-OK tables; never deletes). Core `GET`/`PATCH /qqq/v1/preferences/assistant`; `/config` reports `features.writeMode`, `features.autoConfirm` and `preferences`.
|
||||
- The denial matrix covers the write tools in both modes.
|
||||
|
||||
### Changed (0.1.0-RC.7, needs QQQ with QRun-IO/qqq#1128)
|
||||
- The assistant's endpoints are spec'd `/qqq/v1` operations (UI API extension `assistant` 1.0.0, prefix `/assistant`): same URLs, bodies and statuses, now in the served OpenAPI; turns and events are `text/event-stream`. The raw routes are gone.
|
||||
- Tools call core's `UiApiInvoker` with the caller's own request credentials (`UiApiCredentials`); the loopback stand-in is removed. The assistant's own operations are never invocable by its tools.
|
||||
|
||||
@@ -29,7 +29,7 @@ Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed def
|
||||
|
||||
## Modules
|
||||
|
||||
- `qbit-assistant-provider-api`: standalone provider SPI/value types and `ModelPricing`, all exclusively in `com.kingsrook.qbits.assistant.spi`; only Jackson annotations in production. Provider validation takes unresolved `ProviderSettings`, not an entity.
|
||||
- `qbit-assistant-provider-api`: standalone provider SPI/value types and `engine.ModelPricing` (original package names); only Jackson annotations in production. Provider validation takes unresolved `ProviderSettings`, not an entity.
|
||||
|
||||
- `qbit-assistant` (`com.kingsrook.qbits.assistant`):
|
||||
- `AssistantQBitProducer`, `AssistantQBitConfig` (with `AssistantModelRouting` and `AssistantBudgets`), `AssistantRuntime`, `AssistantSecurity`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# QBit Assistant
|
||||
|
||||
An in-app AI assistant for any QQQ application. The user asks a question in a chat panel in the Next UI. The assistant reads the application's data **with exactly that user's permissions**, answers with numbers taken from the data, and links to the records and lists it used. This first version only reads data and opens screens. It cannot change anything.
|
||||
An in-app AI assistant for any QQQ application. The user asks a question in a chat panel in the Next UI. The assistant reads the application's data **with exactly that user's permissions**, answers with numbers taken from the data, and links to the records and lists it used. Conversations start Read-only. Users can select Write mode when policy permits it; changes normally wait for their confirmation.
|
||||
|
||||
Design spec: `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md`.
|
||||
|
||||
@@ -20,19 +20,42 @@ Design spec: `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md`.
|
||||
|
||||
Depend on `com.kingsrook.qbits:qbit-assistant-provider-api` at the same version as
|
||||
QBit Assistant, and implement `AssistantModelProvider` and optionally
|
||||
`AssistantProviderType`. No assistant runtime dependency is needed. All API types, including
|
||||
`ModelPricing`, live exclusively in `com.kingsrook.qbits.assistant.spi`. External
|
||||
consumers such as qbit-marketing allow that package in their G1 guardrails;
|
||||
qbit-bom will add it to the G1 defaults. See [migration imports](UPGRADE.md#provider-api-extraction-30).
|
||||
`AssistantProviderType`. No assistant runtime dependency is needed. Existing
|
||||
`com.kingsrook.qbits.assistant.provider.*` imports and
|
||||
`com.kingsrook.qbits.assistant.engine.ModelPricing` stay the same.
|
||||
`AssistantProviderType.validate(ProviderSettings)` receives unresolved settings
|
||||
(including `apiKeyEnvName`), without a database entity or resolved credentials.
|
||||
Hosts register providers through `AssistantQBitConfig` in `qbit-assistant`.
|
||||
|
||||
Data tools use the QQQ UI API (`/qqq/v1`) with the signed-in user's session. The UI API enforces permissions and record locks; assistant data-access policy and field exposure can only narrow access. Only record-free navigation and help use the core AI tool kernel. Every tool result reaches the model wrapped as untrusted data.
|
||||
|
||||
## Write mode and personal preferences
|
||||
|
||||
The **Assistant** section on the core Preferences page controls **Start conversations
|
||||
in Write mode** and **Auto-confirm low-risk changes**. It is available to users
|
||||
with `assistant.hasAccess`, without requiring `assistant.admin` or permission on
|
||||
the backing table. The standard visible `assistant_user_preference` table remains
|
||||
available to administrators.
|
||||
|
||||
Clients read and partially update settings through `GET` / `PATCH
|
||||
/qqq/v1/preferences/assistant`, with a PATCH body such as
|
||||
`{"values":{"defaultWriteMode":true}}`. There is no user-id parameter. The former
|
||||
`/qqq/v1/assistant/preferences` operations are removed. The panel reads effective
|
||||
defaults from `/qqq/v1/assistant/config` under `preferences`; conversation responses
|
||||
include `writeMode`. Policy can make the effective default Read-only even when
|
||||
the saved preference is Write.
|
||||
|
||||
Write tools, including bulk profile saves and bulk loads, are absent in Read-only
|
||||
mode. Write mode requires policy permission and READ_WRITE data access. Low-risk
|
||||
auto-confirm is limited to single-record inserts/updates on `auto_confirm_ok`
|
||||
tables, with both policy and user opt-in. Its preference is read-only with a reason
|
||||
when policy disallows it. Scheduled tasks additionally require their own approval
|
||||
and always wait for confirmation. The assistant client channel cannot access the
|
||||
preferences section.
|
||||
|
||||
## Host audit attribution (X1/Y1)
|
||||
|
||||
Use QQQ **4.1.0-RC.6 or later**. The QBit explicitly denies every assistant
|
||||
Use QQQ **4.1.0-RC.8 or later** (core Preferences, qqq#1162). Until RC.8 is published, build with `-Dqqq.snapshot.version=4.1.0-PREFS-SNAPSHOT`. The QBit explicitly denies every assistant
|
||||
configuration table, process, report and widget to the `assistant` channel;
|
||||
runtime initialization fails if an assistant object is exposed. The same human
|
||||
can still administer it through Next UI (`next-ui`). Core's security-surface
|
||||
@@ -173,15 +196,6 @@ The key never goes in config, tables or logs. It lives in the application's secr
|
||||
|
||||
The provider's `toString` never contains the key. Its errors carry only the HTTP status and the API error type.
|
||||
|
||||
### Starter defaults
|
||||
|
||||
Include `db/liquibase/qbit-assistant/assistant-defaults.changelog.sql` (shipped in
|
||||
the jar) after your assistant DDL. A new installation then has a policy row, a
|
||||
default profile with four starter skills, a $50/month instance budget, current
|
||||
Anthropic prices and a disabled example scheduled task. Every row is insert-only,
|
||||
so administrators can edit them freely. The home's **Setup** card shows what is
|
||||
set up and the next step for the rest (usually: add a provider).
|
||||
|
||||
## Provider settings (the Providers table)
|
||||
|
||||
Admins set up providers on the **Providers** screen (`assistant_provider`), a standard QQQ table:
|
||||
@@ -364,7 +378,7 @@ Table names follow the QRun-IO naming rule: lowercase snake_case with the `assis
|
||||
|
||||
```bash
|
||||
./mvnw verify # against the committed QQQ version
|
||||
./mvnw verify -Dqqq.snapshot.version=4.1.0-ASSISTDEV-SNAPSHOT # against a local QQQ build with #1078 + #1076
|
||||
./mvnw verify -Dqqq.snapshot.version=4.1.0-PREFS-SNAPSHOT # local core Preferences build until RC.8 is published
|
||||
```
|
||||
|
||||
Two test groups run in every build and fail it on any error:
|
||||
@@ -511,7 +525,7 @@ Apache 2.0.
|
||||
|
||||
When the optional [qbit-email](https://github.com/QRun-IO/qbit-email) is on the classpath **and** produced in the instance, a finished scheduled-task run can be emailed to its owner. Without it nothing changes: `AssistantEmail.sendScheduledResult` returns `NOT_AVAILABLE`. The dependency is `optional`; every reference to it is in `AssistantEmailSender`.
|
||||
|
||||
This integration uses qbit-email **0.1.0-RC.2** through its published `com.kingsrook.qbits.email.api` package. Scheduled tasks (P-g) are available, but the runner does not yet wire email delivery. Hosts can call `AssistantEmail.sendScheduledResult(qInstance, config, new ScheduledResultNotice(...))` after a run, supplying its owner, classification, absolute run link and explicit opt-in as `emailResult`. Persisting a task-level email opt-in and wiring the runner remain a separate integration step.
|
||||
This integration uses qbit-email **0.1.0-RC.1**. Scheduled tasks (P-g) are available, but the runner does not yet wire email delivery. Hosts can call `AssistantEmail.sendScheduledResult(qInstance, config, new ScheduledResultNotice(...))` after a run, supplying its owner, classification, absolute run link and explicit opt-in as `emailResult`. Persisting a task-level email opt-in and wiring the runner remain a separate integration step.
|
||||
|
||||
Gates, in order (each returns a status and queues nothing):
|
||||
1. the task opted in (`NOT_REQUESTED`);
|
||||
|
||||
+27
-36
@@ -1,28 +1,32 @@
|
||||
# Upgrading qbit-assistant
|
||||
|
||||
## Starter defaults, setup card and table help
|
||||
## W1 preferences migration (F13 / #29)
|
||||
|
||||
The jar now ships `db/liquibase/qbit-assistant/assistant-defaults.changelog.sql`.
|
||||
Include it from the host's master changelog **after** the changesets that create
|
||||
the `assistant_*` tables:
|
||||
Requires QQQ **4.1.0-RC.8** for core Preferences (qqq#1162). Until publication,
|
||||
verify with `.\mvnw.cmd -q verify "-Dqqq.snapshot.version=4.1.0-PREFS-SNAPSHOT"`.
|
||||
The root default is RC.8; do not commit the local snapshot override.
|
||||
|
||||
```yaml
|
||||
- include: { file: db/liquibase/qbit-assistant/assistant-defaults.changelog.sql }
|
||||
```
|
||||
Replace `GET` / `PATCH /qqq/v1/assistant/preferences` with
|
||||
`GET` / `PATCH /qqq/v1/preferences/assistant`. PATCH now wraps changed fields in
|
||||
`values`, for example `{"values":{"defaultWriteMode":true}}`; GET returns the
|
||||
core section metadata and `values`. Do not send a user id. Core returns 400 for
|
||||
invalid or policy-read-only values and 404 for a hidden section. These removed
|
||||
operations were on the unmerged W1 branch; the fragment and baseline now describe
|
||||
the core integration. The panel's initial toggle continues to use effective
|
||||
`preferences.defaultWriteMode` from `/qqq/v1/assistant/config`.
|
||||
|
||||
It is insert-only and never changes a host that already configured the
|
||||
assistant: the policy, the **$50/month instance budget** (warn 80%, downgrade
|
||||
100%, stop 120%) and the Anthropic list prices are seeded only into empty tables,
|
||||
and the `default` profile only when no default profile exists. Price rows take
|
||||
precedence over `AssistantQBitConfig.withPricing`, which is why they are seeded
|
||||
only when there are none. The four starter skills and a disabled example
|
||||
scheduled task are always added (by name); on a host with its own default
|
||||
profile the skills are not linked to it, and the Setup card says so. Requires
|
||||
Liquibase 4.x or later (preconditions are `sql-check`s).
|
||||
Existing `assistant_user_preference` rows need no data migration. Hosts installing
|
||||
W1 for the first time must apply its DDL in `docs/schema-postgres.sql` (conversation
|
||||
`write_mode`, policy fields, data-access `auto_confirm_ok`, and the preference
|
||||
table). Keep it as a standard visible admin table. The Preferences page is scoped
|
||||
by `assistant.hasAccess`, not the admin-only Assistant app. The provider explicitly
|
||||
refuses the `assistant` channel because core's section channel checks only apply
|
||||
to app-bound sections.
|
||||
|
||||
Scheduled writes now require task approval plus global/role Write-mode policy.
|
||||
Bulk write tools obey the same conversation mode. Record history stays default-off
|
||||
when running on core versions whose channel defaults allow audit reads.
|
||||
|
||||
No DDL changes. New: the Assistant home's Setup card, the `assistant_data_access`
|
||||
table action **Add rows for every table** (`assistantDataAccessAddDefaults`,
|
||||
admin only) and bundle help on every assistant table (`help/qbit-assistant.yaml`).
|
||||
|
||||
## X1/Y1: channel wall and audit attribution
|
||||
|
||||
@@ -45,22 +49,9 @@ exceed the old 500-character limit. No new QBit column is required. `GET /assist
|
||||
|
||||
## Provider API extraction (#30)
|
||||
|
||||
**BREAKING:** every provider API type now lives in the exclusive package
|
||||
`com.kingsrook.qbits.assistant.spi`. Update imports and recompile consumers.
|
||||
Hosts still receive `qbit-assistant-provider-api` transitively from `qbit-assistant`.
|
||||
No schema migration is needed.
|
||||
|
||||
| Old import | New import |
|
||||
|---|---|
|
||||
| `com.kingsrook.qbits.assistant.provider.X` (API types) | `com.kingsrook.qbits.assistant.spi.X` |
|
||||
| `com.kingsrook.qbits.assistant.engine.ModelPricing` | `com.kingsrook.qbits.assistant.spi.ModelPricing` |
|
||||
|
||||
Internal classes such as `provider.RegistryModelProvider` and
|
||||
`provider.fake.ScriptedModelProvider` stay in the runtime and are not API types.
|
||||
External consumers such as qbit-marketing depend only on
|
||||
`com.kingsrook.qbits:qbit-assistant-provider-api` for these contracts and allow
|
||||
package `com.kingsrook.qbits.assistant.spi` in their guardrails. qbit-bom will add
|
||||
this package to the G1 defaults.
|
||||
Hosts that depend on `qbit-assistant` keep working: it now brings in
|
||||
`qbit-assistant-provider-api` transitively. Package names are unchanged, including
|
||||
`com.kingsrook.qbits.assistant.engine.ModelPricing`. No schema migration is needed.
|
||||
|
||||
External providers and applications using only provider/pricing types should
|
||||
replace their `qbit-assistant` dependency with
|
||||
@@ -73,7 +64,7 @@ Recompile custom provider types that override `validate(AssistantProvider)` and
|
||||
change the signature to `validate(ProviderSettings)`. The settings carry the
|
||||
configured kind, URL, API version, region, project, CA bundle name, timeouts and
|
||||
`apiKeyEnvName`; the key and bearer-token supplier are not resolved for validation.
|
||||
Providers using the default validator need only the import migration. Keep the runtime and
|
||||
Providers using the default validator need no source change. Keep the runtime and
|
||||
provider artifacts on the same version; old validator overrides must be rebuilt.
|
||||
## P-g: scheduled tasks
|
||||
|
||||
|
||||
+36
-20
@@ -16,13 +16,6 @@ All of it is in standard QQQ tables with full record screens (D6), prefixed `ass
|
||||
|
||||
`assistant_turn` keeps the per-turn totals and `assistant_usage_daily` the budget rollups, as before. `assistant_usage` adds the per-call detail the home needs: the provider, the failures and latency.
|
||||
|
||||
## Provider SPI boundary
|
||||
|
||||
All provider contracts and value types, including `ModelPricing`, belong exclusively
|
||||
to `com.kingsrook.qbits.assistant.spi` in `qbit-assistant-provider-api`. Consumers
|
||||
can depend on this artifact alone and allow its package in G1. Runtime provider
|
||||
registry and fake implementations remain in their existing internal packages.
|
||||
|
||||
## API keys
|
||||
|
||||
Decision: **reference, not storage.** A provider row holds the NAME of an environment variable (`apiKeyEnvName`); `ProviderService` resolves it through `config.getSecretResolver()` (default `System::getenv`) when it builds a client.
|
||||
@@ -66,7 +59,7 @@ The result goes on the row (`lastTest*`) and into `assistant_usage` (`TEST`), so
|
||||
|
||||
## Cost
|
||||
|
||||
`cost = input × in + output × out + cacheRead × read + cacheWrite5m × w5 + cacheWrite1h × w1`, in micro-dollars, rounded up (`com.kingsrook.qbits.assistant.spi.ModelPricing.costMicros`).
|
||||
`cost = input × in + output × out + cacheRead × read + cacheWrite5m × w5 + cacheWrite1h × w1`, in micro-dollars, rounded up (`ModelPricing.costMicros`).
|
||||
|
||||
Prices come from the `assistant_model_price` row for (type, model). Empty cache prices are 0.1x, 1.25x and 2x the input price. A model without a row uses the `pricing` config map, else costs 0. A price change affects later calls only; each usage row keeps the cost it was recorded with.
|
||||
|
||||
@@ -245,7 +238,7 @@ The golden and red-team evals now run through the UI API too.
|
||||
- **Tools call core's `UiApiInvoker`** (`CoreUiApiGateway`), and the loopback stand-in is removed.
|
||||
- The request's credentials (`UiApiCredentials.fromContext`: session cookies, Authorization, API key, exactly as they came in) are captured by the spec that starts the turn, confirms an action or explains access. They are passed to the tools, so OAuth2, table auth and API-key sessions all work. They are never logged.
|
||||
- The gateway also refuses the assistant's own operations, so a turn can never start a turn.
|
||||
- QQQ 4.1.0-RC.6 (the default `qqq.snapshot.version`) contains #1128, so no local build is needed.
|
||||
- QQQ 4.1.0-RC.6 contains #1128; W1 now targets RC.8 for core Preferences (see W as built).
|
||||
|
||||
## Owner decisions (2026-10-13), continued: write mode, the hard wall, audit attribution, providers
|
||||
|
||||
@@ -271,6 +264,34 @@ Owner: *"the assistant should be able to go into 'write mode' too - with a user
|
||||
- The ArchUnit rule (no core actions from the tool code) and the denial matrix stay.
|
||||
- The matrix is extended to every write operation, in both modes.
|
||||
|
||||
### W as built (F13 / #29)
|
||||
|
||||
- W1 is integrated with develop's spec'd operations, `CoreUiApiGateway`, client
|
||||
attribution, provider routing, presentation and bulk-load tools. Conversation
|
||||
`writeMode` and all policy/data-access gates remain in force.
|
||||
- `UserPreferencesMetaData` registers section `assistant` (Assistant, smart_toy),
|
||||
scoped to `assistant.hasAccess`. It has no app scope because the Assistant app
|
||||
requires admin access. `AssistantPreferencesProvider` implements core's SPI,
|
||||
loads and updates `assistant_user_preference` through `AssistantStore`, and
|
||||
obtains ownership only from the request context. The visible table remains the
|
||||
administrative interface. No new preference storage is introduced.
|
||||
- Core serves `GET` / `PATCH /qqq/v1/preferences/assistant`; the bespoke
|
||||
`/assistant/preferences` specs are removed. PATCH uses `values` and preserves
|
||||
omitted settings. Policy rejects unavailable Write defaults and auto-confirm.
|
||||
Fresh field metadata marks auto-confirm read-only with policy help text when
|
||||
unavailable, without mutating shared metadata. Core validates types and names.
|
||||
- `/assistant/config.preferences` returns effective defaults for the panel;
|
||||
new conversations apply the saved default only if Write mode is available.
|
||||
The provider refuses `assistant` (including scheduled details) before accessing
|
||||
rows: core only applies section channel rules through an app binding.
|
||||
- Bulk write tools are absent and forged calls are denied in Read-only mode.
|
||||
Scheduled mode requires the task approval and policy's global/role permission;
|
||||
scheduled tools always require confirmation, regardless of user auto-confirm.
|
||||
- HTTP tests cover permission visibility, own-user GET/PATCH, rejected identity
|
||||
injection, policy metadata/validation, defaults and channel denial. W1's denial
|
||||
matrix and both record and bulk mode paths remain covered. Build against QQQ
|
||||
RC.8, or `-Dqqq.snapshot.version=4.1.0-PREFS-SNAPSHOT` before publication.
|
||||
|
||||
### X. The hard wall between the assistant and its configuration
|
||||
Owner: *"there should also be a hard wall between the assistant and the configuration for the assistant. he should never be able to see his own configuration tables etc."*
|
||||
|
||||
@@ -430,7 +451,7 @@ AI exposure NONE is not enough, because the assistant acts as the user and an ad
|
||||
model and effort. Empty slots retain provider row/default/policy selection.
|
||||
- The documented heuristic is a pure rule table: >600 characters, two distinct app
|
||||
table names, complexity keywords, pending write context, >8 previous turns or a
|
||||
previous tool limit. This branch has no conversation write-mode field.
|
||||
previous tool limit. W1 additionally persists conversation write mode.
|
||||
- Classifier transport is non-streaming, bounded to four output tokens/three seconds,
|
||||
and contains only the fixed instruction and user question. PHI guards apply before
|
||||
dispatch. Errors/unexpected replies use the heuristic. ROUTE usage enters budget
|
||||
@@ -543,12 +564,8 @@ AI exposure NONE is not enough, because the assistant acts as the user and an ad
|
||||
failed calls and knowledge-query embeddings. STOP is checked at dispatch
|
||||
boundaries; an in-flight call can exceed the cap, and no next call is sent.
|
||||
- Every scheduled UI API request, including metadata and knowledge retrieval,
|
||||
carries `X-QQQ-Client: assistant/scheduled/<taskId>`. The only uiapi change is
|
||||
additive `LoopbackUiApiGateway.invokeWithClient`; custom gateways currently
|
||||
fail closed for scheduled requests because their channel contract is unknown.
|
||||
Tool-call rows also record `clientChannel`. Core channel deny enforcement
|
||||
remains pending the client-channel RC; existing AI exposure/data-access
|
||||
restrictions continue to apply on RC.5.
|
||||
carries `X-QQQ-Client: assistant/scheduled/<taskId>` through `CoreUiApiGateway`.
|
||||
Tool-call rows also record `clientChannel`; core enforces the configuration wall.
|
||||
- Approve write mode, Revoke write mode and Run now are admin record actions
|
||||
using standard streamed ETL Review/Result. Review shows the exact prompt's
|
||||
SHA-256 and run-as user. Result rereads and compares the complete reviewed
|
||||
@@ -556,10 +573,9 @@ AI exposure NONE is not enough, because the assistant acts as the user and an ad
|
||||
A persisted approval hash additionally binds prompt, run-as user, credential
|
||||
reference, profile and requested mode. Editing any of these clears approval;
|
||||
ordinary saves cannot set approval or computed bookkeeping fields.
|
||||
- This base has create/update/delete tools, but no W1 conversation mode/policy
|
||||
switch. Scheduled runs explicitly narrow the existing tool policy: write tools
|
||||
require both request and matching approval, a READ_WRITE table and the policy's
|
||||
allowed-tools list. All scheduled writes require panel confirmation, including
|
||||
- Scheduled runs narrow W1's tool policy: record and bulk write tools require
|
||||
both request and matching approval, the designated user's global/role Write-mode
|
||||
permission, a READ_WRITE table and the policy's allowed-tools list. All scheduled writes require panel confirmation, including
|
||||
when normal policy allows immediate writes. A pending proposal adds a waiting
|
||||
notice to the run summary; the scheduler never confirms it.
|
||||
- RECORD keeps the run and conversation; NOTIFICATION and BOTH also submit an
|
||||
|
||||
@@ -369,6 +369,22 @@ CREATE TABLE assistant_profile_skill
|
||||
CONSTRAINT assistant_profile_skill_pair UNIQUE (profile_id, skill_id)
|
||||
);
|
||||
|
||||
-- 0.1.0-RC.7: write mode (per conversation, user preference, policy) and auto-confirm.
|
||||
ALTER TABLE assistant_conversation ADD COLUMN IF NOT EXISTS write_mode BOOLEAN;
|
||||
ALTER TABLE assistant_policy ADD COLUMN IF NOT EXISTS write_mode_allowed BOOLEAN;
|
||||
ALTER TABLE assistant_policy ADD COLUMN IF NOT EXISTS write_mode_permission VARCHAR(128);
|
||||
ALTER TABLE assistant_policy ADD COLUMN IF NOT EXISTS auto_confirm_allowed BOOLEAN;
|
||||
ALTER TABLE assistant_data_access ADD COLUMN IF NOT EXISTS auto_confirm_ok BOOLEAN;
|
||||
CREATE TABLE assistant_user_preference
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
default_write_mode BOOLEAN,
|
||||
auto_confirm_low_risk BOOLEAN,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_data_audit
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<archunit.version>1.5.1</archunit.version>
|
||||
<jackson.version>2.21.7</jackson.version>
|
||||
<!-- optional email delivery; qbit-email from Maven Central -->
|
||||
<qbit-email.version>0.1.0-RC.2</qbit-email.version>
|
||||
<qbit-email.version>0.1.0-RC.1</qbit-email.version>
|
||||
<!-- live evals call the real API; run them with -Dtest.excludedGroups= -Dgroups=live -->
|
||||
<test.excludedGroups>live</test.excludedGroups>
|
||||
</properties>
|
||||
@@ -37,7 +37,7 @@
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.1</version>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -215,7 +215,8 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
|
||||
<!-- requires QQQ RC.8 (core Preferences, qqq#1162); until it is published build with -Dqqq.snapshot.version=4.1.0-PREFS-SNAPSHOT -->
|
||||
<qqq.snapshot.version>4.1.0-RC.8</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
+9
-9
@@ -40,15 +40,15 @@ import com.anthropic.models.messages.ToolUseBlock;
|
||||
import com.anthropic.models.messages.ToolUseBlockParam;
|
||||
import com.anthropic.models.messages.Usage;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.assistant.spi.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.provider.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+8
-8
@@ -29,14 +29,14 @@ import com.anthropic.core.http.StreamResponse;
|
||||
import com.anthropic.helpers.MessageAccumulator;
|
||||
import com.anthropic.models.messages.MessageCreateParams;
|
||||
import com.anthropic.models.messages.RawMessageStreamEvent;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ package com.kingsrook.qbits.assistant.provider.anthropic;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+10
-10
@@ -24,16 +24,16 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.assistant.spi.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.provider.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
+3
-3
@@ -26,10 +26,10 @@ import java.util.Set;
|
||||
import com.anthropic.core.ObjectMappers;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.fake.ScriptedModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qbits.assistant.testing.TurnRunner;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
|
||||
+1
-1
@@ -24,6 +24,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.engine.ConnectionTest;
|
||||
import com.kingsrook.qbits.assistant.engine.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantMetaDataBuilder;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModelPrice;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
@@ -31,7 +32,6 @@ import com.kingsrook.qbits.assistant.provider.anthropic.AnthropicModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.anthropic.AnthropicProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.anthropic.RecordedAnthropicServer;
|
||||
import com.kingsrook.qbits.assistant.provider.fake.ScriptedModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qbits.assistant.testing.TurnRunner;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
|
||||
+2
-1
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.engine;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+2
-2
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.Set;
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keep external provider consumers independent of the assistant runtime and QQQ.
|
||||
*******************************************************************************/
|
||||
class ProviderApiArchitectureTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Allow only the extracted types, including their nested enums, under the
|
||||
** assistant namespace. A future dependency on any runtime package must fail.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void dependsOnlyOnApiTypes()
|
||||
{
|
||||
Set<String> apiTypes = Set.of(
|
||||
"com.kingsrook.qbits.assistant.provider.AssistantModelProvider",
|
||||
"com.kingsrook.qbits.assistant.provider.AssistantProviderType",
|
||||
"com.kingsrook.qbits.assistant.provider.CacheHint",
|
||||
"com.kingsrook.qbits.assistant.provider.CompletableStreamHandle",
|
||||
"com.kingsrook.qbits.assistant.provider.DiscoveredModel",
|
||||
"com.kingsrook.qbits.assistant.provider.EmbeddingRequest",
|
||||
"com.kingsrook.qbits.assistant.provider.EmbeddingResponse",
|
||||
"com.kingsrook.qbits.assistant.provider.HealthResult",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelCapabilities",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelEventListener",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelMessage",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelPart",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelProviderException",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelRequest",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelResponse",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelToolDefinition",
|
||||
"com.kingsrook.qbits.assistant.provider.ModelUsage",
|
||||
"com.kingsrook.qbits.assistant.provider.ProviderSettings",
|
||||
"com.kingsrook.qbits.assistant.provider.StreamHandle",
|
||||
"com.kingsrook.qbits.assistant.provider.SystemBlock",
|
||||
"com.kingsrook.qbits.assistant.provider.TokenCount",
|
||||
"com.kingsrook.qbits.assistant.provider.TrustedCaBundle",
|
||||
"com.kingsrook.qbits.assistant.engine.ModelPricing");
|
||||
JavaClasses imported = new ClassFileImporter()
|
||||
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
|
||||
.importPackages("com.kingsrook.qbits.assistant");
|
||||
DescribedPredicate<JavaClass> outsideRuntime = DescribedPredicate.describe("outside assistant or an extracted API type", type ->
|
||||
!type.getName().startsWith("com.kingsrook.qbits.assistant.") || apiTypes.contains(type.getName().split("\\$", 2)[0]));
|
||||
classes().should().onlyDependOnClassesThat(outsideRuntime).check(imported);
|
||||
noClasses().should().dependOnClassesThat().resideInAnyPackage("com.kingsrook.qqq..").check(imported);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -23,10 +23,11 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import com.kingsrook.qbits.assistant.engine.ModelPricing;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
+2
-1
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
import com.kingsrook.qbits.assistant.engine.ModelPricing;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import java.security.KeyStore;
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.spi;
|
||||
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
|
||||
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackage;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keep external provider consumers independent of the assistant runtime and QQQ.
|
||||
*******************************************************************************/
|
||||
class ProviderApiArchitectureTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Import the entire artifact so an accidental package outside spi also fails.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void dependsOnlyOnApiTypes()
|
||||
{
|
||||
String spiPackage = "com.kingsrook.qbits.assistant.spi";
|
||||
JavaClasses imported = new ClassFileImporter().importUrl(AssistantModelProvider.class.getProtectionDomain().getCodeSource().getLocation());
|
||||
classes().should().resideInAPackage(spiPackage).check(imported);
|
||||
noClasses().should().dependOnClassesThat(resideInAPackage("com.kingsrook.qbits..").and(resideOutsideOfPackage(spiPackage))).check(imported);
|
||||
noClasses().should().dependOnClassesThat().resideInAnyPackage("com.kingsrook.qqq..").check(imported);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -24,11 +24,11 @@ import com.anthropic.backends.Backend;
|
||||
import com.anthropic.bedrock.backends.BedrockBackend;
|
||||
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
|
||||
import com.anthropic.core.Timeout;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.anthropic.AnthropicModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
|
||||
+6
-6
@@ -27,13 +27,13 @@ import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.engine.ProviderService;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.anthropic.AnthropicModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.testing.CloudProviderTestServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
+13
-13
@@ -36,19 +36,19 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.EmbeddingResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+6
-6
@@ -18,12 +18,12 @@ package com.kingsrook.qbits.assistant.provider.openai;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.spi.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.provider.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+4
-4
@@ -33,12 +33,12 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kingsrook.qbits.assistant.spi.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import com.kingsrook.qbits.assistant.provider.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+6
-6
@@ -24,12 +24,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+4
-4
@@ -21,10 +21,10 @@ import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
+10
-10
@@ -22,16 +22,16 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
+3
-3
@@ -18,9 +18,9 @@ package com.kingsrook.qbits.assistant.provider.openai;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
+6
-6
@@ -24,14 +24,14 @@ import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
+1
-1
@@ -36,9 +36,9 @@ import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.processes.DiscoverModelsProcess;
|
||||
import com.kingsrook.qbits.assistant.processes.TestConnectionProcess;
|
||||
import com.kingsrook.qbits.assistant.processes.TestModelConnectionProcess;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.fake.ScriptedModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.openai.OpenAiCompatibleProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
|
||||
import com.kingsrook.qbits.assistant.testing.EvalSupport;
|
||||
import com.kingsrook.qbits.assistant.testing.GoldenTasks;
|
||||
|
||||
+6
-6
@@ -23,14 +23,14 @@ import com.anthropic.client.okhttp.AnthropicOkHttpClient;
|
||||
import com.anthropic.core.Timeout;
|
||||
import com.anthropic.vertex.backends.VertexBackend;
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.anthropic.AnthropicModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.openai.OpenAiCompatibleModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+4
-4
@@ -19,10 +19,10 @@ package com.kingsrook.qbits.assistant.provider.vertex;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+5
-5
@@ -29,11 +29,11 @@ import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.engine.ProviderService;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.testing.CloudProviderTestServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
|
||||
@@ -73,20 +73,6 @@
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- AssistantDefaultsChangelogTest runs the shipped seed changelog against H2 (PostgreSQL mode). -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.3.232</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- ...through real Liquibase (the version kof22 runs), so preconditions and bookkeeping are exercised. -->
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>4.33.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.kingsrook.qbits.assistant;
|
||||
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -31,11 +32,11 @@ import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qbits.assistant.classification.ClassificationSource;
|
||||
import com.kingsrook.qbits.assistant.classification.ConfiguredClassificationSource;
|
||||
import com.kingsrook.qbits.assistant.engine.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.TrustedCaBundle;
|
||||
import com.kingsrook.qbits.assistant.rag.VectorStore;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.spi.TrustedCaBundle;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiGateway;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiExposure;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
@@ -44,7 +45,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomize
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+7
-4
@@ -26,12 +26,11 @@ import com.kingsrook.qbits.assistant.developer.AssistantDeveloperPanels;
|
||||
import com.kingsrook.qbits.assistant.home.AssistantAccessWidget;
|
||||
import com.kingsrook.qbits.assistant.home.AssistantHome;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantChannelWall;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantHelp;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantInstanceMetaData;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantMetaDataBuilder;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantRecordScreens;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantPreferencesProvider;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantBudget;
|
||||
import com.kingsrook.qbits.assistant.processes.DataAccessDefaultsProcess;
|
||||
import com.kingsrook.qbits.assistant.processes.DiscoverModelsProcess;
|
||||
import com.kingsrook.qbits.assistant.processes.KnowledgeIndexProcess;
|
||||
import com.kingsrook.qbits.assistant.processes.ScheduledTaskProcess;
|
||||
@@ -182,7 +181,7 @@ public class AssistantQBitProducer implements QBitMetaDataProducer<AssistantQBit
|
||||
QProcessMetaData testConnection = TestConnectionProcess.process();
|
||||
testConnection.setSourceQBitName(qBitMetaData.getName());
|
||||
qInstance.addProcess(testConnection);
|
||||
for(QProcessMetaData process : List.of(DiscoverModelsProcess.process(), TestModelConnectionProcess.process(), DataAccessDefaultsProcess.process()))
|
||||
for(QProcessMetaData process : List.of(DiscoverModelsProcess.process(), TestModelConnectionProcess.process()))
|
||||
{
|
||||
process.setSourceQBitName(qBitMetaData.getName());
|
||||
qInstance.addProcess(process);
|
||||
@@ -204,13 +203,17 @@ public class AssistantQBitProducer implements QBitMetaDataProducer<AssistantQBit
|
||||
|
||||
QAppMetaData app = builder.buildAdminApp(tables);
|
||||
qInstance.addApp(app);
|
||||
AssistantHelp.apply(qInstance);
|
||||
|
||||
AssistantPreferencesProvider.register(qInstance);
|
||||
AssistantChannelWall.register(qInstance);
|
||||
if(Boolean.TRUE.equals(qBitConfig.getRecordHistory()))
|
||||
{
|
||||
enableRecordHistory(ClientChannelsMetaData.ofOrWithNew(qInstance));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientChannelsMetaData.ofOrWithNew(qInstance).withDeniedPattern("assistant", "audit*");
|
||||
}
|
||||
postProduceActions(new MetaDataProducerMultiOutput(), qInstance);
|
||||
return (new MetaDataProducerMultiOutput());
|
||||
}
|
||||
|
||||
+12
@@ -132,6 +132,18 @@ public class DataAccessService
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether an admin marked the table's single-record inserts and updates as
|
||||
** low-risk (auto-confirmable, when the policy and the user allow it).
|
||||
*******************************************************************************/
|
||||
public boolean autoConfirmOk(String tableName)
|
||||
{
|
||||
AssistantDataAccess row = rows().get(key(AssistantDataAccess.TYPE_TABLE, tableName));
|
||||
return (row != null && Boolean.TRUE.equals(row.getAutoConfirmOk()) && forTable(tableName).mode().allowsWrite());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Drop the cache (after a change to the table).
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.kingsrook.qbits.assistant.model.AssistantPendingAction;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurnEvent;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUserPreference;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.tools.UiApiToolbox;
|
||||
import com.kingsrook.qbits.assistant.tools.bulkload.BulkLoadTools;
|
||||
@@ -140,7 +141,10 @@ public class AssistantApi
|
||||
result.put("shortcut", config.getShortcut());
|
||||
result.put("model", reason == null ? activeProvider().model() : config.getModels().getMainModel());
|
||||
result.put("limits", Map.of("maxTurnSeconds", config.getMaxTurnSeconds(), "maxToolCallsPerTurn", config.getMaxToolCallsPerTurn()));
|
||||
result.put("features", Map.of("writes", runtime.getToolbox().anyWritableTable(), "pendingActions", true));
|
||||
boolean writeMode = reason == null && writeModeAvailable();
|
||||
result.put("features", Map.of("writes", runtime.getToolbox().anyWritableTable(), "pendingActions", true, "writeMode", writeMode,
|
||||
"autoConfirm", writeMode && runtime.getPolicyService().current(false).tools().autoConfirmAllowed()));
|
||||
result.put("preferences", reason == null ? preferences() : Map.of());
|
||||
ProfileService.Persona persona = runtime.getProfileService().persona(session, null);
|
||||
Map<String, Object> profile = new LinkedHashMap<>();
|
||||
profile.put("displayName", persona.displayName());
|
||||
@@ -180,6 +184,12 @@ public class AssistantApi
|
||||
String title = body.get("title") instanceof String s && StringUtils.hasContent(s) ? truncate(s.trim(), 255) : null;
|
||||
Map<String, Object> context = body.get("context") instanceof Map<?, ?> m ? ContextBuilder.sanitizePageContext((Map<String, Object>) m) : null;
|
||||
AssistantConversation conversation = store.createConversation(userId, title, context == null || context.isEmpty() ? null : JsonUtils.toJson(context));
|
||||
boolean write = body.get("writeMode") instanceof Boolean requested ? requested : defaultWriteMode();
|
||||
if(write && writeModeAvailable())
|
||||
{
|
||||
store.updateFields(AssistantConversation.TABLE_NAME, new QRecord().withValue("id", conversation.getId()).withValue("writeMode", true));
|
||||
conversation.setWriteMode(true);
|
||||
}
|
||||
return (toJson(conversation));
|
||||
}
|
||||
|
||||
@@ -255,6 +265,19 @@ public class AssistantApi
|
||||
conversation.setStatus((String) status);
|
||||
}
|
||||
|
||||
if(body.containsKey("writeMode"))
|
||||
{
|
||||
if(!(body.get("writeMode") instanceof Boolean writeMode))
|
||||
{
|
||||
throw (new AssistantApiException(400, "writeMode must be true or false."));
|
||||
}
|
||||
if(writeMode && !writeModeAvailable())
|
||||
{
|
||||
throw (new AssistantApiException(403, "WRITE_MODE_UNAVAILABLE", "Write mode is not available to you. An administrator controls it in the assistant policy."));
|
||||
}
|
||||
update.setValue("writeMode", writeMode);
|
||||
conversation.setWriteMode(writeMode);
|
||||
}
|
||||
store.updateFields(AssistantConversation.TABLE_NAME, update);
|
||||
return (toJson(conversation));
|
||||
}
|
||||
@@ -776,6 +799,43 @@ public class AssistantApi
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this user may use Write mode at all: the policy allows it, they
|
||||
** hold its permission (when one is set), and some table is READ_WRITE.
|
||||
*******************************************************************************/
|
||||
public boolean writeModeAvailable() throws QException
|
||||
{
|
||||
return (runtime.getPolicyService().current(false).tools().writeModeAvailableTo(session) && runtime.getToolbox().anyWritableTable());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The default mode of the user's new conversations (Read-only unless they
|
||||
** chose Write).
|
||||
*******************************************************************************/
|
||||
private boolean defaultWriteMode() throws QException
|
||||
{
|
||||
AssistantUserPreference preference = store.getPreference(userId);
|
||||
return (preference != null && Boolean.TRUE.equals(preference.getDefaultWriteMode()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Effective defaults for the panel config, narrowed by current policy.
|
||||
*******************************************************************************/
|
||||
private Map<String, Object> preferences() throws QException
|
||||
{
|
||||
AssistantUserPreference preference = userId == null ? null : store.getPreference(userId);
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("defaultWriteMode", writeModeAvailable() && preference != null && Boolean.TRUE.equals(preference.getDefaultWriteMode()));
|
||||
map.put("autoConfirmLowRisk", runtime.getPolicyService().current(false).tools().autoConfirmAllowed() && preference != null && Boolean.TRUE.equals(preference.getAutoConfirmLowRisk()));
|
||||
return (map);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configured turn time limit (replay loops are capped by it).
|
||||
*******************************************************************************/
|
||||
@@ -874,6 +934,7 @@ public class AssistantApi
|
||||
map.put("title", conversation.getTitle());
|
||||
map.put("status", conversation.getStatus());
|
||||
map.put("pinned", Boolean.TRUE.equals(conversation.getPinned()));
|
||||
map.put("writeMode", Boolean.TRUE.equals(conversation.getWriteMode()));
|
||||
map.put("lastMessageAt", conversation.getLastMessageAt() == null ? null : conversation.getLastMessageAt().toString());
|
||||
map.put("unreadCount", conversation.getUnreadCount() == null ? 0 : conversation.getUnreadCount());
|
||||
map.put("createDate", conversation.getCreateDate() == null ? null : conversation.getCreateDate().toString());
|
||||
|
||||
+11
-3
@@ -199,9 +199,10 @@ public final class AssistantSpecs
|
||||
"model", string("The model that answers."),
|
||||
"limits", freeObject("maxTurnSeconds and maxToolCallsPerTurn."),
|
||||
"features", freeObject("writes: some table allows writes; pendingActions: writes wait for confirmation."),
|
||||
"preferences", ref("AssistantPreferencesV1"),
|
||||
"profile", ref("AssistantProfileV1")));
|
||||
rs.put("AssistantConversationV1", object("One of the user's own conversations.",
|
||||
"id", integer(null), "title", string(null).withNullable(true), "status", string("ACTIVE or ARCHIVED."), "pinned", bool(null),
|
||||
"id", integer(null), "title", string(null).withNullable(true), "status", string("ACTIVE or ARCHIVED."), "pinned", bool(null), "writeMode", bool("Write mode (default Read-only): the write tools exist only in Write mode."),
|
||||
"lastMessageAt", string("ISO-8601 instant.").withNullable(true), "unreadCount", integer(null), "createDate", string("ISO-8601 instant.").withNullable(true)));
|
||||
rs.put("AssistantConversationListResponseV1", object("The user's conversations, newest first.", "conversations", array(ref("AssistantConversationV1"), null)));
|
||||
rs.put("AssistantMessageV1", object("A message: the user's text or the assistant's reply as parts (text, tool, record_refs, navigate, notice).",
|
||||
@@ -221,6 +222,9 @@ public final class AssistantSpecs
|
||||
rs.put("AssistantTurnEventV1", object("One server-sent event of a turn (event: <type>, id: <seq>, data: this object). Types: message.start, text.delta, tool.start, tool.result, ui.navigate, notice, usage, message.end, error, ping.",
|
||||
"seq", integer("Sequence number; resume with ?after=<seq>."), "type", string("The event type (also the SSE event name)."),
|
||||
"data", freeObject("The event's fields, by type.")));
|
||||
rs.put("AssistantPreferencesV1", object("The user's own assistant preferences.",
|
||||
"defaultWriteMode", bool("New conversations start in Write mode (when write mode is available)."),
|
||||
"autoConfirmLowRisk", bool("Auto-confirm low-risk writes (only when the policy allows it; never deletes, bulk writes or processes).")));
|
||||
rs.put("AssistantErrorV1", object("An error.", "error", string("A message for the user."), "code", string("BUDGET, RATE_LIMIT, NOT_CONFIGURED, KILL_SWITCH, DISABLED, NO_PERMISSION...").withNullable(true)));
|
||||
return (rs);
|
||||
}
|
||||
@@ -594,7 +598,8 @@ public final class AssistantSpecs
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (object("Optional title and the page context it starts from.", "title", string(null), "context", freeObject("route, tableName, recordId, filter, selectedIds.")));
|
||||
return (object("Optional title, the page context it starts from, and the mode (default: the user's preference).", "title", string(null),
|
||||
"context", freeObject("route, tableName, recordId, filter, selectedIds."), "writeMode", bool(null)));
|
||||
}
|
||||
|
||||
|
||||
@@ -719,7 +724,8 @@ public final class AssistantSpecs
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (object("The changed properties.", "title", string(null), "pinned", bool(null), "status", string("ACTIVE or ARCHIVED.")));
|
||||
return (object("The changed properties.", "title", string(null), "pinned", bool(null), "status", string("ACTIVE or ARCHIVED."),
|
||||
"writeMode", bool("Switch to Write mode (true) or Read-only (false); 403 when write mode is not available to the user.")));
|
||||
}
|
||||
|
||||
|
||||
@@ -1333,4 +1339,6 @@ public final class AssistantSpecs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -55,7 +55,7 @@ public final class AssistantEmail
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("com.kingsrook.qbits.email.api.EmailApi");
|
||||
Class.forName("com.kingsrook.qbits.email.outbox.EmailService");
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
{
|
||||
|
||||
+13
-11
@@ -24,11 +24,13 @@ package com.kingsrook.qbits.assistant.email;
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.email.api.EmailApi;
|
||||
import com.kingsrook.qbits.email.api.EmailApiCategory;
|
||||
import com.kingsrook.qbits.email.api.EmailApiRequest;
|
||||
import com.kingsrook.qbits.email.api.EmailApiResult;
|
||||
import com.kingsrook.qbits.email.api.EmailApiTemplate;
|
||||
import com.kingsrook.qbits.email.EmailRuntime;
|
||||
import com.kingsrook.qbits.email.outbox.EmailCategory;
|
||||
import com.kingsrook.qbits.email.outbox.EmailRequest;
|
||||
import com.kingsrook.qbits.email.outbox.EmailSendResult;
|
||||
import com.kingsrook.qbits.email.outbox.EmailService;
|
||||
import com.kingsrook.qbits.email.template.EmailTemplateDefinition;
|
||||
import com.kingsrook.qbits.email.template.EmailTemplates;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@@ -64,7 +66,7 @@ final class AssistantEmailSender
|
||||
*******************************************************************************/
|
||||
static boolean isInstalled(QInstance qInstance)
|
||||
{
|
||||
return (EmailApi.isInstalled(qInstance));
|
||||
return (EmailRuntime.config(qInstance) != null);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +79,7 @@ final class AssistantEmailSender
|
||||
{
|
||||
boolean linkOnly = notice.classification() != null && notice.classification().isAtLeast(DataClassification.CONFIDENTIAL);
|
||||
|
||||
EmailApiRequest request = new EmailApiRequest().withTo(recipient)
|
||||
EmailRequest request = new EmailRequest().withTo(recipient)
|
||||
.withIdempotencyKey("assistant:scheduled:" + notice.runId())
|
||||
.withLinkedRecord(notice.runTable(), notice.runId())
|
||||
.withSender(SENDER_QBIT, SENDER_PROCESS);
|
||||
@@ -98,7 +100,7 @@ final class AssistantEmailSender
|
||||
}
|
||||
request.withVariables(variables);
|
||||
|
||||
return (map(EmailApi.send(request)));
|
||||
return (map(EmailService.send(request)));
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +123,7 @@ final class AssistantEmailSender
|
||||
/*******************************************************************************
|
||||
** Outbox result to the qbit's own vocabulary.
|
||||
*******************************************************************************/
|
||||
private static ScheduledEmailStatus map(EmailApiResult result)
|
||||
private static ScheduledEmailStatus map(EmailSendResult result)
|
||||
{
|
||||
return (switch(result.status())
|
||||
{
|
||||
@@ -139,10 +141,10 @@ final class AssistantEmailSender
|
||||
*******************************************************************************/
|
||||
static void ensureTemplate() throws QException
|
||||
{
|
||||
EmailApi.ensureTemplate(new EmailApiTemplate()
|
||||
EmailTemplates.ensure(new EmailTemplateDefinition()
|
||||
.withName(TEMPLATE_NAME)
|
||||
.withLabel("Assistant scheduled task result")
|
||||
.withCategory(EmailApiCategory.NOTIFICATION)
|
||||
.withCategory(EmailCategory.NOTIFICATION)
|
||||
.withKind("MARKDOWN")
|
||||
.withOwnerQbit(SENDER_QBIT)
|
||||
.withSubject("Assistant task result: {{taskName}}")
|
||||
|
||||
+7
-8
@@ -23,16 +23,15 @@ import java.util.List;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.RegistryModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.rag.EmbeddingService;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
+7
-7
@@ -28,16 +28,16 @@ import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.provider.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.tools.KnowledgeTool;
|
||||
import com.kingsrook.qbits.assistant.tools.PresentResultsTool;
|
||||
import com.kingsrook.qbits.assistant.tools.UiApiToolbox;
|
||||
import com.kingsrook.qbits.assistant.tools.bulkload.BulkLoadTools;
|
||||
import com.kingsrook.qbits.assistant.tools.UiApiToolbox;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolDefinition;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolKernel;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
|
||||
+5
-5
@@ -30,11 +30,11 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
+5
-5
@@ -24,11 +24,11 @@ import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.DiscoveredModel;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
@@ -34,15 +34,15 @@ import com.kingsrook.qbits.assistant.model.AssistantPendingAction;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProfile;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.provider.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.SystemBlock;
|
||||
import com.kingsrook.qbits.assistant.scheduled.ScheduledTurn;
|
||||
import com.kingsrook.qbits.assistant.spi.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.SystemBlock;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
|
||||
+47
-3
@@ -24,6 +24,7 @@ import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
@@ -81,8 +82,50 @@ public class PolicyService
|
||||
** @param redactPii redact secrets and personal identifiers from tool
|
||||
** results (default true)
|
||||
*******************************************************************************/
|
||||
public record ToolPolicy(boolean writesRequireConfirmation, Set<String> allowedTools, int maxRowsPerCall, boolean redactPii)
|
||||
public record ToolPolicy(boolean writesRequireConfirmation, Set<String> allowedTools, int maxRowsPerCall, boolean redactPii,
|
||||
boolean writeModeAllowed, String writeModePermission, boolean autoConfirmAllowed, boolean writeMode, boolean autoConfirm)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Write mode on, no auto-confirm, write mode allowed for everyone (tests
|
||||
** and hosts that build a policy by hand).
|
||||
*******************************************************************************/
|
||||
public ToolPolicy(boolean writesRequireConfirmation, Set<String> allowedTools, int maxRowsPerCall, boolean redactPii)
|
||||
{
|
||||
this(writesRequireConfirmation, allowedTools, maxRowsPerCall, redactPii, true, null, false, true, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** This policy for a conversation in Write (true) or Read-only (false) mode.
|
||||
*******************************************************************************/
|
||||
public ToolPolicy withWriteMode(boolean writeMode)
|
||||
{
|
||||
return (new ToolPolicy(writesRequireConfirmation, allowedTools, maxRowsPerCall, redactPii, writeModeAllowed, writeModePermission, autoConfirmAllowed, writeMode, autoConfirm));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** This policy with the user's auto-confirm (only effective when the policy
|
||||
** allows it).
|
||||
*******************************************************************************/
|
||||
public ToolPolicy withAutoConfirm(boolean autoConfirm)
|
||||
{
|
||||
return (new ToolPolicy(writesRequireConfirmation, allowedTools, maxRowsPerCall, redactPii, writeModeAllowed, writeModePermission, autoConfirmAllowed, writeMode,
|
||||
autoConfirm && autoConfirmAllowed));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this user may switch a conversation to Write mode (the policy
|
||||
** allows it, and they hold the write-mode permission when one is set).
|
||||
*******************************************************************************/
|
||||
public boolean writeModeAvailableTo(QSession session)
|
||||
{
|
||||
return (writeModeAllowed && (!StringUtils.hasContent(writeModePermission) || (session != null && session.hasPermission(writeModePermission.strip()))));
|
||||
}
|
||||
public static final int DEFAULT_MAX_ROWS_PER_CALL = 50;
|
||||
public static final int MAX_ROWS_PER_CALL_CEILING = 200;
|
||||
|
||||
@@ -93,7 +136,7 @@ public class PolicyService
|
||||
*******************************************************************************/
|
||||
public static ToolPolicy defaults()
|
||||
{
|
||||
return (new ToolPolicy(true, Set.of(), DEFAULT_MAX_ROWS_PER_CALL, true));
|
||||
return (new ToolPolicy(true, Set.of(), DEFAULT_MAX_ROWS_PER_CALL, true, true, null, false, false, false));
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +172,8 @@ public class PolicyService
|
||||
}
|
||||
}
|
||||
int maxRows = row.getMaxRowsPerCall() == null || row.getMaxRowsPerCall() < 1 ? DEFAULT_MAX_ROWS_PER_CALL : Math.min(MAX_ROWS_PER_CALL_CEILING, row.getMaxRowsPerCall());
|
||||
return (new ToolPolicy(!Boolean.FALSE.equals(row.getWritesRequireConfirmation()), allowed, maxRows, !Boolean.FALSE.equals(row.getRedactPii())));
|
||||
return (new ToolPolicy(!Boolean.FALSE.equals(row.getWritesRequireConfirmation()), allowed, maxRows, !Boolean.FALSE.equals(row.getRedactPii()),
|
||||
!Boolean.FALSE.equals(row.getWriteModeAllowed()), row.getWriteModePermission(), Boolean.TRUE.equals(row.getAutoConfirmAllowed()), false, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -23,9 +23,9 @@ import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.HealthResult;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
+3
-4
@@ -36,11 +36,10 @@ import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModelPrice;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
|
||||
import com.kingsrook.qbits.assistant.provider.RegistryModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.kingsrook.qbits.assistant.engine;
|
||||
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.classification.ClassificationSource;
|
||||
import com.kingsrook.qbits.assistant.classification.Classifications;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantConversation;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
|
||||
@@ -43,20 +43,20 @@ import com.kingsrook.qbits.assistant.model.AssistantPendingAction;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantScheduledTask;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUserPreference;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.scheduled.ScheduledTurn;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.CacheHint;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelMessage;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.tools.KnowledgeTool;
|
||||
import com.kingsrook.qbits.assistant.tools.PresentResultsTool;
|
||||
import com.kingsrook.qbits.assistant.tools.UiApiToolbox;
|
||||
@@ -135,7 +135,7 @@ public class TurnEngine implements Runnable
|
||||
private long lastTextFlush = System.currentTimeMillis();
|
||||
|
||||
private AssistantScheduledTask scheduledTask;
|
||||
private PolicyService.ToolPolicy toolPolicy;
|
||||
|
||||
|
||||
private Instant startedAt;
|
||||
private Instant deadline;
|
||||
@@ -151,6 +151,7 @@ public class TurnEngine implements Runnable
|
||||
|
||||
private volatile boolean finished = false;
|
||||
private UiApiCredentials credentials;
|
||||
private PolicyService.ToolPolicy toolPolicy;
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +215,7 @@ public class TurnEngine implements Runnable
|
||||
{
|
||||
throw (new QException("Scheduled execution context is no longer available; start a new conversation."));
|
||||
}
|
||||
toolPolicy = scheduledTask == null ? policy.tools() : ScheduledTurn.tools(runtime, scheduledTask, policy.tools());
|
||||
toolPolicy = scheduledTask == null ? toolPolicy() : ScheduledTurn.tools(runtime, scheduledTask, policy.tools());
|
||||
ScheduledTurn.checkCost(runtime, conversation.getId(), turn.getId());
|
||||
classification = new TurnClassification(config.getClassificationSource(), conversation, pageContext);
|
||||
store.saveClassification(userId, conversation.getId(), turn.getId(), classification);
|
||||
@@ -911,6 +912,34 @@ public class TurnEngine implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The policy's tool settings for this turn: Write mode only when the
|
||||
** conversation is in Write mode and write mode is available to this user;
|
||||
** auto-confirm only when the policy allows it and the user chose it.
|
||||
*******************************************************************************/
|
||||
PolicyService.ToolPolicy toolPolicy()
|
||||
{
|
||||
if(toolPolicy == null)
|
||||
{
|
||||
PolicyService.ToolPolicy base = policy.tools();
|
||||
boolean write = Boolean.TRUE.equals(conversation.getWriteMode()) && base.writeModeAvailableTo(session);
|
||||
boolean auto = false;
|
||||
try
|
||||
{
|
||||
AssistantUserPreference preference = store.getPreference(userId);
|
||||
auto = preference != null && Boolean.TRUE.equals(preference.getAutoConfirmLowRisk());
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
LOG.debug("Could not read the user's assistant preferences", e);
|
||||
}
|
||||
toolPolicy = base.withWriteMode(write).withAutoConfirm(auto);
|
||||
}
|
||||
return (toolPolicy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Where a data tool runs: the user's own session (by its UUID, for the UI
|
||||
** API), the gateway, the policy's tool settings, and the pending-action
|
||||
|
||||
+3
-4
@@ -19,10 +19,9 @@ package com.kingsrook.qbits.assistant.engine;
|
||||
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
+11
-24
@@ -17,15 +17,7 @@
|
||||
package com.kingsrook.qbits.assistant.home;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.AssistantSecurity;
|
||||
import com.kingsrook.qbits.assistant.engine.BudgetService;
|
||||
@@ -37,15 +29,23 @@ import com.kingsrook.qbits.assistant.model.AssistantBudgetOverride;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantConversation;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -75,7 +75,6 @@ public class AssistantHomeData
|
||||
private final List<Totals> byProvider = new ArrayList<>();
|
||||
|
||||
private Long fallbackCount = 0L;
|
||||
private List<AssistantSetupStatus.Item> setup = List.of();
|
||||
private Boolean hipaaMode;
|
||||
private Integer hipaaRetentionDays;
|
||||
private LocalDate today;
|
||||
@@ -314,7 +313,6 @@ public class AssistantHomeData
|
||||
data.providers.add(new ProviderHealth(row, type == null ? row.getProviderType() : type.getLabel(), status, (status.startsWith("Set") || status.startsWith("Not needed")), service.validate(row), active != null && active.getId().equals(row.getId()),
|
||||
providerMonth.getOrDefault(row.getId(), 0L), ProviderService.capMicros(row), service.kind(row), service.location(row), service.models(row.getId()).stream().filter(m -> Boolean.TRUE.equals(m.getEnabled())).count()));
|
||||
}
|
||||
data.setup = AssistantSetupStatus.load(runtime, data.providers);
|
||||
return (data);
|
||||
}
|
||||
|
||||
@@ -580,15 +578,4 @@ public class AssistantHomeData
|
||||
{
|
||||
return (fallbackCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card: each configuration table, its state and the next step.
|
||||
*******************************************************************************/
|
||||
public List<AssistantSetupStatus.Item> getSetup()
|
||||
{
|
||||
return (setup);
|
||||
}
|
||||
|
||||
}
|
||||
-47
@@ -90,7 +90,6 @@ public final class AssistantHomeView
|
||||
public static String render(AssistantHomeData data, String prefix)
|
||||
{
|
||||
StringBuilder html = new StringBuilder("<div class=\"assistant-home\" style=\"" + ROOT + "\">");
|
||||
setup(html, data, prefix);
|
||||
status(html, data, prefix);
|
||||
providers(html, data, prefix);
|
||||
html.append("<div style=\"").append(GRID).append("\">");
|
||||
@@ -112,52 +111,6 @@ public final class AssistantHomeView
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card: each part of the assistant, set up or not, and the next
|
||||
** step, linked to its table.
|
||||
*******************************************************************************/
|
||||
private static void setup(StringBuilder html, AssistantHomeData data, String prefix)
|
||||
{
|
||||
if(data.getSetup().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
open(html, "Setup");
|
||||
long todo = data.getSetup().stream().filter(i -> i.state() == AssistantSetupStatus.State.TODO).count();
|
||||
html.append("<p style=\"margin:0 0 .6rem;font-size:.9rem;\">")
|
||||
.append(todo == 0 ? "Everything the assistant needs is set up." : esc(todo + (todo == 1 ? " item needs" : " items need") + " attention."))
|
||||
.append("</p><ul style=\"list-style:none;margin:0;padding:0;\">");
|
||||
for(AssistantSetupStatus.Item item : data.getSetup())
|
||||
{
|
||||
String color = switch(item.state())
|
||||
{
|
||||
case OK -> GREEN;
|
||||
case TODO -> RED;
|
||||
case DEFAULT -> AMBER;
|
||||
case OPTIONAL -> GREY;
|
||||
};
|
||||
String label = switch(item.state())
|
||||
{
|
||||
case OK -> "Set up";
|
||||
case TODO -> "Needs setup";
|
||||
case DEFAULT -> "Using defaults";
|
||||
case OPTIONAL -> "Optional";
|
||||
};
|
||||
html.append("<li style=\"").append(ROW).append("\"><div style=\"display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .75rem;\">")
|
||||
.append("<strong><a style=\"").append(LINK).append("\" href=\"").append(esc(prefix + "/" + item.tableName())).append("\">").append(esc(item.label())).append("</a></strong>")
|
||||
.append(chip(color, label)).append("</div><div style=\"font-size:.9rem;margin-top:.35rem;\">").append(esc(item.detail())).append("</div>");
|
||||
if(item.nextStep() != null)
|
||||
{
|
||||
html.append("<div style=\"").append(MUTED).append("font-size:.9rem;margin-top:.2rem;\">Next: ").append(esc(item.nextStep())).append("</div>");
|
||||
}
|
||||
html.append("</li>");
|
||||
}
|
||||
html.append("</ul>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The status row: is the assistant answering, with what, at what cost.
|
||||
*******************************************************************************/
|
||||
|
||||
-231
@@ -1,231 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.home;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.engine.ProviderService;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantBudget;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantDataAccess;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModelPrice;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantPolicy;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProfile;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProfileSkill;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantRagSource;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantScheduledTask;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantSkill;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPricing;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card on the Assistant home: each configuration table, whether it
|
||||
** is set up, and the next step. Only reads the QBit's own configuration
|
||||
** tables (small), as the signed-in administrator.
|
||||
*******************************************************************************/
|
||||
public final class AssistantSetupStatus
|
||||
{
|
||||
private static final int LIMIT = 1000;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** OK: working. TODO: the assistant needs it. DEFAULT: working on built-in
|
||||
** defaults (nothing to do). OPTIONAL: a feature that is off until set up.
|
||||
*******************************************************************************/
|
||||
public enum State
|
||||
{
|
||||
OK, TODO, DEFAULT, OPTIONAL
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One line of the card.
|
||||
*******************************************************************************/
|
||||
public record Item(String label, String tableName, State state, String detail, String nextStep)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private AssistantSetupStatus()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the items. `providers` is what the home already loaded.
|
||||
*******************************************************************************/
|
||||
public static List<Item> load(AssistantRuntime runtime, List<AssistantHomeData.ProviderHealth> providers) throws QException
|
||||
{
|
||||
AssistantStore store = runtime.getStore();
|
||||
List<Item> items = new ArrayList<>();
|
||||
|
||||
///////////////
|
||||
// providers //
|
||||
///////////////
|
||||
AssistantHomeData.ProviderHealth active = providers.stream().filter(AssistantHomeData.ProviderHealth::active).findFirst().orElse(null);
|
||||
if(active != null && active.keyPresent() && active.problems().isEmpty())
|
||||
{
|
||||
items.add(new Item("Provider", AssistantProvider.TABLE_NAME, State.OK, active.row().getName() + " answers questions.", null));
|
||||
}
|
||||
else if(providers.isEmpty())
|
||||
{
|
||||
items.add(new Item("Provider", AssistantProvider.TABLE_NAME, State.TODO, "No provider yet.", "Add one with New: pick the type and model, and name the environment variable that holds the API key."));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.add(new Item("Provider", AssistantProvider.TABLE_NAME, State.TODO, "The default provider cannot be used.", "Turn one on, set it as default, make sure its key variable is set, then run Test connection."));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// prices: resolved exactly as calls are costed (registry row, price //
|
||||
// row, then the configured price table); local models cost nothing //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
int priceRows = store.query(AssistantModelPrice.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantModelPrice.class).size();
|
||||
String rowsText = priceRows + " price row" + (priceRows == 1 ? "" : "s");
|
||||
if(active == null)
|
||||
{
|
||||
items.add(new Item("Model prices", AssistantModelPrice.TABLE_NAME, State.DEFAULT, rowsText + "; checked once a provider answers.", null));
|
||||
}
|
||||
else
|
||||
{
|
||||
AssistantProvider row = active.row();
|
||||
ModelPricing price = runtime.getProviderService().pricing(new ProviderService.Active(row, row.getProviderType(), row.getModel(), null), row.getModel());
|
||||
boolean local = "LOCAL".equals(active.location());
|
||||
boolean known = price != null && (positive(price.getInputUsdPerMTok()) || positive(price.getOutputUsdPerMTok()));
|
||||
items.add(known || local
|
||||
? new Item("Model prices", AssistantModelPrice.TABLE_NAME, State.OK, local && !known ? "The default model is local and costs nothing. " + rowsText + "."
|
||||
: "The default model is priced at $" + price.getInputUsdPerMTok() + " in / $" + price.getOutputUsdPerMTok() + " out per million tokens. " + rowsText + ".", null)
|
||||
: new Item("Model prices", AssistantModelPrice.TABLE_NAME, State.TODO, "The default model (" + row.getModel() + ") has no price, so its calls cost $0 and cost budgets never trigger.",
|
||||
"Add a price row for its provider type and model."));
|
||||
}
|
||||
|
||||
////////////
|
||||
// policy //
|
||||
////////////
|
||||
List<AssistantPolicy> policies = store.query(AssistantPolicy.TABLE_NAME, new QQueryFilter().withLimit(1), AssistantPolicy.class);
|
||||
if(policies.isEmpty())
|
||||
{
|
||||
items.add(new Item("Policy", AssistantPolicy.TABLE_NAME, State.DEFAULT, "No policy row: the code configuration applies.", "Add one to control the kill switch, confirmations and tool limits here."));
|
||||
}
|
||||
else if(Boolean.TRUE.equals(policies.get(0).getKillSwitch()))
|
||||
{
|
||||
items.add(new Item("Policy", AssistantPolicy.TABLE_NAME, State.TODO, "The kill switch is on: the assistant answers nothing.", "Turn it off on the policy row when ready."));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.add(new Item("Policy", AssistantPolicy.TABLE_NAME, State.OK, "Writes " + (Boolean.FALSE.equals(policies.get(0).getWritesRequireConfirmation()) ? "do not wait" : "wait") + " for confirmation.", null));
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// profiles and skills //
|
||||
////////////////////////
|
||||
List<AssistantProfile> profiles = enabled(store.query(AssistantProfile.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantProfile.class));
|
||||
boolean hasDefault = profiles.stream().anyMatch(p -> Boolean.TRUE.equals(p.getIsDefault()));
|
||||
items.add(profiles.isEmpty()
|
||||
? new Item("Profiles", AssistantProfile.TABLE_NAME, State.DEFAULT, "No profile: the built-in assistant answers, with no skills.", "Add a default profile to name the assistant, set its tone and offer skills.")
|
||||
: new Item("Profiles", AssistantProfile.TABLE_NAME, hasDefault ? State.OK : State.DEFAULT,
|
||||
profiles.size() + " profile" + (profiles.size() == 1 ? "" : "s") + (hasDefault ? ", with a default." : ", none of them the default."),
|
||||
hasDefault ? null : "Mark one as the default so it applies everywhere else."));
|
||||
|
||||
Set<Integer> linkedSkillIds = new HashSet<>();
|
||||
Set<Integer> profileIds = new HashSet<>(profiles.stream().map(AssistantProfile::getId).toList());
|
||||
for(AssistantProfileSkill link : store.query(AssistantProfileSkill.TABLE_NAME, new QQueryFilter().withLimit(LIMIT * 5), AssistantProfileSkill.class))
|
||||
{
|
||||
if(profileIds.contains(link.getProfileId()))
|
||||
{
|
||||
linkedSkillIds.add(link.getSkillId());
|
||||
}
|
||||
}
|
||||
List<AssistantSkill> skills = enabled(store.query(AssistantSkill.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantSkill.class));
|
||||
long offered = skills.stream().filter(s -> linkedSkillIds.contains(s.getId())).count();
|
||||
items.add(skills.isEmpty()
|
||||
? new Item("Skills", AssistantSkill.TABLE_NAME, State.OPTIONAL, "No skills.", "Write know-how for your business terms, then link it to a profile.")
|
||||
: offered == 0
|
||||
? new Item("Skills", AssistantSkill.TABLE_NAME, State.TODO, skills.size() + " skill" + (skills.size() == 1 ? "" : "s") + ", but none is linked to a turned-on profile, so the model never sees them.", "Link them to a profile (Profile skills).")
|
||||
: new Item("Skills", AssistantSkill.TABLE_NAME, State.OK, offered + " of " + skills.size() + " skill" + (skills.size() == 1 ? " is" : "s are") + " offered through a profile.", null));
|
||||
|
||||
/////////////
|
||||
// budgets //
|
||||
/////////////
|
||||
List<AssistantBudget> budgets = store.budgets();
|
||||
items.add(budgets.isEmpty()
|
||||
? new Item("Budgets", AssistantBudget.TABLE_NAME, State.TODO, "No budget: spending is limited only by the policy's configured caps.", "Add a monthly instance budget (warn, downgrade, stop).")
|
||||
: new Item("Budgets", AssistantBudget.TABLE_NAME, State.OK, budgets.size() + " budget" + (budgets.size() == 1 ? "" : "s") + " checked before every question.", null));
|
||||
|
||||
/////////////////
|
||||
// data access //
|
||||
/////////////////
|
||||
int accessRows = store.query(AssistantDataAccess.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantDataAccess.class).size();
|
||||
items.add(accessRows == 0
|
||||
? new Item("Data access", AssistantDataAccess.TABLE_NAME, State.DEFAULT, "Defaults: the assistant may read what each user can read; it proposes no changes.",
|
||||
"Run Add rows for every table to review them, then raise tables to READ_WRITE where it may propose changes.")
|
||||
: new Item("Data access", AssistantDataAccess.TABLE_NAME, State.OK, accessRows + " row" + (accessRows == 1 ? "" : "s") + "; tables without a row use the default.", null));
|
||||
|
||||
///////////////////////////////////////
|
||||
// optional: knowledge and schedules //
|
||||
///////////////////////////////////////
|
||||
long sources = enabled(store.query(AssistantRagSource.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantRagSource.class)).size();
|
||||
items.add(sources == 0
|
||||
? new Item("Knowledge sources", AssistantRagSource.TABLE_NAME, State.OPTIONAL, "Off: search by meaning is not offered.", "Add a source (a readable table, its text fields and an embedding model), then index it.")
|
||||
: new Item("Knowledge sources", AssistantRagSource.TABLE_NAME, State.OK, sources + " source" + (sources == 1 ? "" : "s") + " turned on.", null));
|
||||
|
||||
List<AssistantScheduledTask> tasks = store.query(AssistantScheduledTask.TABLE_NAME, new QQueryFilter().withLimit(LIMIT), AssistantScheduledTask.class);
|
||||
long onTasks = tasks.stream().filter(t -> Boolean.TRUE.equals(t.getEnabled())).count();
|
||||
items.add(onTasks == 0
|
||||
? new Item("Scheduled tasks", AssistantScheduledTask.TABLE_NAME, State.OPTIONAL, tasks.isEmpty() ? "None." : tasks.size() + " task" + (tasks.size() == 1 ? "" : "s") + ", all turned off.",
|
||||
"Set a run-as user and an allowed credential variable on a task, then turn it on.")
|
||||
: new Item("Scheduled tasks", AssistantScheduledTask.TABLE_NAME, State.OK, onTasks + " task" + (onTasks == 1 ? "" : "s") + " turned on.", null));
|
||||
return (items);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Rows not explicitly turned off.
|
||||
*******************************************************************************/
|
||||
private static <T extends QRecordEntity> List<T> enabled(List<T> rows)
|
||||
{
|
||||
return (rows.stream().filter(row -> !Boolean.FALSE.equals(row.toQRecord().getValueBoolean("enabled"))).toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static boolean positive(Double value)
|
||||
{
|
||||
return (value != null && value > 0);
|
||||
}
|
||||
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.metadata;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Applies help/qbit-assistant.yaml (the QQQ help-bundle format) to the
|
||||
** Assistant app and its assistant_* tables: what each table is for and how
|
||||
** its rows get created, shown on the table screen and when it is empty.
|
||||
** STAND-IN(core=#1198): replace with HelpBundles.register once the pinned
|
||||
** QQQ release includes it; the bundle file stays the same.
|
||||
*******************************************************************************/
|
||||
public final class AssistantHelp
|
||||
{
|
||||
public static final String RESOURCE = "/help/qbit-assistant.yaml";
|
||||
public static final String SLOT = "help";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private AssistantHelp()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bundle's entries by key, validated (fails startup if malformed).
|
||||
*******************************************************************************/
|
||||
public static Map<String, JsonNode> entries() throws QException
|
||||
{
|
||||
try(InputStream stream = AssistantHelp.class.getResourceAsStream(RESOURCE))
|
||||
{
|
||||
if(stream == null)
|
||||
{
|
||||
throw (new IOException("missing resource"));
|
||||
}
|
||||
JsonNode bundle = new ObjectMapper(new YAMLFactory()).readTree(stream);
|
||||
if(!"qbit-assistant".equals(bundle.path("bundle").asText()) || !"QBIT".equals(bundle.path("layer").asText()))
|
||||
{
|
||||
throw (new IOException("invalid bundle header"));
|
||||
}
|
||||
Map<String, JsonNode> entries = new LinkedHashMap<>();
|
||||
for(JsonNode entry : bundle.path("entries"))
|
||||
{
|
||||
String key = entry.path("key").asText();
|
||||
String summary = entry.path("summary").asText();
|
||||
if(key.isBlank() || summary.isBlank() || summary.length() > 120 || entry.path("content").asText().isBlank() || entries.put(key, entry) != null)
|
||||
{
|
||||
throw (new IOException("invalid or duplicate entry: " + key));
|
||||
}
|
||||
}
|
||||
return (entries);
|
||||
}
|
||||
catch(IOException | RuntimeException e)
|
||||
{
|
||||
throw (new QException("Could not load " + RESOURCE, e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply the help to the app and every assistant_* table in the instance.
|
||||
** A host's own help on a table is kept (the bundle only fills the slot).
|
||||
*******************************************************************************/
|
||||
public static void apply(QInstance qInstance) throws QException
|
||||
{
|
||||
Map<String, JsonNode> entries = entries();
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
JsonNode entry = entries.get("table:" + table.getName() + ";slot:" + SLOT);
|
||||
if(entry != null && (table.getHelpContent() == null || !table.getHelpContent().containsKey(SLOT)))
|
||||
{
|
||||
table.withHelpContent(SLOT, content(entry));
|
||||
}
|
||||
}
|
||||
QAppMetaData app = qInstance.getApp(AssistantMetaDataBuilder.APP_NAME);
|
||||
JsonNode appEntry = entries.get("app:" + AssistantMetaDataBuilder.APP_NAME + ";slot:" + SLOT);
|
||||
if(app != null && appEntry != null && (app.getHelpContent() == null || !app.getHelpContent().containsKey(SLOT)))
|
||||
{
|
||||
app.withHelpContent(SLOT, content(appEntry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Summary as the first paragraph, then the content, as markdown.
|
||||
*******************************************************************************/
|
||||
static QHelpContent content(JsonNode entry)
|
||||
{
|
||||
return (new QHelpContent().withContentAsMarkdown(entry.path("summary").asText().strip() + "\n\n" + entry.path("content").asText().strip()));
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -47,6 +47,7 @@ import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurnEvent;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUserPreference;
|
||||
import com.kingsrook.qbits.assistant.processes.AssistantRetentionStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiExposure;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiTableMetaData;
|
||||
@@ -236,6 +237,10 @@ public class AssistantMetaDataBuilder
|
||||
.withUniqueKey(new UniqueKey("profileId", "skillId"))
|
||||
.withRecordLabelFormat("%s · %s").withRecordLabelFields("profileId", "skillId"));
|
||||
|
||||
tables.add(owned(base(AssistantUserPreference.TABLE_NAME, "Assistant User Preference", "tune", AssistantUserPreference.class), false)
|
||||
.withUniqueKey(new UniqueKey("userId"))
|
||||
.withRecordLabelFormat("%s").withRecordLabelFields("userId"));
|
||||
|
||||
tables.add(readOnly(owned(base(AssistantPendingAction.TABLE_NAME, "Assistant Pending Action", "pending_actions", AssistantPendingAction.class), false))
|
||||
.withRecordLabelFormat("%s #%s").withRecordLabelFields("toolName", "id"));
|
||||
text(tables.get(tables.size() - 1), "paramsJson");
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.assistant.AssistantQBitConfig;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.AssistantSecurity;
|
||||
import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUserPreference;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferenceChange;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferenceProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferenceRequestContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferenceSectionMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferenceValues;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.preferences.UserPreferencesMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Core preferences over the standard assistant_user_preference table. Core
|
||||
** supplies the authenticated user and trusted scope, retaining audit identity.
|
||||
*******************************************************************************/
|
||||
public class AssistantPreferencesProvider implements UserPreferenceProviderInterface
|
||||
{
|
||||
public static final String SECTION = "assistant";
|
||||
public static final String AUTO_CONFIRM_REASON = "Auto-confirm is not allowed by the assistant policy.";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The admin app is intentionally not the scope: ordinary assistant users
|
||||
** must see their own preferences without assistant.admin.
|
||||
*******************************************************************************/
|
||||
public static void register(QInstance qInstance)
|
||||
{
|
||||
UserPreferencesMetaData.ofOrWithNew(qInstance).withSection(new Section()
|
||||
.withName(SECTION).withLabel("Assistant").withIcon(new QIcon("smart_toy"))
|
||||
.withDescription("Defaults for new conversations and confirmation of changes.")
|
||||
.withPermissionName(AssistantQBitConfig.ACCESS_PERMISSION)
|
||||
.withProvider(new QCodeReference(AssistantPreferencesProvider.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Core only applies channel rules to app-scoped sections. Explicitly deny
|
||||
** this unbound configuration section before loading or changing any rows.
|
||||
*******************************************************************************/
|
||||
private AssistantRuntime runtime(UserPreferenceRequestContext context) throws QException
|
||||
{
|
||||
if("assistant".equals(context.getSession().getClientChannelName()))
|
||||
{
|
||||
throw (new QNotFoundException("Preference section not found"));
|
||||
}
|
||||
AssistantRuntime runtime = AssistantRuntime.of(QContext.getQInstance());
|
||||
AssistantSecurity.ownerKey(context.getSession(), runtime.getConfig());
|
||||
return (runtime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read only the context user's row, even if they hold an admin owner key.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public UserPreferenceValues load(UserPreferenceRequestContext context) throws QException
|
||||
{
|
||||
AssistantUserPreference preference = runtime(context).getStore().getPreference(context.getUserId());
|
||||
return (new UserPreferenceValues().withValues(Map.of(
|
||||
"defaultWriteMode", preference != null && Boolean.TRUE.equals(preference.getDefaultWriteMode()),
|
||||
"autoConfirmLowRisk", preference != null && Boolean.TRUE.equals(preference.getAutoConfirmLowRisk()))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Re-read policy at the write boundary; the submitted defaults grant nothing.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<String> validate(UserPreferenceRequestContext context, UserPreferenceChange change) throws QException
|
||||
{
|
||||
PolicyService.ToolPolicy policy = runtime(context).getPolicyService().current(true).tools();
|
||||
List<String> errors = new ArrayList<>();
|
||||
if(change.getValues().containsKey("autoConfirmLowRisk") && !policy.autoConfirmAllowed())
|
||||
{
|
||||
errors.add("Auto-confirm low-risk changes: " + AUTO_CONFIRM_REASON);
|
||||
}
|
||||
if(Boolean.TRUE.equals(change.getValues().get("defaultWriteMode")) && !policy.writeModeAvailableTo(context.getSession()))
|
||||
{
|
||||
errors.add("Start conversations in Write mode: Write mode is not available to you under the assistant policy.");
|
||||
}
|
||||
return (errors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply only supplied fields; all ownership comes from the context.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void store(UserPreferenceRequestContext context, UserPreferenceChange change) throws QException
|
||||
{
|
||||
AssistantRuntime runtime = runtime(context);
|
||||
AssistantUserPreference preference = runtime.getStore().getPreference(context.getUserId());
|
||||
if(preference == null)
|
||||
{
|
||||
preference = runtime.getStore().insert(new AssistantUserPreference().withUserId(context.getUserId())
|
||||
.withDefaultWriteMode(false).withAutoConfirmLowRisk(false));
|
||||
}
|
||||
QRecord update = new QRecord().withValue("id", preference.getId());
|
||||
change.getValues().forEach(update::setValue);
|
||||
runtime.getStore().updateFields(AssistantUserPreference.TABLE_NAME, update);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Return fresh field metadata for each request, so policy editability never
|
||||
** mutates the shared instance metadata or leaks between concurrent users.
|
||||
*******************************************************************************/
|
||||
public static class Section extends UserPreferenceSectionMetaData
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Scalar read-only metadata uses the standard field help text for its reason.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QFieldMetaData> getFields()
|
||||
{
|
||||
boolean autoConfirmAllowed = false;
|
||||
AssistantRuntime runtime = AssistantRuntime.of(QContext.getQInstance());
|
||||
if(runtime != null && QContext.getQSession() != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
autoConfirmAllowed = runtime.getPolicyService().current(false).tools().autoConfirmAllowed();
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
// Fail closed when policy cannot be read; provider validation also rechecks it.
|
||||
}
|
||||
}
|
||||
QFieldMetaData autoConfirm = new QFieldMetaData("autoConfirmLowRisk", QFieldType.BOOLEAN)
|
||||
.withLabel("Auto-confirm low-risk changes").withIsEditable(autoConfirmAllowed);
|
||||
if(!autoConfirmAllowed)
|
||||
{
|
||||
autoConfirm.withHelpContent(new QHelpContent(AUTO_CONFIRM_REASON));
|
||||
}
|
||||
return (List.of(new QFieldMetaData("defaultWriteMode", QFieldType.BOOLEAN).withLabel("Start conversations in Write mode"), autoConfirm));
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -51,7 +51,7 @@ import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurnEvent;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
@@ -574,8 +574,10 @@ public final class AssistantRecordScreens
|
||||
.withHelpContent(new QHelpContent("The table or process name, e.g. blog_post. Tables without a row use the default: NONE for sensitive tables, READ for the others. Processes default to NONE."));
|
||||
table.getField("mode").withLabel("Assistant access").withDefaultValue(DataAccessMode.READ.name())
|
||||
.withHelpContent(new QHelpContent("NONE: the assistant never sees it. READ: the read tools may use it. READ and write: the create, update and delete tools may too. The assistant always acts with the user's own permissions and record locks; this can only narrow them. A sensitive table (AI exposure NONE) stays NONE."));
|
||||
table.getField("autoConfirmOk").withLabel("Auto-confirm OK")
|
||||
.withHelpContent(new QHelpContent("Single-record inserts and updates here count as low-risk: users who chose auto-confirm (where the policy allows it) skip the confirmation card. Never deletes."));
|
||||
sections(table,
|
||||
section("access", "Data Access", "admin_panel_settings", Tier.T1, "id", "objectType", "objectName", "mode"),
|
||||
section("access", "Data Access", "admin_panel_settings", Tier.T1, "id", "objectType", "objectName", "mode", "autoConfirmOk"),
|
||||
section("notes", "Notes", "notes", Tier.T2, "notes"));
|
||||
}
|
||||
|
||||
@@ -712,6 +714,9 @@ public final class AssistantRecordScreens
|
||||
help(table, "killSwitch", "On: the assistant takes no new requests from anyone.");
|
||||
help(table, "killSwitchReason", "Shown to users while the kill switch is on.");
|
||||
help(table, "writesRequireConfirmation", "On (or empty): every create, update or delete the assistant proposes waits for the user to confirm it in the panel. Off: writes run at once, still with the user's own permissions.");
|
||||
help(table, "writeModeAllowed", "On (or empty): users may switch a conversation to Write mode, where the create, update and delete tools exist. Off: every conversation stays Read-only.");
|
||||
help(table, "writeModePermission", "Optional: only users holding this permission (a role) may use Write mode.");
|
||||
help(table, "autoConfirmAllowed", "On: users may choose to auto-confirm low-risk writes (single-record inserts and updates on tables marked auto-confirm OK). Never deletes, bulk writes or processes. Off (or empty): every write waits for confirmation.");
|
||||
help(table, "allowedTools", "Comma-separated tool names the assistant may use (e.g. list_tables, query_records, count_records). Empty allows every tool.");
|
||||
help(table, "maxRowsPerCall", "Most rows one tool call returns to the model. Empty: 50; at most 200.");
|
||||
help(table, "redactPii", "On (or empty): API keys, tokens, private keys, card numbers and social security numbers are redacted from tool results before the model sees them.");
|
||||
@@ -728,7 +733,7 @@ public final class AssistantRecordScreens
|
||||
sections(table,
|
||||
section("policy", "Policy", "policy", Tier.T1, "id", "name", "killSwitch", "killSwitchReason"),
|
||||
section("model", "Model", "smart_toy", Tier.T2, "mainModel", "mainEffort"),
|
||||
section("tools", "Tools and data", "build", Tier.T2, "writesRequireConfirmation", "allowedTools", "maxRowsPerCall", "redactPii"),
|
||||
section("tools", "Tools and data", "build", Tier.T2, "writeModeAllowed", "writeModePermission", "writesRequireConfirmation", "autoConfirmAllowed", "allowedTools", "maxRowsPerCall", "redactPii"),
|
||||
section("userDaily", "Per user, per day", "person", Tier.T2, "userDailyTokensSoft", "userDailyTokensHard", "userDailyCostMicrosSoft", "userDailyCostMicrosHard"),
|
||||
section("instanceMonthly", "Whole instance, per month", "domain", Tier.T2, "instanceMonthlyTokensSoft", "instanceMonthlyTokensHard", "instanceMonthlyCostMicrosSoft", "instanceMonthlyCostMicrosHard"));
|
||||
}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantProvider;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantProviderType;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantProviderType;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
+33
@@ -71,6 +71,9 @@ public class AssistantConversation extends QRecordEntity
|
||||
@QField(isEditable = false)
|
||||
private String contextJson;
|
||||
|
||||
@QField()
|
||||
private Boolean writeMode;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Long totalTokensIn;
|
||||
|
||||
@@ -509,6 +512,36 @@ public class AssistantConversation extends QRecordEntity
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for writeMode (Write mode for this conversation (default Read-only); the write tools exist only in Write mode)
|
||||
*******************************************************************************/
|
||||
public Boolean getWriteMode()
|
||||
{
|
||||
return (this.writeMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for writeMode
|
||||
*******************************************************************************/
|
||||
public void setWriteMode(Boolean writeMode)
|
||||
{
|
||||
this.writeMode = writeMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for writeMode
|
||||
*******************************************************************************/
|
||||
public AssistantConversation withWriteMode(Boolean writeMode)
|
||||
{
|
||||
this.writeMode = writeMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxClassification
|
||||
|
||||
+33
@@ -49,6 +49,9 @@ public class AssistantDataAccess extends QRecordEntity
|
||||
@QField(maxLength = 500, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
|
||||
private String notes;
|
||||
|
||||
@QField()
|
||||
private Boolean autoConfirmOk;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@@ -285,4 +288,34 @@ public class AssistantDataAccess extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for autoConfirmOk (single-record inserts and updates here may be auto-confirmed (when the policy and the user allow it))
|
||||
*******************************************************************************/
|
||||
public Boolean getAutoConfirmOk()
|
||||
{
|
||||
return (this.autoConfirmOk);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for autoConfirmOk
|
||||
*******************************************************************************/
|
||||
public void setAutoConfirmOk(Boolean autoConfirmOk)
|
||||
{
|
||||
this.autoConfirmOk = autoConfirmOk;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for autoConfirmOk
|
||||
*******************************************************************************/
|
||||
public AssistantDataAccess withAutoConfirmOk(Boolean autoConfirmOk)
|
||||
{
|
||||
this.autoConfirmOk = autoConfirmOk;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,6 +94,15 @@ public class AssistantPolicy extends QRecordEntity
|
||||
@QField()
|
||||
private Boolean redactPii;
|
||||
|
||||
@QField()
|
||||
private Boolean writeModeAllowed;
|
||||
|
||||
@QField(maxLength = 128)
|
||||
private String writeModePermission;
|
||||
|
||||
@QField()
|
||||
private Boolean autoConfirmAllowed;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@@ -735,4 +744,94 @@ public class AssistantPolicy extends QRecordEntity
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for writeModeAllowed (whether any conversation may switch to Write mode (default true))
|
||||
*******************************************************************************/
|
||||
public Boolean getWriteModeAllowed()
|
||||
{
|
||||
return (this.writeModeAllowed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for writeModeAllowed
|
||||
*******************************************************************************/
|
||||
public void setWriteModeAllowed(Boolean writeModeAllowed)
|
||||
{
|
||||
this.writeModeAllowed = writeModeAllowed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for writeModeAllowed
|
||||
*******************************************************************************/
|
||||
public AssistantPolicy withWriteModeAllowed(Boolean writeModeAllowed)
|
||||
{
|
||||
this.writeModeAllowed = writeModeAllowed;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for writeModePermission (only holders of this permission may switch to Write mode (empty: everyone))
|
||||
*******************************************************************************/
|
||||
public String getWriteModePermission()
|
||||
{
|
||||
return (this.writeModePermission);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for writeModePermission
|
||||
*******************************************************************************/
|
||||
public void setWriteModePermission(String writeModePermission)
|
||||
{
|
||||
this.writeModePermission = writeModePermission;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for writeModePermission
|
||||
*******************************************************************************/
|
||||
public AssistantPolicy withWriteModePermission(String writeModePermission)
|
||||
{
|
||||
this.writeModePermission = writeModePermission;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for autoConfirmAllowed (whether users may auto-confirm low-risk writes (default false))
|
||||
*******************************************************************************/
|
||||
public Boolean getAutoConfirmAllowed()
|
||||
{
|
||||
return (this.autoConfirmAllowed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for autoConfirmAllowed
|
||||
*******************************************************************************/
|
||||
public void setAutoConfirmAllowed(Boolean autoConfirmAllowed)
|
||||
{
|
||||
this.autoConfirmAllowed = autoConfirmAllowed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for autoConfirmAllowed
|
||||
*******************************************************************************/
|
||||
public AssistantPolicy withAutoConfirmAllowed(Boolean autoConfirmAllowed)
|
||||
{
|
||||
this.autoConfirmAllowed = autoConfirmAllowed;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.model;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A user's own assistant preferences: the default mode of new conversations and auto-confirm of low-risk writes (only where the policy allows it).
|
||||
*******************************************************************************/
|
||||
public class AssistantUserPreference extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "assistant_user_preference";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField(isEditable = false, maxLength = 255)
|
||||
private String userId;
|
||||
|
||||
@QField()
|
||||
private Boolean defaultWriteMode;
|
||||
|
||||
@QField()
|
||||
private Boolean autoConfirmLowRisk;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant modifyDate;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that populates from a QRecord.
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference(QRecord record) throws QException
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Integer getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for userId
|
||||
*******************************************************************************/
|
||||
public String getUserId()
|
||||
{
|
||||
return (this.userId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for userId
|
||||
*******************************************************************************/
|
||||
public void setUserId(String userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for userId
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withUserId(String userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultWriteMode
|
||||
*******************************************************************************/
|
||||
public Boolean getDefaultWriteMode()
|
||||
{
|
||||
return (this.defaultWriteMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for defaultWriteMode
|
||||
*******************************************************************************/
|
||||
public void setDefaultWriteMode(Boolean defaultWriteMode)
|
||||
{
|
||||
this.defaultWriteMode = defaultWriteMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for defaultWriteMode
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withDefaultWriteMode(Boolean defaultWriteMode)
|
||||
{
|
||||
this.defaultWriteMode = defaultWriteMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for autoConfirmLowRisk
|
||||
*******************************************************************************/
|
||||
public Boolean getAutoConfirmLowRisk()
|
||||
{
|
||||
return (this.autoConfirmLowRisk);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for autoConfirmLowRisk
|
||||
*******************************************************************************/
|
||||
public void setAutoConfirmLowRisk(Boolean autoConfirmLowRisk)
|
||||
{
|
||||
this.autoConfirmLowRisk = autoConfirmLowRisk;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for autoConfirmLowRisk
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withAutoConfirmLowRisk(Boolean autoConfirmLowRisk)
|
||||
{
|
||||
this.autoConfirmLowRisk = autoConfirmLowRisk;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for createDate
|
||||
*******************************************************************************/
|
||||
public Instant getCreateDate()
|
||||
{
|
||||
return (this.createDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for createDate
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for modifyDate
|
||||
*******************************************************************************/
|
||||
public Instant getModifyDate()
|
||||
{
|
||||
return (this.modifyDate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for modifyDate
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference withModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
+12
-1
@@ -51,8 +51,9 @@ import com.kingsrook.qbits.assistant.model.AssistantTurn;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantTurnEvent;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUserPreference;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.scheduled.ScheduledTasks;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
@@ -384,6 +385,16 @@ public class AssistantStore
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The user's own preferences, or null.
|
||||
*******************************************************************************/
|
||||
public AssistantUserPreference getPreference(String userId) throws QException
|
||||
{
|
||||
return (first(query(AssistantUserPreference.TABLE_NAME, owned(userId).withLimit(1), AssistantUserPreference.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One of the user's own pending actions, or null.
|
||||
*******************************************************************************/
|
||||
|
||||
-268
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.assistant.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessMode;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessService;
|
||||
import com.kingsrook.qbits.assistant.metadata.AssistantMetaDataBuilder;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantDataAccess;
|
||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractExtractStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "Add rows for every table": a table action on assistant_data_access that
|
||||
** writes one row per application table that has none yet, with the access
|
||||
** the assistant already gets by default (READ, or NONE where the table's AI
|
||||
** exposure is NONE). It changes no behavior on its own; it makes the defaults
|
||||
** visible and editable. Existing rows are never touched. Standard streamed
|
||||
** ETL: Review lists what will be added, Result what was added.
|
||||
*******************************************************************************/
|
||||
public final class DataAccessDefaultsProcess
|
||||
{
|
||||
public static final String NAME = "assistantDataAccessAddDefaults";
|
||||
|
||||
public static final String NOTE = "Added by 'Add rows for every table': the assistant's default for this table. The user's own permissions still apply.";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private DataAccessDefaultsProcess()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The process: a table-level action (no records need to be selected).
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData process()
|
||||
{
|
||||
Map<String, Serializable> values = new HashMap<>();
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, AssistantDataAccess.TABLE_NAME);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, AssistantDataAccess.TABLE_NAME);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_SUPPORTS_FULL_VALIDATION, false);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE,
|
||||
"These rows will be added: one per table that has none, with the access the assistant already has by default. Existing rows are not changed.");
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(Extract.class, Transform.class, Load.class, values);
|
||||
process.setName(NAME);
|
||||
process.setLabel("Add rows for every table");
|
||||
process.setIcon(new QIcon("playlist_add"));
|
||||
process.setTableName(AssistantDataAccess.TABLE_NAME);
|
||||
process.setMinInputRecords(0);
|
||||
process.setPermissionRules(AssistantMetaDataBuilder.adminRules());
|
||||
return (process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The rows that are missing: one per application table without a row.
|
||||
*******************************************************************************/
|
||||
public static List<AssistantDataAccess> missingRows(QInstance qInstance, DataAccessService access, AssistantStore store) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// fresh rows: a row deleted on another server must not come back //
|
||||
// from this server's cache as a "default" //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
access.invalidate();
|
||||
Set<String> existing = new HashSet<>();
|
||||
for(AssistantDataAccess row : store.query(AssistantDataAccess.TABLE_NAME, new QQueryFilter().withLimit(10_000), AssistantDataAccess.class))
|
||||
{
|
||||
if(row.getObjectType() == null || AssistantDataAccess.TYPE_TABLE.equals(row.getObjectType()))
|
||||
{
|
||||
existing.add(row.getObjectName());
|
||||
}
|
||||
}
|
||||
|
||||
List<AssistantDataAccess> rs = new ArrayList<>();
|
||||
for(QTableMetaData table : qInstance.getTables().values().stream().sorted((a, b) -> a.getName().compareTo(b.getName())).toList())
|
||||
{
|
||||
if(table.getName().startsWith("assistant_") || existing.contains(table.getName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
DataAccessMode mode = access.forTable(table.getName()).mode();
|
||||
rs.add(new AssistantDataAccess().withObjectType(AssistantDataAccess.TYPE_TABLE).withObjectName(table.getName())
|
||||
.withMode(mode.name()).withNotes(NOTE));
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The running assistant (or a clear error).
|
||||
*******************************************************************************/
|
||||
private static AssistantRuntime runtime() throws QException
|
||||
{
|
||||
AssistantRuntime runtime = AssistantRuntime.of(QContext.getQInstance());
|
||||
if(runtime == null)
|
||||
{
|
||||
throw (new QException("The assistant is not running on this server."));
|
||||
}
|
||||
return (runtime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Extract: the missing rows, computed (no query of the selected records).
|
||||
*******************************************************************************/
|
||||
public static class Extract extends AbstractExtractStep
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Integer doCount(RunBackendStepInput input) throws QException
|
||||
{
|
||||
AssistantRuntime runtime = runtime();
|
||||
return (missingRows(QContext.getQInstance(), runtime.getDataAccessService(), runtime.getStore()).size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
AssistantRuntime runtime = runtime();
|
||||
for(AssistantDataAccess row : missingRows(QContext.getQInstance(), runtime.getDataAccessService(), runtime.getStore()))
|
||||
{
|
||||
getRecordPipe().addRecord(row.toQRecord());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Transform: counts by mode for the review screen.
|
||||
*******************************************************************************/
|
||||
public static class Transform extends AbstractTransformStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
private final Map<String, ProcessSummaryLine> byMode = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
for(QRecord record : input.getRecords())
|
||||
{
|
||||
String mode = record.getValueString("mode");
|
||||
byMode.computeIfAbsent(mode, m -> DiscoverModelsProcess.line("NONE".equals(m) ? Status.INFO : Status.OK,
|
||||
"table will get a row with access " + m, "tables will get a row with access " + m,
|
||||
"table got a row with access " + m, "tables got a row with access " + m)).incrementCountAndAddPrimaryKey(record.getValue("objectName"));
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
|
||||
byMode.values().forEach(line -> line.addSelfToListIfAnyCount(lines));
|
||||
if(lines.isEmpty())
|
||||
{
|
||||
lines.add(new ProcessSummaryLine(Status.INFO, null, "Every table already has a row."));
|
||||
}
|
||||
return (lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Load: insert through the store (the user's own session; admin only).
|
||||
*******************************************************************************/
|
||||
public static class Load extends AbstractLoadStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
private final ProcessSummaryLine added = DiscoverModelsProcess.line(Status.OK, "row was added", "rows were added", "row was added", "rows were added");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
AssistantRuntime runtime = runtime();
|
||||
for(QRecord record : input.getRecords())
|
||||
{
|
||||
AssistantDataAccess row = runtime.getStore().insert(new AssistantDataAccess(record));
|
||||
added.incrementCountAndAddPrimaryKey(row.getId());
|
||||
output.getRecords().add(row.toQRecord());
|
||||
}
|
||||
runtime.getDataAccessService().invalidate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
|
||||
added.addSelfToListIfAnyCount(lines);
|
||||
return (lines);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-9
@@ -18,15 +18,6 @@ package com.kingsrook.qbits.assistant.provider;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.assistant.model.AssistantModel;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.TokenCount;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+10
-10
@@ -24,16 +24,16 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.spi.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.spi.StreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
|
||||
import com.kingsrook.qbits.assistant.provider.CompletableStreamHandle;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelPart;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelUsage;
|
||||
import com.kingsrook.qbits.assistant.provider.StreamHandle;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -30,11 +30,11 @@ import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.engine.ProviderService;
|
||||
import com.kingsrook.qbits.assistant.engine.UsageRecorder;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantDataAudit;
|
||||
import com.kingsrook.qbits.assistant.provider.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.provider.EmbeddingResponse;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelResponse;
|
||||
import com.kingsrook.qbits.assistant.scheduled.ScheduledTurn;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingRequest;
|
||||
import com.kingsrook.qbits.assistant.spi.EmbeddingResponse;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelResponse;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
+4
-10
@@ -23,14 +23,13 @@ package com.kingsrook.qbits.assistant.scheduled;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.engine.BudgetService;
|
||||
import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantScheduledTask;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantUsage;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@@ -103,14 +102,9 @@ public class ScheduledTurn
|
||||
*******************************************************************************/
|
||||
public static PolicyService.ToolPolicy tools(AssistantRuntime runtime, AssistantScheduledTask task, PolicyService.ToolPolicy policy) throws QException
|
||||
{
|
||||
Set<String> allowed = runtime.getContextBuilder().toolNames(policy).stream()
|
||||
.filter(name -> !Set.of("create_record", "update_record", "delete_record").contains(name) || approved(task))
|
||||
.collect(Collectors.toSet());
|
||||
if(allowed.isEmpty())
|
||||
{
|
||||
allowed = Set.of("__no_tools__");
|
||||
}
|
||||
return (new PolicyService.ToolPolicy(true, allowed, policy.maxRowsPerCall(), policy.redactPii()));
|
||||
boolean write = approved(task) && policy.writeModeAvailableTo(QContext.getQSession());
|
||||
return (new PolicyService.ToolPolicy(true, policy.allowedTools(), policy.maxRowsPerCall(), policy.redactPii(),
|
||||
policy.writeModeAllowed(), policy.writeModePermission(), policy.autoConfirmAllowed(), write, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.engine.TurnEngine;
|
||||
import com.kingsrook.qbits.assistant.model.AssistantRagSource;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.rag.EmbeddingService;
|
||||
import com.kingsrook.qbits.assistant.rag.RagService;
|
||||
import com.kingsrook.qbits.assistant.rag.TextChunks;
|
||||
import com.kingsrook.qbits.assistant.rag.VectorStore;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolOutcome;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolResult;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiCall;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.AiToolOutcome;
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.TreeMap;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessMode;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessService;
|
||||
import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiCall;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiGateway;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiResult;
|
||||
@@ -400,7 +400,8 @@ public class UiApiToolbox
|
||||
List<ModelToolDefinition> rs = new ArrayList<>();
|
||||
for(ModelToolDefinition definition : definitions())
|
||||
{
|
||||
if((policy == null || policy.allows(definition.getName())) && (anyWritableTable || !WRITE_TOOLS.contains(definition.getName())))
|
||||
boolean write = WRITE_TOOLS.contains(definition.getName());
|
||||
if((policy == null || policy.allows(definition.getName())) && (!write || (anyWritableTable && (policy == null || policy.writeMode()))))
|
||||
{
|
||||
rs.add(definition);
|
||||
}
|
||||
@@ -448,6 +449,11 @@ public class UiApiToolbox
|
||||
deny(result, "policy:tool", "The administrator has not allowed the " + toolName + " tool.");
|
||||
return (outcome);
|
||||
}
|
||||
if(WRITE_TOOLS.contains(toolName) && context.policy() != null && !context.policy().writeMode())
|
||||
{
|
||||
deny(result, "mode:READ_ONLY", "This conversation is in Read-only mode: the assistant cannot change data. The user can switch it to Write mode in the panel.");
|
||||
return (outcome);
|
||||
}
|
||||
if(!context.gateway().isInvocable(operation[0], operation[1]))
|
||||
{
|
||||
deny(result, "uiApi:notSpecd", "The operation " + outcome.operation + " is not a spec'd UI API operation.");
|
||||
@@ -1211,7 +1217,12 @@ public class UiApiToolbox
|
||||
}
|
||||
}
|
||||
|
||||
if(context.policy() == null || context.policy().writesRequireConfirmation())
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// auto-confirm: only when the policy allows it, the user chose //
|
||||
// it, and the admin marked the table low-risk; never deletes //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
boolean autoConfirmed = context.policy() != null && context.policy().autoConfirm() && !DELETE_RECORD.equals(toolName) && dataAccess.autoConfirmOk(tableName);
|
||||
if(!autoConfirmed && (context.policy() == null || context.policy().writesRequireConfirmation()))
|
||||
{
|
||||
String summary = describeWrite(toolName, table, recordId, values);
|
||||
Integer actionId = context.pendingActions() == null ? null
|
||||
|
||||
+8
-2
@@ -25,7 +25,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.assistant.access.DataAccessService;
|
||||
import com.kingsrook.qbits.assistant.engine.PolicyService;
|
||||
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
|
||||
import com.kingsrook.qbits.assistant.tools.UiApiToolbox;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiCall;
|
||||
import com.kingsrook.qbits.assistant.uiapi.UiApiResult;
|
||||
@@ -151,7 +151,7 @@ public class BulkLoadTools
|
||||
AiJsonSchema.object(AiJsonSchema.properties("uploadRef", upload, "tableName", table, "profile", profile,
|
||||
"mode", AiJsonSchema.enumOf(List.of("SAMPLE", "FULL"), "SAMPLE (default) or FULL.")), List.of("uploadRef", "tableName", "profile")));
|
||||
|
||||
if(anyWritable)
|
||||
if(anyWritable && (policy == null || policy.writeMode()))
|
||||
{
|
||||
Map<String, Object> share = AiJsonSchema.object(AiJsonSchema.properties(
|
||||
"audienceType", AiJsonSchema.enumOf(List.of("user", "group"), "Share with a user (email) or a group."),
|
||||
@@ -205,6 +205,12 @@ public class BulkLoadTools
|
||||
return (UiApiToolbox.Outcome.of(result, null, null, null));
|
||||
}
|
||||
|
||||
if(WRITE_TOOLS.contains(toolName) && context.policy() != null && !context.policy().writeMode())
|
||||
{
|
||||
deny(result, "mode:readOnly", "This conversation is Read-only. Switch to Write mode to make changes.");
|
||||
return (UiApiToolbox.Outcome.of(result, null, null, null));
|
||||
}
|
||||
|
||||
String tableName = in.get("tableName") instanceof String t ? t.strip() : null;
|
||||
if(tableName != null)
|
||||
{
|
||||
|
||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user