build: assistant provider SPI RC.12 and parent 2.1.2, no allow-lists #16

Merged
KofTwentyTwo merged 1 commits from feature/assistant-spi-rc12 into develop 2026-10-14 16:38:13 +00:00
12 changed files with 56 additions and 65 deletions

No files matched your search

+2 -2
View File
@@ -46,8 +46,8 @@ Services: `approval.ApprovalService`, `execution.ExecutionService`, `measurement
## Dependencies
- Java 21.
- Parent `com.kingsrook:qbit-build-parent:2.1.0` (qqq 4.0.0), with architecture enforcement and strict UI API routes enabled. The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.6` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
- Assistant provider API and OpenAI-compatible provider `0.1.0-RC.11`; no full assistant dependency. For local verification before publication only, use `-Dqbit-assistant.version=0.1.0-ARCHLOCAL-SNAPSHOT -Dqbit.guardrails.enforce=false` and inspect guardrail findings.
- Parent `com.kingsrook:qbit-build-parent:2.1.2` (qqq 4.0.0), with architecture enforcement and strict UI API routes enabled. The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.6` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
- Assistant provider SPI `qbit-assistant-provider-api` `0.1.0-RC.12` (package `com.kingsrook.qbits.assistant.spi`); the OpenAI-compatible provider is test scope; no full assistant dependency. No per-repository guardrail allow-lists.
- Tests: JUnit 5, AssertJ.
## Release
+2
View File
@@ -5,6 +5,7 @@ All notable changes to this project are documented here. The format follows Keep
## [0.1.0] - Unreleased
### Changed
- Move to the assistant provider SPI `0.1.0-RC.12` (package `com.kingsrook.qbits.assistant.spi`) and `qbit-build-parent` `2.1.2`, and drop the per-repository guardrail allow-lists. `qbit-assistant-provider-openai` becomes a test dependency; hosts that register it add it themselves.
- Switch the optional qbit-email integration to the published `com.kingsrook.qbits.email.api` package in `0.1.0-RC.3` (fixes #13). Preserve MARKETING messages, idempotency keys, execution links and outcome mapping; use typed readiness failures so missing providers or brand postal addresses remain send-time rejections. Upgrade `qbit-build-parent` to `2.1.1` and remove all per-repository guardrail allow-lists.
- Resolve architecture finding F5 (#9): depend on `qbit-assistant-provider-api` and `qbit-assistant-provider-openai` `0.1.0-RC.11`, with no full assistant dependency, including in tests.
- Replace the local OpenAI-compatible implementation with the published provider and document `ProviderSettings`, environment-key resolution and provider-name migration. Agent tests use a local scripted double.
@@ -51,5 +52,6 @@ All notable changes to this project are documented here. The format follows Keep
- **Docs:** README, DESIGN, OPERATIONS and STARTER-DATA.
### Changed
- Move to the assistant provider SPI `0.1.0-RC.12` (package `com.kingsrook.qbits.assistant.spi`) and `qbit-build-parent` `2.1.2`, and drop the per-repository guardrail allow-lists. `qbit-assistant-provider-openai` becomes a test dependency; hosts that register it add it themselves.
- **Email:** new `QbitEmailTransport` sends campaign email through qbit-email (one MARKETING message per recipient, unsubscribe and suppression enforced there, idempotent, linked to the execution). It is selected automatically for the `qqq_email` channel when qbit-email is produced and the host registered no such transport; `QqqMessagingEmailTransport` remains the fallback. qbit-email is an optional dependency; QQQ moves to `4.1.0-RC.5`.
+8 -16
View File
@@ -5,9 +5,9 @@
> Marketing for QQQ applications: products and their verified evidence, campaigns, content with revision history, exact human approval, controlled execution, honest measurement, and a marketing agent that prepares work but never publishes.
**Status:** 0.1.0, not yet published.
- Built against QQQ `4.1.0-RC.6` with `qbit-build-parent` `2.1.1`.
- Hosted in QRun-IO/Voyage next to qbit-crm, on a local branch.
**Status:** 0.1.0 release candidates on Maven Central (`com.kingsrook.qbits:qbit-marketing`, latest `0.1.0-RC.2`); 0.1.0 GA is not yet released.
- Built against QQQ `4.1.0-RC.6` with `qbit-build-parent` `2.1.2` and the assistant provider SPI `0.1.0-RC.12`.
- Hosted in QRun-IO/Voyage next to qbit-crm (QRun-IO/Voyage#113).
- The real integrations are implemented but unconfigured. See [what is configured](#what-is-implemented-what-is-not-configured).
It lives inside the host QQQ application: its navigation, authentication, permissions, API, record screens and widgets. It is not a separate marketing app.
@@ -25,7 +25,7 @@ It lives inside the host QQQ application: its navigation, authentication, permis
| **Measurement** | Delivery, awareness, engagement, activation, repeat use and qualified demand are reported separately. Each metric shows raw events and unique actors. "Not measured" is never shown as 0. Test data is kept apart. |
| **CRM** | Uses the host CRM's own contacts, organizations, consent, suppression, lists, form submissions and activities through a narrow gateway. A qbit-crm adapter is included. Nothing is copied, and nobody becomes a lead without the campaign's explicit rule. |
| **Agent** | Plans campaigns, drafts and revises content, flags unsupported claims, stale sources and readiness gaps, and analyzes results to recommend the next experiment. It uses narrow typed tools, runs as the requesting user, sees data only as untrusted input, and sees no CRM personal data. Every run is recorded with its steps, provider, model, tokens and cost. It can create drafts and findings; it cannot approve, schedule or send. |
| **Models** | Interchangeable providers through `qbit-assistant-provider-api`'s `AssistantModelProvider`: OpenAI-compatible endpoints through `qbit-assistant-provider-openai` (included), or a host-supplied provider such as Claude. Tests use a local scripted double. With no provider configured, the agent says so and everything else works. Installing marketing does not install the assistant runtime, tables or routes. |
| **Models** | Interchangeable providers through `qbit-assistant-provider-api`'s `AssistantModelProvider`: OpenAI-compatible endpoints through `qbit-assistant-provider-openai` (added by the host), or a host-supplied provider such as Claude. Tests use a local scripted double. With no provider configured, the agent says so and everything else works. Installing marketing does not install the assistant runtime, tables or routes. |
## Install
@@ -38,7 +38,7 @@ It lives inside the host QQQ application: its navigation, authentication, permis
```
```java
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
import com.kingsrook.qbits.assistant.provider.openai.OpenAiCompatibleModelProvider;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
@@ -66,8 +66,8 @@ Then:
[`docs/OPERATIONS.md`](docs/OPERATIONS.md) covers every setting, the run-books and the limits.
The provider SPI, pricing and OpenAI-compatible implementation are pinned to `0.1.0-RC.11`.
Replace imports of marketing's former `agent.provider.OpenAiCompatibleModelProvider` with the published class above and replace its builder with `ProviderSettings`.
Marketing depends only on the provider SPI (`qbit-assistant-provider-api` `0.1.0-RC.12`, package `com.kingsrook.qbits.assistant.spi`). To use the OpenAI-compatible provider, the host adds `com.kingsrook.qbits:qbit-assistant-provider-openai:0.1.0-RC.12` itself.
Replace imports of marketing's former `agent.provider.OpenAiCompatibleModelProvider` with the published class above and replace its builder with `ProviderSettings`. Hosts upgrading from RC.11 change `com.kingsrook.qbits.assistant.provider.*` and `…assistant.engine.ModelPricing` imports to `com.kingsrook.qbits.assistant.spi.*`.
The host resolves the API key reference; `ProviderSettings.withApiKeyEnvName(...)` alone does not resolve it.
Set any existing Marketing Policy provider override to `openai-compatible` as well. The base URL, key environment variable name and model remain host-configurable; omit the key for a local endpoint that needs none.
@@ -131,15 +131,7 @@ Scale gaps (single worker per JVM, in-memory rate limits) are listed in [`docs/O
Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM. The entities, enums and DDL are generated from one spec: `python -I scripts/generate-model.py .` (commit the output).
Architecture guardrails and strict UI API route checks are enabled. The artifact allow-list preserves the parent's defaults and adds only `qbit-assistant-provider-openai`; the package allow-list also permits the extracted provider SPI and pricing. Parent 2.1.0 matches package prefixes using `prefix + "."`, so the pricing allowance must be `com.kingsrook.qbits.assistant.engine`, not the class name `…engine.ModelPricing`. The API artifact contains only `ModelPricing` in that package; the full assistant artifact remains prohibited.
Until provider `0.1.0-RC.11` is available on Central, verify against the locally installed assistant develop build only with:
```bash
./mvnw -Dqbit-assistant.version=0.1.0-ARCHLOCAL-SNAPSHOT -Dqbit.guardrails.enforce=false verify
```
This runs the guardrails in report mode so G7 can report the local SNAPSHOT dependencies. Keep the release version and enforcement enabled in the POM.
Architecture guardrails and strict UI API route checks are enabled with the parent's default allow-lists only (no per-repository lists): `qbit-build-parent` 2.1.2 allows the `*-api` artifacts and the `com.kingsrook.qbits.assistant.spi` and `com.kingsrook.qbits.email.api` packages. `qbit-assistant-provider-openai` is a test dependency only, and the full assistant artifact is never a dependency.
## License
+1 -1
View File
@@ -18,7 +18,7 @@ The work is truthful and auditable: facts come from sources with dates, people a
| From | Reused for |
|---|---|
| **qbit-assistant** (0.1.0-RC.2) | The model-provider SPI: `AssistantModelProvider`, `ModelRequest` / `ModelResponse` / `ModelPart`, `StreamHandle`, `ModelProviderException` with its retryable kinds, and `ModelUsage`. Also the Claude provider (`qbit-assistant-provider-anthropic`), `ScriptedModelProvider` for tests and demos, and `ModelPricing` for cost. Marketing adds an OpenAI-compatible provider that implements the same SPI. There is no second provider abstraction. |
| **qbit-assistant** (0.1.0-RC.2) | The model-provider SPI: `AssistantModelProvider`, `ModelRequest` / `ModelResponse` / `ModelPart`, `StreamHandle`, `ModelProviderException` with its retryable kinds, and `ModelUsage`. Since RC.12 it lives in `com.kingsrook.qbits.assistant.spi` (`qbit-assistant-provider-api`), together with `ModelPricing` for cost; hosts register a provider such as `qbit-assistant-provider-anthropic` or `qbit-assistant-provider-openai`. There is no second provider abstraction. |
| **QQQ AI tool kernel** (qqq#1078, core) | The tool contract: `AbstractAiTool` / `AiToolKind` (READ, WRITE_PROPOSAL), `AiToolOutcome`, the `qqq_data` untrusted-data envelope (`AiDataEnvelope`), secret redaction (`AiRedactor`), strict input keys, and running as the user's session. The RC.6 baseline includes `AiToolRegistry` (QRun-IO/qqq#1096); Marketing contributes its read tools there and runs its own agent tools with the same contract. |
| **QQQ core permission enforcement** (qqq#1061) | Agent tools read with `PermissionEnforcement.ENFORCE`. Service entry points check their step's permission, then run as trusted internal work in an authorized scope, as QQQ does for calls nested in a core action. Record security locks always apply. |
| **qbit-assistant / sql-workbench metadata builders** | Explicit builder-produced tables (not annotation scanning), snake_case `marketing_*` names, QQQ standard permission suffixes, `DenyBehavior.HIDDEN`, read-only system tables, record locks, reference DDL in `docs/ddl`, `tableMetaDataCustomizer` and `inferBackendNames`. |
+5 -8
View File
@@ -22,15 +22,10 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
<!-- Share the provider SPI and pricing without installing the assistant runtime. -->
<qbit-assistant.version>0.1.0-RC.11</qbit-assistant.version>
<!-- The provider SPI (com.kingsrook.qbits.assistant.spi) only: no assistant runtime. -->
<qbit-assistant.version>0.1.0-RC.12</qbit-assistant.version>
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
<!-- qbit-email from Maven Central -->
<!-- Assistant provider SPI only (not email): these replace the parent lists, so they repeat -->
<!-- the parent defaults (object storage, qbit-email's api, *-api) plus the assistant entries. -->
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email.api,com.kingsrook.qbits.assistant.provider,com.kingsrook.qbits.assistant.engine</qbit.guardrails.allowedApiPackages>
<qbit.guardrails.allowedQbitArtifacts>com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:qbit-email,com.kingsrook.qbits:*-api,com.kingsrook.qbits:qbit-assistant-provider-openai</qbit.guardrails.allowedQbitArtifacts>
<qbit-email.version>0.1.0-RC.3</qbit-email.version>
<h2.version>2.3.232</h2.version>
</properties>
@@ -38,7 +33,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.1.1</version>
<version>2.1.2</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -92,6 +87,8 @@
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-assistant-provider-openai</artifactId>
<version>${qbit-assistant.version}</version>
<!-- tests only: hosts add the provider they register (README, Install) -->
<scope>test</scope>
</dependency>
<!-- optional: email through qbit-email when it is produced in the host instance (QbitEmailTransport) -->
@@ -21,8 +21,8 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.assistant.engine.ModelPricing;
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
import com.kingsrook.qbits.assistant.spi.ModelPricing;
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
import com.kingsrook.qbits.marketing.crm.MarketingCrmGateway;
import com.kingsrook.qbits.marketing.transport.MarketingTransport;
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
@@ -27,7 +27,7 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
import com.kingsrook.qbits.marketing.crm.MarketingCrmGateway;
import com.kingsrook.qbits.marketing.email.QbitEmailTransport;
import com.kingsrook.qbits.marketing.model.MarketingPolicy;
@@ -25,18 +25,18 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.assistant.engine.ModelPricing;
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
import com.kingsrook.qbits.assistant.provider.CacheHint;
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
import com.kingsrook.qbits.assistant.provider.ModelMessage;
import com.kingsrook.qbits.assistant.provider.ModelPart;
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
import com.kingsrook.qbits.assistant.provider.ModelRequest;
import com.kingsrook.qbits.assistant.provider.ModelResponse;
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
import com.kingsrook.qbits.assistant.provider.ModelUsage;
import com.kingsrook.qbits.assistant.provider.SystemBlock;
import com.kingsrook.qbits.assistant.spi.ModelPricing;
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
import com.kingsrook.qbits.assistant.spi.CacheHint;
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
import com.kingsrook.qbits.assistant.spi.ModelMessage;
import com.kingsrook.qbits.assistant.spi.ModelPart;
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
import com.kingsrook.qbits.assistant.spi.ModelRequest;
import com.kingsrook.qbits.assistant.spi.ModelResponse;
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
import com.kingsrook.qbits.assistant.spi.ModelUsage;
import com.kingsrook.qbits.assistant.spi.SystemBlock;
import com.kingsrook.qbits.marketing.MarketingQBitConfig;
import com.kingsrook.qbits.marketing.MarketingRuntime;
import com.kingsrook.qbits.marketing.MarketingStore;
@@ -19,8 +19,8 @@ package com.kingsrook.qbits.marketing;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
import com.kingsrook.qbits.assistant.provider.ModelUsage;
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
import com.kingsrook.qbits.assistant.spi.ModelUsage;
import com.kingsrook.qbits.marketing.agent.MarketingAgentService;
import com.kingsrook.qbits.marketing.agent.tools.MarketingTools;
import com.kingsrook.qbits.marketing.approval.ApprovalService;
@@ -315,8 +315,8 @@ class AgentAndSecurityTest extends BaseMarketingTest
{
throw (new RuntimeException(e));
}
return (new com.kingsrook.qbits.assistant.provider.ModelResponse().withParts(new java.util.ArrayList<>(List.of(
com.kingsrook.qbits.assistant.provider.ModelPart.toolUse("t1", MarketingTools.LIST_CHANNELS, Map.of())))).withStopReason("tool_use"));
return (new com.kingsrook.qbits.assistant.spi.ModelResponse().withParts(new java.util.ArrayList<>(List.of(
com.kingsrook.qbits.assistant.spi.ModelPart.toolUse("t1", MarketingTools.LIST_CHANNELS, Map.of())))).withStopReason("tool_use"));
});
MarketingAgentRun cancelled = new MarketingAgentService().run(AgentTask.ANALYZE_RESULTS, campaignId, null, null);
assertThat(AgentRunStatus.getById(cancelled.getStatus())).isEqualTo(AgentRunStatus.CANCELLED);
@@ -21,7 +21,7 @@ import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.assistant.provider.ModelUsage;
import com.kingsrook.qbits.assistant.spi.ModelUsage;
import com.kingsrook.qbits.marketing.agent.MarketingAgentService;
import com.kingsrook.qbits.marketing.agent.tools.MarketingTools;
import com.kingsrook.qbits.marketing.measurement.MeasurementReport;
@@ -22,16 +22,16 @@ import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.assistant.provider.AssistantModelProvider;
import com.kingsrook.qbits.assistant.provider.CompletableStreamHandle;
import com.kingsrook.qbits.assistant.provider.ModelCapabilities;
import com.kingsrook.qbits.assistant.provider.ModelEventListener;
import com.kingsrook.qbits.assistant.provider.ModelPart;
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
import com.kingsrook.qbits.assistant.provider.ModelRequest;
import com.kingsrook.qbits.assistant.provider.ModelResponse;
import com.kingsrook.qbits.assistant.provider.ModelUsage;
import com.kingsrook.qbits.assistant.provider.StreamHandle;
import com.kingsrook.qbits.assistant.spi.AssistantModelProvider;
import com.kingsrook.qbits.assistant.spi.CompletableStreamHandle;
import com.kingsrook.qbits.assistant.spi.ModelCapabilities;
import com.kingsrook.qbits.assistant.spi.ModelEventListener;
import com.kingsrook.qbits.assistant.spi.ModelPart;
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
import com.kingsrook.qbits.assistant.spi.ModelRequest;
import com.kingsrook.qbits.assistant.spi.ModelResponse;
import com.kingsrook.qbits.assistant.spi.ModelUsage;
import com.kingsrook.qbits.assistant.spi.StreamHandle;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@@ -25,14 +25,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import com.kingsrook.qbits.assistant.provider.ModelMessage;
import com.kingsrook.qbits.assistant.provider.ModelPart;
import com.kingsrook.qbits.assistant.provider.ModelProviderException;
import com.kingsrook.qbits.assistant.provider.ModelRequest;
import com.kingsrook.qbits.assistant.provider.ModelResponse;
import com.kingsrook.qbits.assistant.provider.ModelToolDefinition;
import com.kingsrook.qbits.assistant.provider.ProviderSettings;
import com.kingsrook.qbits.assistant.provider.SystemBlock;
import com.kingsrook.qbits.assistant.spi.ModelMessage;
import com.kingsrook.qbits.assistant.spi.ModelPart;
import com.kingsrook.qbits.assistant.spi.ModelProviderException;
import com.kingsrook.qbits.assistant.spi.ModelRequest;
import com.kingsrook.qbits.assistant.spi.ModelResponse;
import com.kingsrook.qbits.assistant.spi.ModelToolDefinition;
import com.kingsrook.qbits.assistant.spi.ProviderSettings;
import com.kingsrook.qbits.assistant.spi.SystemBlock;
import com.kingsrook.qbits.assistant.provider.openai.OpenAiCompatibleModelProvider;
import com.kingsrook.qbits.marketing.MarketingQBitConfig;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;