Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248cfdc1d1
|
||
|
|
02586b21b6
|
||
|
|
6b7ca13ba9
|
||
|
|
dde08500f5
|
||
|
|
855aa37e00 | ||
|
|
66bac3e5af |
No files matched your search
@@ -1,60 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/^v\d+\.\d+\.\d+$/]
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
ignore: /.*/
|
||||
@@ -1,20 +0,0 @@
|
||||
# QRun-IO qbit .editorconfig: the Kingsrook layout (3-space indentation, never tabs).
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 3
|
||||
tab_width = 3
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{cmd,bat}]
|
||||
end_of_line = crlf
|
||||
@@ -1,3 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
*.cmd text eol=crlf
|
||||
mvnw text eol=lf
|
||||
@@ -0,0 +1,25 @@
|
||||
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
|
||||
name: qrun-release
|
||||
on:
|
||||
push: {}
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
jobs:
|
||||
release:
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
docker-runner: container-build
|
||||
browsers: true
|
||||
browser-runner: browser-build
|
||||
postpublish:
|
||||
if: ${{ needs.release.outputs.capture == 'true' }}
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
@@ -1,25 +0,0 @@
|
||||
# Weekly, grouped dependency updates. Minor and patch updates arrive as one PR per ecosystem;
|
||||
# major updates stay separate so each gets its own review. At most three version-update PRs per
|
||||
# ecosystem are open at once. Security updates are enabled in the repository settings and are
|
||||
# not limited by this schedule.
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: maven
|
||||
directory: /
|
||||
target-branch: develop
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
maven-minor-and-patch:
|
||||
update-types: [minor, patch]
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: develop
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ['*']
|
||||
@@ -0,0 +1,23 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,25 +0,0 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print the plan without merging or creating branches
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
@@ -1,24 +1,20 @@
|
||||
# Maven
|
||||
target/
|
||||
.env
|
||||
.idea
|
||||
*.iml
|
||||
*.class
|
||||
*.log
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
hs_err_pid*
|
||||
.DS_Store
|
||||
*.sw*
|
||||
.flattened-pom.xml
|
||||
dependency-reduced-pom.xml
|
||||
/.env.local
|
||||
/.cache/
|
||||
.claude/settings.local.json
|
||||
|
||||
# Runtime output from every provider module
|
||||
log/
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -1,3 +0,0 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
@@ -0,0 +1,15 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: none
|
||||
test_branch_policy: all
|
||||
publish:
|
||||
snapshot: false
|
||||
release_candidate: false
|
||||
release: false
|
||||
asciidoc: false
|
||||
@@ -1,100 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**QBit Assistant** adds an in-app AI assistant to any QQQ application. It owns the chat backend:
|
||||
- conversations and audit tables
|
||||
- the turn engine (the agent loop)
|
||||
- the `/qqq/v1/assistant` routes, with SSE
|
||||
- budgets, rate limits and the kill switch
|
||||
- the model-provider SPI.
|
||||
|
||||
The panel UI lives in `qqq-frontend-next`. Data access goes only through the **AI tool kernel in QQQ core** (`com.kingsrook.qqq.backend.core.actions.ai.AiToolKernel`).
|
||||
|
||||
- Design spec: `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md`. The owner decisions D1 to D6 at its top are binding.
|
||||
- Phase 1 is read and navigate only: no write tools and no approval cards.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
./mvnw verify # all modules, tests, JaCoCo gate (70% instructions / 90% classes)
|
||||
./mvnw verify -Dqqq.snapshot.version=4.1.0-ASSISTDEV-SNAPSHOT # against a locally installed QQQ build with the kernel (+ developer panels)
|
||||
./mvnw test -pl qbit-assistant -Dtest=RedTeamTest
|
||||
ANTHROPIC_API_KEY=... ./mvnw verify -Dassistant.live=true -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true -pl qbit-assistant-provider-anthropic -am
|
||||
```
|
||||
|
||||
Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed default QQQ version (`qqq.snapshot.version` in the root POM) must stay resolvable from Maven Central. Use a local kernel build only through `-D`.
|
||||
|
||||
## Modules
|
||||
|
||||
- `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`.
|
||||
- `metadata/`: tables, owner key type, the Assistant app (home + main tables in the nav), the retention process, the settings customizer (key-reference checks, masked key status), and the `assistant.admin` custom permission checker.
|
||||
- `home/`: the app home widget (`AssistantHomeData` loads, `AssistantHomeView` renders flow-layout HTML).
|
||||
- `model/`: entities for the `assistant*` tables.
|
||||
- `persistence/AssistantStore`: the **only** code that calls QQQ data actions, and only on the QBit's own tables, under the user's session.
|
||||
- `engine/`: `TurnEngine` (the loop), `ContextBuilder` (cache-prefix order), `SystemPrompt`, `PolicyService` (policy row, budgets), `RateLimiter`, the `TurnEvent` / `TurnEventLog` stream, and `MessageParts`.
|
||||
- `api/`: `AssistantApi` (transport-free) and `AssistantRouteProvider` (Javalin, SSE).
|
||||
- `provider/`: runtime-backed `RegistryModelProvider`, plus `fake/ScriptedModelProvider` for tests, evals and demos. The SPI lives in `qbit-assistant-provider-api`.
|
||||
- `developer/`: the record Developer-screen panel, on the core developer-panel contract.
|
||||
- `processes/`: `AssistantRetentionStep` and `TestConnectionProcess` (record action on `assistant_provider`).
|
||||
- `engine/ProviderService` (active provider from `assistant_provider` rows, key references, prices), `UsageRecorder` (one `assistant_usage` row per model call), `ConnectionTest`.
|
||||
- `qbit-assistant-provider-anthropic`: `AnthropicModelProvider` and `AnthropicMapping` (anthropic-java streaming).
|
||||
- `qbit-assistant-provider-openai`: OpenAI-compatible Chat Completions, model discovery and health using JDK HTTP and Jackson.
|
||||
- `qbit-assistant-provider-bedrock`: Claude through the Anthropic Bedrock backend, AWS default credentials and SigV4; IRSA/web-identity hosts supply STS.
|
||||
- `qbit-assistant-provider-vertex`: Claude through the Anthropic Vertex backend and Gemini through OpenAI-compatible Chat Completions, with refreshed Google ADC tokens.
|
||||
|
||||
- `qbit-assistant-rag-pgvector`: JDBC vector storage; depends on the runtime RAG interfaces (`VectorStore`, `ChunkVector`).
|
||||
|
||||
## Invariants (do not break)
|
||||
|
||||
- **Sessions:**
|
||||
- Never use `QSystemUserSession` or construct a `QSession` in main code (ArchUnit).
|
||||
- Every route uses `ExecutorSessionUtils.setupSession` (the user's own session).
|
||||
- `AssistantSecurity` only ever adds the user's OWN id key, or the all-access key for `assistant.admin` holders.
|
||||
- **Data access:**
|
||||
- Only `persistence/` calls QueryAction, InsertAction and the other data actions (ArchUnit).
|
||||
- Tools run only through `AiToolKernel.execute`.
|
||||
- Panel API queries also filter `userId = me`, so an admin's all-access key never mixes other users' conversations into the panel.
|
||||
- **Caching:** the request prefix stays byte-stable. The order is tools (kernel-sorted), then system block 1 (rules + primer + app digest, LONG/1h), then system block 2 (user digest + date, SHORT/5m), then messages. Exactly one SHORT hint goes on the newest message. Don't put timestamps or ids in the system prompt.
|
||||
- **Tool results:**
|
||||
- Parallel tool results go back in ONE user message, in call order.
|
||||
- Errors (DENIED, NOT_FOUND, INVALID_INPUT, ERROR) are `is_error` results.
|
||||
- Unknown tools and over-limit calls also get error results.
|
||||
- **Keys:** never stored in a table. `assistant_provider.apiKeyEnvName` holds only an allowed environment variable NAME; `apiKeyStatus` is computed on read (last 4 at most) and stripped on save.
|
||||
- **Logging:** never log message content, prompts, tool results or keys. Structured end-of-turn log only.
|
||||
- **Thinking:** thinking is never streamed or stored. The Anthropic adapter keeps the raw assistant message (with signatures) only in memory, as `providerState`, for the tool loop.
|
||||
- **Links:** only `/app` or `/app/...` hrefs are emitted (`TurnEngine.isAppHref`).
|
||||
- **Kill switch:** read fresh before every model call and every tool step.
|
||||
|
||||
## Tests
|
||||
|
||||
- `testing/` holds the fixture app on the memory backend:
|
||||
- blog, page, media and analytics tables, plus audits
|
||||
- sentinel values in hidden and password fields
|
||||
- a prompt-injection record
|
||||
- MOCK auth where the user-id prefix picks the permissions.
|
||||
- `testing/` also holds `TurnRunner`, `GoldenTasks` and `EvalSupport`. They are published as a test-jar for the provider's live evals.
|
||||
- Tags:
|
||||
- `golden` (`GoldenEvalTest`) and `redteam` (`RedTeamTest`) run in every build. Any failure fails the build.
|
||||
- `live` is excluded unless you pass `-Dtest.excludedGroups= -Dgroups=live` and set the key.
|
||||
|
||||
## Code Style
|
||||
|
||||
QQQ style:
|
||||
- 3-space indent, braces on the next line, `return (x);`
|
||||
- flower-box javadoc on every class and method
|
||||
- fluent `withX` setters
|
||||
- wrapper types.
|
||||
|
||||
## Release
|
||||
|
||||
- CircleCI qqq-orb 0.6.8.
|
||||
- `develop` publishes SNAPSHOTs.
|
||||
- `release/*` publishes RCs; the orb bumps `<revision>` in the root POM.
|
||||
- `v*` tags publish releases.
|
||||
- All seven modules and the parent POM publish. The flatten plugin resolves `${revision}`.
|
||||
@@ -1,228 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
## Unreleased - X1/Y1
|
||||
|
||||
- Add default-off `withRecordHistory(true)` host opt-in: remove only `audit*` on RC.6, or invoke core #1193 audit-read/write-deny support when available. Preserve user permissions and all other channel denials; kof22 does not opt in.
|
||||
|
||||
- Register explicit assistant-channel denials on every assistant table, process,
|
||||
report and widget, preserving core security patterns; fail runtime startup
|
||||
when an assistant object is exposed.
|
||||
- Bump the assistant UI API extension to 1.1.0 for optional owned-turn attribution
|
||||
on explain-access, and regenerate its OpenAPI contract.
|
||||
- Preserve turn attribution in knowledge discovery, RAG reads and explain-access
|
||||
calls; confirm actions retain the original turn and scheduled runs retain the task.
|
||||
- Build audit links with the presentation filter-link builder, filtering by
|
||||
record, table and channel, including deletes; widen audit link storage to TEXT.
|
||||
Document the host audit migration and the QBit column widening.
|
||||
- Verify the admin denial matrix through the real invoker and Next UI, startup
|
||||
failures, and confirmed-write human/channel attribution with decoded audit links.
|
||||
## Unreleased - Email
|
||||
|
||||
### Changed
|
||||
|
||||
- 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)
|
||||
|
||||
- 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
|
||||
|
||||
- Add admin Scheduled tasks, owner-locked run history, Quartz cron/timezones,
|
||||
and a 60-second QQQ scheduled process using the host's scheduler context.
|
||||
- Log in through the loopback UI API with a separately allow-listed credential
|
||||
environment reference; verify the designated user before starting a normal turn.
|
||||
- Add Review/Result Approve write mode, Revoke write mode and Run now actions.
|
||||
Approval binds the task definition; scheduled writes always await confirmation.
|
||||
- Attribute scheduled UI API requests and tool logs, apply per-run STOP caps,
|
||||
and send optional Alerts notifications to the designated user. HIPAA summaries
|
||||
contain only `see conversation`.
|
||||
- Add unique schedule-slot claims, fixed-clock fixture integration tests and
|
||||
additive Postgres DDL. No API routes or provider modules change.
|
||||
|
||||
## Unreleased - P-f
|
||||
|
||||
- Add OpenAI-compatible embeddings (including Azure), explicit CHAT/EMBEDDING
|
||||
model kinds, editable discovery hints, and budgeted EMBED usage.
|
||||
- Add admin Knowledge sources with streamed Review/Result Index now and Clear
|
||||
index actions. Indexing uses the clicking admin's UI API session, chunks text,
|
||||
skips unchanged hashes and reconciles disappeared records.
|
||||
- Add content-free in-memory and optional JDBC pgvector stores, per-instance
|
||||
schemas, HNSW cosine search and transactional record replacement.
|
||||
- Add permission-filtered `search_knowledge`, fresh UI API reads, existing
|
||||
masking/redaction, PHI embedding guards, classification propagation and citations.
|
||||
- Add embedding HTTP contracts, real UI API RAG security/golden tests and
|
||||
always-on JDBC contracts; the pgvector container test skips without Docker.
|
||||
## Unreleased - Answer presentation
|
||||
|
||||
- Add `present_results`: server-formatted metrics, tables, computed percentages,
|
||||
charts and validated in-app filter/record/action links.
|
||||
- Persist and stream presentation parts with a multi-line GFM fallback and
|
||||
record-data retention; add a static default presentation skill.
|
||||
- Add the owner's 81 JPEG / 20 PNG golden example, shared live eval coverage,
|
||||
permission/formatting/input tests and strict-schema budget coverage.
|
||||
|
||||
## Unreleased - P-e
|
||||
|
||||
- Add per-profile heuristic/classifier model routing, with budgeted ROUTE usage.
|
||||
- Add bounded cross-provider fallback chains, PHI-safe selection, neutral history
|
||||
rebuilding, and a 60-second connection circuit breaker with cached local probes.
|
||||
- Show provider health, fallback totals and turn routing; include additive schema
|
||||
migration and admin record-screen controls.
|
||||
|
||||
|
||||
## Unreleased - P-d
|
||||
|
||||
- Require QQQ RC.5 and send budget alerts directly through core QAlerts, with admin-visible alert type metadata and core sink tests.
|
||||
|
||||
- Admin budgets, expiring overrides, deduplicated warning/downgrade/stop events.
|
||||
- Per-dispatch limits preserving PHI routing and existing caps.
|
||||
- Home budget cards, 30-day cost/token bars and provider breakdowns.
|
||||
|
||||
|
||||
## Unreleased — P-c
|
||||
|
||||
- Add configurable table/field classification, persistent turn/conversation maxima,
|
||||
and PHI routing checks before every model call, with a safe blocked-turn reason.
|
||||
- Add HIPAA mode, eligible-provider enforcement, AES-256-GCM conversation storage,
|
||||
provider diagnostic redaction, and independent PHI conversation retention.
|
||||
- Add owner-locked, admin read-only data routing audits and HIPAA status on the home.
|
||||
- Classification levels are core's `DataClassification` (the qbit stand-in is removed); the default source reads core AI metadata merged with configuration.
|
||||
|
||||
All notable changes to this project are documented here. The format follows Keep a Changelog, and versions follow semantic versioning.
|
||||
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
- Add optional AWS Bedrock (Claude) and Google Vertex (Claude/Gemini) adapters using server credentials, static model discovery and allow-listed endpoint overrides. AWS IRSA/web-identity hosts supply STS themselves; no additional dependency is bundled.
|
||||
- Add the provider `cloudProject` column, cloud settings validation, injected Anthropic clients and per-request OpenAI bearer-token suppliers. Vertex Gemini prefixes model ids and omits strict tools.
|
||||
- Add localhost cloud contract tests and golden tool turns, including AWS binary event-stream framing, SigV4, token refresh and cloud HTTP error mapping.
|
||||
|
||||
- Add named, server-only CA bundles for HTTPS providers, additive JVM trust and sanitized TLS diagnostics; tolerate repeated/missing streamed tool identities and invalid arguments, with a golden tool round-trip test.
|
||||
- Add the provider/model registry, per-model capabilities and prices, keyless local providers, discovery and health, and provider/model connection tests.
|
||||
- Add the optional JDK HTTP OpenAI-compatible adapter for OpenAI, Azure, OpenRouter, Ollama, LM Studio, vLLM and llama.cpp, with strict-schema safeguards and localhost contract tests.
|
||||
|
||||
Phase 0/1 of the QQQ Assistant: read-only Q&A and navigation. Requires QQQ 4.1 with:
|
||||
- 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.
|
||||
- The OpenAPI fragment is committed (`openapi/qqq-v1/assistant.yaml`) and contract-tested.
|
||||
|
||||
### Changed (0.1.0-RC.6): tools go through the UI API (owner decision 2026-10-13)
|
||||
- **Every data tool runs a spec'd `/qqq/v1` operation with the user's own session** (`UiApiGateway`, loopback HTTP with the `sessionUUID` cookie): the same authentication, permissions, record locks, validation, audit and errors as the UI. A user without a permission gets the identical denial through the assistant. `navigate` and `search_help` (no records) stay on the core kernel. `LoopbackUiApiGateway` is a stand-in for core's `UiApiInvoker` (QRun-IO/qqq#1128).
|
||||
- Tools: `list_tables`, `describe_table`, `query_records`, `count_records`, `get_record`, `search_records`, `render_widget`, `get_record_history`, and new **`create_record`, `update_record`, `delete_record`**.
|
||||
|
||||
### Added (0.1.0-RC.6)
|
||||
- **Profiles** (`assistant_profile`, nav "Profiles"): display name, icon, personality (≤ 2,000 chars, appended to the per-user system block), greeting, starter prompts; one default, optional app / permission scope. `/assistant/config` returns `profile`.
|
||||
- **Skills** (`assistant_skill`, nav "Skills", linked through `assistant_profile_skill`, a child list on profiles): admin-written Markdown instructions read only when relevant (keywords / apps), ≤ 6,000 chars each and 12,000 per question. Instructions only: they never grant tools, endpoints or access (ArchUnit-enforced).
|
||||
- **Explain access**: `GET /qqq/v1/assistant/access?table=&prompt=` and the `assistantAccess` home widget: per table and tool, allowed or the reason.
|
||||
- **Recent denials** card on the Assistant home.
|
||||
- `AssistantStore` refuses any table that is not `assistant_*`; ArchUnit forbids the tool / UI API / access packages from using QQQ actions, the AI kernel or the profile and skill classes.
|
||||
- **Data access** (`assistant_data_access`, nav table "Data access"): NONE / READ / READ_WRITE per table or process. Default: NONE for sensitive tables (AI exposure NONE; cannot be raised), READ for other tables, NONE for processes. It only narrows the UI API.
|
||||
- **Pending actions** (`assistant_pending_action`): with confirmation on (the default), a write only proposes; `POST /qqq/v1/assistant/actions/{id}/confirm` runs it through the UI API with the confirming user's session, once; `.../reject`; `GET .../actions/{id}`. Child list on conversations.
|
||||
- **Policy columns** on `assistant_policy`: `writesRequireConfirmation`, `allowedTools`, `maxRowsPerCall`, `redactPii`.
|
||||
- **Tool-call log columns** on `assistant_tool_call`: `uiApiOperation`, `httpStatus`, `auditHref`, `pendingActionId`; tool inputs are stored with secrets redacted.
|
||||
- Config: `uiApiBaseUrl`, `withUiApiGateway`, `pendingActionTtlSeconds`. `/assistant/config` reports `features.writes` (some table is READ_WRITE) and `features.pendingActions`.
|
||||
|
||||
### Fixed (0.1.0-RC.5)
|
||||
- **Every turn failed with "The AI service could not answer this request."** Anthropic refused the request with 400 `invalid_request_error` "The compiled grammar is too large ... reduce the number of strict tools": all ten read tools were sent with `strict: true`, and the API compiles every strict schema of a request into one grammar. Tools are now **not strict by default**. The kernel already validates every tool input and answers `INVALID_INPUT`. A host can opt individual tools in with `withStrictToolNames(...)`.
|
||||
- If a provider still refuses the strict schemas, the turn **retries once without strict tools** and logs a warning (`ModelProviderException.isStrictSchemaRejected`).
|
||||
- Provider errors now carry the **API's own error type, a sanitized message** (one line, capped, key-like tokens redacted) and the **request id**:
|
||||
- they are logged at WARN for 4xx;
|
||||
- they are recorded in the `assistant_usage` row's request id;
|
||||
- they are shown to `assistant.admin` holders in the panel and in Test connection.
|
||||
Before this, a 400 was logged as "INVALID_REQUEST httpStatus=400" with no reason.
|
||||
- A turn that hits a JVM `Error` (for example a linkage problem) now ends as `ERROR` with a log line, instead of being swallowed by the executor and ending as an empty `COMPLETE` answer.
|
||||
- `AnthropicRequestContractTest` checks the exact request against the Messages API constraints and a conservative strict-tool budget.
|
||||
|
||||
### Added (0.1.0-RC.4)
|
||||
- **Provider settings** (`assistant_provider`): type (registered `AssistantProviderType`s; `AnthropicProviderType` in the Anthropic module), model, enabled, default, base URL override, monthly cost cap, max output tokens per request, max concurrent requests, and the last connection test. The default enabled row is the active provider; with no rows the provider registered in code is used, so existing hosts keep working.
|
||||
- **Key handling:** the key is never stored. A row names the environment variable that holds it (`apiKeyEnvName`, allow-listed); `apiKeyStatus` is computed on read (`Set • …abcd`) and dropped on save. A pasted key, an unrelated variable or an unlisted base URL is refused. New config: `withProviderType`, `allowedApiKeyEnvNames`, `allowedBaseUrls`, `withSecretResolver`, `providerTestTimeoutSeconds`.
|
||||
- **Test connection**, a record action on providers (standard streamed ETL Review and Result lines): one short live call, result saved on the row and recorded as a `TEST` usage row.
|
||||
- **Per-call usage** (`assistant_usage`): one row per model call (turn steps and tests) with provider, model, status, error kind, HTTP status, latency, tokens and cost. Child list on conversations and providers.
|
||||
- **Model prices** (`assistant_model_price`), editable, used for every call's cost; the reference DDL seeds the Anthropic list prices of 2026-10. The built-in `pricing` defaults now cover every current Claude model.
|
||||
- **The app home** (`assistantHome` widget): health, providers, usage by day, costs against caps, by user, top tools and recent failures, from real rows with empty states.
|
||||
- `ModelResponse.model`: the model that answered (set by the Anthropic adapter).
|
||||
|
||||
### Changed (0.1.0-RC.4)
|
||||
- The app is labelled **Assistant** (name `assistantAdmin` unchanged). Its nav lists the home and the main tables only (Providers, Conversations, Model prices, Policy); other tables are child lists. `assistantRetention` is a table action on conversations instead of a nav entry.
|
||||
- Every `assistant_*` table has AI exposure `NONE`, so the assistant never reads its own settings or audit tables (before, only the `assistant.admin` permission kept them out).
|
||||
- `AssistantMetaDataBuilder.buildAdminApp(tables)` no longer takes the retention process.
|
||||
|
||||
### Changed (0.1.0-RC.3)
|
||||
- **Snake_case names** (QRun-IO naming rule, 2026-10-12). Metadata only; the physical tables were already `assistant_*`:
|
||||
- tables `assistantConversation` ... `assistantUsageDaily` are now `assistant_conversation`, `assistant_message`, `assistant_turn`, `assistant_turn_event`, `assistant_tool_call`, `assistant_feedback`, `assistant_policy` and `assistant_usage_daily`; the conversation possible-value source follows the table name
|
||||
- permissions are unchanged: `assistant.hasAccess` keeps QQQ's standard `hasAccess` suffix, and `assistant.admin` stays as is
|
||||
- security key type `assistantUserId` is now `assistant_user_id`, with all-access key `assistant_user_id_all_access`
|
||||
- the old names remain as `@Deprecated` `LEGACY_*` constants until 0.2.0. See [UPGRADE.md](UPGRADE.md) for the stored references to update (permissions, saved views and reports, `audit_table` names).
|
||||
|
||||
### Added
|
||||
- **`qbit-assistant`**
|
||||
- `AssistantQBitProducer` and `AssistantQBitConfig`:
|
||||
- enable flag, plus the `QQQ_ASSISTANT_ENABLED` environment variable
|
||||
- provider name, model routing (Sonnet 5.5 at effort medium)
|
||||
- limits: 25 tool calls, 4 parallel, 50 rows, 500 characters per cell, 300 s
|
||||
- budgets, rate limits
|
||||
- retention of 90 / 30 / 365 days
|
||||
- owner key type and pricing.
|
||||
- Standard, visible tables: `assistant_conversation`, `assistant_message`, `assistant_turn`, `assistant_turn_event`, `assistant_tool_call`, `assistant_feedback`, `assistant_policy` and `assistant_usage_daily`. User-owned tables are locked to their owner (`assistant_user_id`), and admins read everything through `assistant_user_id_all_access`.
|
||||
- The Assistant Admin app, gated by `assistant.admin`.
|
||||
- The `assistantRetention` process.
|
||||
- `/qqq/v1/assistant` routes: config probe, conversations CRUD, turns as Server-Sent Events, replay through `/events?after=`, cancel, and feedback. The panel requires `assistant.hasAccess`. The kill switch and budgets are checked when a turn starts, and the kill switch again before every step.
|
||||
- `TurnEngine`:
|
||||
- streams the model's answer
|
||||
- runs read and navigation tools through `AiToolKernel`, with parallel calls bounded
|
||||
- sends results back in one message, with errors marked `is_error`
|
||||
- enforces the limits on tool calls, time, budget, cancel and kill switch
|
||||
- persists messages, turns, tool calls, events and usage.
|
||||
- `ContextBuilder` in cache-prefix order: tools, then the static system block (1 h), then the user digest (5 m), then history, then the new turn.
|
||||
- The provider SPI (`AssistantModelProvider`, `ModelRequest`, `StreamHandle`, `ModelCapabilities`) and `ScriptedModelProvider`.
|
||||
- The record Developer-screen "Assistant" panel, built on the core developer-panel contract, with the "Ask the assistant about this record" deep link.
|
||||
- **`qbit-assistant-provider-anthropic`**: `AnthropicModelProvider` on anthropic-java 2.71.0. It provides:
|
||||
- strict tools and `tool_choice: auto`
|
||||
- adaptive thinking with display omitted, and an explicit effort
|
||||
- cache breakpoints (1 h / 5 m)
|
||||
- refusal and max_tokens handling
|
||||
- cancel
|
||||
- an API key taken only from the environment or a host secret.
|
||||
- **Tests**:
|
||||
- ArchUnit rules
|
||||
- golden evals v1: examples D and the read half of A, plus count, navigate and search
|
||||
- the permission red team (a build gate)
|
||||
- Anthropic tests against recorded SSE streams
|
||||
- live evals against Claude (opt-in).
|
||||
|
||||
### Security review follow-ups
|
||||
- Kernel settings come from config: `allowedWidgetNames` (the kernel denies any widget not listed), `hiddenWidgetNames` and `customRedactionPatterns`.
|
||||
- **Orphaned turns.** A turn left RUNNING by a dead replica is finished as ERROR the first time it is seen, by replay, cancel, delete or a new turn. Its message gets status ERROR, and synthetic `error` (INTERNAL) and `message.end` events are persisted. Replay loops are capped at `maxTurnSeconds` + 60 s.
|
||||
- **Retention.** The tool-result cutoff now also deletes stream events, and removes render payloads and record references from stored messages.
|
||||
- **Usage.** `addUsage` uses per-key locks, a unique key on (scope, userId, usageDate) and an insert-race retry. Usage of cancelled, timed-out and failed model calls is counted when the provider reports it.
|
||||
- **Kill switch.** It fails closed when the policy can't be read. A watcher cancels running turns promptly when it flips.
|
||||
- **Limits.** Page-context scalars are capped (strings up to 512 characters, at most 100 selected ids). Conversation lists are limited to 200, running turns to 50, and replay is paged at 500 events.
|
||||
- **Ordering.** Terminal events (`usage`, `error`, `message.end`) are persisted before the turn's status becomes final.
|
||||
- **Supplemental meta-data.** The `assistant` supplemental instance meta-data is registered when the QBit is enabled.
|
||||
|
||||
## [0.1.0-RC.2] - 2026-10-12
|
||||
|
||||
**Do not use.** 0.1.0-RC.2 on Maven Central is an accidental republish of the RC.1 code by a tag build; do not use it. The next real candidate is 0.1.0-RC.3. (Cause: QRun-IO/qqq-orb#20; the `publish_release` workflow now runs on GA tags only.)
|
||||
@@ -1,111 +1,17 @@
|
||||
# CLAUDE.md
|
||||
# qbit-template-extension
|
||||
|
||||
This file provides guidance to Claude Code when working with code in this repository.
|
||||
GitHub template repository for scaffolding QQQ "Extension QBits" (infrastructure-level plugins: table customizers, auth, audit, action wrappers). Not a shipping library — consumers click "Use this template" and rename the `com.kingsrook.qbits.example` package.
|
||||
|
||||
## Project Overview
|
||||
## QQQ 4.0 baseline — 2026-09-24
|
||||
|
||||
**QBit Assistant** adds an in-app AI assistant to any QQQ application. It owns the chat backend:
|
||||
- conversations and audit tables
|
||||
- the turn engine (the agent loop)
|
||||
- the `/qqq/v1/assistant` routes, with SSE
|
||||
- budgets, rate limits and the kill switch
|
||||
- the model-provider SPI.
|
||||
The historical review below predates the 4.0 migration. This template targets Java 21 and the public QQQ 4.0.0 release from Maven Central. The corrected APIs, license references and actual template commands are in README.md and docs/00-getting-started.md. Preserve the documented multi-instance and scaffold limitations.
|
||||
|
||||
The panel UI lives in `qqq-frontend-next`. Data access goes only through the **QQQ UI API** (`/qqq/v1`, spec'd operations, the user's own session) via `uiapi/UiApiGateway` and `tools/UiApiToolbox`; only `navigate` and `search_help` (no records) use the core AI tool kernel.
|
||||
## Knowledge base
|
||||
|
||||
- Design spec: `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md`. The owner decisions D1 to D6 at its top are binding.
|
||||
- Writes (create/update/delete) are offered only for tables an admin set to READ_WRITE in `assistant_data_access`, and by default wait for the user's confirmation (pending action card).
|
||||
Reviewed dossiers for this repo and the wider QQQ platform live in the second-brain vault:
|
||||
|
||||
## Build Commands
|
||||
- Hub / entry point: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
|
||||
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-template-extension.md`
|
||||
- QBit mechanics refresher: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/architecture/metadata-model.md`
|
||||
|
||||
```bash
|
||||
./mvnw verify # all modules, tests, JaCoCo gate (70% instructions / 90% classes)
|
||||
./mvnw verify -Dqqq.snapshot.version=4.1.0-ASSISTDEV-SNAPSHOT # against a locally installed QQQ build with the kernel (+ developer panels)
|
||||
./mvnw test -pl qbit-assistant -Dtest=RedTeamTest
|
||||
ANTHROPIC_API_KEY=... ./mvnw verify -Dassistant.live=true -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true -pl qbit-assistant-provider-anthropic -am
|
||||
```
|
||||
|
||||
Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed default QQQ version (`qqq.snapshot.version` in the root POM) must stay resolvable from Maven Central. Use a local kernel build only through `-D`.
|
||||
|
||||
## Modules
|
||||
|
||||
- `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`.
|
||||
- `metadata/`: tables, owner key type, the Assistant app (home + main tables in the nav), the retention process, the settings customizer (key-reference checks, masked key status), and the `assistant.admin` custom permission checker.
|
||||
- `home/`: the app home widget (`AssistantHomeData` loads, `AssistantHomeView` renders flow-layout HTML).
|
||||
- `model/`: entities for the `assistant_*` tables.
|
||||
- `persistence/AssistantStore`: the **only** code that calls QQQ data actions, and only on the QBit's own tables, under the user's session.
|
||||
- `engine/`: `TurnEngine` (the loop), `ContextBuilder` (cache-prefix order), `SystemPrompt`, `PolicyService` (policy row, budgets), `RateLimiter`, the `TurnEvent` / `TurnEventLog` stream, and `MessageParts`.
|
||||
- `api/`: `AssistantApi` (transport-free) and `AssistantRouteProvider` (Javalin, SSE).
|
||||
- `provider/`: runtime-backed `RegistryModelProvider`, plus `fake/ScriptedModelProvider` for tests, evals and demos. The SPI lives in `qbit-assistant-provider-api`.
|
||||
- `developer/`: the record Developer-screen panel, on the core developer-panel contract.
|
||||
- `processes/`: `AssistantRetentionStep` and `TestConnectionProcess` (record action on `assistant_provider`).
|
||||
- `engine/ProviderService` (active provider from `assistant_provider` rows, key references, prices), `UsageRecorder` (one `assistant_usage` row per model call), `ConnectionTest`.
|
||||
- `qbit-assistant-provider-anthropic`: `AnthropicModelProvider` and `AnthropicMapping` (anthropic-java streaming).
|
||||
- `qbit-assistant-provider-openai`: OpenAI-compatible Chat Completions, model discovery and health using JDK HTTP and Jackson.
|
||||
- `qbit-assistant-provider-bedrock`: Claude through the Anthropic Bedrock backend, AWS default credentials and SigV4; IRSA/web-identity hosts supply STS.
|
||||
- `qbit-assistant-provider-vertex`: Claude through the Anthropic Vertex backend and Gemini through OpenAI-compatible Chat Completions, with refreshed Google ADC tokens.
|
||||
|
||||
- `qbit-assistant-rag-pgvector`: optional host-supplied JDBC vector store, isolated by instance schema; depends on runtime RAG interfaces (`VectorStore`, `ChunkVector`), with no QQQ RDBMS internals.
|
||||
|
||||
## Invariants (do not break)
|
||||
|
||||
- **Sessions:**
|
||||
- Scheduled runs log in as the designated user through `POST /qqq/v1/manageSession`; resolve only its returned UUID, verify the user id, and never construct or elevate a session. The QQQ-supplied scheduler context touches only assistant bookkeeping.
|
||||
- Never use `QSystemUserSession` or construct a `QSession` in main code (ArchUnit).
|
||||
- Every route uses `ExecutorSessionUtils.setupSession` (the user's own session).
|
||||
- `AssistantSecurity` only ever adds the user's OWN id key, or the all-access key for `assistant.admin` holders.
|
||||
- **Channel wall:** every `assistant_*` object (table, process, report and widget) is denied to the `assistant` channel; startup fails otherwise. Requests carry `assistant/<turnId>` or `assistant/scheduled/<taskId>`; administration uses `next-ui`.
|
||||
- **Data access:**
|
||||
- The vector index stores no record content: only source/table/key/field/chunk, content hash and embedding. Retrieval re-reads every hit through the UI API as the asking user, then applies the existing get_record exposure/masking path.
|
||||
- Only `persistence/` calls QueryAction, InsertAction and the other data actions (ArchUnit).
|
||||
- Data tools run only through `UiApiToolbox` → `UiApiGateway` (spec'd `/qqq/v1` operations with the user's `sessionUUID`); never core data actions for user data. Only `navigate` and `search_help` use `AiToolKernel.execute`.
|
||||
- Data access (`DataAccessService`) and the policy only ever narrow what the UI API allows; a table whose AI exposure is NONE stays NONE.
|
||||
- A write runs only after the owner confirms its pending action (unless the policy turns confirmation off), with the confirming request's session, once.
|
||||
- Panel API queries also filter `userId = me`, so an admin's all-access key never mixes other users' conversations into the panel.
|
||||
- **Caching:** the request prefix stays byte-stable. The order is tools (kernel-sorted), then system block 1 (rules + primer + app digest, LONG/1h), then system block 2 (user digest + date, SHORT/5m), then messages. Exactly one SHORT hint goes on the newest message. Don't put timestamps or ids in the system prompt.
|
||||
- **Tool results:**
|
||||
- Parallel tool results go back in ONE user message, in call order.
|
||||
- Errors (DENIED, NOT_FOUND, INVALID_INPUT, ERROR) are `is_error` results.
|
||||
- Unknown tools and over-limit calls also get error results.
|
||||
- **Keys:** never stored in a table. `assistant_provider.apiKeyEnvName` holds only an allowed environment variable NAME; `apiKeyStatus` is computed on read (last 4 at most) and stripped on save.
|
||||
- Scheduled task `credentialEnvName` is a separately allow-listed environment variable NAME. Tokens never enter rows, model context or logs. `credentialStatus` is computed and stripped on save.
|
||||
- Scheduled write approval is invalidated by changes to prompt, run-as user, credential reference, profile or write request. Scheduled writes always require the designated user's panel confirmation.
|
||||
- **Logging:** never log message content, prompts, tool results or keys. Structured end-of-turn log only.
|
||||
- **Thinking:** thinking is never streamed or stored. The Anthropic adapter keeps the raw assistant message (with signatures) only in memory, as `providerState`, for the tool loop.
|
||||
- **Links:** only `/app` or `/app/...` hrefs are emitted (`TurnEngine.isAppHref`).
|
||||
- **PHI routing:** PHI only to `hipaaEligible` providers; checked before every model call. Conversation classification never decreases.
|
||||
- **HIPAA storage:** Conversation storage encrypted in HIPAA mode, only in `AssistantStore`; encryption keys are environment references and never logged or stored in tables.
|
||||
- **Fallback:** fallback never crosses the PHI rule; never local-to-cloud by default. No retry after partial output from that call; provider switches rebuild neutral history without providerState.
|
||||
- **Budgets:** budgets are checked before every model call; downgrade never crosses the PHI rule.
|
||||
- **Kill switch:** read fresh before every model call and every tool step.
|
||||
|
||||
## Tests
|
||||
|
||||
- `testing/` holds the fixture app on the memory backend:
|
||||
- blog, page, media and analytics tables, plus audits
|
||||
- sentinel values in hidden and password fields
|
||||
- a prompt-injection record
|
||||
- MOCK auth where the user-id prefix picks the permissions.
|
||||
- `testing/` also holds `TurnRunner`, `GoldenTasks` and `EvalSupport`. They are published as a test-jar for the provider's live evals.
|
||||
- Tags:
|
||||
- `golden` (`GoldenEvalTest`) and `redteam` (`RedTeamTest`) run in every build. Any failure fails the build.
|
||||
- `live` is excluded unless you pass `-Dtest.excludedGroups= -Dgroups=live` and set the key.
|
||||
|
||||
## Code Style
|
||||
|
||||
QQQ style:
|
||||
- 3-space indent, braces on the next line, `return (x);`
|
||||
- flower-box javadoc on every class and method
|
||||
- fluent `withX` setters
|
||||
- wrapper types.
|
||||
|
||||
## Release
|
||||
|
||||
- CircleCI qqq-orb 0.6.8.
|
||||
- `develop` publishes SNAPSHOTs.
|
||||
- `release/*` publishes RCs; the orb bumps `<revision>` in the root POM.
|
||||
- `v*` tags publish releases.
|
||||
- All seven modules and the parent POM publish. The flatten plugin resolves `${revision}`.
|
||||
Reviewed at commit `e7d4b4d672cc` (branch `main`, 2026-05-21). Known drift as of that review: no `qbit-build-parent` parent pom (org convention), qqq pinned at 0.35.0 on main (0.40.0-SNAPSHOT on develop), README still claims AGPL-3.0 while LICENSE/NOTICE are Apache-2.0, and README references `scripts/customize_template.py` / `actions/ExampleActionCustomizer.java` which do not exist. See the dossier's "Maturity & risks" and "v4.0 impact" sections before modifying the scaffold.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Code of Conduct
|
||||
|
||||
See Project level [Code of Conduct](https://github.com/Kingsrook/qqq/blob/develop/CODE_OF_CONDUCT.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Code Style Guide
|
||||
|
||||
* See Project [Code Style Guide](https://github.com/Kingsrook/qqq/blob/develop/CODE_STYLE.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Contributing to QBit Quick Search
|
||||
|
||||
* See Project [Contributing](https://github.com/Kingsrook/qqq/blob/develop/CONTRIBUTING.md)
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2024 Kingsrook, LLC
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
QBit Assistant
|
||||
Copyright 2026 Kingsrook, LLC
|
||||
qBit Template - Extension
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
This product includes software developed at QRunIO, LLC (https://qrun.io/).
|
||||
|
||||
|
||||
@@ -1,722 +1,42 @@
|
||||
# QBit Assistant
|
||||
# QBit Template: Extension
|
||||
|
||||
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.
|
||||
Template repository for creating QQQ Extension QBits - infrastructure extensions, customizers, and action handlers.
|
||||
|
||||
Design spec: `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md`.
|
||||
## What is an Extension QBit?
|
||||
|
||||
## Modules
|
||||
Extension QBits extend or enhance how QQQ operates at the infrastructure/framework level. They provide:
|
||||
- New backend implementations
|
||||
- Authentication mechanisms
|
||||
- Audit capabilities
|
||||
- Table customizers
|
||||
- Action handlers
|
||||
|
||||
| Artifact | What it is |
|
||||
|---|---|
|
||||
| `com.kingsrook.qbits:qbit-assistant-provider-api` | Provider SPI, connection settings, messages, usage and pricing types; only Jackson annotations at runtime. |
|
||||
| `com.kingsrook.qbits:qbit-assistant` | The QBit: conversation tables, turn engine, `/qqq/v1/assistant` routes (JSON + Server-Sent Events), budgets, kill switch, the provider registry, and a scripted provider for tests and demos. |
|
||||
| `com.kingsrook.qbits:qbit-assistant-provider-anthropic` | The Claude provider (`com.anthropic:anthropic-java`). Add it to use Claude. The core QBit has no LLM SDK dependency. |
|
||||
| `com.kingsrook.qbits:qbit-assistant-provider-openai` | OpenAI-compatible chat and embedding providers. |
|
||||
| `com.kingsrook.qbits:qbit-assistant-provider-bedrock` | Claude through AWS Bedrock. |
|
||||
| `com.kingsrook.qbits:qbit-assistant-provider-vertex` | Claude and Gemini through Google Vertex AI. |
|
||||
| `com.kingsrook.qbits:qbit-assistant-rag-pgvector` | JDBC vector store; retains the runtime dependency for `VectorStore` and `ChunkVector`. |
|
||||
## Quick Start
|
||||
|
||||
### Implementing a provider outside qbit-assistant
|
||||
Requires **Java 21**, **Maven 3.8+**, and QQQ **4.0.0**. Create your repository, customize its Maven coordinates and Java package/classes, implement the table customizer, then run `mvn clean verify`. See [Getting Started](docs/00-getting-started.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. 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`.
|
||||
## Structure
|
||||
|
||||
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.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
|
||||
deny patterns remain in place.
|
||||
|
||||
Enable the audit client field in the host's core audit provider **after adding
|
||||
the backing column**:
|
||||
|
||||
```java
|
||||
new AuditsMetaDataProvider().withClientChannelField(true)
|
||||
.defineAll(qInstance, backendName, null);
|
||||
```
|
||||
src/main/java/com/kingsrook/qbits/example/
|
||||
├── ExampleExtensionQBitConfig.java # Configuration
|
||||
├── ExampleExtensionQBitProducer.java # Entry point
|
||||
└── customizers/
|
||||
└── ExampleTableCustomizer.java # Table customization
|
||||
```
|
||||
|
||||
Add nullable `audit.client_channel VARCHAR(100)` using the host's migration and
|
||||
column-naming conventions. For kof22 this requires a feature-named changeset
|
||||
under `changesets/features/`; this QBit does not migrate the host's audit table.
|
||||
The actor remains the signed-in human; `clientChannel` distinguishes
|
||||
`assistant/<turnId>`, `assistant/scheduled/<taskId>` and `next-ui`.
|
||||
## Key Characteristics
|
||||
|
||||
Apply the X1/Y1 DDL in [UPGRADE.md](UPGRADE.md) to widen the existing
|
||||
`assistant_tool_call.audit_href` column to TEXT for encoded filters.
|
||||
Successful record writes store `assistant_tool_call.auditHref` as an `/app/audit`
|
||||
filter link over `recordId`, `auditTable.name` and `clientChannel`. The human
|
||||
opens this link with their UI permissions; the assistant cannot read audits by
|
||||
default. Without the audit channel field, the QBit leaves `auditHref` empty.
|
||||
Confirmed scheduled actions use their original turn id when the human approves.
|
||||
- **No QAppSection** - Pure infrastructure
|
||||
- **No user-facing tables** (or minimal operational)
|
||||
- **Wraps existing tables/processes** with new behavior
|
||||
- **Validates dependencies** on other QQQ components
|
||||
|
||||
## Documentation
|
||||
|
||||
## RAG setup
|
||||
|
||||
Knowledge retrieval is optional. The vector index stores **no record content**:
|
||||
only source/table/key/field/chunk identifiers, SHA-256 hashes and embeddings.
|
||||
Indexing reads through the UI API as the clicking administrator. Retrieval
|
||||
re-reads each hit through that same API as the asking user, silently filters
|
||||
403/404 results, applies `get_record` field exposure/masking/redaction and returns
|
||||
fresh snippets with `/app/...` citations. `search_knowledge` follows `allowedTools`
|
||||
and is offered only with an enabled source the user may read in the UI.
|
||||
|
||||
1. Register the OpenAI-compatible provider module and configure the embedding
|
||||
endpoint through the existing provider settings/URL allowlist. Discover or add
|
||||
a model, set **Model kind = EMBEDDING** and its dimensions. Known discovery
|
||||
defaults are `bge-m3` (1024) and `nomic-embed-text` (768); these remain editable.
|
||||
CHAT models cannot be selected for embedding, and EMBEDDING models cannot answer
|
||||
chat turns. Each embedding batch contains at most 64 inputs.
|
||||
2. Use the default `InMemoryVectorStore` for tests/small hosts, or add
|
||||
`com.kingsrook.qbits:qbit-assistant-rag-pgvector` at the QBit's version for
|
||||
persistence. The host supplies its JDBC driver and DataSource; this module
|
||||
does not depend on QQQ RDBMS internals.
|
||||
3. For pgvector, have the DBA install the extension once in the chosen database:
|
||||
`CREATE EXTENSION IF NOT EXISTS vector`. Give the application a distinct schema
|
||||
per QQQ instance. Configure the store before producing the QBit:
|
||||
|
||||
```java
|
||||
PgVectorStore vectors = new PgVectorStore(dataSource, 1024)
|
||||
.withSchema("assistant_rag_my_instance");
|
||||
vectors.ensureSchema();
|
||||
config.withVectorStore(vectors);
|
||||
config.withUiApiBaseUrl("http://127.0.0.1:8000");
|
||||
```
|
||||
|
||||
The UI API URL is this server's own listening address. Configure it explicitly
|
||||
so Index now works before any chat request has taught the runtime its local
|
||||
port. The store creates a content-free `assistant_rag_chunk` table and an HNSW
|
||||
cosine index. Its fixed dimension must match every source using the store;
|
||||
different models are searched separately. No credentials go in source rows.
|
||||
See the [pgvector installation/index documentation](https://github.com/pgvector/pgvector)
|
||||
and the reference DDL in [schema-postgres.sql](docs/schema-postgres.sql).
|
||||
4. Open **Assistant → Knowledge sources** as an `assistant.admin`. Add the table,
|
||||
comma-separated text fields, optional title field and QQQ query-filter JSON,
|
||||
embedding model, and chunk size/overlap (defaults 1500/200). Tables with AI data
|
||||
access NONE, `assistant_*` tables, and fields with NONE/METADATA_ONLY exposure
|
||||
are refused. MASKED fields are masked before embedding and in snippets.
|
||||
5. Select **Index now**. Review makes no embedding calls; Result reports records
|
||||
seen, chunks embedded, unchanged, removed and errors. Re-running unchanged
|
||||
content sends no embeddings. A complete successful scan removes disappeared
|
||||
records; failed scans preserve unvisited entries. **Clear index** removes only
|
||||
that source's vectors. Changes after indexing are read fresh, but semantic
|
||||
ranking becomes current only after re-indexing.
|
||||
|
||||
Embedding usage has purpose `EMBED`, counts toward budgets, and observes the kill
|
||||
switch and provider concurrency limits. PHI sources and queries in PHI
|
||||
conversations require an eligible embedding provider; the eligibility flag is an
|
||||
operator assertion, as for chat. Automatic budget downgrades cannot change an
|
||||
embedding model's vector space, so an incompatible downgrade stops the call.
|
||||
|
||||
Scheduling is deferred to P-g. Its designated-user runner can call
|
||||
`RagService.index` within that user's authenticated context; P-f creates no system
|
||||
session, stored admin credentials, scheduler or background content crawler.
|
||||
Data tools call the QQQ UI API through `UiApiToolbox` and `UiApiGateway` with the signed-in user's session. The UI enforces permissions and record security; the assistant further narrows access and caps and redacts returned values. Data tool results reach the model wrapped as untrusted data. Only record-free navigation and help use the core AI tool kernel.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21.
|
||||
- **QQQ 4.1 with the AI tool kernel (QRun-IO/qqq#1078) and the developer-panel contract (QRun-IO/qqq#1076).** The first QQQ 4.1 RC that contains both is the minimum. `qqq-middleware-javalin` is needed for the routes. Until that RC exists, build against a local QQQ build. See [Build](#build).
|
||||
- A backend for the assistant's tables, e.g. the app's RDBMS backend.
|
||||
- For Claude: an Anthropic API key held in the application's secret store.
|
||||
|
||||
## Install
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-assistant</artifactId>
|
||||
<version>${qbit-assistant.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-assistant-provider-anthropic</artifactId>
|
||||
<version>${qbit-assistant.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```java
|
||||
AssistantQBitConfig config = new AssistantQBitConfig()
|
||||
.withBackendName("rdbms") // where the assistant_* tables live
|
||||
.withTableMetaDataCustomizer(snakeCaseCustomizer) // optional: backend names for your schema
|
||||
.withModelProvider(AnthropicModelProvider.fromEnvironment()) // fallback while no provider row exists
|
||||
.withProviderType(new AnthropicProviderType()); // offered on the Providers settings screen
|
||||
|
||||
new AssistantQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
```
|
||||
|
||||
`produce` does the following:
|
||||
- Registers the QBit.
|
||||
- Adds the tables, the `assistant_user_id` security key type, the `assistantRetention` and **Test connection** processes, and the **Assistant** app with its home.
|
||||
- Builds the runtime.
|
||||
- Registers the `/qqq/v1/assistant/*` routes when Javalin is on the classpath.
|
||||
- Registers the record Developer-screen panel when QQQ core has the developer-panel contract.
|
||||
|
||||
Create the tables in your database: see [Tables](#tables). Then grant the permissions.
|
||||
|
||||
### The API key
|
||||
|
||||
The key never goes in config, tables or logs. It lives in the application's secret store and reaches the server as an environment variable (for kof22: Infisical, then an ExternalSecret, then the pod env).
|
||||
|
||||
- **Provider settings rows** (recommended) name the variable: `assistant_provider.apiKeyEnvName`, default the type's own (`ANTHROPIC_API_KEY`). The server reads the variable when it builds the client.
|
||||
- Without a row, the provider registered in code is used: `AnthropicModelProvider.fromEnvironment()` reads `ANTHROPIC_API_KEY`, and `AnthropicModelProvider.builder().apiKey(valueFromYourSecretStore).build()` takes the key from your own secret store (it also accepts `"${env.NAME}"`).
|
||||
|
||||
The provider's `toString` never contains the key. Its errors carry only the HTTP status and the API error type.
|
||||
|
||||
## Provider settings (the Providers table)
|
||||
|
||||
Admins set up providers on the **Providers** screen (`assistant_provider`), a standard QQQ table:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| Name, Type, Model | The type is one of the registered provider types (`withProviderType`; Anthropic now, others plug in the same way). The model list comes from the type and the price table. An empty model uses the policy's main model. |
|
||||
| Enabled, Default | The assistant uses the default enabled provider, else the first enabled one. Marking one default unmarks the others. With no enabled row, the provider registered in code is used. |
|
||||
| API Key Variable | The **name** of the environment variable that holds the key, never the key. Only allowed names are accepted: each registered type's default plus `allowedApiKeyEnvNames`. A pasted key (or `DATABASE_PASSWORD`) is refused and nothing is stored. |
|
||||
| API Key | Read-only, computed on every read: `Set • …abcd (from ANTHROPIC_API_KEY)` or `Not set (...)`. It shows the last four characters at most. No read path (screen, API, query, export) returns the key. |
|
||||
| Base URL Override | Empty uses the provider's own API. Any other URL must be listed in `allowedBaseUrls`, so a row cannot send the key to a host of an admin's choosing. |
|
||||
| Monthly Cost Cap (USD) | When this provider's cost this month (UTC) reaches the cap, new requests are refused (429 `BUDGET`) and a running answer stops before its next model call. |
|
||||
| Max Output Tokens Per Request, Max Concurrent Requests | Caps one model call's output, and how many calls run at once on this server. |
|
||||
| Last test | Status, time, latency, the model that answered, and the error text, from **Test connection**. |
|
||||
|
||||
**Why a reference and not an encrypted column.** QQQ core has no encrypted or secret field type. The key-provider and cipher work in the QQQ 4.1.0-RC.6 batch (QRun-IO/qqq#1123, #1107) encrypts backup artifacts, not table columns, and is not released. The design spec (§2.8) also says keys never go in tables. A reference keeps the key in the secret store that already holds it, so rotating it there is the "replace key" step, and the database never holds one.
|
||||
|
||||
**Test connection** is a record action on a provider (select one or more). It is a standard streamed ETL: Review lists what will be called and anything wrong with the row (missing key, disallowed variable or URL) before any call. Result has one line per provider: `connected to claude-sonnet-5-5 in 812 ms`, or the error with its kind and HTTP status. It makes one short live call (a 1-sentence prompt, at most 32 output tokens, effort low), saves the result on the row, and records the call in `assistant_usage` with purpose `TEST`.
|
||||
|
||||
## The app home
|
||||
|
||||
The **Assistant** app's home is the one place for the assistant's health, usage and cost (no other dashboard repeats it). All of it comes from the tables below, read as the viewing admin; an empty table gives an empty state, never sample data.
|
||||
|
||||
- **Health:** is it answering (kill switch, default provider, key present, last test), the 7-day error rate of model calls, cost and requests this month against the instance's monthly limit.
|
||||
- **Providers:** one row per provider: type, model, masked key status, last test, cost this month against its cap, limits, and any problem with its settings.
|
||||
- **Usage, last 30 days:** bars of model requests per day, totals (conversations, requests, tokens in and out, cost) and the last 7 days in words.
|
||||
- **Costs this month:** month to date against the policy limit and each provider cap, then by model. Cost is tokens times the model's price from **Model prices**.
|
||||
- **By user, this month**, **Top tools** (calls, failures, average time) and **Recent failures** (model calls and tool calls, linked).
|
||||
|
||||
The nav lists the home and the main tables: **Providers**, **Conversations**, **Model prices** and **Policy**. Messages, turns, tool calls, feedback and model calls are child lists on a conversation; model calls are also listed on their provider. Retention and Test connection are table and record actions, never nav entries.
|
||||
|
||||
## Model prices
|
||||
|
||||
`assistant_model_price` holds USD per million tokens per provider type and model: input, output, cache read, and cache writes (5 minute and 1 hour). Empty cache prices default to 0.1x, 1.25x and 2x the input price. The reference DDL seeds the Anthropic list prices of 2026-10. Edit a row to change what later calls cost; past rows keep the cost they were recorded with. A model with no row uses the `pricing` config map, else costs 0.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `enabled` | `true` | Per-app switch. The environment variable `QQQ_ASSISTANT_ENABLED=false` also turns it off. |
|
||||
| `backendName` | (required) | Backend of the `assistant_*` tables. |
|
||||
| `providerName` | `anthropic` | Which registered provider to use. |
|
||||
| `models` | Sonnet 5.5 at effort `medium` | `mainModel`, `mainEffort`, `mainMaxOutputTokens` (16000). The escalation (Opus 5.5, `high`) and summary (Haiku 5.5) slots are reserved for phase 2. Model ids are the exact strings `claude-sonnet-5-5`, `claude-opus-5-5` and `claude-haiku-5-5`. |
|
||||
| `defaultExposure` | `VALUES` | The kernel's default exposure. Use `METADATA_ONLY` for high-sensitivity apps. |
|
||||
| `maxToolCallsPerTurn` | 25 | Tool calls allowed in one answer. |
|
||||
| `maxParallelToolCalls` | 4 | Independent read tools that run at the same time. |
|
||||
| `maxRowsToModel` / `maxCellChars` | 50 / 500 | Caps on what the model sees (the kernel applies them). |
|
||||
| `maxTurnSeconds` | 300 | Wall-clock limit for one answer. |
|
||||
| `budgets` | none | `userDaily{Tokens,CostMicros}{Soft,Hard}` and `instanceMonthly{Tokens,CostMicros}{Soft,Hard}`. A soft limit adds a warning. A hard limit refuses new turns (429) and stops a running turn after its current step. Tokens counted are uncached input, cache writes and output. Cost is in micro-USD. |
|
||||
| `maxTurnsPerUserPerMinute` / `maxConcurrentTurnsPerUser` | 10 / 1 | Rate limits. |
|
||||
| `conversationRetentionDays` / `toolResultRetentionDays` / `approvalRetentionDays` | 90 / 30 / 365 | Used by `assistantRetention`. |
|
||||
| `retentionSchedulerName` | none | Runs `assistantRetention` daily on that scheduler. |
|
||||
| `ownerSecurityKeyType` | `assistant_user_id` | Key type of the owner record locks. |
|
||||
| `pricing` | Anthropic list prices, 2026-10 | USD per million tokens per model, used for cost and budgets. |
|
||||
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The kernel denies every widget not on this list. This config list is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. Allow only widgets that render the same data for every user who can see them. |
|
||||
| `hiddenWidgetNames` | none | Widgets that are never rendered, even when allowed. |
|
||||
| `customRedactionPatterns` | none | Extra regular expressions the kernel redacts from tool results. |
|
||||
| `killSwitchPollMillis` | 2000 | How often a running turn re-checks the kill switch while it waits on the model. |
|
||||
| `providerTypes` | none | `withProviderType(...)`: the types the Providers screen offers (`AnthropicProviderType`). |
|
||||
| `allowedApiKeyEnvNames` | none | Extra environment variable names a provider row may name as its key. Each type's default is always allowed. |
|
||||
| `allowedBaseUrls` | none | Base URL overrides a provider row may use. The type's own URL is always allowed. |
|
||||
| `secretResolver` | `System::getenv` | How a key variable name becomes the key (a host secret store, or tests). |
|
||||
| `providerTestTimeoutSeconds` | 30 | How long Test connection waits for the model. |
|
||||
| `homeMaxRows` | 20000 | Rows per table the app home reads for its period; it says when totals are partial. |
|
||||
| `tablePathPrefix` | `/app` | The front of the links the app home makes to tables and records. |
|
||||
| `emailExternalApproved` | `false` | Stands in for the EXTERNAL approval rule (not built yet): scheduled-task result email is sent only when `true`. |
|
||||
| `emailRecipientResolver` | owner id if it is an address | Hook: task owner user id to email address. |
|
||||
|
||||
At runtime, an admin can override the kill switch, the main model and effort, and the budgets in the single **`assistant_policy`** row.
|
||||
|
||||
A running turn notices the kill switch in two ways:
|
||||
- It re-reads the switch before every model call and tool step.
|
||||
- A watcher polls it every `killSwitchPollMillis` while the turn waits on the model.
|
||||
|
||||
When a turn sees the switch on, it cancels every turn running on that replica. If the policy row cannot be read, the switch counts as **on**: it fails closed.
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Grants |
|
||||
|---|---|
|
||||
| `assistant.hasAccess` | Use the panel. It grants **no data access**: the assistant reads only what the user can read anyway. |
|
||||
| `assistant.admin` | The Assistant app (home, settings and audit tables), Test connection and the retention process. |
|
||||
|
||||
There is no assistant-specific data permission. Everything the kernel reads is checked against the user's normal table permissions and record locks. The QBit never uses a system session (an ArchUnit test enforces this).
|
||||
|
||||
When the QBit is enabled, it registers the supplemental instance meta-data `assistant`. The v1 `/metaData` response publishes it as `supplementalInstanceMetaData.assistant: {"available": true, "version": 1, "basePath": "/qqq/v1/assistant"}`. The Next UI only probes `/config` when that object is present.
|
||||
|
||||
### Record locks on the assistant's own tables
|
||||
|
||||
Conversation data belongs to its user. Every user-owned table has a record lock on `userId` with key type `assistant_user_id`:
|
||||
- The panel routes add the user's own id to their session, so users only ever see their own rows.
|
||||
- Admins see everyone's rows in the Assistant app through the key type's all-access key, `assistant_user_id_all_access`. Grant that key to `assistant.admin` holders from your authentication customizer:
|
||||
|
||||
```java
|
||||
// in your QAuthenticationModuleCustomizerInterface.customizeSession(...)
|
||||
AssistantSecurity.applySessionKeys(qSession, assistantConfig);
|
||||
```
|
||||
|
||||
## Data policy
|
||||
|
||||
What reaches the model provider:
|
||||
- the tool definitions
|
||||
- a fixed safety prompt
|
||||
- an app digest (labels and descriptions, no data)
|
||||
- the user's name, timezone and readable-table list
|
||||
- the conversation's text
|
||||
- tool results produced by the kernel.
|
||||
|
||||
The kernel applies these rules to tool results:
|
||||
- Hidden fields, password fields and fields with a security lock are never sent.
|
||||
- Exposure `METADATA_ONLY` and `MASKED` fields are filtered or masked.
|
||||
- Rows are capped at 50 and cells at 500 characters.
|
||||
- Secrets such as keys, JWTs and card numbers are redacted.
|
||||
- The `assistant_*` tables are never visible to the model.
|
||||
|
||||
Old tool results are cleared from the context of later turns; only one-line summaries remain.
|
||||
|
||||
Stored in your database:
|
||||
- every message
|
||||
- every tool call: sanitized input, outcome, permission check, row count, duration, redactions, and the model-visible result. After `toolResultRetentionDays`, the result is cleared, stream events are deleted, and render payloads and record references are removed from stored messages. Summaries and answer text stay until `conversationRetentionDays`.
|
||||
- turn accounting: tokens, cache reads and writes, and cost
|
||||
- streamed events, kept for replay
|
||||
- feedback
|
||||
- daily usage.
|
||||
|
||||
Logs never contain message content.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Base path `/qqq/v1/assistant`. Every request is authenticated through the app's normal session (cookie, bearer token or API key). Request bodies must be JSON.
|
||||
|
||||
| Method | Path | |
|
||||
|---|---|---|
|
||||
| GET | `/config` | Feature probe: `{enabled, reason?, shortcut, model, limits, features}`. A 404 means the QBit is not installed. |
|
||||
| GET / POST | `/conversations` | List your conversations (`?status=ARCHIVED`), or create one. |
|
||||
| GET / PATCH / DELETE | `/conversations/{id}` | Load with paged messages (`?before=&limit=`), rename / pin / archive, or delete. |
|
||||
| POST | `/conversations/{id}/turns` | Ask a question. The response is an SSE stream. |
|
||||
| GET | `/turns/{id}/events?after={seq}` | Replay a turn's events after a sequence number, then follow it live. |
|
||||
| POST | `/turns/{id}/cancel` | Stop an answer. |
|
||||
| POST | `/messages/{id}/feedback` | Rate an answer: `{rating: 1 or -1, reason?, comment?}`. |
|
||||
|
||||
The stream sends these events, each with a `seq`:
|
||||
- `message.start`, `text.delta`
|
||||
- `tool.start`, `tool.result` (with `render` and `recordRefs`)
|
||||
- `presentation` (`messageId`, `part`: summary, blocks and server-built GFM markdown)
|
||||
- `ui.navigate`, `notice`
|
||||
- `usage`, `message.end`
|
||||
- `error`
|
||||
- `ping` every 15 seconds.
|
||||
|
||||
The stream ends after `message.end`. Thinking is never streamed.
|
||||
|
||||
### SSE behind a proxy (Traefik, nginx)
|
||||
|
||||
- The responses carry `Cache-Control: no-cache` and `X-Accel-Buffering: no`. Do not compress or buffer `text/event-stream`.
|
||||
- Give the routes an idle timeout longer than the 15-second ping interval and a total timeout of at least `maxTurnSeconds`. For Traefik, raise the entrypoint's `respondingTimeouts` (`readTimeout`, `writeTimeout`, `idleTimeout`) or put the assistant routes on a router whose transport allows it. Don't add a buffering middleware to these routes.
|
||||
- A dropped connection does not stop the answer. The client resumes with `/turns/{id}/events?after=<last seq>`.
|
||||
|
||||
## Tables
|
||||
|
||||
All of these are standard, visible QQQ tables in the Assistant app. The model never reads any of them (AI exposure `NONE`).
|
||||
|
||||
| Table | Holds |
|
||||
|---|---|
|
||||
| `assistant_conversation` | Conversations. |
|
||||
| `assistant_message` | Messages (`partsJson`). |
|
||||
| `assistant_turn` | Turn accounting. |
|
||||
| `assistant_turn_event` | Stream events, for replay. |
|
||||
| `assistant_tool_call` | The audit of every tool call. |
|
||||
| `assistant_feedback` | Feedback on answers. |
|
||||
| `assistant_policy` | The kill switch and overrides. |
|
||||
| `assistant_usage_daily` | Usage rollups: per user per day, plus an instance row whose `userId` is null. |
|
||||
| `assistant_provider` | Provider settings: type, model, key variable (never the key), limits, last connection test. |
|
||||
| `assistant_model_price` | USD per million tokens per model, used for every call's cost. |
|
||||
| `assistant_usage` | One row per model call (a turn step or a connection test): user, conversation, turn, provider, model, status, latency, tokens, cost. Locked to its user like the other audit tables. |
|
||||
|
||||
Table names follow the QRun-IO naming rule: lowercase snake_case with the `assistant_` prefix, the same as the physical tables in `docs/schema-postgres.sql` (the reference DDL). Field names are camelCase. Map them to your columns with a table customizer. Upgrading from 0.1.0-RC.1, which used camelCase table names and the `assistantUserId` key type: see [UPGRADE.md](UPGRADE.md).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./mvnw verify # against the committed QQQ version
|
||||
./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:
|
||||
- the golden evals (JUnit tag `golden`): scripted conversations against a seeded fixture app
|
||||
- the permission red team (tag `redteam`).
|
||||
|
||||
The **live** evals run the same golden tasks against Claude. They are skipped unless both `ANTHROPIC_API_KEY` and `-Dassistant.live=true` are set:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=... ./mvnw verify -Dassistant.live=true -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true -pl qbit-assistant-provider-anthropic -am
|
||||
```
|
||||
|
||||
## Providers
|
||||
|
||||
Add `com.kingsrook.qbits:qbit-assistant-provider-openai` at the same version as
|
||||
the QBit and register the adapter alongside any Anthropic adapter:
|
||||
|
||||
```java
|
||||
config.withProviderType(new OpenAiCompatibleProviderType());
|
||||
```
|
||||
|
||||
`OpenAiCompatibleProviderType` is in
|
||||
`com.kingsrook.qbits.assistant.provider.openai`. Its installed type id is
|
||||
`openai-compatible`; provider rows choose OPENAI, AZURE_OPENAI, OPENROUTER,
|
||||
OLLAMA, LM_STUDIO, VLLM, LLAMA_CPP or OPENAI_COMPATIBLE as their kind.
|
||||
|
||||
- Local kinds use their localhost endpoint by default and need no key. Leave
|
||||
`apiKeyEnvName` empty. An auth proxy can use an explicitly allowed key env name.
|
||||
- Cloud defaults use `OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY` or
|
||||
`OPENROUTER_API_KEY`. Values stay in the host's secret store.
|
||||
- An override must match an entry in `config.withAllowedBaseUrls(Set.of(...))`.
|
||||
Azure and OPENAI_COMPATIBLE have no default URL and always require an entry.
|
||||
Redirects are not followed. Azure uses the deployment model id and defaults
|
||||
`apiVersion` to `2024-10-21`.
|
||||
- **Discover models** adds a provider's Models child list; existing edits survive.
|
||||
Large cloud catalogs (over 20 models) start disabled. **Test connection** is
|
||||
available on both providers and models and records a TEST usage row.
|
||||
- A selected registered model must be enabled. With no model selected, the first
|
||||
enabled registry model wins; providers with no model rows retain legacy selection.
|
||||
Registry prices override type/model prices, then configured prices. Unpriced
|
||||
local models cost zero. Capabilities can be adjusted per registered model.
|
||||
|
||||
For Ollama behind a TLS proxy with a private CA and bearer authentication:
|
||||
|
||||
```java
|
||||
config.withProviderType(new OpenAiCompatibleProviderType())
|
||||
.withAllowedBaseUrls(Set.of("https://llm.example.lan:8443/v1"))
|
||||
.withAllowedApiKeyEnvNames(Set.of("LOCAL_LLM_API_KEY"))
|
||||
.withTrustedCaBundleFile("local-llm", Path.of("/etc/app/llm-root-ca.pem"));
|
||||
```
|
||||
|
||||
Set `LOCAL_LLM_API_KEY` in the server environment from your secret store. In the
|
||||
provider record choose kind `OLLAMA`, base URL `https://llm.example.lan:8443/v1`,
|
||||
`apiKeyEnvName = LOCAL_LLM_API_KEY`, and `tlsCaBundle = local-llm`. Only the names
|
||||
are stored. `withTrustedCaBundle("local-llm", pem)` also accepts one or more PEM
|
||||
certificates directly at startup. Invalid PEM fails configuration immediately.
|
||||
The named bundle adds to the JVM roots; hostname verification stays enabled.
|
||||
Leave `tlsCaBundle` empty to use JVM trust alone. Restart after changing a CA file.
|
||||
|
||||
Default tests use localhost mocks. To exercise an existing development server,
|
||||
set `ASSISTANT_LIVE_OPENAI_BASE_URL` and `ASSISTANT_LIVE_OPENAI_MODEL`, optionally
|
||||
`ASSISTANT_LIVE_OPENAI_KIND` (default OLLAMA), `ASSISTANT_LIVE_OPENAI_KEY_ENV`
|
||||
(the variable name containing the key), and `ASSISTANT_LIVE_OPENAI_CA_FILE`
|
||||
(a PEM bundle path). Unset or unreachable endpoints, including TLS failures, skip
|
||||
the live test. Run:
|
||||
|
||||
```sh
|
||||
./mvnw verify -pl qbit-assistant-provider-openai -am -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true
|
||||
```
|
||||
|
||||
### AWS Bedrock (Claude)
|
||||
|
||||
Add `com.kingsrook.qbits:qbit-assistant-provider-bedrock` at the QBit version and
|
||||
register `com.kingsrook.qbits.assistant.provider.bedrock.BedrockProviderType`:
|
||||
|
||||
```java
|
||||
config.withProviderType(new BedrockProviderType());
|
||||
```
|
||||
|
||||
Create a provider row with type `bedrock`, kind `BEDROCK`, and the required AWS
|
||||
`region` (for example `us-east-1`). Leave `apiKeyEnvName` empty. The server uses
|
||||
the AWS default credential chain: environment variables, profiles and container
|
||||
credentials (ECS/EKS Pod Identity) work with the transitive dependencies.
|
||||
**For IRSA / web-identity credentials, the host application must add
|
||||
`software.amazon.awssdk:sts` to its runtime classpath**, matching the transitive
|
||||
AWS SDK version. The default chain picks it up when present; this adapter does
|
||||
not include STS or the full AWS SDK. Credentials are never stored in tables.
|
||||
|
||||
Leave the base URL empty for the SDK's
|
||||
`https://bedrock-runtime.<region>.amazonaws.com` endpoint. Overrides, including
|
||||
VPC endpoints, must be listed in `withAllowedBaseUrls`. Model ids are Bedrock
|
||||
Claude ids or inference profile ids, for example
|
||||
`us.anthropic.claude-sonnet-4-5-20250929-v1:0`. The model must be available to the
|
||||
server's AWS role in that region.
|
||||
|
||||
### Google Vertex AI (Claude, Gemini)
|
||||
|
||||
Add `com.kingsrook.qbits:qbit-assistant-provider-vertex` at the QBit version and
|
||||
register `com.kingsrook.qbits.assistant.provider.vertex.VertexProviderType`:
|
||||
|
||||
```java
|
||||
config.withProviderType(new VertexProviderType());
|
||||
```
|
||||
|
||||
Create a row with type `vertex`, kind `VERTEX`, required `region` (for example
|
||||
`us-east5`) and `cloudProject` (the GCP project id). Leave `apiKeyEnvName` empty.
|
||||
The server uses Google Application Default Credentials, such as
|
||||
`GOOGLE_APPLICATION_CREDENTIALS` or workload identity. Access tokens are refreshed
|
||||
when expired, held only in memory, and obtained afresh for each Gemini request.
|
||||
|
||||
Models beginning `claude-` (for example `claude-sonnet-4-5@20250929`) use the
|
||||
Anthropic Vertex backend's `streamRawPredict` API. All other model ids use Vertex's
|
||||
OpenAI-compatible endpoint:
|
||||
|
||||
```text
|
||||
https://{region}-aiplatform.googleapis.com/v1/projects/{project}/locations/{region}/endpoints/openapi
|
||||
```
|
||||
|
||||
Unprefixed ids, such as `gemini-2.5-flash`, are sent as `google/gemini-2.5-flash`;
|
||||
already-prefixed ids are preserved. Gemini requests omit `strict`, including when
|
||||
a tool opts in. An allow-listed base URL overrides the Claude API origin or the
|
||||
complete OpenAI-compatible base URL, according to the selected model. Use separate
|
||||
provider rows when an override needs different URL prefixes for the two APIs.
|
||||
|
||||
Both cloud types default to CLOUD and expose a short static model catalog.
|
||||
**Discover models** returns those defaults; it does not call Bedrock's
|
||||
`ListFoundationModels` (which needs the control-plane SDK) or Vertex Model Garden.
|
||||
Health probes are unsupported; **Test connection** performs the existing one-shot
|
||||
completion. Model access and availability depend on the configured region/project.
|
||||
`hipaaEligible` is an administrator-recorded fact: a BAA and the associated legal
|
||||
and operational requirements remain owner responsibilities. The flag does not
|
||||
establish eligibility or implement PHI routing.
|
||||
|
||||
The default build tests both clouds using localhost only, fake credentials, AWS
|
||||
binary event-stream framing, Claude SSE and Gemini Chat Completions, including
|
||||
engine/tool golden turns. No cloud credentials or live endpoints are required.
|
||||
- [Getting Started](docs/00-getting-started.md)
|
||||
- [Extension Patterns](docs/01-extension-patterns.md)
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0.
|
||||
|
||||
|
||||
## Email through qbit-email
|
||||
|
||||
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.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`);
|
||||
2. qbit-email is installed (`NOT_AVAILABLE`);
|
||||
3. `emailExternalApproved` is `true` (`NOT_APPROVED`). The EXTERNAL approval rule exists only in the spec, so this explicit flag gates it until the rule is built;
|
||||
4. a recipient resolves (`NO_RECIPIENT`): `emailRecipientResolver` maps the owner's user id to an address; by default an owner id that already is an address is used.
|
||||
|
||||
What is sent, one message per recipient, idempotency key `assistant:scheduled:<run id>`, linked record the run:
|
||||
- result classified PUBLIC (or none): template `assistant_scheduled_result` (NOTIFICATION, Markdown, registered on first send and never overwritten) with the task name, run time, a summary cut to 280 characters and a link to the run. The full model output is never emailed;
|
||||
- result classified above PUBLIC (core's `DataClassification`): only the link, through qbit-email's `email_secure_message` pattern. No task name, summary or output.
|
||||
|
||||
The outbox result maps to `QUEUED`, `DUPLICATE`, `SUPPRESSED` or `REFUSED` (for example a link that is not absolute http(s)). Addresses and bodies are never logged.
|
||||
|
||||
## Data classification and HIPAA mode
|
||||
|
||||
Classify application tables and fields in core's AI metadata (`AiTableMetaData` / `AiFieldMetaData`, QQQ 4.1.0-RC.6), or in the host configuration (a configured value wins when both are set):
|
||||
|
||||
```java
|
||||
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
|
||||
|
||||
AssistantQBitConfig config = new AssistantQBitConfig()
|
||||
.withDataClassification("patient", DataClassification.PHI)
|
||||
.withDataClassification("patient", "id", DataClassification.PUBLIC)
|
||||
.withDataClassification("patient", "email", DataClassification.PII)
|
||||
.withDataClassification("patient", "status", DataClassification.PUBLIC)
|
||||
.withHipaaMode(true)
|
||||
.withHipaaRetentionDays(2190)
|
||||
.withConversationEncryptionKeyEnvName("ASSISTANT_CONVERSATION_KEY");
|
||||
```
|
||||
|
||||
Set that environment variable through your secret manager to a base64-encoded,
|
||||
random 256-bit (32-byte) key. Supply only its **name** in config, never the key.
|
||||
The existing server-side `withSecretResolver` hook also resolves this reference.
|
||||
Startup validates the key. HIPAA mode defaults off; encryption can also be enabled
|
||||
with the key reference while HIPAA mode is off.
|
||||
|
||||
The levels are core's `DataClassification` (PUBLIC < CONFIDENTIAL < PII < PHI); the
|
||||
qbit's former stand-in enum is gone. Unconfigured tables default to PUBLIC; a field
|
||||
falls back to its table unless explicitly overridden. A field subset uses its
|
||||
selected fields; unrestricted reads and record page contexts use the highest
|
||||
classification across the table and its fields. Returned columns and record-label
|
||||
fields also count: tools include a primary key even for subsets, so classify it
|
||||
explicitly if the host considers it safe for that subset. Cross-table results with unknown
|
||||
tables use the highest classification anywhere. Metadata-only tools remain PUBLIC.
|
||||
The assistant's own tables remain denied to tools.
|
||||
|
||||
The default `ClassificationSource` reads core's metadata (`AiDataClassifications`,
|
||||
`AiTableMetaData`, `AiFieldMetaData`) merged with the configured map; configuration
|
||||
wins when both are set. Hosts can still supply `withClassificationSource(ClassificationSource)`.
|
||||
|
||||
PHI is sent only to a persisted provider row explicitly marked `hipaaEligible`.
|
||||
Every model dispatch rechecks eligibility, including calls after tools and strict
|
||||
schema retries. Blocks end the turn with `PHI_BLOCKED` or `HIPAA_PROVIDER`; the
|
||||
PHI tool result is never sent to that provider. There is no automatic fallback.
|
||||
A conversation keeps its highest classification, so moving to a PUBLIC page does
|
||||
not make existing PHI history safe for a non-eligible provider.
|
||||
|
||||
HIPAA mode also refuses non-eligible providers at resolution and when enabling
|
||||
settings rows, disables code-only fallback, requires encrypted conversation
|
||||
storage, and suppresses provider error details in logs and turn errors. The app
|
||||
home displays HIPAA mode and PHI retention. The eligibility flag records the
|
||||
operator's decision (including any applicable BAA); it does not establish one.
|
||||
|
||||
Both modes audit each PII/PHI model call and every routing block in
|
||||
`assistant_data_audit` (admin read-only, owner-locked, on the conversation screen).
|
||||
PHI conversations use `hipaaRetentionDays` even with HIPAA mode off. A null value
|
||||
uses normal retention; tool-result/replay cleanup remains independently configured.
|
||||
See [UPGRADE.md](UPGRADE.md) for schema changes, plaintext migration, and key handling.
|
||||
|
||||
## Data-driven budgets
|
||||
|
||||
Admins configure **Assistant > Budgets** for UTC DAY or MONTH allowances in USD or
|
||||
TOKENS, scoped to INSTANCE, USER, ROLE (a permission), PROVIDER or MODEL. Provider
|
||||
and model scope values are registry record ids. Warnings default to 80%; at 100%
|
||||
the default action is DOWNGRADE, and the hard ceiling defaults to 120% (always
|
||||
STOP). Multiple budgets apply together and STOP wins.
|
||||
|
||||
An explicit downgrade model is optional. Otherwise the cheapest enabled usable
|
||||
model is selected, preferring LOCAL providers. PHI never goes to a provider that
|
||||
is not HIPAA-eligible. No eligible target means STOP. Administrators can grant
|
||||
extra allowance or temporary unlimited use from the budget's Overrides child list,
|
||||
with a required reason and expiry; the session sets the grantor.
|
||||
|
||||
Budget state changes appear once per scope/period/state in the Events child list,
|
||||
as panel notices and, through core QAlerts, bell alerts when an alerts sink is installed (logging otherwise).
|
||||
The Assistant home shows budgets, overrides, daily cost and tokens, and breakdowns
|
||||
by user, model, provider and tool. Existing `AssistantBudgets`/policy limits and
|
||||
provider monthly caps continue to apply independently of these rows and overrides.
|
||||
See [UPGRADE.md](UPGRADE.md) for the required additive schema.
|
||||
|
||||
|
||||
### Profile routing and provider fallback
|
||||
|
||||
On an Assistant Profile, **Model routing** defaults to OFF. HEURISTIC selects the
|
||||
complex slot for long questions (>600 characters), two mentioned app tables,
|
||||
analysis/comparison/explanation/trend/planning requests, pending writes, more than
|
||||
eight previous turns, or a previous tool limit; otherwise it selects the simple
|
||||
slot. CLASSIFIER uses an optional registry model with just the question and a
|
||||
fixed instruction, four output tokens and a three-second timeout. An invalid reply,
|
||||
error, unavailable classifier or PHI-ineligible classifier uses HEURISTIC. ROUTE
|
||||
usage counts toward budgets. Empty model slots keep the provider/policy default;
|
||||
budgets may downgrade either choice. Without a profile, enabled code-level
|
||||
`AssistantModelRouting.escalation*` supplies the complex default.
|
||||
|
||||
A model's **Fallback model** points to its next candidate, including another
|
||||
provider. Chains are cycle-free and bounded to four hops. Only retryable failures
|
||||
before any text or tool start from that call permit fallback. Disabled, down,
|
||||
PHI-ineligible or budget-blocked candidates are skipped. Without explicit links,
|
||||
only another LOCAL model in the same adapter family is eligible; local requests
|
||||
never automatically move to cloud. Provider state/signatures are removed when
|
||||
switching, preserving neutral tool history. Each attempted call is audited as usage.
|
||||
|
||||
NETWORK/TIMEOUT marks a provider DOWN for 60 seconds; success marks it UP. Local
|
||||
health is probed when unknown/stale with a two-second timeout. Provider cards show
|
||||
health and down-until, failures show fallback totals, and turn records show the
|
||||
route and final destination. See [UPGRADE.md](UPGRADE.md) for the additive schema.
|
||||
|
||||
## Scheduled tasks
|
||||
|
||||
Administrators configure recurring prompts in **Assistant → Scheduled tasks**.
|
||||
Tasks use Quartz cron syntax (for example `0 0 8 * * ?`) and an IANA timezone
|
||||
(for example `America/Chicago`; default `UTC`). Tasks start disabled.
|
||||
|
||||
```java
|
||||
config.withUiApiBaseUrl("http://127.0.0.1:8000")
|
||||
.withScheduledTasksSchedulerName("hostScheduler")
|
||||
.withAllowedScheduledCredentialEnvNames(Set.of("ASSISTANT_SCHEDULED_TOKEN"));
|
||||
```
|
||||
|
||||
Provide a long-lived access token accepted by the host's authentication provider
|
||||
in that environment variable. Store only its **name** on the task and set
|
||||
`runAsUserId` to the corresponding QQQ user id. The runner posts the token to
|
||||
`/qqq/v1/manageSession`, reads its session cookie and verifies the returned
|
||||
session's user before starting a new conversation. No system session reads user
|
||||
data. Configure the loopback URL at startup; a scheduled job cannot rely on a
|
||||
browser having visited first.
|
||||
|
||||
For kof22, provision a designated Authentik account with the intended application
|
||||
permissions, obtain a token that kof22's QQQ authentication provider accepts as an
|
||||
`accessToken`, and inject it through the existing secret-manager/ExternalSecret
|
||||
pipeline. An Authentik management API token is suitable only if the host's auth
|
||||
adapter explicitly accepts it. Rotate the token in the secret manager, never in
|
||||
the task table. This feature does not provision an account or token.
|
||||
|
||||
**Run now**, **Approve write mode** and **Revoke write mode** are Review/Result
|
||||
record actions. Write approval names the designated user and displays the exact
|
||||
prompt SHA-256. Changing prompt, user, credential reference, profile or write-mode
|
||||
request clears approval. Scheduled writes always wait for that user's panel
|
||||
confirmation, even if normal policy disables confirmation.
|
||||
|
||||
Each run keeps an owner-locked audit row and a normal conversation. RECORD adds
|
||||
no notification; NOTIFICATION and BOTH also notify the designated user through
|
||||
the optional Alerts QBit SPI. Without Alerts, notification delivery is log-only.
|
||||
HIPAA summaries are always `see conversation`. Cost caps stop at dispatch
|
||||
boundaries, so the last completed call can cross the cap; no further call starts.
|
||||
Apply the [P-g migration](UPGRADE.md#p-g-scheduled-tasks) before enabling jobs.
|
||||
### Answer presentation
|
||||
|
||||
The always-available `present_results` tool turns retrieved results into metrics,
|
||||
tables, charts and actionable in-app links. The server validates the user's UI API
|
||||
visibility, formats field values, computes shares and builds all links. It emits
|
||||
and stores a `presentation` part with a complete GFM Markdown fallback. Profile
|
||||
skills can extend the built-in presentation instructions. See the
|
||||
[part, stream and input contract](docs/answer-presentation.md).
|
||||
|
||||
The image-breakdown golden task also runs in the existing opt-in live provider suite:
|
||||
`./mvnw verify -Dassistant.live=true -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true -pl qbit-assistant-provider-anthropic -am`
|
||||
(with `ANTHROPIC_API_KEY` set). Live calls are excluded from normal verification.
|
||||
|
||||
### Record history and the assistant channel
|
||||
|
||||
Core's default `ClientChannelsMetaData` denies `audit*` to the `assistant`
|
||||
channel. Accordingly, `get_record_history` is offered only when all three audit
|
||||
tables it queries (`auditTable`, `audit`, and `auditDetail`) exist and are not
|
||||
channel-denied, including their effective permission rules. A host that intends
|
||||
to expose record history can opt in during instance configuration:
|
||||
|
||||
```java
|
||||
new AssistantQBitConfig()
|
||||
.withRecordHistory(true);
|
||||
```
|
||||
|
||||
This is an owner decision and a hard wall: record history defaults to disabled;
|
||||
**kof22 does not opt in**. On QQQ RC.6, the opt-in removes exactly `audit*`
|
||||
from the assistant's denied patterns (the former manual configuration step).
|
||||
RC.6 does not add a separate channel write denial for audits. On later RCs that
|
||||
include QRun-IO/qqq#1193, it calls `withAssistantAuditReads()`, which allows audit
|
||||
reads but denies audit writes. The user's own audit permissions still apply.
|
||||
Other patterns, including `worm_audit_*`, `assistant_*`, and security surfaces,
|
||||
remain untouched, as do explicit object and permission-rule denials.
|
||||
|
||||
History visibility uses core's `isDenied` read/all-operation check. RC.6 has no
|
||||
operation argument; the later write-only deny patterns must remain separate
|
||||
from that check so they do not hide the read tool.
|
||||
See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
||||
@@ -1,3 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
* See Project [Security Policy](https://github.com/Kingsrook/qqq/blob/develop/SECURITY.md)
|
||||
@@ -1,457 +0,0 @@
|
||||
# Upgrading qbit-assistant
|
||||
|
||||
## W1 preferences migration (F13 / #29)
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## X1/Y1: channel wall and audit attribution
|
||||
|
||||
Requires QQQ 4.1.0-RC.6. Every assistant table, process, report and widget is
|
||||
explicitly denied to channel `assistant`; startup rejects an exposed object.
|
||||
Host customizations must preserve these denials. Administration uses `next-ui`.
|
||||
|
||||
Before enabling `AuditsMetaDataProvider.withClientChannelField(true)`, add the
|
||||
nullable `audit.client_channel VARCHAR(100)` column using the host migration
|
||||
conventions. kof22 needs a feature changeset under `changesets/features/` (for
|
||||
example `2026-10-14-audit-client-channel.sql`); no host migration is applied here.
|
||||
Existing audit rows remain null. The test fixture enables the field.
|
||||
|
||||
The existing `assistant_tool_call.auditHref` column now points to audit rows
|
||||
filtered by record, table and client channel, including for deletes. It is empty
|
||||
when the host has not enabled the audit channel field. Apply the X1/Y1 DDL in
|
||||
`docs/schema-postgres.sql` to widen the existing `assistant_tool_call.audit_href`
|
||||
column to TEXT: encoded filters with long table names or string record ids can
|
||||
exceed the old 500-character limit. No new QBit column is required. `GET /assistant/access` accepts an optional owned `turnId` for attribution.
|
||||
|
||||
## Provider API extraction (#30)
|
||||
|
||||
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
|
||||
`com.kingsrook.qbits:qbit-assistant-provider-api` at the same version. This removes
|
||||
the assistant runtime and QQQ from that dependency path; only Jackson annotations
|
||||
are required by the API. Anthropic, OpenAI, Bedrock and Vertex adapters use this
|
||||
split; pgvector retains `qbit-assistant` for its runtime RAG interfaces.
|
||||
|
||||
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 no source change. Keep the runtime and
|
||||
provider artifacts on the same version; old validator overrides must be rebuilt.
|
||||
## P-g: scheduled tasks
|
||||
|
||||
Apply the **P-g** DDL in [docs/schema-postgres.sql](docs/schema-postgres.sql)
|
||||
before deploying. The unique `(task_id, scheduled_for)` constraint is required
|
||||
for multi-replica safety; retain run rows as schedule-slot claim history.
|
||||
Existing conversations default to channel `panel`. Tasks start disabled.
|
||||
|
||||
Set `withScheduledTasksSchedulerName(...)` to an existing QQQ scheduler and
|
||||
configure `withUiApiBaseUrl(...)` with this replica's own listener URL. The runner
|
||||
polls every 60 seconds. QQQ must supply a scheduler session able to manage the
|
||||
assistant tables; this QBit never creates that session. The configured backend
|
||||
must enforce unique keys atomically (Postgres does).
|
||||
|
||||
Allow only designated credential references with
|
||||
`withAllowedScheduledCredentialEnvNames(Set.of("ASSISTANT_SCHEDULED_TOKEN"))`.
|
||||
Provide that variable through the host's secret manager. It must hold a token
|
||||
accepted by the host's `manageSession` authentication provider for `runAsUserId`.
|
||||
The runner fails closed on missing/disallowed credentials or an identity mismatch.
|
||||
See [scheduled-task setup](README.md#scheduled-tasks) for kof22 provisioning.
|
||||
|
||||
QQQ RC.5 has no conditional UpdateInput. The runner uses a database-unique
|
||||
schedule-slot insertion as its optimistic claim, then advances `nextRunAt`
|
||||
before starting work. Crashes can skip a slot; they never retry it. No ArchUnit
|
||||
exception or fabricated session is introduced. Core RC.6 enforces client-channel denials; scheduled requests carry
|
||||
`assistant/scheduled/<taskId>`.
|
||||
|
||||
Notifications use the optional Alerts QBit notification SPI with a single-user
|
||||
audience. Without it, delivery logs run id/status only. All output modes retain
|
||||
the owner-locked audit row and conversation. HIPAA run summaries never copy the
|
||||
answer; they contain only `see conversation`.
|
||||
|
||||
## P-f: knowledge retrieval
|
||||
|
||||
Apply the **P-f** assistant-table DDL in
|
||||
[docs/schema-postgres.sql](docs/schema-postgres.sql) before deployment. It adds
|
||||
`assistant_model.model_kind` (existing rows default to CHAT),
|
||||
`embedding_dimensions`, and the admin-only `assistant_rag_source` table.
|
||||
No sources are seeded. Existing chat selections remain unchanged; explicitly
|
||||
EMBEDDING models are excluded from chat, classifier, fallback and downgrade
|
||||
selection. Correct discovery's kind/dimensions if a model name is ambiguous.
|
||||
|
||||
For persistent vectors, add `qbit-assistant-rag-pgvector` at the same version as
|
||||
the QBit, supply the host's JDBC DataSource/driver, and install pgvector in the
|
||||
database. A DBA runs `CREATE EXTENSION IF NOT EXISTS vector`; the application
|
||||
role needs schema creation rights for initial setup (or a DBA-created schema),
|
||||
then normal DML rights. `ensureSchema()` checks for an existing extension first
|
||||
and does not require extension installation privileges when it is already present.
|
||||
The optional pgvector DDL is a **commented reference block**, separate from the
|
||||
assistant-table migration. See [RAG setup](README.md#rag-setup).
|
||||
|
||||
Use a distinct validated schema for each QQQ instance. `vector(N)` fixes dimensions
|
||||
for that store; changing dimensions requires a new schema and re-indexing.
|
||||
Sources can use different models with that dimension; ranking is restricted to
|
||||
sources using the query's model. If the weights behind an unchanged model id
|
||||
change, clear and rebuild its sources. Editing source chunk/filter/model settings
|
||||
or registry model identity/dimensions automatically clears affected vectors.
|
||||
|
||||
Every indexing batch produces EMBED usage and updates existing budget rollups.
|
||||
PHI source content and PHI conversation queries require a HIPAA-eligible embedding
|
||||
provider. No background runner or designated credentials are installed in P-f.
|
||||
The default in-memory store is lost on restart and must be indexed again.
|
||||
|
||||
## P-e: profile routing and provider fallback
|
||||
|
||||
Apply the additive **P-e** ALTER TABLE statements at the end of
|
||||
[docs/schema-postgres.sql](docs/schema-postgres.sql) before deploying. Existing
|
||||
profiles default to OFF; optional model references and health columns start null.
|
||||
No routing or fallback configuration is seeded. Existing turns retain unknown
|
||||
routing/final-provider values; fallback_count defaults to zero.
|
||||
|
||||
Profiles now have a **Model routing** section. Select HEURISTIC or CLASSIFIER,
|
||||
then optional simple/complex registry models and a classifier model. Empty route
|
||||
slots retain provider/policy selection. CLASSIFIER with no usable classifier
|
||||
uses HEURISTIC. OFF preserves prior provider selection. With no effective profile,
|
||||
code-configured escalation supplies the complex model/effort when enabled.
|
||||
|
||||
Set a model's **Fallback model** to opt into a cross-provider chain. Saves reject
|
||||
cycles, more than four edges, or disabled/missing targets. Provider health is
|
||||
read-only; NETWORK/TIMEOUT opens a 60-second circuit, and successful calls close
|
||||
it. Local endpoints with stale health get a bounded two-second probe. Automatic
|
||||
fallback without an explicit link is confined to local models of the same adapter
|
||||
family. PHI/HIPAA eligibility and budgets apply to every destination.
|
||||
|
||||
Classifier usage is recorded with purpose ROUTE and included in budgets. Reports
|
||||
that filter usage purpose should include ROUTE. Turn screens show route decision,
|
||||
source, fallback count and final provider; the home shows health and fallback totals.
|
||||
All configuration remains assistant.admin-only, with AI exposure NONE.
|
||||
|
||||
## P-d: data-driven budgets
|
||||
|
||||
Requires QQQ 4.1.0-RC.5 for the core Alerts API and alert type metadata.
|
||||
|
||||
Create `assistant_budget`, `assistant_budget_override`, `assistant_budget_event`
|
||||
and `assistant_budget_usage`
|
||||
using the P-d DDL in [schema-postgres.sql](docs/schema-postgres.sql) before deploying.
|
||||
The event unique key is required: it deduplicates each state per budget, scope and
|
||||
UTC period across replicas. Do not remove it or truncate events during a period.
|
||||
|
||||
Budgets appear in Assistant navigation. Overrides and immutable events are child
|
||||
lists. All four tables require `assistant.admin` and have AI exposure NONE.
|
||||
Overrides record the actual session grantor and require a reason and expiry.
|
||||
The standard host QQQ audit configuration applies to budget and override edits.
|
||||
`assistant_budget_usage` is a private accounting projection populated from persisted
|
||||
usage writes. It copies only counters, time, user, provider and model, with no owner
|
||||
lock, so shared INSTANCE/PROVIDER/MODEL sums can run under the caller's own session.
|
||||
The source usage table retains its owner locks. The projection is read-only, has no
|
||||
nav entry and no AI exposure. **Pause usage writes during migration and run the DDL's
|
||||
backfill before restarting**; otherwise shared totals would omit pre-upgrade usage.
|
||||
Keep accounting rows through their budget periods even if detail rows are removed.
|
||||
ROLE budgets give each permission holder a separate allowance, checked with their
|
||||
current session. Override scopeValue, when set, narrows the allowance to one user id.
|
||||
|
||||
Existing policy/config budgets and provider monthly caps remain additional limits;
|
||||
an override of a budget row does not waive those existing caps.
|
||||
|
||||
Provider and MODEL scope values are numeric provider/model registry row ids;
|
||||
USER is a user id and ROLE is a permission name. INSTANCE has no scope value.
|
||||
Periods reset at midnight UTC or the first of the month UTC. Token usage includes
|
||||
input, output, cache-read and cache-write tokens; USD uses the recorded micro-dollar
|
||||
cost, not today's model price. No default budget rows are seeded.
|
||||
|
||||
## P-c: classification, PHI routing and encrypted storage
|
||||
|
||||
Apply these additive schema changes before deploying P-c (adjust column mappings
|
||||
for your backend). New conversations/turns record `PUBLIC`, `CONFIDENTIAL`, `PII`
|
||||
or `PHI`; legacy null classifications read as PUBLIC. Backfill known historical
|
||||
PHI conversations to PHI before allowing their history to be used with a provider.
|
||||
|
||||
```sql
|
||||
ALTER TABLE assistant_conversation ADD COLUMN max_classification VARCHAR(16);
|
||||
ALTER TABLE assistant_turn ADD COLUMN classification VARCHAR(16);
|
||||
ALTER TABLE assistant_turn ADD COLUMN classification_tables VARCHAR(500);
|
||||
ALTER TABLE assistant_conversation ALTER COLUMN title TYPE TEXT;
|
||||
ALTER TABLE assistant_tool_call ALTER COLUMN result_summary TYPE TEXT;
|
||||
ALTER TABLE assistant_pending_action ALTER COLUMN summary TYPE TEXT;
|
||||
ALTER TABLE assistant_pending_action ALTER COLUMN result_summary TYPE TEXT;
|
||||
CREATE TABLE assistant_data_audit
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(255),
|
||||
conversation_id INTEGER,
|
||||
turn_id INTEGER,
|
||||
provider_id INTEGER,
|
||||
provider_name VARCHAR(200),
|
||||
model VARCHAR(200),
|
||||
hipaa_eligible BOOLEAN,
|
||||
classification VARCHAR(16),
|
||||
tables VARCHAR(500),
|
||||
decision VARCHAR(16),
|
||||
reason VARCHAR(1000),
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_data_audit_user_id ON assistant_data_audit (user_id);
|
||||
CREATE INDEX assistant_data_audit_conversation_id ON assistant_data_audit (conversation_id);
|
||||
CREATE INDEX assistant_data_audit_turn_id ON assistant_data_audit (turn_id);
|
||||
```
|
||||
|
||||
`assistant_data_audit` is an admin read-only, owner-locked child list on a
|
||||
conversation, outside navigation. It records each model call carrying PII/PHI and
|
||||
each routing block, without record values. It expires with the conversation.
|
||||
PHI conversations use `hipaaRetentionDays` (2190 by default), even outside HIPAA
|
||||
mode; null uses normal retention. Existing tool-result/replay cleanup still uses
|
||||
`toolResultRetentionDays`.
|
||||
|
||||
Before enabling HIPAA mode, mark the permitted provider rows `hipaaEligible = true`
|
||||
and provision `conversationEncryptionKeyEnvName` with a base64-encoded 32-byte
|
||||
key. Startup refuses missing or invalid keys. A code-only provider is not eligible.
|
||||
The flag records the operator's eligibility decision; it does not establish a BAA.
|
||||
|
||||
New/updated message parts, titles, conversation context, tool payloads/redactions
|
||||
and summaries, replay data, and pending-action payloads/summaries use
|
||||
`enc:v1:<base64(iv|ciphertext|tag)>` (AES-256-GCM, random 12-byte IV, 16-byte tag).
|
||||
Only `AssistantStore` decrypts; direct backend/admin record views show ciphertext.
|
||||
String columns holding encrypted summaries/titles must be TEXT to allow expansion.
|
||||
Legacy plaintext reads are supported; existing rows are not automatically rewritten.
|
||||
For an existing deployment, migrate legacy payloads through AssistantStore under
|
||||
the appropriate owner/admin session before treating the whole archive as encrypted.
|
||||
Keep the key available for the lifetime of encrypted data and backups. Changing the
|
||||
key requires a controlled decrypt/re-encrypt migration; P-c has no key rotation job.
|
||||
Corrupt ciphertext, a wrong key, and encrypted data without a key fail closed.
|
||||
|
||||
## 0.1.0-RC.3 to 0.1.0-RC.4: provider settings, prices, per-call usage, app home
|
||||
|
||||
Three new tables. Create them before you deploy (see `docs/schema-postgres.sql`, which also seeds the Anthropic prices):
|
||||
- `assistant_provider` (with the `api_key_status` column: it is filled on read and never written, but the backend selects it)
|
||||
- `assistant_model_price`
|
||||
- `assistant_usage`
|
||||
|
||||
Then, to keep answering exactly as before:
|
||||
1. Register the provider type: `config.withProviderType(new AnthropicProviderType())`. Keep `withModelProvider(AnthropicModelProvider.fromEnvironment())` as the fallback.
|
||||
2. Add one provider row (or seed it in your migration): type `anthropic`, model = your current main model, enabled, default, `apiKeyEnvName` = `ANTHROPIC_API_KEY` (or leave it empty for the default). No key value is ever written.
|
||||
3. Run **Test connection** on it.
|
||||
|
||||
Until a row exists, the provider registered in code answers, as in RC.3. Other changes: the app label is **Assistant**; the nav no longer lists the retention process (it is a table action on conversations) or the audit tables (they are child lists). Grants on `assistant.admin` cover everything new.
|
||||
|
||||
## 0.1.0-RC.1 to 0.1.0-RC.3: snake_case names
|
||||
|
||||
The rename shipped in 0.1.0-RC.3. 0.1.0-RC.2 on Maven Central is an accidental republish of the RC.1 code (see CHANGELOG.md), so upgrading from RC.2 is the same as upgrading from RC.1.
|
||||
|
||||
RC.3 applies the QRun-IO naming rule (owner decision, 2026-10-12): *"table names should always be lowercase and use _, and qbits should always be prefixed with their name"*. The QQQ table names, the owner security key type and the possible-value source change. Permissions do not. **This is a metadata-only change**: no table, column or index in your database changes, and no Liquibase or DDL step is needed.
|
||||
|
||||
### What changed
|
||||
|
||||
| Kind | RC.1 (and RC.2) name | RC.3 name |
|
||||
|---|---|---|
|
||||
| Table | `assistantConversation` | `assistant_conversation` |
|
||||
| Table | `assistantMessage` | `assistant_message` |
|
||||
| Table | `assistantTurn` | `assistant_turn` |
|
||||
| Table | `assistantTurnEvent` | `assistant_turn_event` |
|
||||
| Table | `assistantToolCall` | `assistant_tool_call` |
|
||||
| Table | `assistantFeedback` | `assistant_feedback` |
|
||||
| Table | `assistantPolicy` | `assistant_policy` |
|
||||
| Table | `assistantUsageDaily` | `assistant_usage_daily` |
|
||||
| Possible-value source (conversations) | `assistantConversation` | `assistant_conversation` |
|
||||
| Permissions | `assistant.hasAccess`, `assistant.admin` | unchanged (QQQ's standard `<base>.hasAccess` suffix; the `assistant` base already follows the rule) |
|
||||
| Security key type | `assistantUserId` | `assistant_user_id` |
|
||||
| All-access key | `assistantUserIdAllAccess` | `assistant_user_id_all_access` |
|
||||
|
||||
Unchanged: Java class names, field names (`userId`, `conversationId`, ...), the `assistantAdmin` app, the `assistantRetention` process, the `/qqq/v1/assistant` routes and the `assistant` supplemental instance meta-data.
|
||||
|
||||
The old names stay available as `@Deprecated` constants for one release, so migration code can refer to them: `<Entity>.LEGACY_TABLE_NAME`, `AssistantQBitConfig.LEGACY_DEFAULT_OWNER_KEY_TYPE` and `AssistantMetaDataBuilder.LEGACY_ALL_ACCESS_SUFFIX`. They will be removed in 0.2.0.
|
||||
|
||||
### Physical tables
|
||||
|
||||
`docs/schema-postgres.sql` already creates `assistant_*` tables. If your RDBMS backend mapped the old QQQ names to those tables (for example with `inferBackendName`), the new QQQ names now equal the physical names and nothing else is needed; the mapping becomes a no-op. If you created tables literally named `assistantConversation` and so on, either rename them:
|
||||
|
||||
```sql
|
||||
ALTER TABLE "assistantConversation" RENAME TO assistant_conversation;
|
||||
ALTER TABLE "assistantMessage" RENAME TO assistant_message;
|
||||
ALTER TABLE "assistantTurn" RENAME TO assistant_turn;
|
||||
ALTER TABLE "assistantTurnEvent" RENAME TO assistant_turn_event;
|
||||
ALTER TABLE "assistantToolCall" RENAME TO assistant_tool_call;
|
||||
ALTER TABLE "assistantFeedback" RENAME TO assistant_feedback;
|
||||
ALTER TABLE "assistantPolicy" RENAME TO assistant_policy;
|
||||
ALTER TABLE "assistantUsageDaily" RENAME TO assistant_usage_daily;
|
||||
```
|
||||
|
||||
or keep them and set `RDBMSTableBackendDetails.tableName` to the old name in your table customizer.
|
||||
|
||||
### Stored references to update
|
||||
|
||||
QQQ stores table names, permission names and key names outside this qbit. Update them in the same deployment. The examples are for Postgres with QQQ's default snake_case backend names; adjust the table and column names to your schema.
|
||||
|
||||
1. **Security keys.** Permission grants (`assistant.hasAccess`, `assistant.admin`) need no change. If your authentication customizer grants the all-access key itself rather than calling `AssistantSecurity.applySessionKeys`, grant `assistant_user_id_all_access`. If you set `ownerSecurityKeyType` explicitly, the all-access key is now `<keyType>_all_access`.
|
||||
|
||||
2. **Saved views and saved reports** on the assistant tables:
|
||||
|
||||
```sql
|
||||
UPDATE saved_view SET table_name = CASE table_name
|
||||
WHEN 'assistantConversation' THEN 'assistant_conversation'
|
||||
WHEN 'assistantMessage' THEN 'assistant_message'
|
||||
WHEN 'assistantTurn' THEN 'assistant_turn'
|
||||
WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
|
||||
WHEN 'assistantToolCall' THEN 'assistant_tool_call'
|
||||
WHEN 'assistantFeedback' THEN 'assistant_feedback'
|
||||
WHEN 'assistantPolicy' THEN 'assistant_policy'
|
||||
WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
|
||||
ELSE table_name END
|
||||
WHERE table_name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
|
||||
'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
|
||||
```
|
||||
|
||||
Run the same statement against `saved_report` (and `quick_saved_view` / `shared_saved_view` only if they store a table name in your schema).
|
||||
|
||||
3. **Audit table names.** QQQ audits reference tables through the `audit_table` lookup. Rename the rows so old and new audits stay under one table:
|
||||
|
||||
```sql
|
||||
UPDATE audit_table SET name = CASE name
|
||||
WHEN 'assistantConversation' THEN 'assistant_conversation'
|
||||
WHEN 'assistantMessage' THEN 'assistant_message'
|
||||
WHEN 'assistantTurn' THEN 'assistant_turn'
|
||||
WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
|
||||
WHEN 'assistantToolCall' THEN 'assistant_tool_call'
|
||||
WHEN 'assistantFeedback' THEN 'assistant_feedback'
|
||||
WHEN 'assistantPolicy' THEN 'assistant_policy'
|
||||
WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
|
||||
ELSE name END
|
||||
WHERE name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
|
||||
'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
|
||||
```
|
||||
|
||||
If a new-name row was already created (the app ran RC.3 before this update), repoint `audit.audit_table_id` from the old row to the new one first, then delete the old row.
|
||||
|
||||
4. **Anything else that stores a table name**: frontend links or bookmarks to `/app/.../assistantConversation`, Quick Search index rows (`quick_search_index.table_name`; reindex instead of editing), ESB topics or reports you configured by table name.
|
||||
|
||||
### Order
|
||||
|
||||
Deploy RC.3 and run the statements above in the same maintenance window. Until the update runs, saved views and reports on the assistant tables do not open, and admins who get the all-access key from their own customizer only see their own rows.
|
||||
|
||||
## 0.1.0-RC.6: tools through the UI API
|
||||
|
||||
Database (see `docs/schema-postgres.sql`):
|
||||
- new tables `assistant_data_access`, `assistant_pending_action`, `assistant_profile`, `assistant_skill` and `assistant_profile_skill`;
|
||||
- new columns `assistant_tool_call.ui_api_operation`, `http_status`, `audit_href`, `pending_action_id`;
|
||||
- new columns `assistant_policy.writes_require_confirmation`, `allowed_tools`, `max_rows_per_call`, `redact_pii`.
|
||||
|
||||
Runtime: the tools call this server's own `/qqq/v1` on `127.0.0.1` and the port the first assistant request arrived on. If the server is not reachable that way (a different bind address, a TLS-only listener), set `withUiApiBaseUrl(...)`. Grant `assistant.admin` holders the new tables through the qbit's existing admin permission (no new permission names).
|
||||
|
||||
Behavior: nothing is writable until an admin adds a READ_WRITE data access row; before that the assistant reads exactly what it did before, now through the UI API.
|
||||
|
||||
## P-a: provider and model registry
|
||||
|
||||
Before starting the new version, database-backed hosts add the provider columns
|
||||
and model sub-table below. Memory-backed hosts need no migration. Existing rows
|
||||
derive kind and location from the adapter and keep their model behavior until
|
||||
models are registered. Keys remain environment-variable references.
|
||||
|
||||
```sql
|
||||
ALTER TABLE assistant_provider
|
||||
ADD COLUMN kind VARCHAR(30),
|
||||
ADD COLUMN tls_ca_bundle VARCHAR(100),
|
||||
ADD COLUMN location VARCHAR(10),
|
||||
ADD COLUMN hipaa_eligible BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN region VARCHAR(50),
|
||||
ADD COLUMN api_version VARCHAR(30),
|
||||
ADD COLUMN connect_timeout_seconds INTEGER DEFAULT 10 CHECK (connect_timeout_seconds BETWEEN 1 AND 600),
|
||||
ADD COLUMN request_timeout_seconds INTEGER DEFAULT 120 CHECK (request_timeout_seconds BETWEEN 1 AND 600),
|
||||
ADD COLUMN last_discovery_at TIMESTAMP WITH TIME ZONE,
|
||||
ADD COLUMN last_discovery_status VARCHAR(16),
|
||||
ADD COLUMN last_discovery_error VARCHAR(500);
|
||||
ALTER TABLE assistant_provider ALTER COLUMN model TYPE VARCHAR(200);
|
||||
ALTER TABLE assistant_provider ALTER COLUMN last_test_model TYPE VARCHAR(200);
|
||||
ALTER TABLE assistant_usage ALTER COLUMN model TYPE VARCHAR(200);
|
||||
ALTER TABLE assistant_message ALTER COLUMN model TYPE VARCHAR(200);
|
||||
ALTER TABLE assistant_turn ALTER COLUMN model TYPE VARCHAR(200);
|
||||
|
||||
CREATE TABLE assistant_model
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_id INTEGER NOT NULL REFERENCES assistant_provider(id),
|
||||
model_id VARCHAR(200) NOT NULL,
|
||||
display_name VARCHAR(200),
|
||||
enabled BOOLEAN,
|
||||
supports_tools BOOLEAN,
|
||||
supports_vision BOOLEAN,
|
||||
supports_streaming BOOLEAN,
|
||||
supports_reasoning_effort BOOLEAN,
|
||||
context_window_tokens INTEGER,
|
||||
max_output_tokens INTEGER,
|
||||
input_usd_per_m_tok NUMERIC(12, 4),
|
||||
output_usd_per_m_tok NUMERIC(12, 4),
|
||||
source VARCHAR(16),
|
||||
discovered_at TIMESTAMP WITH TIME ZONE,
|
||||
last_test_at TIMESTAMP WITH TIME ZONE,
|
||||
last_test_status VARCHAR(16),
|
||||
last_test_latency_ms INTEGER,
|
||||
last_test_error VARCHAR(500),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
UNIQUE (provider_id, model_id)
|
||||
);
|
||||
```
|
||||
|
||||
Register the optional `qbit-assistant-provider-openai` module and
|
||||
`OpenAiCompatibleProviderType` to offer its kinds. Azure and generic endpoints
|
||||
require an explicit `allowedBaseUrls` entry; local defaults may be keyless.
|
||||
Discover models preserves existing rows. Cloud catalogs larger than 20 are added
|
||||
disabled; enable the models to offer before leaving the provider's model empty.
|
||||
HIPAA eligibility records an administrator's fact; P-a does not implement PHI routing.
|
||||
|
||||
Custom CA bundles are server configuration, never database content. Register them
|
||||
with `withTrustedCaBundle(name, pem)` or `withTrustedCaBundleFile(name, path)` at
|
||||
startup, then set the HTTPS provider row's `tlsCaBundle` to that name. Empty
|
||||
references retain JVM default trust. The bundle adds trust without replacing JVM
|
||||
roots; hostname verification remains enabled. If the P-a columns were already
|
||||
applied, add only `tls_ca_bundle VARCHAR(100)` to `assistant_provider`.
|
||||
|
||||
|
||||
## P-b: AWS Bedrock and Google Vertex
|
||||
|
||||
Before starting this version, database-backed hosts that already applied P-a add:
|
||||
|
||||
```sql
|
||||
ALTER TABLE assistant_provider ADD COLUMN cloud_project VARCHAR(100);
|
||||
```
|
||||
|
||||
Fresh installations use `docs/schema-postgres.sql`; memory-backed hosts need no
|
||||
migration. Existing providers leave the new column null. Vertex requires the GCP
|
||||
project id in `cloudProject`; Bedrock and Vertex both require `region` and reject
|
||||
`apiKeyEnvName`. They default to CLOUD; `hipaaEligible` remains a manual admin fact.
|
||||
|
||||
Add the optional `qbit-assistant-provider-bedrock` and/or
|
||||
`qbit-assistant-provider-vertex` module at the QBit version and register
|
||||
`BedrockProviderType` / `VertexProviderType`. Credentials come from the server's
|
||||
AWS default chain / Google Application Default Credentials. No credentials are
|
||||
stored in provider rows. Hosts using AWS IRSA or web identity supply
|
||||
`software.amazon.awssdk:sts` on their own runtime classpath (matching the
|
||||
transitive AWS SDK version); this QBit does not add it. See README Providers for
|
||||
configuration, endpoint overrides and static discovery limitations.
|
||||
@@ -1,246 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ QQQ - Low-code Application Framework for Engineers.
|
||||
~ Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
~ 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
~ contact@kingsrook.com
|
||||
~ https://github.com/Kingsrook/
|
||||
~
|
||||
~ 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
|
||||
~
|
||||
~ https://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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
|
||||
<!--
|
||||
See reference at https://checkstyle.sourceforge.io/checks.html
|
||||
-->
|
||||
<module name = "Checker">
|
||||
<property name="charset" value="UTF-8"/>
|
||||
|
||||
<property name="severity" value="warning"/>
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
|
||||
<module name="FileTabCharacter">
|
||||
<property name="eachLine" value="true"/>
|
||||
</module>
|
||||
|
||||
<!--
|
||||
<module name="LineLength">
|
||||
<property name="max" value="100"/>
|
||||
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
|
||||
</module>
|
||||
-->
|
||||
|
||||
<module name="TreeWalker">
|
||||
<module name="SuppressWarningsHolder"/>
|
||||
<module name="OuterTypeFilename"/>
|
||||
<module name="IllegalTokenText">
|
||||
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
|
||||
<property name="format" value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
|
||||
<property name="message" value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
|
||||
</module>
|
||||
<module name="AvoidEscapedUnicodeCharacters">
|
||||
<property name="allowEscapesForControlCharacters" value="true"/>
|
||||
<property name="allowByTailComment" value="true"/>
|
||||
<property name="allowNonPrintableEscapes" value="true"/>
|
||||
</module>
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="OneTopLevelClass"/>
|
||||
<module name="NoLineWrap"/>
|
||||
<module name="EmptyBlock">
|
||||
<property name="option" value="TEXT"/>
|
||||
<property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
|
||||
</module>
|
||||
<module name="NeedBraces"/>
|
||||
<module name="LeftCurly">
|
||||
<property name="option" value="nl"/>
|
||||
</module>
|
||||
<module name="RightCurly">
|
||||
<property name="option" value="alone"/>
|
||||
</module>
|
||||
<!--
|
||||
<module name="WhitespaceAround">
|
||||
<property name="allowEmptyConstructors" value="true"/>
|
||||
<property name="allowEmptyMethods" value="true"/>
|
||||
<property name="allowEmptyTypes" value="true"/>
|
||||
<property name="allowEmptyLoops" value="true"/>
|
||||
<message key="ws.notFollowed"
|
||||
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
|
||||
<message key="ws.notPreceded"
|
||||
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
-->
|
||||
<module name="OneStatementPerLine"/>
|
||||
<module name="MultipleVariableDeclarations"/>
|
||||
<module name="ArrayTypeStyle"/>
|
||||
<module name="MissingSwitchDefault"/>
|
||||
<module name="FallThrough"/>
|
||||
<module name="UpperEll"/>
|
||||
<module name="ModifierOrder"/>
|
||||
<module name="EmptyLineSeparator">
|
||||
<property name="allowNoEmptyLineBetweenFields" value="true"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<property name="id" value="SeparatorWrapDot"/>
|
||||
<property name="tokens" value="DOT"/>
|
||||
<property name="option" value="nl"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<property name="id" value="SeparatorWrapComma"/>
|
||||
<property name="tokens" value="COMMA"/>
|
||||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
|
||||
<property name="id" value="SeparatorWrapEllipsis"/>
|
||||
<property name="tokens" value="ELLIPSIS"/>
|
||||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
|
||||
<property name="id" value="SeparatorWrapArrayDeclarator"/>
|
||||
<property name="tokens" value="ARRAY_DECLARATOR"/>
|
||||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<property name="id" value="SeparatorWrapMethodRef"/>
|
||||
<property name="tokens" value="METHOD_REF"/>
|
||||
<property name="option" value="nl"/>
|
||||
</module>
|
||||
<module name="PackageName">
|
||||
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Package name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="TypeName">
|
||||
<message key="name.invalidPattern"
|
||||
value="Type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MemberName">
|
||||
<property name="format" value="^[_a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern" value="Member name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ParameterName">
|
||||
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern" value="Parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="CatchParameterName">
|
||||
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern" value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="LocalVariableName">
|
||||
<property name="tokens" value="VARIABLE_DEF"/>
|
||||
<property name="format" value="^[a-zA-Z]([a-z0-9A-Z][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern" value="Local variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ClassTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
|
||||
<message key="name.invalidPattern" value="Class type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MethodTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
|
||||
<message key="name.invalidPattern" value="Method type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="InterfaceTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][_A-Z0-9]*$)"/>
|
||||
<message key="name.invalidPattern" value="Interface type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="NoFinalizer"/>
|
||||
<module name="GenericWhitespace">
|
||||
<message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
|
||||
<message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
|
||||
<message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
|
||||
<message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
<module name="Indentation">
|
||||
<property name="basicOffset" value="3"/>
|
||||
<property name="braceAdjustment" value="0"/>
|
||||
<property name="caseIndent" value="3"/>
|
||||
<property name="throwsIndent" value="6"/>
|
||||
<property name="lineWrappingIndentation" value="3"/>
|
||||
<property name="arrayInitIndent" value="6"/>
|
||||
</module>
|
||||
<!--
|
||||
<module name="AbbreviationAsWordInName">
|
||||
<property name="ignoreFinal" value="false"/>
|
||||
<property name="allowedAbbreviationLength" value="1"/>
|
||||
</module>
|
||||
<module name="OverloadMethodsDeclarationOrder"/>
|
||||
<module name="VariableDeclarationUsageDistance"/>
|
||||
<module name="CustomImportOrder">
|
||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||
<property name="separateLineBetweenGroups" value="true"/>
|
||||
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
|
||||
</module>
|
||||
-->
|
||||
<module name="CustomImportOrder">
|
||||
<property name="customImportOrderRules" value="SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###THIRD_PARTY_PACKAGE###STATIC"/>
|
||||
<property name="specialImportsRegExp" value="^javax\."/>
|
||||
<property name="standardPackageRegExp" value="^java\."/>
|
||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||
<property name="separateLineBetweenGroups" value="false"/>
|
||||
</module>
|
||||
<module name="MethodParamPad"/>
|
||||
<module name="NoWhitespaceBefore">
|
||||
<property name="tokens" value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
|
||||
<property name="allowLineBreaks" value="true"/>
|
||||
</module>
|
||||
<module name="ParenPad"/>
|
||||
<module name="OperatorWrap">
|
||||
<property name="option" value="NL"/>
|
||||
<property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationMostCases"/>
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationVariables"/>
|
||||
<property name="tokens" value="VARIABLE_DEF"/>
|
||||
<property name="allowSamelineMultipleAnnotations" value="true"/>
|
||||
</module>
|
||||
<module name="JavadocMethod">
|
||||
<property name="allowMissingParamTags" value="true"/>
|
||||
<property name="allowMissingReturnTag" value="true"/>
|
||||
<property name="allowedAnnotations" value="Override, Test"/>
|
||||
</module>
|
||||
<module name="MissingJavadocMethod">
|
||||
<property name="scope" value="private"/>
|
||||
</module>
|
||||
<module name="MissingJavadocType">
|
||||
<property name="scope" value="private"/>
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^[a-z][a-zA-Z0-9_]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="EmptyCatchBlock">
|
||||
<property name="exceptionVariableName" value="expected"/>
|
||||
</module>
|
||||
<module name="CommentsIndentation"/>
|
||||
|
||||
<!-- this would be nice, but requires the the @inheritDoc javadoc tag... -->
|
||||
<module name="MissingOverride"/>
|
||||
|
||||
</module>
|
||||
<module name="Header">
|
||||
<property name="headerFile" value="checkstyle/license.txt"/>
|
||||
<property name="fileExtensions" value="java"/>
|
||||
<property name="ignoreLines" value="3"/>
|
||||
</module>
|
||||
<module name="SuppressWarningsFilter"/>
|
||||
</module>
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
@@ -0,0 +1,28 @@
|
||||
# Getting Started
|
||||
|
||||
Targets Java 21, Maven 3.8 or later, and QQQ 4.0.0. Maven resolves the framework dependencies from Maven Central.
|
||||
|
||||
1. Choose **Use this template** on GitHub, then clone the repository you created.
|
||||
2. In `pom.xml`, set your own `groupId`, `artifactId`, name, description, and version.
|
||||
3. Use your IDE's package refactoring to rename `com.kingsrook.qbits.example` throughout `src/`. Rename the `Example*` classes and update their imports and references with the IDE's rename refactoring.
|
||||
4. Update the QBit producer's `GROUP_ID`, `ARTIFACT_ID`, and `VERSION` constants to match your project. Give the metadata names their own stable names before combining the QBit with other examples.
|
||||
5. Build the generated project:
|
||||
|
||||
```bash
|
||||
mvn clean verify
|
||||
```
|
||||
|
||||
The repository provides example Java sources; customization uses normal package/class refactoring. Keep example coordinates unpublished. After customizing, add tests for your QBit's behavior before setting up its publishing workflow.
|
||||
|
||||
## Register with a host application
|
||||
|
||||
The following uses the original example names; substitute the names chosen above:
|
||||
|
||||
```java
|
||||
new ExampleExtensionQBitProducer()
|
||||
.withConfig(new ExampleExtensionQBitConfig()
|
||||
.withTargetTableName("order"))
|
||||
.produce(qInstance, "my-extension");
|
||||
```
|
||||
|
||||
Register the target table before producing the extension, then implement its customizer. See [Extension Patterns](01-extension-patterns.md).
|
||||
@@ -0,0 +1,16 @@
|
||||
# Extension Patterns
|
||||
|
||||
[ExampleExtensionQBitProducer](../src/main/java/com/kingsrook/qbits/example/ExampleExtensionQBitProducer.java) registers the QBit identity and calls `applyTableCustomizations` for the configured target. That method is the extension point: look up the table, validate that it exists, and call your customized [ExampleTableCustomizer](../src/main/java/com/kingsrook/qbits/example/customizers/ExampleTableCustomizer.java).
|
||||
|
||||
```java
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
throw new QException("Target table not found: " + tableName);
|
||||
}
|
||||
new ExampleTableCustomizer().customize(table);
|
||||
```
|
||||
|
||||
When implementing this method, add `throws QException` to its declaration. Run the config's `validate(QInstance, List<String>)` before registering metadata, and report validation errors rather than silently ignoring a missing target. The uncustomized template contains placeholders for this behavior.
|
||||
|
||||
For record hooks, use QQQ's existing table customizer interfaces or `AbstractPreInsertCustomizer`. Its `apply(List<QRecord>)` method returns the records for the next stage; register the customizer through the table's `TableCustomizers.PRE_INSERT_RECORD` role. Check the actual QQQ interface before adding another hook. Add tests proving that the intended table changes and invalid configurations are handled.
|
||||
@@ -1,655 +0,0 @@
|
||||
# qbit-assistant: settings, usage and app home (design)
|
||||
|
||||
The assistant's overall design is the QQQ spec `QRun-IO/qqq` `docs/specs/2026-10-11-qqq-assistant-qbit-design.md` (owner decisions D1 to D7). This note covers what 0.1.0-RC.4 adds on top of it: provider settings, per-call usage, prices and the app home.
|
||||
|
||||
Owner request (2026-10-13): *"it needs a good homepage for the app for it, and it needs a settings screen where you setup the API keys, provider, test connectivity etc. and the app homepage should show health, usage, costs, etc."*
|
||||
|
||||
## Data
|
||||
|
||||
All of it is in standard QQQ tables with full record screens (D6), prefixed `assistant_`, secured by `assistant.admin` and, for user-owned rows, the `assistant_user_id` record lock. Every `assistant_*` table has AI exposure `NONE`.
|
||||
|
||||
| Table | Written by | Purpose |
|
||||
|---|---|---|
|
||||
| `assistant_provider` | admins | Which provider and model answer, the key reference, limits, the last connection test. |
|
||||
| `assistant_model_price` | admins (seeded) | USD per million tokens per model. |
|
||||
| `assistant_usage` | the qbit only | One row per model call: turn steps (`TURN`) and connection tests (`TEST`). |
|
||||
|
||||
`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.
|
||||
|
||||
## 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.
|
||||
|
||||
Why:
|
||||
- QQQ core has no encrypted or secret field type. The key-provider SPI and Tink cipher in the RC.6 batch (qqq#1123, #1107) encrypt streamed backup artifacts, not columns, and are not released.
|
||||
- Spec §2.8: keys are `${env.*}` secrets and are never stored in tables.
|
||||
- Deployments already hold the key in a secret store (kof22: Infisical, then an ExternalSecret, then the pod env). Rotating it there is the "replace key" step; the database never has a copy to leak through a backup, an export or the API.
|
||||
|
||||
Guards:
|
||||
- Only allow-listed names: each registered type's default (`ANTHROPIC_API_KEY`) plus `allowedApiKeyEnvNames`. Otherwise an admin could aim a row at `DATABASE_PASSWORD` and send it to a provider.
|
||||
- A base URL override must be in `allowedBaseUrls`. Otherwise an admin could send the real key to their own host.
|
||||
- The variable name must look like one (`[A-Z_][A-Z0-9_]*`), so a key pasted into the field is refused before it is stored. The error never echoes the value.
|
||||
- `apiKeyStatus` is a real, read-only column filled by the post-query customizer: `Set • …abcd (from VAR)` (no characters for keys under 12) or `Not set`. Saves drop it, so it is always null in the database. (A virtual field cannot sit in a base section in QQQ 4.1, hence the column.)
|
||||
- Clients are cached by row settings and a SHA-256 of the key. Keys are never logged, and neither `toString` nor the errors contain one.
|
||||
|
||||
## Choosing the provider
|
||||
|
||||
`ProviderService.resolve(policyModel)`:
|
||||
1. The enabled row marked default (marking one default unmarks the others), else the first enabled row.
|
||||
2. With no enabled row, the provider registered in code (`withModelProvider`) and the policy's main model: hosts that never add a row keep working.
|
||||
3. A row that cannot be used (no key, a disallowed name or URL, an uninstalled type) makes the assistant report `NOT_CONFIGURED` rather than fall back silently.
|
||||
|
||||
The model is the row's, else the policy's main model. The rows and prices are cached for `policyCacheSeconds` and dropped on any change.
|
||||
|
||||
## Limits
|
||||
|
||||
- **Monthly cost cap (USD):** checked when a turn starts (429 `BUDGET`) and before each model call of a running turn, from the provider's `assistant_usage` cost since the 1st (UTC).
|
||||
- **Max output tokens per request:** lowers the request's `max_tokens`.
|
||||
- **Max concurrent requests:** a per-provider semaphore around each model call on this replica.
|
||||
|
||||
These sit next to the existing policy budgets (per user per day, per instance per month), which still apply.
|
||||
|
||||
## Test connection
|
||||
|
||||
A record action on `assistant_provider`, as a standard streamed ETL with Review and Result summary lines and no custom form.
|
||||
- **Review** lists the providers, the masked key status and every settings problem, without calling anything.
|
||||
- **Result** makes one call per provider: the prompt "Connection test. Reply with the single word OK.", at most 32 output tokens, effort low. It reports `connected to <model> in <n> ms`, or a hint, the kind, the HTTP status and the provider's own credential-free message.
|
||||
|
||||
The result goes on the row (`lastTest*`) and into `assistant_usage` (`TEST`), so a test's cost is counted like any other call.
|
||||
|
||||
## Cost
|
||||
|
||||
`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.
|
||||
|
||||
## App home
|
||||
|
||||
One HTML widget (`assistantHome`) on the Assistant app. It is the only place this data is summarized: no other dashboard repeats it, and the developer panel stays per-record.
|
||||
|
||||
`AssistantHomeData` reads the tables as the viewing admin, at most `homeMaxRows` rows each, for the last 30 days (UTC) and the current month, and says when totals are partial. `AssistantHomeView` renders it in flow and grid layout only:
|
||||
- no tables, fixed widths or inner scroll areas
|
||||
- inherited text colors, so light and dark both work
|
||||
- a status row, one row per provider, then cards for usage by day, costs, users, tools and failures.
|
||||
|
||||
Every section has an empty state; nothing is invented.
|
||||
|
||||
The nav is the home plus the main tables: Providers, Conversations, Model prices, Policy. Messages, turns, tool calls, feedback and model calls are child lists on a conversation, and model calls also on a provider. Processes are actions on their tables.
|
||||
|
||||
## Strict tools (0.1.0-RC.5)
|
||||
|
||||
**Incident (kof22, 2026-10-13).** Every turn failed on its first model call with 400 `invalid_request_error`: "The compiled grammar is too large, which would cause performance issues. Simplify your tool schemas or reduce the number of strict tools."
|
||||
- The request sent all ten read tools with `strict: true`.
|
||||
- Anthropic compiles every strict schema in a request into one grammar.
|
||||
- The request was inside every documented limit: 20 strict tools, 24 optional parameters, 16 union parameters. The real limit is not published.
|
||||
- The log line only said `INVALID_REQUEST httpStatus=400`, so the reason was invisible.
|
||||
|
||||
**Decision.**
|
||||
- **No tool is strict by default.** `AssistantQBitConfig.strictToolNames` opts tools in.
|
||||
- Strict mode only guaranteed schema-valid input. The kernel already validates every input and returns `INVALID_INPUT` results, and the UI-API tools (below) go through the same validation as the UI.
|
||||
- If a provider still refuses strict schemas (`ModelProviderException.isStrictSchemaRejected`), the turn retries once with no strict tools and logs a WARN.
|
||||
|
||||
**Budget.**
|
||||
- `AnthropicRequestContractTest` fails the build when the strict tools of the default request exceed 3 tools, 6 optional parameters or 2 union parameters. These budgets are far below the shape that failed.
|
||||
- It also checks the documented request rules:
|
||||
- adaptive thinking without `budget_tokens`, and no sampling parameters;
|
||||
- a valid effort, and no forced `tool_choice`;
|
||||
- at most 4 cache breakpoints, with longer TTLs first;
|
||||
- a user-first, user-last message order;
|
||||
- strict-schema keywords.
|
||||
|
||||
**Real-API check (dev).** `ANTHROPIC_API_KEY=... ./mvnw verify -Dassistant.live=true -Dtest.excludedGroups= -Dgroups=live -Djacoco.skip=true -pl qbit-assistant-provider-anthropic -am` runs the live golden evals. They send exactly this request shape.
|
||||
|
||||
**Diagnostics.** On a 4xx the provider's error type, sanitized message and request id are:
|
||||
- logged at WARN;
|
||||
- recorded as the `assistant_usage` request id;
|
||||
- shown to `assistant.admin` holders, in the panel's error and in Test connection.
|
||||
|
||||
## Owner decisions (2026-10-13)
|
||||
|
||||
Owner: *"the agent should have to go through the same API that the UIUX does. that way it is held to the same security boundaries etc"*, plus the ability to configure the assistant and verify its security and data access.
|
||||
|
||||
1. **Tools via the UI API.** Every tool that reads or writes data, or runs a process, report or widget, runs a spec'd `/qqq/v1` operation with the calling user's own session. That means the UI's authentication, permission checks, record locks, validation, audit and error mapping. There are no direct core data actions and no elevated sessions.
|
||||
2. **Data access is configured and verifiable in the UI.**
|
||||
- Each table and process has an AI data access setting: NONE, READ or READ_WRITE. Sensitive tables default to NONE.
|
||||
- Admins can see what the assistant could do for a given user, and why.
|
||||
3. **Writes need confirmation.** A write tool only *proposes* the change, as a pending action card. Nothing changes until the user confirms in the panel, unless the policy turns confirmation off.
|
||||
4. **Persona and skills, never more access** (owner, 2026-10-13): *"in the config for the AI settings, we should be able to set a name and personality and extra skills etc for the agent - and again - the agent should be limited to only using our UIUX API and being able to only do the things that the currently logged in user could do with the same UIUX API via the UIUX"*.
|
||||
- **Profiles** (`assistant_profile`, admin-edited): display name, icon, personality (tone instructions, at most 2,000 characters), greeting and up to 6 starter prompts.
|
||||
- One profile is the default. Others are scoped to an app, to users holding a permission (a role), or to both. The most specific profile wins.
|
||||
- `/assistant/config` returns the profile for the panel header, the greeting and the starter prompts.
|
||||
- **Skills** (`assistant_skill`, linked to profiles through `assistant_profile_skill`): named Markdown instruction packs of at most 6,000 characters, with a description, an enabled flag, trigger keywords and apps.
|
||||
- A skill is read only when relevant: a keyword appears in the question, the user is in one of its apps, or it has neither, in which case it is always read.
|
||||
- All skills read for one question total at most 12,000 characters.
|
||||
- Profile and skill text goes into the per-user system block, framed as "tone and know-how only". The rules come first, and rule 10 says these can never override the rules or give other tools, data or permissions.
|
||||
- **Hard guarantee:**
|
||||
- Every data, process, report or widget capability is a `/qqq/v1` operation, called through the gateway (core `UiApiInvoker` once it is released) with the CURRENT user's session.
|
||||
- The tool list is the spec'd operations ∩ data access ∩ policy. Profiles and skills are never read when tools are built or run.
|
||||
- It is enforced by ArchUnit: the tool, UI API and access code may not use QQQ table, process, report, dashboard or async actions, the core AI kernel, or the profile and skill classes. It is also enforced by a runtime guard: `AssistantStore` refuses any table that is not `assistant_*`.
|
||||
- `UiApiToolsSecurityTest.testDenialMatrix` checks that a user without permission gets the identical status and message from the UI API and through the assistant for query, get, insert, update and delete. The assistant has no process or report tool, and process data access defaults to NONE.
|
||||
5. All other open questions in this section were decided as recommended (shared rule: "all recommendations accepted").
|
||||
|
||||
## Tools through the UI API (0.1.0-RC.6)
|
||||
|
||||
### Transport: loopback HTTP with the user's session
|
||||
|
||||
`UiApiGateway.invoke(UiApiCall)` makes an HTTP request to this server's own `/qqq/v1`, carrying the user's `sessionUUID` cookie and `X-QQQ-Invoked-By: assistant`.
|
||||
- Only operations defined by `MiddlewareVersionV1`'s endpoint specs can be called. Anything else, including the assistant's own raw routes, is refused before a request is made.
|
||||
- A call without a session is also refused.
|
||||
|
||||
**Why loopback rather than calling the spec handlers in process.** The spec's `buildInput(Context)`, session setup, the version's `preExecute`, Javalin filters, permission checks and error mapping all hang off the Javalin request pipeline.
|
||||
- Calling the executors directly would skip some of these and drift over time.
|
||||
- Building a synthetic Javalin `Context` relies on internal API.
|
||||
- A request to the server's own port runs the identical pipeline by construction. It costs one local hop, which is milliseconds next to a model call.
|
||||
|
||||
This matches core's design (`docs/UI-API-EXTENSIONS.md` §5, `UiApiInvoker`, QRun-IO/qqq#1128).
|
||||
- `CoreUiApiGateway` adapts core's `UiApiInvoker` (the loopback stand-in is deleted). It labels calls with the client channel `assistant`, plus the turn id as detail (`assistant/<turnId>`). Form params go to the invoker's `withFormParam`; extra headers (the bulk-load tools' `X-QQQ-AI-Route`) go through `HeaderedUiApiInvoker`, a small subclass in core's package because core's request has no `withHeader` yet. A host can still inject its own gateway with `withUiApiGateway`.
|
||||
- The base URL is `uiApiBaseUrl` if configured. Otherwise it is `127.0.0.1` plus the local port the first assistant request arrived on, so in a multi-replica deployment the call stays on the replica that holds the session's process state.
|
||||
|
||||
### The tools
|
||||
|
||||
| tool | `/qqq/v1` operation |
|
||||
|---|---|
|
||||
| `list_tables` | `GET /metaData` (tables with the user's read/insert/edit/delete flags) |
|
||||
| `describe_table` | `GET /metaData/table/{tableName}` |
|
||||
| `query_records` | `POST /table/{tableName}/query` (plus `.../count` when there are more rows) |
|
||||
| `count_records` | `POST /table/{tableName}/count` |
|
||||
| `get_record` | `GET /table/{tableName}/{primaryKey}` |
|
||||
| `search_records` | `POST /search` |
|
||||
| `render_widget` | `POST /widget/{widgetName}` |
|
||||
| `get_record_history` | `GET /table/{tableName}/{primaryKey}`, then `POST /table/{tableName}/query` on `audit_table`, `audit` and `audit_detail` |
|
||||
| `create_record` / `update_record` / `delete_record` | `POST /table/{tableName}` / `PATCH /table/{tableName}/{primaryKey}` / `DELETE /table/{tableName}/{primaryKey}` |
|
||||
|
||||
`navigate` and `search_help` stay on the core kernel because they read no records:
|
||||
- `navigate` only builds an `/app` link, and the page enforces permissions when it opens.
|
||||
- `search_help` reads help text from metadata.
|
||||
|
||||
Running processes and reports through the assistant (`/processes/...`, `/reports/...`) is not offered yet. Process data access rows are stored so that the screens can already show and set them.
|
||||
|
||||
### Layers (each one can only narrow the one before)
|
||||
|
||||
1. **The user's own permissions and record locks**, enforced by the UI API itself. A user who cannot do something in the UI gets the identical status and message through the assistant: the tool result is `DENIED`/`NOT_FOUND`/`INVALID_INPUT` with `"<operation> answered HTTP <status>: <message>"`.
|
||||
2. **Data access** (`assistant_data_access`, editable by `assistant.admin`). A row sets NONE, READ or READ_WRITE per table (`TABLE`) or process (`PROCESS`). Without a row:
|
||||
- tables whose AI exposure is NONE (the `assistant_*` tables, sessions, OAuth, and any table whose metadata says so) are NONE, and a row cannot raise them;
|
||||
- other tables are READ, so the assistant never writes unless an admin allows it;
|
||||
- processes are NONE.
|
||||
The write tools are offered only when some table is READ_WRITE.
|
||||
3. **Policy** (`assistant_policy`):
|
||||
- `writesRequireConfirmation` (default true);
|
||||
- `allowedTools` (empty allows all);
|
||||
- `maxRowsPerCall` (default 50, ceiling 200);
|
||||
- `redactPii` (default true: secrets, card numbers and SSNs are redacted, with the core `AiRedactor`).
|
||||
4. **AI field exposure** from table metadata:
|
||||
- NONE and METADATA_ONLY values never reach the model;
|
||||
- MASKED values are masked;
|
||||
- only VALUES fields can be filtered or sorted on.
|
||||
|
||||
### Writes and confirmation
|
||||
|
||||
With confirmation on, a write tool validates the table's data access and the fields. It then inserts an `assistant_pending_action` row (owner-locked, `PENDING`, expiring after `pendingActionTtlSeconds`, default 15 minutes) and returns a `pending_action` render, which the panel shows as a card. The model is told the change is not done.
|
||||
- `POST /qqq/v1/assistant/actions/{id}/confirm` claims the action (`RUNNING`, so a double click cannot run it twice). It then runs the stored operation through the UI API **with the confirming request's session** and records the outcome as `EXECUTED` or `FAILED`.
|
||||
- `POST /qqq/v1/assistant/actions/{id}/reject` runs nothing.
|
||||
- `GET /qqq/v1/assistant/actions/{id}` reads the action's state.
|
||||
- Only the user who proposed an action can see or decide it.
|
||||
|
||||
### Tool-call log
|
||||
|
||||
Each `assistant_tool_call` row records:
|
||||
- the user and the tool;
|
||||
- the parameters, with secrets redacted;
|
||||
- the UI API operation and the HTTP status the UI would have seen;
|
||||
- the rows touched and the outcome;
|
||||
- the pending action, if there is one;
|
||||
- an in-app link to the written record, whose page shows its audit entries.
|
||||
|
||||
A confirmed write adds its own row. The rows are a child list on conversations, and pending actions are another one.
|
||||
|
||||
### Tests
|
||||
|
||||
`UiApiToolsSecurityTest` runs the fixture app on a real QQQ application server. For each case it makes the UI's own request (a direct `/qqq/v1` call with the user's cookie) next to the assistant's tool call:
|
||||
- read denied: the same 403 and message;
|
||||
- write denied without the insert permission, even with READ_WRITE and no confirmation;
|
||||
- data access only narrows: NONE, READ, and sensitive tables that cannot be raised;
|
||||
- a confirmed write runs once, through the UI API, and is logged;
|
||||
- only the owner can decide an action;
|
||||
- only spec'd operations can be called, and never without a session;
|
||||
- the policy narrows the tools and caps rows.
|
||||
|
||||
The golden and red-team evals now run through the UI API too.
|
||||
|
||||
## Explain access and recent denials (0.1.0-RC.6)
|
||||
|
||||
- **Explain access:**
|
||||
- `GET /qqq/v1/assistant/access?table=&prompt=` returns, for the calling user and each table (or only the named table, or only the tables a prompt mentions):
|
||||
- the user's UI permissions, from `GET /qqq/v1/metaData` with their own session;
|
||||
- the assistant's data access and where it comes from (setting, default, or sensitive table);
|
||||
- each tool's operation, with allowed or the reason it would be denied (policy, data access, not visible in the UI, or no read/insert/edit/delete permission).
|
||||
- The `assistantAccess` widget on the Assistant home renders the same thing for the viewing admin.
|
||||
- Choosing *another* user would need that user's permissions without their session. QQQ core has no API to evaluate another user's permissions, and the assistant never builds or borrows a session. That part waits for a core API, so the report is always computed with the requester's own session.
|
||||
- **Recent denials:** the home's "Recent denials" card lists the newest tool calls the UI API, data access or the policy refused, with who, the table, the operation and the HTTP status, or the assistant setting that refused it.
|
||||
|
||||
## Spec'd assistant endpoints and the core invoker (phase d, QQQ 4.1.0-RC.6)
|
||||
|
||||
- **The endpoints are spec'd `/qqq/v1` operations.** `api/specs/AssistantSpecs` registers the UI API extension `assistant` (semver 1.0.0, established prefix `/assistant`) through `MiddlewareSpecExtensionsMetaData`.
|
||||
- The URLs, bodies and statuses stay the same (201 create, 202 cancel, 204 delete and feedback, and the assistant's `{"error","code"}` errors), so Next needs no change.
|
||||
- They appear in the served `/qqq/v1/openapi.yaml`. The turn and events streams are declared as `text/event-stream`, with `AssistantTurnEventV1` describing the events.
|
||||
- The fragment is committed as `qbit-assistant/src/main/resources/openapi/qqq-v1/assistant.yaml`. `AssistantSpecsContractTest` checks it against the code, and the release baseline in test resources, with core's `UiApiContract`.
|
||||
- **No raw routes.** `AssistantRouteProvider` keeps only a before-handler that learns the server's local port from the first `/qqq/v1` request.
|
||||
- **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 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
|
||||
|
||||
These were added on the same day. All recommendations below are accepted; we adjust once they can be seen working.
|
||||
|
||||
### W. Write mode: the assistant is just a UI client
|
||||
Owner: *"the assistant should be able to go into 'write mode' too - with a user selection or preference? allowing it to do things like create and schedule a report for you, or update a record. it should ALWAYS only have access via the UIUX API (the assistant is just a UI) and all permissions that the user has via the UIUX should always hard apply at a layer above the assistant. All auditing needs to be aware of this too and audit accordingly"*
|
||||
|
||||
- **Mode per conversation.** A panel toggle switches between **Read-only** (the default) and **Write**.
|
||||
- The default comes from the user's preference (`assistant_user_preference.default_write_mode`).
|
||||
- An admin policy can disable write mode globally (`assistant_policy.write_mode_allowed`), per role (`write_mode_permission`: only holders may switch to Write), or per table or process (data access READ or NONE).
|
||||
- **Read-only:** the write tools are not in the tool list at all. This is decided when the request is built, not by the model.
|
||||
- **Write:** every write is still a pending-action card that the user confirms.
|
||||
- A user may set "auto-confirm low-risk writes" only when the policy allows it (`auto_confirm_allowed`).
|
||||
- Low-risk means a single-record insert or update on a table the admin marked `auto_confirm_ok`.
|
||||
- Deletes, bulk operations, processes, report schedules and notifications are never auto-confirmed.
|
||||
- **What counts as a write.** All of these are `/qqq/v1` UI API operations:
|
||||
- insert, update and delete records;
|
||||
- run a process (`/processes/{name}/init` and steps; every step is a confirmed action);
|
||||
- create, save or schedule a report or saved view (through the saved-report and scheduled-job tables' own insert operations);
|
||||
- create a notification.
|
||||
- **Enforcement is beneath the assistant.** The UI API and core permission layer decide, exactly as for Next UI. The assistant only forwards the user's credentials (`UiApiInvoker`, `UiApiCredentials`, qqq#1128).
|
||||
- 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."*
|
||||
|
||||
AI exposure NONE is not enough, because the assistant acts as the user and an admin user can edit the configuration. The wall is enforced in core, beneath the assistant, by client channel (UI API 1.2.x, approved design):
|
||||
|
||||
- Every `/qqq/v1` request the assistant makes carries `X-QQQ-Client: assistant/<turnId>`. It is set server-side by `UiApiInvoker.forServer(server, "assistant")` and `withClientDetail(turnId)`, and the invoker drops any client-supplied value. Core stores it as `QSession.getClientChannel()`.
|
||||
- **Deny rules:**
|
||||
- `ClientChannelsMetaData.withDeniedPattern("assistant", "assistant_*")`, plus core's defaults: `authentik_*`, `audit*`, `*api_key*`, `*apiKey*`, `*credential*`, `*secret*` and `webhooks_*secret*`;
|
||||
- `QPermissionRules.withDeniedClients(Set.of("assistant"))` on every assistant process (retention, test connection) and widget.
|
||||
|
||||
The rules are enforced in `PermissionsHelper`: the object is left out of metadata, and calls get 403, failing closed.
|
||||
- The qbit registers **every** `assistant_*` table, process and widget in the deny set. A **startup check** fails the instance if any `assistant_*` object is not denied to the `assistant` channel.
|
||||
- Profiles and skills reach the model only as server-assembled instructions. No tool reads or edits them (this is already enforced by ArchUnit), so a prompt injection can never persist itself.
|
||||
- The current conversation's context is read by internal code (`AssistantStore`, assistant tables only), never by a tool.
|
||||
- **Tests:** an admin through the assistant is denied on every configuration table and process (403, and absent from `list_tables` and `/metaData`), while the same admin through Next UI is allowed.
|
||||
|
||||
### Y. Audit attribution
|
||||
- Every action records the human user as the actor **and** the client channel: `actor=james@kof22.com via=assistant/<turnId>`, versus `via=next-ui`.
|
||||
- Core writes `clientChannel` on `audit` (`AuditsMetaDataProvider.withClientChannelField(true)`, filled by `DMLAuditAction`), so audit screens can filter by channel.
|
||||
- `assistant_tool_call` keeps the turn, conversation and model, and links to the audit rows (`auditHref`), so `via=assistant/<turnId>` leads to the conversation, the turn and the model.
|
||||
- **Tests:** an assistant write produces audit rows with the human actor and `via=assistant/<turnId>`; the same write from the UI produces `via=next-ui`.
|
||||
- kof22 needs a feature-named changeset (`changesets/features/...`) for the audit column.
|
||||
|
||||
### X1/Y1 as built
|
||||
- Record history is an explicit, default-off host opt-in (`withRecordHistory(true)`); kof22 does not opt in. RC.6 removes only `audit*`; later core #1193 support allows audit reads with a write-only denial. Other walls and user audit permissions remain intact.
|
||||
- RC.6's invoker fixes the `assistant` channel server-side and ignores spoofed client headers. Turn context survives knowledge discovery, RAG reads, confirm actions and owned-turn explain-access; scheduled calls retain `scheduled/<taskId>`.
|
||||
- The producer enumerates assistant tables, processes, reports and widgets (including source-QBit metadata), adds explicit denied-client rules without replacing user permissions, and registers `assistant_*` idempotently. Runtime construction independently validates the wall with core's `isDenied` and fails on exposed objects.
|
||||
- The fixture enables core's audit channel field. Hosts must migrate the nullable audit column before enabling it; kof22 needs its own feature changeset.
|
||||
- Successful insert/update/delete tool calls link to `/app/audit` with `recordId`, `auditTable.name` and `clientChannel` criteria, using the same encoder as `present_results`. Hosts without the channel field get no audit link. Existing audit link storage widens to TEXT for long encoded filters.
|
||||
- HTTP tests enumerate the configuration denial matrix for an admin, contrast Next UI access, reject broken startup metadata, and verify confirmed-write actor/channel plus the decoded audit filter.
|
||||
|
||||
### P. Providers, local models, HIPAA routing, budgets
|
||||
1. **Providers and models as data, any number, each with an enabled toggle.**
|
||||
- Kinds:
|
||||
- Anthropic (exists);
|
||||
- **OpenAI-compatible**, one adapter for OpenAI, Azure OpenAI, OpenRouter, Ollama, LM Studio, vLLM and the llama.cpp server;
|
||||
- **AWS Bedrock** (Claude and others);
|
||||
- **Google Vertex** (Claude and Gemini).
|
||||
- `assistant_provider` gains: kind, base URL, key env reference (unchanged pattern, no stored secrets), local or cloud, `hipaa_eligible` and region.
|
||||
- The new **`assistant_model`** table holds: provider, model id, display name, capabilities (tools, context window, vision, streaming), price in and out per million tokens (0 or a configured compute cost for local models), and an enabled flag.
|
||||
- Test connection works for every provider and every model.
|
||||
- Tool calling follows each provider's rules. The strict-grammar lesson applies: strict only where supported, and a fallback when it is refused.
|
||||
2. **Local models** (Mac Studio, and the workstation "lora" with an RTX A5000 24 GB, running Ollama, LM Studio or vLLM; another agent sets up the hosts). Any OpenAI-compatible endpoint is supported, with health checks, model discovery (`GET /v1/models` fills `assistant_model`) and per-provider timeouts.
|
||||
3. **HIPAA/PHI routing:**
|
||||
- A data classification on tables and fields in metadata: PHI, PII, CONFIDENTIAL or PUBLIC.
|
||||
- Each turn tracks the highest classification its context and tool results touched.
|
||||
- The policy allows PHI only to `hipaa_eligible` providers (local, or cloud under a BAA). Otherwise it blocks, with a clear reason.
|
||||
- A per-instance **HIPAA mode** (on for MHP, off for kof22) also enforces:
|
||||
- PHI redaction in logs;
|
||||
- encrypted conversation storage;
|
||||
- a separate retention period;
|
||||
- a full audit (who, which classification, which model);
|
||||
- no non-eligible providers.
|
||||
- **BAAs are an owner and legal step**: the flag records a fact; it does not create one.
|
||||
4. **Budgets:**
|
||||
- daily and monthly cost and token budgets per user, group or role, provider or model, and instance;
|
||||
- default behaviour: warn at 80% (bell and alert, through the Alerts SPI when present), downgrade to a cheaper or local model at 100%, and stop at a configurable ceiling;
|
||||
- admins can override;
|
||||
- usage and cost over time on the app home, by user, model, provider and tool.
|
||||
5. **Extras:**
|
||||
- model routing (simple versus complex, per profile) and fallback chains across providers;
|
||||
- **RAG** over the instance's content: a local embedding model, pgvector, and retrieval filtered by permissions through the UI API as the user;
|
||||
- **scheduled assistant tasks**: recurring prompts that run as a designated user (with that user's stored credentials through the UI API, never a system session) and post results as notifications or records.
|
||||
6. Everything goes through the `/qqq/v1` UI API, as the user, with the guarantees above.
|
||||
|
||||
### P-a as built
|
||||
|
||||
- Per-provider `tlsCaBundle` references startup-parsed, transient server CA bundles. OpenAI-compatible chat, health and discovery trust JVM roots plus the bundle, retain hostname verification, and report sanitized NETWORK errors with a trust hint.
|
||||
- `assistant_provider` records kind, location, HIPAA eligibility, region, API
|
||||
version, timeouts and discovery status. `assistant_model` is its admin-only
|
||||
child list: enablement, capabilities, prices and test status, unique per provider/model.
|
||||
- `ProviderSettings`, discovery and health extend the provider SPI through default
|
||||
methods. Legacy `create(apiKey, baseUrl)` implementations keep working.
|
||||
- The OpenAI-compatible module uses JDK HTTP and Jackson, Chat Completions with
|
||||
SSE or single JSON replies, Azure deployment URLs and api-key auth, and keyless
|
||||
local endpoints. Keys and URL overrides pass the existing allowlists first.
|
||||
- Strict tools require explicit opt-in, OpenAI or Azure, and recursively strict
|
||||
object schemas. A strict/schema/grammar 400 signals the existing one-retry fallback.
|
||||
- Discovery only adds missing models; it preserves edits and disables catalogs
|
||||
larger than 20 for cloud providers. Health uses a short models request. Provider
|
||||
tests probe health then complete; model tests can probe a disabled model before
|
||||
enabling it. Completion tests record TEST usage.
|
||||
- Registry prices and capabilities override adapter/type defaults. Empty provider
|
||||
model selects the first enabled registry model; an explicitly disabled selection
|
||||
is a setup error. Rows without models retain the prior behavior.
|
||||
- Localhost contract tests cover dialects, fragmented parallel tool calls, usage,
|
||||
errors, timeouts, cancellation, discovery and a golden tool round trip. The live
|
||||
smoke is environment-driven and excluded by default.
|
||||
|
||||
### P-b as built
|
||||
|
||||
- Optional Bedrock and Vertex modules reuse the Anthropic mapping and streaming
|
||||
adapter through an injected SDK client. Bedrock retains SDK SigV4 and binary
|
||||
event-stream decoding; a small Backend delegate supplies allow-listed endpoints
|
||||
because SDK 2.71.0 has no Bedrock endpoint setter.
|
||||
- Bedrock uses the server's AWS default credential chain; IRSA/web-identity hosts
|
||||
supply STS on their runtime classpath. No STS or control-plane SDK is bundled.
|
||||
- Vertex uses scoped Google ADC. `claude-*` routes to the Anthropic backend; other
|
||||
ids route to the OpenAI-compatible endpoint with a per-request token supplier,
|
||||
`google/` for unprefixed ids, and no strict tools.
|
||||
- Both types require region, reject key references and default to CLOUD. Vertex
|
||||
additionally requires `cloudProject` (`cloud_project VARCHAR(100)`); it is part
|
||||
of resolved settings and the provider cache key. Type hooks own validation.
|
||||
- Discovery is a static catalog, health is unsupported, and Test connection uses
|
||||
the existing completion. HIPAA eligibility remains an admin fact, not a BAA.
|
||||
- Localhost contract tests cover wire paths, authentication, text/tools/usage,
|
||||
expired-token refresh, errors, allowlists and golden turns for all three routes.
|
||||
|
||||
### P-c as built
|
||||
|
||||
- The qbit classification stand-in is gone: the levels are core's `actions.ai.DataClassification` (QQQ 4.1.0-RC.6, core #1142).
|
||||
The default `ClassificationSource` reads core metadata (`AiDataClassifications`, `AiTableMetaData`, `AiFieldMetaData`) merged with the configured table/field map; configuration wins when both are set. Hosts can still supply their own source with `withClassificationSource`.
|
||||
- Turn/page/tool classification is monotonic, persisted on the turn and retained
|
||||
on the conversation. Field subsets use overrides; unknown cross-table results
|
||||
conservatively use the highest configured level. Metadata tools stay PUBLIC.
|
||||
- Small engine hooks check a fresh provider row before every model dispatch,
|
||||
including strict retries. PHI requires explicit row eligibility; HIPAA mode
|
||||
requires it for all calls. Blocks withhold results, explain the rule without
|
||||
values, and write an audit. `ProviderService.eligibleAlternative` is an empty
|
||||
seam for P-e; P-c never changes providers automatically.
|
||||
- HIPAA mode refuses non-eligible settings/selection and requires a valid key
|
||||
reference at startup. AssistantStore alone encrypts/decrypts conversation
|
||||
payloads using AES-256-GCM; legacy plaintext reads remain supported and corrupt
|
||||
ciphertext fails closed. No key is persisted. Provider diagnostic text is
|
||||
suppressed in HIPAA logs and turn errors.
|
||||
- Admin read-only, owner-locked `assistant_data_audit` records each PII/PHI model
|
||||
dispatch and each block, outside navigation. PHI conversation retention defaults
|
||||
to 2190 days in either mode; shorter tool-result/replay cleanup is unchanged.
|
||||
The app home shows HIPAA mode and PHI retention.
|
||||
|
||||
### P-d as built
|
||||
|
||||
- ROLE allowances are per permission holder. Shared scopes sum a private immutable
|
||||
accounting projection of usage counters; owner-locked source rows stay locked.
|
||||
Migration backfills existing usage before writes resume. No sessions are elevated.
|
||||
|
||||
- Standard admin budget rows add daily/monthly cost or token allowances above the
|
||||
existing config/policy budgets and provider monthly caps. Overrides carry a
|
||||
reason, session grantor and expiry; they never waive the other limit layers.
|
||||
- Start-turn checks use the existing 429 BUDGET path through PolicyService.
|
||||
Every dispatch, including strict retries, evaluates current usage and overrides.
|
||||
Events deduplicate WARNED, DOWNGRADED and STOPPED.
|
||||
- QQQ RC.5 core QAlerts sends budget signals alongside panel notices, with three
|
||||
admin-visible alert types and a default logging sink. Resolution awaits active
|
||||
alert/recurrence tracking so overrides cannot clear shared alerts incorrectly.
|
||||
- Explicit registry model selection leaves provider defaults untouched. Automatic
|
||||
downgrades prefer LOCAL, then price. PHI eligibility and destination provider caps
|
||||
still apply; a missing eligible target stops the turn.
|
||||
- The home owns the budget cards and 30-day cost/token bars, with user, model,
|
||||
provider and tool breakdowns, inherited colors and empty states.
|
||||
|
||||
### P-e as built
|
||||
|
||||
- Profiles select OFF (the migration default), HEURISTIC or CLASSIFIER and optional
|
||||
simple/complex/classifier registry models. Selection uses ProfileService's existing
|
||||
specificity. Without a profile, enabled escalation config supplies the complex
|
||||
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. 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
|
||||
accounting; normal dispatch budgets may override the selected route.
|
||||
- FallbackChain follows at most four edges on retryable failures, only before any
|
||||
text/tool start of that call. Candidates are checked for enablement, health,
|
||||
PHI/HIPAA eligibility and budgets. Without explicit links only the same LOCAL
|
||||
adapter family is considered. Provider switches rebuild neutral messages and
|
||||
discard providerState; failed attempts remain visible in usage.
|
||||
- ProviderHealth persists UP/DOWN, checked-at and a 60-second connection-failure
|
||||
circuit. Local stale/unknown health uses a bounded two-second probe. Successful
|
||||
completions close the breaker. Turn rows persist route/source, fallback count and
|
||||
final provider/model; notices name only the unavailable and answering providers.
|
||||
- Admin record screens, additive Postgres DDL and the flow-layout home expose these
|
||||
settings and counters without granting AI access to assistant configuration.
|
||||
|
||||
### P-f as built
|
||||
|
||||
- `AssistantModelProvider.embed` is optional and unsupported by default. The
|
||||
OpenAI-compatible implementation shares chat's JDK HTTP authentication, TLS,
|
||||
Azure deployment paths, timeout and sanitized error mapping. A request accepts
|
||||
1–64 inputs; `EmbeddingService` batches larger jobs and records each call as
|
||||
EMBED, including daily rollups and provider/model budget accounting.
|
||||
- Registry rows have CHAT/EMBEDDING kinds and embedding dimensions. Legacy rows
|
||||
remain CHAT. Discovery suggests EMBEDDING for ids containing embed, bge, e5,
|
||||
gte or nomic; administrators can correct it. Chat routing/fallback/downgrades
|
||||
cannot select an embedding model. Embedding resolution is explicit and checks
|
||||
fresh provider enablement and HIPAA eligibility before dispatch.
|
||||
- `assistant_rag_source` is an admin-only navigation table. Its text fields must
|
||||
exist and allow VALUES/MASKED exposure; application data access must allow READ.
|
||||
Index now and Clear index are standard streamed ETL record actions with Review
|
||||
and Result, never navigation entries. Indexing pages the UI API as the clicking
|
||||
admin, reuses get_record's masking path, chunks deterministically and compares
|
||||
hashes. Complete scans alone prune disappeared records; per-record replacement
|
||||
also removes obsolete field chunks. Configuration/model identity changes clear
|
||||
stale vectors. No source text is saved on a source or vector row.
|
||||
- `VectorStore` supplies upsert/delete/search/count plus content-free chunk
|
||||
reconciliation and atomic record replacement. The in-memory implementation uses
|
||||
exact cosine; the optional pgvector module uses host JDBC, a validated instance
|
||||
schema, bound values and an HNSW cosine index. Sources from other embedding
|
||||
models are excluded before ranking. The DBA installs pgvector; existing
|
||||
installations do not require extension-installation privileges on startup.
|
||||
- `KnowledgeTool` is separate from the unchanged `UiApiToolbox`. A small
|
||||
ContextBuilder hook adds it only for an enabled source, policy permission and
|
||||
UI-readable source table. Search embeds the question, requests 4× the result
|
||||
limit, then re-reads each hit through get_record with the asking user's session.
|
||||
403/404 hits contribute only to the filtered count. Safe current values produce
|
||||
snippets and `/app` citations; record refs propagate touched tables into P-c
|
||||
classification and the normal tool-call log. The citation instruction is in
|
||||
the static rules block. The result limit is capped at eight.
|
||||
- PHI source content and PHI conversation queries only reach eligible embedding
|
||||
providers. Embedding budgets, kill switch and provider concurrency checks apply
|
||||
to each batch. A budget downgrade into another vector space stops embedding.
|
||||
- Tests exercise real fixture UI API reads, denials, record locks, field masking,
|
||||
PHI refusal, classification, reconciliation, embedding wire contracts and a
|
||||
golden cited answer. JDBC shape/transaction tests always run; a pgvector:pg16
|
||||
Testcontainers test skips on hosts without Docker.
|
||||
- P-g may invoke the manual service within a designated user's authenticated
|
||||
context. P-f supplies no scheduled runner or stored/elevated session. Multiple
|
||||
replicas should serialize manual re-indexing of a source at the host level;
|
||||
per-record pgvector replacement is transactional, while scan reconciliation is
|
||||
not a snapshot of concurrently changing application data.
|
||||
|
||||
### P-g as built
|
||||
|
||||
- `assistant_scheduled_task` is admin-only, AI exposure NONE, and appears as
|
||||
Scheduled tasks. The owner-locked, read-only `assistant_scheduled_run` child
|
||||
list retains run status, accounting and a bounded summary. Conversations gain
|
||||
read-only `scheduledTaskId` and `channel` (`panel` or `assistant/scheduled`).
|
||||
Task and run timestamps use the runtime clock; cron uses Quartz and a validated
|
||||
ZoneId, including DST. Enablement and schedule edits recompute `nextRunAt`.
|
||||
- Credentials are references, never stored tokens. `credentialEnvName` must
|
||||
match `[A-Z_][A-Z0-9_]*` and the dedicated
|
||||
`allowedScheduledCredentialEnvNames` allowlist. The existing secret resolver
|
||||
supplies its value. Read-only `credentialStatus` reports presence without
|
||||
exposing token characters and is stripped on every save.
|
||||
- A run sends JSON `{accessToken: ...}` to the same loopback base URL's
|
||||
`POST /qqq/v1/manageSession`. RC.5 with HTTP-only cookies returns the UUID in
|
||||
`Set-Cookie: sessionUUID`; legacy JSON `uuid` is also accepted. Only then does
|
||||
the runner resolve that UUID through the host authentication module, exactly
|
||||
the UUID lookup used by the UI request pipeline. No direct token authentication,
|
||||
automated-user session or constructed QSession exists in assistant code.
|
||||
The resolved user id must equal `runAsUserId` before any model call. MOCK in
|
||||
the integration fixture maps test tokens to the existing user-prefix sessions;
|
||||
that token adapter is test code only.
|
||||
- kof22 provisions an Authentik designated account with normal QQQ permissions
|
||||
and injects an accepted long-lived token through its secret manager and
|
||||
ExternalSecret into an allow-listed environment variable. The task holds the
|
||||
variable name and QQQ user id. Host authentication must accept that token type
|
||||
as `accessToken`; an Authentik management API token is not automatically an
|
||||
application access token. Neither account nor credential is provisioned here.
|
||||
- `assistantScheduledTasksRunner` registers a 60-second QScheduleMetaData when
|
||||
`scheduledTasksSchedulerName` names a host scheduler. QQQ supplies its normal
|
||||
scheduler context for assistant-table bookkeeping; no ArchUnit exception is
|
||||
necessary. Configure `uiApiBaseUrl` at startup. No browser visit is required.
|
||||
- **Claim compatibility:** RC.5 UpdateInput cannot express a conditional
|
||||
`WHERE next_run_at = expected`. AssistantStore therefore claims the expected
|
||||
schedule slot using the database-enforced unique `(taskId, scheduledFor)` run
|
||||
key and advances `nextRunAt` before login/dispatch. This is a unique-insert
|
||||
optimistic claim rather than a conditional update. The memory fixture also
|
||||
serializes claim operations on its instance. The database constraint, not
|
||||
that monitor, prevents cross-replica duplicate execution. Do not remove claim
|
||||
history or deploy to a backend without atomic unique keys. A crash can lose a
|
||||
run; it never retries a claimed slot. A later tick advances an already-claimed
|
||||
slot left behind by a crash between claim and schedule update.
|
||||
- The unchanged AssistantApi starts a new normal turn under the logged-in
|
||||
designated user, preserving start gates, rate limiting, P-c classification,
|
||||
P-d budgets and P-e routing/fallback. An internal context supplies the task's
|
||||
selected profile and additional cost cap. Cost includes chat, classifier,
|
||||
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>` 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
|
||||
definition before recording the actual administrator's user id and time.
|
||||
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.
|
||||
- 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
|
||||
Alerts notification with `NotificationAudience.user(runAsUserId)` and an
|
||||
`/app/...` conversation link. Reflection targets the installed Alerts QBit's
|
||||
RC.5 notification SPI/sink, following WebhooksAlerts' optional-dependency
|
||||
pattern. Without it, a log contains run id and status only. Notification text
|
||||
contains no answer data. In HIPAA mode run summaries contain exactly
|
||||
`see conversation`; conversation payloads retain the existing encryption.
|
||||
The existing record URL is an admin-screen link. Non-admin owners can read
|
||||
their scheduled conversations in the panel's conversation list; opening a
|
||||
particular existing conversation from a notification still needs a frontend
|
||||
deep-link contract (the current panel only supports open/new-prompt links).
|
||||
This phase does not broaden existing conversation record-screen permissions.
|
||||
- Failures close the run as FAILED or BLOCKED with sanitized errors and wait
|
||||
until the next schedule. All output modes retain their audit record. Tests
|
||||
exercise real manageSession and UI API requests with a fixed clock, designated
|
||||
permissions and owner locks, headers, competing runners, approval invalidation,
|
||||
notification modes, PHI refusal, encrypted HIPAA storage and cost STOP.
|
||||
|
||||
### Phases (each a green PR)
|
||||
| | Scope | Depends on |
|
||||
|---|---|---|
|
||||
| d | Spec'd `/assistant` endpoints, tools on `UiApiInvoker` (#19) | QQQ RC.6 |
|
||||
| W1 | Write mode: conversation toggle, user preference, policy (global, role, table), tool list by mode, auto-confirm rules, write matrix in both modes; Next toggle | — |
|
||||
| X1 | Client channel `assistant/<turnId>` on every call, deny registration for all `assistant_*` objects and the security surfaces, startup check, admin-denied tests | core client-channel PR (UI API 1.2.x) |
|
||||
| Y1 | Audit attribution tests and `auditHref` to audit rows filtered by channel | core audit column |
|
||||
| W2 | Process, report-schedule, saved-view and notification writes as confirmed UI API operations | W1, X1 |
|
||||
| P-a | Provider and model registry, OpenAI-compatible adapter, discovery and health | — |
|
||||
| P-b | Bedrock and Vertex | P-a |
|
||||
| P-c | Classification, HIPAA routing and mode | P-a, core metadata |
|
||||
| P-d | Budgets and dashboards | P-a |
|
||||
| P-e | Routing and fallback chains | P-a |
|
||||
| P-f | RAG (embeddings, pgvector, UI-API-filtered retrieval) | P-a |
|
||||
| P-g | Scheduled assistant tasks | W1, X1 |
|
||||
|
||||
|
||||
## Next phases
|
||||
|
||||
- **(c) Data access screens:**
|
||||
- the Data Access table, in the nav;
|
||||
- an "Explain access" action (for a user, and a table or a prompt, which operations the assistant could call and which it would be denied, with the reason);
|
||||
- a recent-denials widget on the app home.
|
||||
- **(d) Spec'd assistant endpoints:** move `/qqq/v1/assistant/...` (conversations, turns, events, feedback, cancel, config, actions) onto `AbstractEndpointSpec`s through the qbit spec extension point (`MiddlewareSpecExtension`, established prefix `/assistant`, QRun-IO/qqq#1128). The SSE turn and events streams stay raw routes until the spec layer can describe a stream.
|
||||
|
||||
## Answer presentation (owner priority 2026-10-13)
|
||||
|
||||
`present_results` is always offered and reads no records. It takes a direct answer,
|
||||
metrics, a table, an optional table-derived chart, filtered list links, record links
|
||||
and next-step actions. The server validates metadata and the user's UI API table
|
||||
visibility once per turn, formats values using QQQ field types/display formats,
|
||||
computes percentages and builds every `/app` href. Unknown or denied items are
|
||||
dropped with an explanation. The assistant must still retrieve every fact through
|
||||
data tools before presenting it.
|
||||
|
||||
The [presentation part and SSE contract](answer-presentation.md) defines `summary`,
|
||||
`blocks` and the complete server-built GFM `markdown` fallback. Each successful
|
||||
call emits `presentation` with `{messageId, part}` and persists that part; the tool
|
||||
response tells the model not to repeat its tables or links. Audit rows contain
|
||||
only accepted presentation data. Normal record-data retention removes these parts.
|
||||
|
||||
The built-in Presentation skill is compact static prompt text in the cached rules
|
||||
block. It asks for a one-sentence answer first, metrics for single numbers, tables
|
||||
for breakdowns/comparisons, charts for useful shares/trends and actual filter links
|
||||
whenever offering a list. Administrator profile skills append after it as before
|
||||
and extend it; there is no replacement boolean or extra permission surface.
|
||||
|
||||
The owner's one-line table was a frontend parsing defect: Next's `AssistantMarkdown`
|
||||
used react-markdown without `remark-gfm`, so table lines became a paragraph.
|
||||
The renderer fix belongs to `qqq-frontend-next`; this worktree preserves real
|
||||
newlines and provides structured blocks plus a proper multi-line GFM fallback.
|
||||
The frontend change is maintained separately and is not verified by this build.
|
||||
|
||||
The shared golden task asks "How many images do I have by type?": five UI count
|
||||
calls establish 81 JPEG, 20 PNG and zero other configured types, then a presentation
|
||||
with two rows, 80.2%/19.8% shares, two pie points and working MIME filter links.
|
||||
The same task is included in the opt-in `live` Anthropic eval suite.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Assistant answer presentation contract (v1)
|
||||
|
||||
Owner priority (2026-10-13): answers must ALWAYS be easy to read and act on. Bad example the owner saw, rendered on ONE line:
|
||||
"| Type | Count | |---|---| | JPEG (image/jpeg) | 81 | | PNG (image/png) | 20 | | WebP, GIF, SVG | 0 |" followed by "You can open the lists filtered by JPEG or PNG" with NO links.
|
||||
|
||||
Root cause found in Next: `AssistantMarkdown` uses react-markdown WITHOUT `remark-gfm`, so GFM tables are not parsed and the table's lines collapse into one paragraph (soft line breaks). Backend streaming preserves `\n` (events are JSON).
|
||||
|
||||
## New message part: `presentation`
|
||||
|
||||
An assistant message's `parts` (persisted in `assistant_message.parts_json`, streamed as an SSE event, returned by the conversation API) may contain, besides `text`, `notice`, `tool`/`render` parts etc., ONE part per `present_results` call:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "presentation",
|
||||
"summary": "101 media files are images: 81 JPEG and 20 PNG.",
|
||||
"markdown": "<server-built GFM fallback of the whole presentation, for clients that do not render blocks (CLI/TUI)>",
|
||||
"blocks": [
|
||||
{ "kind": "metrics", "items": [ { "label": "Images", "value": 101, "display": "101", "href": "/app/media?filter=..." } ] },
|
||||
{ "kind": "table", "title": "Media by type",
|
||||
"columns": [ { "key": "type", "label": "Type", "align": "left" },
|
||||
{ "key": "count", "label": "Count", "align": "right" },
|
||||
{ "key": "share", "label": "Share", "align": "right" } ],
|
||||
"rows": [ { "cells": { "type": "JPEG (image/jpeg)", "count": 81, "share": 0.802 },
|
||||
"display": { "type": "JPEG (image/jpeg)", "count": "81", "share": "80.2%" },
|
||||
"href": "/app/media?filter=..." } ] },
|
||||
{ "kind": "chart", "chartType": "pie", "title": "Share by type",
|
||||
"points": [ { "label": "JPEG", "value": 81, "display": "81", "href": "/app/media?filter=..." },
|
||||
{ "label": "PNG", "value": 20, "display": "20", "href": "/app/media?filter=..." } ] },
|
||||
{ "kind": "filterLinks", "items": [ { "label": "Open JPEG media (81)", "tableName": "media", "href": "/app/media?filter=..." } ] },
|
||||
{ "kind": "recordLinks", "items": [ { "label": "Sunset.jpg", "tableName": "media", "recordId": "12", "href": "/app/media/12" } ] },
|
||||
{ "kind": "actions", "items": [ { "label": "Show the PNG files", "prompt": "Show me the PNG media files" },
|
||||
{ "label": "Open media", "href": "/app/media" } ] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `summary` is required, one sentence, the direct answer first.
|
||||
- `chartType` ∈ `bar`, `pie`, `line`; ≤ 12 points; values are numbers.
|
||||
- Every `href` is built SERVER-SIDE from metadata and is an `/app` path (same rules as `TurnEngine.isAppHref`). Filtered list links are `/app/<tableName>?filter=<URL-encoded JSON {"criteria":[{"fieldName":..,"operator":..,"values":[..]}],"booleanOperator":"AND"}>`, which Next's `deserializeFilter` accepts (JSON starting with `{`). The model never writes URLs.
|
||||
- `display` strings are formatted server-side from field metadata (`displayFormat`, e.g. `%,d`, `$%,.2f`; percent columns as `80.2%`); clients show `display` when present, else the raw value.
|
||||
- `actions` items have either `prompt` (send as the user's next message) or `href` (navigate), never both.
|
||||
- Unknown `kind`s must be ignored by clients (forward compatible); clients that do not render blocks render `markdown`.
|
||||
- The SSE event is `presentation` with `{ "messageId": <id>, "part": <the part above> }`; Next's reducer appends it to the assistant message's parts.
|
||||
|
||||
## Tool input and implementation
|
||||
|
||||
The input schema is [present-results.schema.json](../qbit-assistant/src/main/resources/present-results.schema.json). `table.rowKeys` supplies one raw filter value per row; `table.rowFilter` names the table, field and optional operator (EQUALS by default). Columns receive stable server keys `c0`, `c1`, etc. Input cells remain raw scalars; each row has a parallel display map. The example keys above are illustrative.
|
||||
|
||||
`PresentResultsTool` is instantiated per turn. It caches `GET /metaData` with that turn's session, checks table read visibility and instance fields, validates QQQ criteria operators and builds paths using RC.5 `AiLinkBuilder`. Filter links use URL-encoded JSON accepted by Next. Record ids are encoded as opaque path segments. Metadata denial or invalid item drops that item (an invalid table drops its table/chart), with a short explanation to the model; other items survive. No data query or permission elevation happens here.
|
||||
|
||||
Limits: summary 300 characters, display text 2,000 characters, table 20 columns/200 rows, chart 12 points, item lists 100 entries, filters 20 criteria/200 values. Chart truncation is reported to the model. Percent columns reference a raw numeric column; a zero total yields 0.0%. Numbers still must originate in earlier data tool results; presentation is formatting, not a new source of facts.
|
||||
|
||||
The schema uses small named, nonrecursive definitions to avoid deeply inlined structures. Each object component is at most two levels of inline containers; the resolved table/criteria structure necessarily has the nesting shown in the owner contract. Strict is off by default. Opting `present_results` into strict mode requires all properties (empty arrays/strings, empty table and -1 for unused percentage indexes), removes provider-unsupported length constraints, and retains server validation. This has zero optional parameters and one shared scalar union in the existing strict-budget test.
|
||||
|
||||
Presentation parts are persisted with the assistant message and replayed as `presentation` events with `{messageId, part}` (plus the standard event sequence). They are removed by record-data retention along with tool renders. Model history and text-only consumers can use the server-built Markdown. Unknown block kinds should be ignored. Plain labels must be rendered as text, never parsed as links or HTML; fallback Markdown escapes them. Tool audit input records the accepted presentation only, excluding dropped values.
|
||||
@@ -1,595 +0,0 @@
|
||||
-- Reference PostgreSQL DDL for the qbit-assistant tables (snake_case, as mapped by a
|
||||
-- snake_case table customizer). Adjust to your conventions; the QBit only needs the
|
||||
-- QQQ field names to map onto columns.
|
||||
|
||||
CREATE TABLE assistant_conversation
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(255),
|
||||
title TEXT,
|
||||
max_classification VARCHAR(16),
|
||||
status VARCHAR(16),
|
||||
pinned BOOLEAN,
|
||||
last_message_at TIMESTAMP WITH TIME ZONE,
|
||||
unread_count INTEGER,
|
||||
context_json TEXT,
|
||||
total_tokens_in BIGINT,
|
||||
total_tokens_out BIGINT,
|
||||
total_cost_micros BIGINT,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_conversation_user_id ON assistant_conversation (user_id);
|
||||
|
||||
CREATE TABLE assistant_message
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
parent_message_id INTEGER,
|
||||
role VARCHAR(16),
|
||||
status VARCHAR(16),
|
||||
turn_id INTEGER,
|
||||
seq INTEGER,
|
||||
model VARCHAR(200),
|
||||
parts_json TEXT,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_message_user_id ON assistant_message (user_id);
|
||||
CREATE INDEX assistant_message_conversation_id ON assistant_message (conversation_id);
|
||||
|
||||
CREATE TABLE assistant_turn
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
user_message_id INTEGER,
|
||||
assistant_message_id INTEGER,
|
||||
status VARCHAR(16),
|
||||
classification VARCHAR(16),
|
||||
classification_tables VARCHAR(500),
|
||||
stop_reason VARCHAR(64),
|
||||
error_code VARCHAR(32),
|
||||
cancel_reason VARCHAR(64),
|
||||
model VARCHAR(200),
|
||||
started_at TIMESTAMP WITH TIME ZONE,
|
||||
ended_at TIMESTAMP WITH TIME ZONE,
|
||||
tool_call_count INTEGER,
|
||||
model_call_count INTEGER,
|
||||
input_tokens BIGINT,
|
||||
output_tokens BIGINT,
|
||||
cache_read_tokens BIGINT,
|
||||
cache_write_tokens BIGINT,
|
||||
cost_micros BIGINT,
|
||||
provider_request_ids TEXT,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_turn_user_id ON assistant_turn (user_id);
|
||||
CREATE INDEX assistant_turn_conversation_id ON assistant_turn (conversation_id);
|
||||
|
||||
CREATE TABLE assistant_turn_event
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
turn_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
seq INTEGER,
|
||||
event_type VARCHAR(32),
|
||||
data_json TEXT,
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_turn_event_user_id ON assistant_turn_event (user_id);
|
||||
CREATE INDEX assistant_turn_event_turn ON assistant_turn_event (turn_id, seq);
|
||||
|
||||
CREATE TABLE assistant_tool_call
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
turn_id INTEGER,
|
||||
conversation_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
tool_call_id VARCHAR(128),
|
||||
tool_name VARCHAR(64),
|
||||
input_json TEXT,
|
||||
outcome VARCHAR(16),
|
||||
permission_check VARCHAR(255),
|
||||
table_name VARCHAR(128),
|
||||
process_name VARCHAR(128),
|
||||
widget_name VARCHAR(128),
|
||||
row_count INTEGER,
|
||||
duration_ms BIGINT,
|
||||
result_summary TEXT,
|
||||
result_json TEXT,
|
||||
redactions_json TEXT,
|
||||
ui_api_operation VARCHAR(255),
|
||||
http_status INTEGER,
|
||||
audit_href TEXT,
|
||||
pending_action_id INTEGER,
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_tool_call_user_id ON assistant_tool_call (user_id);
|
||||
CREATE INDEX assistant_tool_call_conversation_id ON assistant_tool_call (conversation_id);
|
||||
CREATE INDEX assistant_tool_call_turn ON assistant_tool_call (turn_id);
|
||||
|
||||
CREATE TABLE assistant_feedback
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
message_id INTEGER,
|
||||
conversation_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
rating INTEGER,
|
||||
reason VARCHAR(64),
|
||||
comment TEXT,
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_feedback_user_id ON assistant_feedback (user_id);
|
||||
CREATE INDEX assistant_feedback_conversation_id ON assistant_feedback (conversation_id);
|
||||
|
||||
CREATE TABLE assistant_policy
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(64),
|
||||
kill_switch BOOLEAN,
|
||||
kill_switch_reason VARCHAR(255),
|
||||
main_model VARCHAR(64),
|
||||
main_effort VARCHAR(16),
|
||||
user_daily_tokens_soft BIGINT,
|
||||
user_daily_tokens_hard BIGINT,
|
||||
user_daily_cost_micros_soft BIGINT,
|
||||
user_daily_cost_micros_hard BIGINT,
|
||||
instance_monthly_tokens_soft BIGINT,
|
||||
instance_monthly_tokens_hard BIGINT,
|
||||
instance_monthly_cost_micros_soft BIGINT,
|
||||
instance_monthly_cost_micros_hard BIGINT,
|
||||
writes_require_confirmation BOOLEAN,
|
||||
allowed_tools VARCHAR(1000),
|
||||
max_rows_per_call INTEGER,
|
||||
redact_pii BOOLEAN,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_usage_daily
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
scope VARCHAR(16),
|
||||
user_id VARCHAR(255),
|
||||
usage_date DATE,
|
||||
turn_count INTEGER,
|
||||
input_tokens BIGINT,
|
||||
output_tokens BIGINT,
|
||||
cache_read_tokens BIGINT,
|
||||
cache_write_tokens BIGINT,
|
||||
cost_micros BIGINT,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_usage_daily_user_id ON assistant_usage_daily (user_id);
|
||||
CREATE INDEX assistant_usage_daily_date ON assistant_usage_daily (usage_date, scope);
|
||||
|
||||
-- 0.1.0-RC.4: provider settings, model prices and per-call usage.
|
||||
-- api_key_status is filled when a row is read (never written); the API key itself
|
||||
-- is never stored: api_key_env_name holds only the NAME of an environment variable.
|
||||
|
||||
CREATE TABLE assistant_provider
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
kind VARCHAR(30),
|
||||
location VARCHAR(10),
|
||||
hipaa_eligible BOOLEAN DEFAULT FALSE,
|
||||
region VARCHAR(50),
|
||||
cloud_project VARCHAR(100),
|
||||
api_version VARCHAR(30),
|
||||
connect_timeout_seconds INTEGER DEFAULT 10 CHECK (connect_timeout_seconds BETWEEN 1 AND 600),
|
||||
request_timeout_seconds INTEGER DEFAULT 120 CHECK (request_timeout_seconds BETWEEN 1 AND 600),
|
||||
last_discovery_at TIMESTAMP WITH TIME ZONE,
|
||||
last_discovery_status VARCHAR(16),
|
||||
last_discovery_error VARCHAR(500),
|
||||
model VARCHAR(200),
|
||||
enabled BOOLEAN,
|
||||
is_default BOOLEAN,
|
||||
base_url VARCHAR(255),
|
||||
tls_ca_bundle VARCHAR(100),
|
||||
api_key_env_name VARCHAR(100),
|
||||
api_key_status VARCHAR(200),
|
||||
monthly_cost_cap_usd NUMERIC(12, 2),
|
||||
max_output_tokens_per_request INTEGER,
|
||||
max_concurrent_requests INTEGER,
|
||||
last_test_at TIMESTAMP WITH TIME ZONE,
|
||||
last_test_status VARCHAR(16),
|
||||
last_test_latency_ms INTEGER,
|
||||
last_test_model VARCHAR(200),
|
||||
last_test_error VARCHAR(500),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_model
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_id INTEGER NOT NULL REFERENCES assistant_provider(id),
|
||||
model_id VARCHAR(200) NOT NULL,
|
||||
display_name VARCHAR(200),
|
||||
enabled BOOLEAN,
|
||||
supports_tools BOOLEAN,
|
||||
supports_vision BOOLEAN,
|
||||
supports_streaming BOOLEAN,
|
||||
supports_reasoning_effort BOOLEAN,
|
||||
context_window_tokens INTEGER,
|
||||
max_output_tokens INTEGER,
|
||||
input_usd_per_m_tok NUMERIC(12, 4),
|
||||
output_usd_per_m_tok NUMERIC(12, 4),
|
||||
source VARCHAR(16),
|
||||
discovered_at TIMESTAMP WITH TIME ZONE,
|
||||
last_test_at TIMESTAMP WITH TIME ZONE,
|
||||
last_test_status VARCHAR(16),
|
||||
last_test_latency_ms INTEGER,
|
||||
last_test_error VARCHAR(500),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
UNIQUE (provider_id, model_id)
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_model_price
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
input_usd_per_m_tok NUMERIC(12, 4) NOT NULL,
|
||||
output_usd_per_m_tok NUMERIC(12, 4) NOT NULL,
|
||||
cache_read_usd_per_m_tok NUMERIC(12, 4),
|
||||
cache_write5m_usd_per_m_tok NUMERIC(12, 4),
|
||||
cache_write1h_usd_per_m_tok NUMERIC(12, 4),
|
||||
notes VARCHAR(255),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
UNIQUE (provider_type, model)
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_usage
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(255),
|
||||
conversation_id INTEGER,
|
||||
turn_id INTEGER,
|
||||
provider_id INTEGER,
|
||||
provider_type VARCHAR(50),
|
||||
model VARCHAR(200),
|
||||
purpose VARCHAR(16),
|
||||
status VARCHAR(16),
|
||||
error_kind VARCHAR(32),
|
||||
http_status INTEGER,
|
||||
latency_ms INTEGER,
|
||||
input_tokens BIGINT,
|
||||
output_tokens BIGINT,
|
||||
cache_read_tokens BIGINT,
|
||||
cache_write_tokens BIGINT,
|
||||
cost_micros BIGINT,
|
||||
request_id VARCHAR(100),
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_usage_create_date ON assistant_usage (create_date);
|
||||
CREATE INDEX assistant_usage_provider ON assistant_usage (provider_id, create_date);
|
||||
CREATE INDEX assistant_usage_conversation ON assistant_usage (conversation_id);
|
||||
CREATE INDEX assistant_usage_user_id ON assistant_usage (user_id);
|
||||
|
||||
-- Anthropic list prices, USD per million tokens (cached 2026-10-06). Cache writes:
|
||||
-- 1.25x input (5 minute) and 2x input (1 hour). Re-check before budgeting.
|
||||
INSERT INTO assistant_model_price (provider_type, model, input_usd_per_m_tok, output_usd_per_m_tok, cache_read_usd_per_m_tok, cache_write5m_usd_per_m_tok, cache_write1h_usd_per_m_tok, notes, create_date, modify_date)
|
||||
VALUES
|
||||
('anthropic', 'claude-sonnet-5-5', 2.00, 10.00, 0.20, 2.50, 4.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-opus-5-5', 4.00, 20.00, 0.20, 5.00, 8.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-haiku-5-5', 0.10, 0.50, 0.01, 0.125, 0.20, 'Anthropic list price, 2026-10 (prompts up to 100K tokens)', NOW(), NOW()),
|
||||
('anthropic', 'claude-fable-5-1', 10.00, 50.00, 0.25, 12.50, 20.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-opus-5', 5.00, 25.00, 0.50, 6.25, 10.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-sonnet-5', 2.00, 10.00, 0.20, 2.50, 4.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-opus-4-8', 5.00, 25.00, 0.50, 6.25, 10.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-sonnet-4-6', 3.00, 15.00, 0.30, 3.75, 6.00, 'Anthropic list price, 2026-10', NOW(), NOW()),
|
||||
('anthropic', 'claude-haiku-4-5', 1.00, 5.00, 0.10, 1.25, 2.00, 'Anthropic list price, 2026-10', NOW(), NOW());
|
||||
|
||||
-- 0.1.0-RC.6: the assistant's data access per table / process, and writes waiting
|
||||
-- for the user's confirmation. Existing databases: add the tool-call and policy
|
||||
-- columns above with ALTER TABLE ... ADD COLUMN IF NOT EXISTS.
|
||||
CREATE TABLE assistant_data_access
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
object_type VARCHAR(16) NOT NULL,
|
||||
object_name VARCHAR(128) NOT NULL,
|
||||
mode VARCHAR(16) NOT NULL,
|
||||
notes VARCHAR(500),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT assistant_data_access_object UNIQUE (object_type, object_name)
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_pending_action
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER,
|
||||
turn_id INTEGER,
|
||||
user_id VARCHAR(255),
|
||||
tool_call_id VARCHAR(128),
|
||||
tool_name VARCHAR(64),
|
||||
ui_api_operation VARCHAR(255),
|
||||
table_name VARCHAR(128),
|
||||
record_id VARCHAR(128),
|
||||
params_json TEXT,
|
||||
summary TEXT,
|
||||
status VARCHAR(16),
|
||||
http_status INTEGER,
|
||||
result_summary TEXT,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
decided_at TIMESTAMP WITH TIME ZONE,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_pending_action_user_id ON assistant_pending_action (user_id);
|
||||
CREATE INDEX assistant_pending_action_conversation_id ON assistant_pending_action (conversation_id);
|
||||
|
||||
-- 0.1.0-RC.6: profiles (persona) and skills. Text the model reads; never tools or access.
|
||||
CREATE TABLE assistant_profile
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(64) NOT NULL,
|
||||
icon VARCHAR(64),
|
||||
personality TEXT,
|
||||
greeting VARCHAR(500),
|
||||
starter_prompts TEXT,
|
||||
is_default BOOLEAN,
|
||||
enabled BOOLEAN,
|
||||
app_name VARCHAR(128),
|
||||
permission_name VARCHAR(128),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_skill
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL UNIQUE,
|
||||
description VARCHAR(500),
|
||||
instructions TEXT NOT NULL,
|
||||
enabled BOOLEAN,
|
||||
trigger_keywords VARCHAR(500),
|
||||
app_names VARCHAR(500),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE assistant_profile_skill
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
profile_id INTEGER NOT NULL,
|
||||
skill_id INTEGER NOT NULL,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
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,
|
||||
user_id VARCHAR(255),
|
||||
conversation_id INTEGER,
|
||||
turn_id INTEGER,
|
||||
provider_id INTEGER,
|
||||
provider_name VARCHAR(200),
|
||||
model VARCHAR(200),
|
||||
hipaa_eligible BOOLEAN,
|
||||
classification VARCHAR(16),
|
||||
tables VARCHAR(500),
|
||||
decision VARCHAR(16),
|
||||
reason VARCHAR(1000),
|
||||
create_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE INDEX assistant_data_audit_user_id ON assistant_data_audit (user_id);
|
||||
CREATE INDEX assistant_data_audit_conversation_id ON assistant_data_audit (conversation_id);
|
||||
CREATE INDEX assistant_data_audit_turn_id ON assistant_data_audit (turn_id);
|
||||
|
||||
-- P-d: data-driven budgets (UTC periods, USD or whole-token allowances).
|
||||
CREATE TABLE assistant_budget
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
scope_type VARCHAR(16) NOT NULL,
|
||||
scope_value VARCHAR(200),
|
||||
period VARCHAR(16) NOT NULL,
|
||||
metric VARCHAR(16) NOT NULL,
|
||||
limit_value NUMERIC NOT NULL CHECK (limit_value > 0),
|
||||
warn_percent INTEGER DEFAULT 80 CHECK (warn_percent >= 0 AND warn_percent < 100),
|
||||
at_limit_action VARCHAR(16) DEFAULT 'DOWNGRADE',
|
||||
downgrade_model_id INTEGER REFERENCES assistant_model(id),
|
||||
hard_ceiling_percent INTEGER DEFAULT 120 CHECK (hard_ceiling_percent >= 100),
|
||||
notes VARCHAR(2000),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
CREATE TABLE assistant_budget_override
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
budget_id INTEGER NOT NULL REFERENCES assistant_budget(id),
|
||||
scope_value VARCHAR(200),
|
||||
extra_value NUMERIC,
|
||||
unlimited_until TIMESTAMP WITH TIME ZONE,
|
||||
reason VARCHAR(1000) NOT NULL,
|
||||
granted_by VARCHAR(200),
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
modify_date TIMESTAMP WITH TIME ZONE,
|
||||
CHECK ((extra_value > 0 AND unlimited_until IS NULL) OR (extra_value IS NULL AND unlimited_until IS NOT NULL))
|
||||
);
|
||||
CREATE INDEX assistant_budget_override_budget_id ON assistant_budget_override(budget_id);
|
||||
CREATE TABLE assistant_budget_event
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
budget_id INTEGER NOT NULL REFERENCES assistant_budget(id),
|
||||
scope_key VARCHAR(400) NOT NULL,
|
||||
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
state VARCHAR(16) NOT NULL,
|
||||
used_value NUMERIC,
|
||||
limit_value NUMERIC,
|
||||
user_id VARCHAR(200),
|
||||
create_date TIMESTAMP WITH TIME ZONE,
|
||||
UNIQUE (budget_id, scope_key, period_start, state)
|
||||
);
|
||||
CREATE INDEX assistant_usage_budget_period ON assistant_usage(create_date, user_id, provider_id, model);
|
||||
|
||||
-- Private projection: standard admin permissions, AI NONE, no owner lock.
|
||||
-- No conversation identifiers, text, request ids or credentials are copied.
|
||||
CREATE TABLE assistant_budget_usage
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
usage_id INTEGER NOT NULL UNIQUE,
|
||||
user_id VARCHAR(255),
|
||||
provider_id INTEGER,
|
||||
model VARCHAR(200),
|
||||
occurred_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
cost_micros BIGINT,
|
||||
input_tokens BIGINT,
|
||||
output_tokens BIGINT,
|
||||
cache_read_tokens BIGINT,
|
||||
cache_write_tokens BIGINT
|
||||
);
|
||||
CREATE INDEX assistant_budget_usage_period ON assistant_budget_usage(occurred_at, provider_id, model);
|
||||
-- Run once before enabling P-d, with writes paused, to include existing usage.
|
||||
INSERT INTO assistant_budget_usage
|
||||
(usage_id, user_id, provider_id, model, occurred_at, cost_micros, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens)
|
||||
SELECT id, user_id, provider_id, model, create_date, cost_micros, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
|
||||
FROM assistant_usage
|
||||
ON CONFLICT (usage_id) DO NOTHING;
|
||||
|
||||
|
||||
-- P-e: profile routing, bounded fallback chains, provider health and turn outcome.
|
||||
ALTER TABLE assistant_profile ADD COLUMN routing_mode VARCHAR(16) DEFAULT 'OFF'
|
||||
CHECK (routing_mode IN ('OFF', 'HEURISTIC', 'CLASSIFIER'));
|
||||
ALTER TABLE assistant_profile ADD COLUMN simple_model_id INTEGER REFERENCES assistant_model(id);
|
||||
ALTER TABLE assistant_profile ADD COLUMN complex_model_id INTEGER REFERENCES assistant_model(id);
|
||||
ALTER TABLE assistant_profile ADD COLUMN classifier_model_id INTEGER REFERENCES assistant_model(id);
|
||||
ALTER TABLE assistant_model ADD COLUMN fallback_model_id INTEGER REFERENCES assistant_model(id);
|
||||
ALTER TABLE assistant_provider ADD COLUMN health_state VARCHAR(8) CHECK (health_state IN ('UP', 'DOWN'));
|
||||
ALTER TABLE assistant_provider ADD COLUMN health_checked_at TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE assistant_provider ADD COLUMN down_until TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE assistant_turn ADD COLUMN route_decision VARCHAR(16);
|
||||
ALTER TABLE assistant_turn ADD COLUMN route_source VARCHAR(16);
|
||||
ALTER TABLE assistant_turn ADD COLUMN fallback_count INTEGER DEFAULT 0;
|
||||
ALTER TABLE assistant_turn ADD COLUMN provider_id INTEGER REFERENCES assistant_provider(id);
|
||||
|
||||
|
||||
-- P-f: embedding registry and admin-only knowledge-source configuration.
|
||||
ALTER TABLE assistant_model ADD COLUMN model_kind VARCHAR(16) DEFAULT 'CHAT'
|
||||
CHECK (model_kind IN ('CHAT', 'EMBEDDING'));
|
||||
ALTER TABLE assistant_model ADD COLUMN embedding_dimensions INTEGER
|
||||
CHECK (embedding_dimensions > 0);
|
||||
|
||||
CREATE TABLE assistant_rag_source (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
table_name VARCHAR(200) NOT NULL,
|
||||
field_names VARCHAR(500) NOT NULL,
|
||||
title_field_name VARCHAR(200),
|
||||
filter_json TEXT,
|
||||
embedding_model_id INTEGER NOT NULL REFERENCES assistant_model(id),
|
||||
chunk_chars INTEGER DEFAULT 1500 CHECK (chunk_chars BETWEEN 100 AND 16000),
|
||||
chunk_overlap_chars INTEGER DEFAULT 200 CHECK (chunk_overlap_chars >= 0 AND chunk_overlap_chars < chunk_chars),
|
||||
last_indexed_at TIMESTAMPTZ,
|
||||
last_index_status VARCHAR(200),
|
||||
last_index_error VARCHAR(500),
|
||||
indexed_record_count INTEGER,
|
||||
indexed_chunk_count INTEGER,
|
||||
create_date TIMESTAMPTZ,
|
||||
modify_date TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Optional pgvector part: reference only. Run the extension step as a DBA.
|
||||
-- PgVectorStore.ensureSchema() performs the remainder for the host's schema/N.
|
||||
-- Use a distinct schema per QQQ instance; match N to the embedding model.
|
||||
-- CREATE EXTENSION IF NOT EXISTS vector;
|
||||
-- CREATE SCHEMA IF NOT EXISTS assistant_rag;
|
||||
-- CREATE TABLE IF NOT EXISTS assistant_rag.assistant_rag_chunk (
|
||||
-- id BIGSERIAL PRIMARY KEY,
|
||||
-- source_id INT NOT NULL,
|
||||
-- table_name TEXT NOT NULL,
|
||||
-- record_pk TEXT NOT NULL,
|
||||
-- field_name TEXT NOT NULL,
|
||||
-- chunk_index INT NOT NULL,
|
||||
-- content_hash CHAR(64) NOT NULL,
|
||||
-- embedding vector(1024) NOT NULL,
|
||||
-- indexed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- UNIQUE(source_id, table_name, record_pk, field_name, chunk_index)
|
||||
-- );
|
||||
-- CREATE INDEX IF NOT EXISTS assistant_rag_chunk_cosine
|
||||
-- ON assistant_rag.assistant_rag_chunk USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- P-g: designated-user scheduled assistant tasks. Install before enabling jobs.
|
||||
CREATE TABLE assistant_scheduled_task (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
prompt TEXT NOT NULL CHECK (char_length(prompt) BETWEEN 1 AND 4000),
|
||||
run_as_user_id VARCHAR(200) NOT NULL,
|
||||
credential_env_name VARCHAR(200) NOT NULL,
|
||||
credential_status VARCHAR(200), -- computed; always NULL in stored rows
|
||||
profile_id INTEGER REFERENCES assistant_profile(id),
|
||||
cron_expression VARCHAR(200) NOT NULL,
|
||||
timezone VARCHAR(100) NOT NULL DEFAULT 'UTC',
|
||||
next_run_at TIMESTAMPTZ,
|
||||
output_mode VARCHAR(20) NOT NULL DEFAULT 'RECORD' CHECK (output_mode IN ('RECORD', 'NOTIFICATION', 'BOTH')),
|
||||
write_mode_requested BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
write_mode_approved_by VARCHAR(200),
|
||||
write_mode_approved_at TIMESTAMPTZ,
|
||||
write_mode_approval_hash VARCHAR(64),
|
||||
max_cost_usd_per_run NUMERIC(18,6) CHECK (max_cost_usd_per_run >= 0),
|
||||
last_run_at TIMESTAMPTZ,
|
||||
last_run_status VARCHAR(20),
|
||||
last_run_error VARCHAR(500),
|
||||
create_date TIMESTAMPTZ,
|
||||
modify_date TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX assistant_scheduled_task_due ON assistant_scheduled_task(next_run_at) WHERE enabled;
|
||||
|
||||
ALTER TABLE assistant_conversation ADD COLUMN scheduled_task_id INTEGER REFERENCES assistant_scheduled_task(id);
|
||||
ALTER TABLE assistant_conversation ADD COLUMN channel VARCHAR(64) NOT NULL DEFAULT 'panel';
|
||||
ALTER TABLE assistant_tool_call ADD COLUMN client_channel VARCHAR(64);
|
||||
|
||||
CREATE TABLE assistant_scheduled_run (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id INTEGER NOT NULL REFERENCES assistant_scheduled_task(id),
|
||||
user_id VARCHAR(200) NOT NULL,
|
||||
scheduled_for TIMESTAMPTZ NOT NULL,
|
||||
started_at TIMESTAMPTZ,
|
||||
ended_at TIMESTAMPTZ,
|
||||
status VARCHAR(20) NOT NULL CHECK (status IN ('RUNNING', 'OK', 'FAILED', 'SKIPPED', 'BLOCKED')),
|
||||
conversation_id INTEGER REFERENCES assistant_conversation(id) ON DELETE SET NULL,
|
||||
turn_id INTEGER REFERENCES assistant_turn(id) ON DELETE SET NULL,
|
||||
summary VARCHAR(2000),
|
||||
cost_micros BIGINT,
|
||||
error VARCHAR(500),
|
||||
create_date TIMESTAMPTZ,
|
||||
modify_date TIMESTAMPTZ,
|
||||
UNIQUE (task_id, scheduled_for)
|
||||
);
|
||||
CREATE INDEX assistant_scheduled_run_owner ON assistant_scheduled_run(user_id, started_at);
|
||||
|
||||
-- X1/Y1: encoded audit filters can exceed the old 500-character record links.
|
||||
ALTER TABLE assistant_tool_call ALTER COLUMN audit_href TYPE TEXT;
|
||||
@@ -1,42 +0,0 @@
|
||||
# Assistant app home and provider settings screenshots
|
||||
|
||||
Taken 2026-10-13 against the local `AssistantDemoServer` (qbit-assistant-provider-anthropic test sources), never production.
|
||||
- Memory backend and the Next UI 1.0.0-RC.16 jar.
|
||||
- The Anthropic API is the recorded test server: real HTTP through the real adapter, engine, Test connection and usage recording, with canned answers and no key.
|
||||
|
||||
Light and dark, 390 and 1440 px:
|
||||
- `home-*`: the Assistant app home.
|
||||
- `provider-*`: the provider settings screen, with the masked key and the model-call child list.
|
||||
- `provider-not-set-*`: a provider whose key variable is not set.
|
||||
- `provider-edit-*`: the edit form.
|
||||
- `prices-*`: the model price table.
|
||||
|
||||
0.1.0-RC.6 additions, taken 2026-10-13 with the same demo server and the Next UI 1.0.0-RC.17 jar. The demo seeds a profile ("Ada"), two skills, and data access rows (page READ_WRITE, media NONE). Its tools run through the `/qqq/v1` UI API.
|
||||
|
||||
Light and dark, 390 and 1440 px:
|
||||
- `home-denials-*`: the home's "Recent denials" card. The analyst's `count_records` on Blog Post got HTTP 403 from `POST /table/{tableName}/count`.
|
||||
- `home-access-*`: the "Data access: what the assistant can do for you" widget (Explain access), with each table's access and each tool allowed or denied, with the reason.
|
||||
- `data-access-*`: the Data access table.
|
||||
- `profile-*`: a profile (persona), with its personality, its scope and its Skills child list.
|
||||
- `skill-*`: a skill: its instructions and when it is used.
|
||||
- `policy-*`: the policy, with its "Tools and data" section.
|
||||
- `panel-persona-*`: the panel with the configured persona. This is the Next UI build of qqq-frontend-next#94; the demo's admin matches the permission-scoped "Ada (analytics)" profile.
|
||||
- `panel-pending-*`: the assistant proposed `update_record` on page 12, which is READ_WRITE. The card shows the change with Save and Cancel; nothing is saved yet.
|
||||
- `panel-confirmed-*`: after Save, the write ran once through `PATCH /qqq/v1/table/page/12` with the user's session. The card says Done, and the record screen behind the panel shows the new value.
|
||||
|
||||
Planned P-a provider registry additions, taken against `OpenAiDemoServer` in the
|
||||
OpenAI provider's test sources with the Next UI **1.0.0-RC.18** jar. See that
|
||||
[demo's run instructions](../../qbit-assistant-provider-openai/src/test/java/com/kingsrook/qbits/assistant/provider/openai/demo/README.md).
|
||||
Discovery and connection-test status, TEST usage and the assistant turn are real
|
||||
executions against the localhost mock; replies/token counts are synthetic, not
|
||||
live GPU measurements. No screenshots in this group have been added yet.
|
||||
|
||||
Light and dark, 390 and 1440 px:
|
||||
- `p-provider-local-*`: Local GPU (Ollama), keyless LOCAL settings, discovery/test results and discovered models.
|
||||
- `p-model-*`: a discovered model's capabilities, compute prices and connection-test result.
|
||||
- `p-home-providers-*`: the home's provider summary and usage from the executed demo calls, including disabled OpenRouter with its key Not set.
|
||||
|
||||
Answer presentation (owner priority, 2026-10-13), taken against `OpenAiDemoServer` with the Next UI build of QRun-IO/qqq-frontend-next#111 (`next dev` proxied to the demo). The demo's OpenAI-compatible mock answers "How many images do I have by type?" the way a model would through the real adapter: five `count_records` calls through the UI API, then `present_results`.
|
||||
|
||||
Light and dark, 390 and 1440 px:
|
||||
- `panel-presentation-*`: the summary line, an Images metric, the "Media by type" table (counts and the server-computed 80.2% and 19.8%, each row a filtered-list link), the pie chart, two filter links (Open JPEG media, Open PNG media) and a follow-up action.
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 92 KiB |