Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b831dd5a
|
No files matched your search
@@ -1,12 +1,12 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
setup: true
|
setup: true
|
||||||
orbs:
|
orbs:
|
||||||
qqq-orb: kingsrook/qqq-orb@0.8
|
qqq-orb: kingsrook/qqq-orb@0.7.12
|
||||||
workflows:
|
workflows:
|
||||||
setup:
|
setup:
|
||||||
jobs:
|
jobs:
|
||||||
- qqq-orb/generate_pipeline:
|
- qqq-orb/generate_pipeline:
|
||||||
orb_version: '0.8'
|
orb_version: 0.7.12
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore: automation/qbit-rc-pending
|
ignore: automation/qbit-rc-pending
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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,72 +0,0 @@
|
|||||||
name: Release candidate
|
|
||||||
'on':
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
dry_run:
|
|
||||||
description: Plan a cut without changing release state
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
finalize:
|
|
||||||
description: Retry finalization of an existing cut; do not cut another RC
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
recover:
|
|
||||||
description: Recover the same published RC after a deployment timeout; never
|
|
||||||
reupload
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
circle_build:
|
|
||||||
description: Original release run id (recovery only)
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
merge_sha:
|
|
||||||
description: Existing cut merge_sha (finalize retry only)
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
bump_sha:
|
|
||||||
description: Existing cut bump_sha (finalize retry only)
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
branch:
|
|
||||||
description: Existing cut branch (finalize retry only)
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
version:
|
|
||||||
description: Existing cut version (finalize retry only)
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
jobs:
|
|
||||||
release-rc:
|
|
||||||
# The old qbit-rc-published repository_dispatch is replaced by the already-proven workflow_dispatch
|
|
||||||
# completion: the publisher's own ActionsWRITE job dispatches this workflow with finalize=true plus
|
|
||||||
# merge_sha/bump_sha/branch/version (same inputs as the manual finalize retry).
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
|
|
||||||
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
|
|
||||||
'release:rc'))
|
|
||||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qbit-release-rc.yaml@qrun-release-v1
|
|
||||||
with:
|
|
||||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
|
|
||||||
}}
|
|
||||||
finalize: ${{ github.event_name == 'workflow_dispatch' && inputs.finalize }}
|
|
||||||
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
|
|
||||||
}}
|
|
||||||
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|
|
||||||
|| '' }}
|
|
||||||
merge_sha: ${{ inputs.merge_sha || '' }}
|
|
||||||
bump_sha: ${{ inputs.bump_sha || '' }}
|
|
||||||
branch: ${{ inputs.branch || '' }}
|
|
||||||
version: ${{ inputs.version || '' }}
|
|
||||||
secrets:
|
|
||||||
QRUN_TAG_SIGNING_KEY: ${{ secrets.QRUN_TAG_SIGNING_KEY }}
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
profile: qbit
|
profile: qbit
|
||||||
orb_version: '0.8'
|
orb_version: 0.7.12
|
||||||
default_branch: develop
|
default_branch: develop
|
||||||
java_version: '21'
|
java_version: '21'
|
||||||
browsers: true
|
browsers: true
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased - Answer visible at the end of the stream
|
|
||||||
|
|
||||||
- The turn's redacted transcript (the question and answer parts, pending actions, stored events) is now written before `message.end` is sent. The panel reloads the conversation the moment the stream ends, and used to read the empty placeholders (`[]`) the transcript redactor stores until its flush, so the newest question and answer showed as empty bubbles until the panel was closed and reopened.
|
|
||||||
|
|
||||||
## Unreleased - Clear reason without a model provider (UI API extension 1.7.0)
|
|
||||||
|
|
||||||
- `GET /config` checks the server-wide causes before the user's permission: `DISABLED`, then `NOT_CONFIGURED` (no usable model provider), then `NO_PERMISSION`, then `KILL_SWITCH`. A server with no provider no longer tells users to ask for access (#82).
|
|
||||||
- UI API extension assistant 1.7.0: `GET /config` has `message` whenever `reason` is set: a sentence for the user. For `NOT_CONFIGURED` it says what an administrator configures (an enabled row on **Assistant > Providers**, for example Anthropic with `ANTHROPIC_API_KEY` set in the server environment, or a local OpenAI-compatible server such as Ollama).
|
|
||||||
- Without a usable provider, calls from a user who also lacks `assistant.hasAccess` answer `503 NOT_CONFIGURED` with that message instead of `403 NO_PERMISSION`; users with access keep their conversations, and a new question answers `503 NOT_CONFIGURED`.
|
|
||||||
- Verified the OpenAI-compatible adapter (kind `OLLAMA`) live against a local Ollama `llama3.3:70b-instruct-q4_K_M`, including a tool call (`LiveOpenAiCompatibleTest`).
|
|
||||||
|
|
||||||
## Unreleased - Conversation management (UI API extension 1.6.0)
|
## Unreleased - Conversation management (UI API extension 1.6.0)
|
||||||
|
|
||||||
- `assistant_conversation` is open to every assistant user (`assistant.hasAccess` or `assistant.admin`, through the new `AssistantAccessPermissionChecker`) in the standard table UI. Rows stay locked to their owner by the `userId` record lock, so a user only ever sees, updates and deletes their own conversations; admins with the all-access key still see everyone's. The Assistant app (its nav) and every other assistant table stay admin-only.
|
- `assistant_conversation` is open to every assistant user (`assistant.hasAccess` or `assistant.admin`, through the new `AssistantAccessPermissionChecker`) in the standard table UI. Rows stay locked to their owner by the `userId` record lock, so a user only ever sees, updates and deletes their own conversations; admins with the all-access key still see everyone's. The Assistant app (its nav) and every other assistant table stay admin-only.
|
||||||
@@ -38,7 +27,6 @@
|
|||||||
- Provider SPI: `ModelPart.Type.IMAGE` (`mediaType`, base64 `data`), mapped by the Anthropic, Bedrock and OpenAI-compatible providers. Claude models report vision. `ModelPart.toString()` never includes text or image data.
|
- Provider SPI: `ModelPart.Type.IMAGE` (`mediaType`, base64 `data`), mapped by the Anthropic, Bedrock and OpenAI-compatible providers. Claude models report vision. `ModelPart.toString()` never includes text or image data.
|
||||||
- `GET /config`: `features.attachments`, `limits.maxAttachmentBytes`, `limits.maxAttachments`, `limits.attachmentTypes`.
|
- `GET /config`: `features.attachments`, `limits.maxAttachmentBytes`, `limits.maxAttachments`, `limits.attachmentTypes`.
|
||||||
- System prompt: when `create_record` / `update_record` are offered, the model proposes the change instead of saying it cannot create records.
|
- System prompt: when `create_record` / `update_record` are offered, the model proposes the change instead of saying it cannot create records.
|
||||||
||||||| parent of c5125f9 (fix(assistant): store the transcript before message.end so the panel shows the answer)
|
|
||||||
|
|
||||||
## Unreleased - Optional provider health
|
## Unreleased - Optional provider health
|
||||||
|
|
||||||
|
|||||||
@@ -479,17 +479,6 @@ OLLAMA, LM_STUDIO, VLLM, LLAMA_CPP or OPENAI_COMPATIBLE as their kind.
|
|||||||
Registry prices override type/model prices, then configured prices. Unpriced
|
Registry prices override type/model prices, then configured prices. Unpriced
|
||||||
local models cost zero. Capabilities can be adjusted per registered model.
|
local models cost zero. Capabilities can be adjusted per registered model.
|
||||||
|
|
||||||
For a local Ollama with no key (the live test ran against `llama3.3:70b-instruct-q4_K_M`):
|
|
||||||
|
|
||||||
```java
|
|
||||||
config.withProviderType(new OpenAiCompatibleProviderType());
|
|
||||||
// provider row: type openai-compatible, kind OLLAMA, model llama3.3:70b-instruct-q4_K_M,
|
|
||||||
// base URL empty (http://localhost:11434/v1) or an allow-listed override such as http://127.0.0.1:11434/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
Without any usable provider `GET /config` answers `enabled: false, reason: NOT_CONFIGURED` with a
|
|
||||||
`message` that says what to configure, before any permission check.
|
|
||||||
|
|
||||||
For Ollama behind a TLS proxy with a private CA and bearer authentication:
|
For Ollama behind a TLS proxy with a private CA and bearer authentication:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ Guards:
|
|||||||
`ProviderService.resolve(policyModel)`:
|
`ProviderService.resolve(policyModel)`:
|
||||||
1. The enabled row marked default (marking one default unmarks the others), else the first enabled row.
|
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.
|
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. `GET /config` reports it before `NO_PERMISSION`, with a `message` naming what to configure (#82).
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -86,20 +86,6 @@ public class AssistantApi
|
|||||||
|
|
||||||
public static final int TITLE_MAX_CHARS = 60;
|
public static final int TITLE_MAX_CHARS = 60;
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
// GET /config reason (and error code) when no model provider is usable: //
|
|
||||||
// no enabled provider row that can be built (a missing key, a disallowed //
|
|
||||||
// variable or URL, an uninstalled type) and no provider registered in code //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
public static final String NOT_CONFIGURED = "NOT_CONFIGURED";
|
|
||||||
|
|
||||||
public static final String NO_MODEL_PROVIDER_MESSAGE = "No model provider is configured on this server. An administrator adds an enabled provider on "
|
|
||||||
+ "Assistant > Providers (for example type Anthropic, whose API key variable ANTHROPIC_API_KEY must be set in the server environment, or a local "
|
|
||||||
+ "OpenAI-compatible server such as Ollama).";
|
|
||||||
public static final String NO_PERMISSION_MESSAGE = "You do not have access to the assistant.";
|
|
||||||
public static final String DISABLED_MESSAGE = "The assistant is turned off for this application.";
|
|
||||||
public static final String KILL_SWITCH_MESSAGE = "The assistant has been turned off by an administrator.";
|
|
||||||
|
|
||||||
private final AssistantRuntime runtime;
|
private final AssistantRuntime runtime;
|
||||||
private final AssistantQBitConfig config;
|
private final AssistantQBitConfig config;
|
||||||
private final AssistantStore store;
|
private final AssistantStore store;
|
||||||
@@ -148,39 +134,27 @@ public class AssistantApi
|
|||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
String reason = null;
|
String reason = null;
|
||||||
|
|
||||||
String message = null;
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
|
||||||
// server-wide causes come before the user's own permission, so a //
|
|
||||||
// server without a usable model provider says so (QRun-IO/qbit- //
|
|
||||||
// assistant#82) instead of sending the user to ask for access //
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
|
||||||
if(!config.isEffectivelyEnabled())
|
if(!config.isEffectivelyEnabled())
|
||||||
{
|
{
|
||||||
reason = "DISABLED";
|
reason = "DISABLED";
|
||||||
message = DISABLED_MESSAGE;
|
|
||||||
}
|
|
||||||
else if(activeProvider() == null)
|
|
||||||
{
|
|
||||||
reason = NOT_CONFIGURED;
|
|
||||||
message = NO_MODEL_PROVIDER_MESSAGE;
|
|
||||||
}
|
}
|
||||||
else if(!hasAccess())
|
else if(!hasAccess())
|
||||||
{
|
{
|
||||||
reason = "NO_PERMISSION";
|
reason = "NO_PERMISSION";
|
||||||
message = NO_PERMISSION_MESSAGE;
|
}
|
||||||
|
else if(activeProvider() == null)
|
||||||
|
{
|
||||||
|
reason = "NOT_CONFIGURED";
|
||||||
}
|
}
|
||||||
else if(runtime.getPolicyService().current(false).killSwitch())
|
else if(runtime.getPolicyService().current(false).killSwitch())
|
||||||
{
|
{
|
||||||
reason = "KILL_SWITCH";
|
reason = "KILL_SWITCH";
|
||||||
message = KILL_SWITCH_MESSAGE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.put("enabled", reason == null);
|
result.put("enabled", reason == null);
|
||||||
if(reason != null)
|
if(reason != null)
|
||||||
{
|
{
|
||||||
result.put("reason", reason);
|
result.put("reason", reason);
|
||||||
result.put("message", message);
|
|
||||||
}
|
}
|
||||||
result.put("shortcut", config.getShortcut());
|
result.put("shortcut", config.getShortcut());
|
||||||
result.put("model", reason == null ? activeProvider().model() : config.getModels().getMainModel());
|
result.put("model", reason == null ? activeProvider().model() : config.getModels().getMainModel());
|
||||||
@@ -451,11 +425,11 @@ public class AssistantApi
|
|||||||
ProviderService.Active active = runtime.getProviderService().resolve(policy.mainModel());
|
ProviderService.Active active = runtime.getProviderService().resolve(policy.mainModel());
|
||||||
if(active == null)
|
if(active == null)
|
||||||
{
|
{
|
||||||
throw (new AssistantApiException(503, NOT_CONFIGURED, NO_MODEL_PROVIDER_MESSAGE));
|
throw (new AssistantApiException(503, "NOT_CONFIGURED", "The assistant is not configured on this server."));
|
||||||
}
|
}
|
||||||
if(policy.killSwitch())
|
if(policy.killSwitch())
|
||||||
{
|
{
|
||||||
throw (new AssistantApiException(503, "KILL_SWITCH", KILL_SWITCH_MESSAGE));
|
throw (new AssistantApiException(503, "KILL_SWITCH", "The assistant has been turned off by an administrator."));
|
||||||
}
|
}
|
||||||
|
|
||||||
Long capMicros = ProviderService.capMicros(active.row());
|
Long capMicros = ProviderService.capMicros(active.row());
|
||||||
@@ -1191,33 +1165,11 @@ public class AssistantApi
|
|||||||
{
|
{
|
||||||
if(!config.isEffectivelyEnabled())
|
if(!config.isEffectivelyEnabled())
|
||||||
{
|
{
|
||||||
throw (new AssistantApiException(503, "DISABLED", DISABLED_MESSAGE));
|
throw (new AssistantApiException(503, "DISABLED", "The assistant is turned off for this application."));
|
||||||
}
|
}
|
||||||
if(!hasAccess())
|
if(!hasAccess())
|
||||||
{
|
{
|
||||||
if(activeProviderOrNull() == null)
|
throw (new AssistantApiException(403, "NO_PERMISSION", "You do not have access to the assistant."));
|
||||||
{
|
|
||||||
throw (new AssistantApiException(503, NOT_CONFIGURED, NO_MODEL_PROVIDER_MESSAGE));
|
|
||||||
}
|
|
||||||
throw (new AssistantApiException(403, "NO_PERMISSION", NO_PERMISSION_MESSAGE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** The active provider, or null when none is usable or it cannot be read
|
|
||||||
** (only used to pick the clearer refusal).
|
|
||||||
*******************************************************************************/
|
|
||||||
private ProviderService.Active activeProviderOrNull()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return (activeProvider());
|
|
||||||
}
|
|
||||||
catch(QException | RuntimeException e)
|
|
||||||
{
|
|
||||||
return (null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -63,7 +63,7 @@ import io.javalin.http.HttpStatus;
|
|||||||
public final class AssistantSpecs
|
public final class AssistantSpecs
|
||||||
{
|
{
|
||||||
public static final String EXTENSION_NAME = "assistant";
|
public static final String EXTENSION_NAME = "assistant";
|
||||||
public static final String EXTENSION_VERSION = "1.7.0";
|
public static final String EXTENSION_VERSION = "1.6.0";
|
||||||
public static final String PREFIX = "/assistant";
|
public static final String PREFIX = "/assistant";
|
||||||
|
|
||||||
|
|
||||||
@@ -198,8 +198,7 @@ public final class AssistantSpecs
|
|||||||
"starterPrompts", array(string(null), "Up to three suggested first questions in total: permitted configured questions first, then generated questions. Table, row and field access are checked on every request.")));
|
"starterPrompts", array(string(null), "Up to three suggested first questions in total: permitted configured questions first, then generated questions. Table, row and field access are checked on every request.")));
|
||||||
rs.put("AssistantConfigResponseV1", object("Whether the assistant is available to this user, and how.",
|
rs.put("AssistantConfigResponseV1", object("Whether the assistant is available to this user, and how.",
|
||||||
"enabled", bool("Whether the panel may be used."),
|
"enabled", bool("Whether the panel may be used."),
|
||||||
"reason", string("Why not: DISABLED, NOT_CONFIGURED (no usable model provider on the server), NO_PERMISSION or KILL_SWITCH, checked in that order.").withNullable(true),
|
"reason", string("Why not: DISABLED, NO_PERMISSION, NOT_CONFIGURED or KILL_SWITCH.").withNullable(true),
|
||||||
"message", string("Since 1.7.0: when reason is set, a sentence for the user that names the cause (for NOT_CONFIGURED, what an administrator configures).").withNullable(true),
|
|
||||||
"shortcut", string("Keyboard shortcut, e.g. mod+j."),
|
"shortcut", string("Keyboard shortcut, e.g. mod+j."),
|
||||||
"model", string("The model that answers."),
|
"model", string("The model that answers."),
|
||||||
"limits", freeObject("maxTurnSeconds and maxToolCallsPerTurn. When features.attachments is true also maxAttachmentBytes (the largest file read, in bytes), "
|
"limits", freeObject("maxTurnSeconds and maxToolCallsPerTurn. When features.attachments is true also maxAttachmentBytes (the largest file read, in bytes), "
|
||||||
|
|||||||
@@ -1343,17 +1343,6 @@ public class TurnEngine implements Runnable
|
|||||||
String finalErrorText = errorText;
|
String finalErrorText = errorText;
|
||||||
safely(() -> emit(TurnEvent.ERROR, map("code", finalErrorCode, "message", finalErrorText)));
|
safely(() -> emit(TurnEvent.ERROR, map("code", finalErrorCode, "message", finalErrorText)));
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
|
||||||
// the stream ends at message.end and the panel reloads the //
|
|
||||||
// conversation at once, so the redacted transcript (message parts, //
|
|
||||||
// pending actions, events) must be stored before that event is sent //
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
|
||||||
if(!alreadyTerminal)
|
|
||||||
{
|
|
||||||
safely(this::persistPendingEvents);
|
|
||||||
safely(() -> transcript.flush(store));
|
|
||||||
}
|
|
||||||
safely(() -> emit(TurnEvent.MESSAGE_END, map("messageId", assistantMessage.getId(), "status", finalStatus, "stopReason", finalStopReason)));
|
safely(() -> emit(TurnEvent.MESSAGE_END, map("messageId", assistantMessage.getId(), "status", finalStatus, "stopReason", finalStopReason)));
|
||||||
if(!alreadyTerminal)
|
if(!alreadyTerminal)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -175,13 +175,7 @@ components:
|
|||||||
description: "Whether the panel may be used."
|
description: "Whether the panel may be used."
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
reason:
|
reason:
|
||||||
description: "Why not: DISABLED, NOT_CONFIGURED (no usable model provider\
|
description: "Why not: DISABLED, NO_PERMISSION, NOT_CONFIGURED or KILL_SWITCH."
|
||||||
\ on the server), NO_PERMISSION or KILL_SWITCH, checked in that order."
|
|
||||||
nullable: true
|
|
||||||
type: "string"
|
|
||||||
message:
|
|
||||||
description: "Since 1.7.0: when reason is set, a sentence for the user that\
|
|
||||||
\ names the cause (for NOT_CONFIGURED, what an administrator configures)."
|
|
||||||
nullable: true
|
nullable: true
|
||||||
type: "string"
|
type: "string"
|
||||||
shortcut:
|
shortcut:
|
||||||
@@ -311,9 +305,7 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
type: "string"
|
type: "string"
|
||||||
starterPrompts:
|
starterPrompts:
|
||||||
description: "Up to three suggested first questions in total: permitted\
|
description: "Up to three suggested first questions in total: permitted configured questions first, then generated questions. Table, row and field access are checked on every request."
|
||||||
\ configured questions first, then generated questions. Table, row and\
|
|
||||||
\ field access are checked on every request."
|
|
||||||
items:
|
items:
|
||||||
type: "string"
|
type: "string"
|
||||||
type: "array"
|
type: "array"
|
||||||
@@ -476,7 +468,7 @@ info:
|
|||||||
description: "QQQ Assistant: conversations, streamed turns, feedback, pending actions\
|
description: "QQQ Assistant: conversations, streamed turns, feedback, pending actions\
|
||||||
\ and data access explanations"
|
\ and data access explanations"
|
||||||
title: "QQQ Middleware API extension: assistant"
|
title: "QQQ Middleware API extension: assistant"
|
||||||
version: "1.7.0"
|
version: "1.6.0"
|
||||||
openapi: "3.0.3"
|
openapi: "3.0.3"
|
||||||
paths:
|
paths:
|
||||||
/qqq/v1/assistant/config:
|
/qqq/v1/assistant/config:
|
||||||
|
|||||||
+1
-41
@@ -166,45 +166,6 @@ class AssistantRouteProviderTest extends BaseAssistantTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** No usable model provider (QRun-IO/qbit-assistant#82): the probe names the
|
|
||||||
** server's configuration, for users with and without access, and calls
|
|
||||||
** answer 503 NOT_CONFIGURED instead of 403 NO_PERMISSION.
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testNoModelProviderIsNotAPermissionProblem() throws Exception
|
|
||||||
{
|
|
||||||
String providerName = config.getProviderName();
|
|
||||||
config.setProviderName("no-such-provider");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for(String user : new String[] {EDITOR, NO_ACCESS})
|
|
||||||
{
|
|
||||||
Map<String, Object> probe = json(send("GET", "/config", user, null));
|
|
||||||
assertThat(probe).containsEntry("enabled", false).containsEntry("reason", "NOT_CONFIGURED");
|
|
||||||
assertThat((String) probe.get("message")).contains("No model provider").contains("Assistant > Providers").contains("ANTHROPIC_API_KEY");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse<String> denied = send("GET", "/conversations", NO_ACCESS, null);
|
|
||||||
assertThat(denied.statusCode()).isEqualTo(503);
|
|
||||||
assertThat(json(denied)).containsEntry("code", "NOT_CONFIGURED");
|
|
||||||
assertThat((String) json(denied).get("message")).contains("No model provider");
|
|
||||||
|
|
||||||
assertThat(send("GET", "/conversations", EDITOR, null).statusCode()).isEqualTo(200);
|
|
||||||
HttpResponse<String> turn = send("POST", "/conversations", EDITOR, "{}");
|
|
||||||
Object id = json(turn).get("id");
|
|
||||||
HttpResponse<String> ask = send("POST", "/conversations/" + id + "/turns", EDITOR, "{\"text\":\"hi\"}");
|
|
||||||
assertThat(ask.statusCode()).isEqualTo(503);
|
|
||||||
assertThat(json(ask)).containsEntry("code", "NOT_CONFIGURED");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
config.setProviderName(providerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@@ -217,8 +178,7 @@ class AssistantRouteProviderTest extends BaseAssistantTest
|
|||||||
assertThat((Map<String, Object>) config.get("limits")).containsEntry("maxTurnSeconds", 300).containsEntry("maxToolCallsPerTurn", 25);
|
assertThat((Map<String, Object>) config.get("limits")).containsEntry("maxTurnSeconds", 300).containsEntry("maxToolCallsPerTurn", 25);
|
||||||
assertThat((Map<String, Object>) config.get("features")).containsEntry("writes", false);
|
assertThat((Map<String, Object>) config.get("features")).containsEntry("writes", false);
|
||||||
|
|
||||||
assertThat(json(send("GET", "/config", NO_ACCESS, null))).containsEntry("enabled", false).containsEntry("reason", "NO_PERMISSION")
|
assertThat(json(send("GET", "/config", NO_ACCESS, null))).containsEntry("enabled", false).containsEntry("reason", "NO_PERMISSION");
|
||||||
.containsEntry("message", "You do not have access to the assistant.");
|
|
||||||
HttpResponse<String> denied = send("GET", "/conversations", NO_ACCESS, null);
|
HttpResponse<String> denied = send("GET", "/conversations", NO_ACCESS, null);
|
||||||
assertThat(denied.statusCode()).isEqualTo(403);
|
assertThat(denied.statusCode()).isEqualTo(403);
|
||||||
assertThat(json(denied)).containsEntry("code", "NO_PERMISSION")
|
assertThat(json(denied)).containsEntry("code", "NO_PERMISSION")
|
||||||
|
|||||||
@@ -141,27 +141,6 @@ class TurnEngineTest extends BaseAssistantTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** The panel reloads the conversation as soon as message.end arrives, so the
|
|
||||||
** question and the answer must already be stored then, not after it.
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testMessagesAreStoredBeforeMessageEnd() throws Exception
|
|
||||||
{
|
|
||||||
provider.thenText("Four posts are published.");
|
|
||||||
|
|
||||||
TurnRunner.Outcome outcome = runner.run(EDITOR, "how many published posts?");
|
|
||||||
assertThat(outcome.end()).containsEntry("status", "COMPLETE");
|
|
||||||
|
|
||||||
List<AssistantMessage> stored = runner.messagesAtEnd();
|
|
||||||
assertThat(stored).hasSize(2);
|
|
||||||
assertThat(stored).extracting(AssistantMessage::getPartsJson).allSatisfy(parts -> assertThat(parts).isNotEqualTo("[]"));
|
|
||||||
assertThat(stored.get(0).getPartsJson()).contains("how many published posts?");
|
|
||||||
assertThat(stored.get(1).getPartsJson()).contains("Four posts are published.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Parallel tool calls: all results in ONE user message, in order.
|
** Parallel tool calls: all results in ONE user message, in order.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
|
|||||||
import com.kingsrook.qbits.assistant.api.AssistantApi;
|
import com.kingsrook.qbits.assistant.api.AssistantApi;
|
||||||
import com.kingsrook.qbits.assistant.engine.RunningTurn;
|
import com.kingsrook.qbits.assistant.engine.RunningTurn;
|
||||||
import com.kingsrook.qbits.assistant.engine.TurnEvent;
|
import com.kingsrook.qbits.assistant.engine.TurnEvent;
|
||||||
import com.kingsrook.qbits.assistant.model.AssistantMessage;
|
|
||||||
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
|
import com.kingsrook.qbits.assistant.model.AssistantToolCall;
|
||||||
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
import com.kingsrook.qbits.assistant.persistence.AssistantStore;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
@@ -48,8 +47,6 @@ public class TurnRunner
|
|||||||
private final QInstance qInstance;
|
private final QInstance qInstance;
|
||||||
private final AssistantQBitConfig config;
|
private final AssistantQBitConfig config;
|
||||||
|
|
||||||
private List<AssistantMessage> messagesAtEnd = List.of();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@@ -172,10 +169,6 @@ public class TurnRunner
|
|||||||
{
|
{
|
||||||
List<TurnEvent> batch = running.getLog().after(last, 1000);
|
List<TurnEvent> batch = running.getLog().after(last, 1000);
|
||||||
events.addAll(batch);
|
events.addAll(batch);
|
||||||
if(batch.stream().anyMatch(e -> TurnEvent.MESSAGE_END.equals(e.getType())))
|
|
||||||
{
|
|
||||||
messagesAtEnd = messagesOf(userId, convId);
|
|
||||||
}
|
|
||||||
if(!batch.isEmpty())
|
if(!batch.isEmpty())
|
||||||
{
|
{
|
||||||
last = batch.get(batch.size() - 1).getSeq();
|
last = batch.get(batch.size() - 1).getSeq();
|
||||||
@@ -201,36 +194,6 @@ public class TurnRunner
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** The conversation's stored messages, read the moment the run's message.end
|
|
||||||
** was seen: what the panel finds when it reloads the conversation at the end
|
|
||||||
** of the stream.
|
|
||||||
*******************************************************************************/
|
|
||||||
public List<AssistantMessage> messagesAtEnd()
|
|
||||||
{
|
|
||||||
return (messagesAtEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** The stored messages of a conversation (read as the user).
|
|
||||||
*******************************************************************************/
|
|
||||||
private List<AssistantMessage> messagesOf(String userId, Integer conversationId) throws Exception
|
|
||||||
{
|
|
||||||
QContext.init(qInstance, AssistantTestFixture.ownerSession(userId, config));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return (new AssistantStore().listMessages(userId, conversationId, null, 50));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
QContext.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** The audited tool calls of a turn (read as the user).
|
** The audited tool calls of a turn (read as the user).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|||||||
Reference in new issue
Block a user