Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3539f5fd93
|
||
|
|
2d6df82f16
|
||
|
|
db8f5398b8
|
||
|
|
f51482f69d
|
||
|
|
4083e5041f
|
||
|
|
45b34b7ccd
|
||
|
|
2652843c29
|
||
|
|
ca9212731d |
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,72 @@
|
||||
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,5 +1,16 @@
|
||||
# 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)
|
||||
|
||||
- `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.
|
||||
@@ -27,6 +38,7 @@
|
||||
- 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`.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -479,6 +479,17 @@ OLLAMA, LM_STUDIO, VLLM, LLAMA_CPP or OPENAI_COMPATIBLE as their kind.
|
||||
Registry prices override type/model prices, then configured prices. Unpriced
|
||||
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:
|
||||
|
||||
```java
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ Guards:
|
||||
`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.
|
||||
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).
|
||||
|
||||
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,6 +86,20 @@ public class AssistantApi
|
||||
|
||||
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 AssistantQBitConfig config;
|
||||
private final AssistantStore store;
|
||||
@@ -134,27 +148,39 @@ public class AssistantApi
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
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())
|
||||
{
|
||||
reason = "DISABLED";
|
||||
message = DISABLED_MESSAGE;
|
||||
}
|
||||
else if(activeProvider() == null)
|
||||
{
|
||||
reason = NOT_CONFIGURED;
|
||||
message = NO_MODEL_PROVIDER_MESSAGE;
|
||||
}
|
||||
else if(!hasAccess())
|
||||
{
|
||||
reason = "NO_PERMISSION";
|
||||
}
|
||||
else if(activeProvider() == null)
|
||||
{
|
||||
reason = "NOT_CONFIGURED";
|
||||
message = NO_PERMISSION_MESSAGE;
|
||||
}
|
||||
else if(runtime.getPolicyService().current(false).killSwitch())
|
||||
{
|
||||
reason = "KILL_SWITCH";
|
||||
message = KILL_SWITCH_MESSAGE;
|
||||
}
|
||||
|
||||
result.put("enabled", reason == null);
|
||||
if(reason != null)
|
||||
{
|
||||
result.put("reason", reason);
|
||||
result.put("message", message);
|
||||
}
|
||||
result.put("shortcut", config.getShortcut());
|
||||
result.put("model", reason == null ? activeProvider().model() : config.getModels().getMainModel());
|
||||
@@ -425,11 +451,11 @@ public class AssistantApi
|
||||
ProviderService.Active active = runtime.getProviderService().resolve(policy.mainModel());
|
||||
if(active == null)
|
||||
{
|
||||
throw (new AssistantApiException(503, "NOT_CONFIGURED", "The assistant is not configured on this server."));
|
||||
throw (new AssistantApiException(503, NOT_CONFIGURED, NO_MODEL_PROVIDER_MESSAGE));
|
||||
}
|
||||
if(policy.killSwitch())
|
||||
{
|
||||
throw (new AssistantApiException(503, "KILL_SWITCH", "The assistant has been turned off by an administrator."));
|
||||
throw (new AssistantApiException(503, "KILL_SWITCH", KILL_SWITCH_MESSAGE));
|
||||
}
|
||||
|
||||
Long capMicros = ProviderService.capMicros(active.row());
|
||||
@@ -1165,11 +1191,33 @@ public class AssistantApi
|
||||
{
|
||||
if(!config.isEffectivelyEnabled())
|
||||
{
|
||||
throw (new AssistantApiException(503, "DISABLED", "The assistant is turned off for this application."));
|
||||
throw (new AssistantApiException(503, "DISABLED", DISABLED_MESSAGE));
|
||||
}
|
||||
if(!hasAccess())
|
||||
{
|
||||
throw (new AssistantApiException(403, "NO_PERMISSION", "You do not have access to the assistant."));
|
||||
if(activeProviderOrNull() == null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -63,7 +63,7 @@ import io.javalin.http.HttpStatus;
|
||||
public final class AssistantSpecs
|
||||
{
|
||||
public static final String EXTENSION_NAME = "assistant";
|
||||
public static final String EXTENSION_VERSION = "1.6.0";
|
||||
public static final String EXTENSION_VERSION = "1.7.0";
|
||||
public static final String PREFIX = "/assistant";
|
||||
|
||||
|
||||
@@ -198,7 +198,8 @@ 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.")));
|
||||
rs.put("AssistantConfigResponseV1", object("Whether the assistant is available to this user, and how.",
|
||||
"enabled", bool("Whether the panel may be used."),
|
||||
"reason", string("Why not: DISABLED, NO_PERMISSION, NOT_CONFIGURED or KILL_SWITCH.").withNullable(true),
|
||||
"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),
|
||||
"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."),
|
||||
"model", string("The model that answers."),
|
||||
"limits", freeObject("maxTurnSeconds and maxToolCallsPerTurn. When features.attachments is true also maxAttachmentBytes (the largest file read, in bytes), "
|
||||
|
||||
@@ -1343,6 +1343,17 @@ public class TurnEngine implements Runnable
|
||||
String finalErrorText = errorText;
|
||||
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)));
|
||||
if(!alreadyTerminal)
|
||||
{
|
||||
|
||||
@@ -175,7 +175,13 @@ components:
|
||||
description: "Whether the panel may be used."
|
||||
type: "boolean"
|
||||
reason:
|
||||
description: "Why not: DISABLED, NO_PERMISSION, NOT_CONFIGURED or KILL_SWITCH."
|
||||
description: "Why not: DISABLED, NOT_CONFIGURED (no usable model provider\
|
||||
\ 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
|
||||
type: "string"
|
||||
shortcut:
|
||||
@@ -305,7 +311,9 @@ components:
|
||||
nullable: true
|
||||
type: "string"
|
||||
starterPrompts:
|
||||
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."
|
||||
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."
|
||||
items:
|
||||
type: "string"
|
||||
type: "array"
|
||||
@@ -468,7 +476,7 @@ info:
|
||||
description: "QQQ Assistant: conversations, streamed turns, feedback, pending actions\
|
||||
\ and data access explanations"
|
||||
title: "QQQ Middleware API extension: assistant"
|
||||
version: "1.6.0"
|
||||
version: "1.7.0"
|
||||
openapi: "3.0.3"
|
||||
paths:
|
||||
/qqq/v1/assistant/config:
|
||||
|
||||
+41
-1
@@ -166,6 +166,45 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@@ -178,7 +217,8 @@ class AssistantRouteProviderTest extends BaseAssistantTest
|
||||
assertThat((Map<String, Object>) config.get("limits")).containsEntry("maxTurnSeconds", 300).containsEntry("maxToolCallsPerTurn", 25);
|
||||
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);
|
||||
assertThat(denied.statusCode()).isEqualTo(403);
|
||||
assertThat(json(denied)).containsEntry("code", "NO_PERMISSION")
|
||||
|
||||
@@ -141,6 +141,27 @@ 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.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.kingsrook.qbits.assistant.AssistantRuntime;
|
||||
import com.kingsrook.qbits.assistant.api.AssistantApi;
|
||||
import com.kingsrook.qbits.assistant.engine.RunningTurn;
|
||||
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.persistence.AssistantStore;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@@ -47,6 +48,8 @@ public class TurnRunner
|
||||
private final QInstance qInstance;
|
||||
private final AssistantQBitConfig config;
|
||||
|
||||
private List<AssistantMessage> messagesAtEnd = List.of();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -169,6 +172,10 @@ public class TurnRunner
|
||||
{
|
||||
List<TurnEvent> batch = running.getLog().after(last, 1000);
|
||||
events.addAll(batch);
|
||||
if(batch.stream().anyMatch(e -> TurnEvent.MESSAGE_END.equals(e.getType())))
|
||||
{
|
||||
messagesAtEnd = messagesOf(userId, convId);
|
||||
}
|
||||
if(!batch.isEmpty())
|
||||
{
|
||||
last = batch.get(batch.size() - 1).getSeq();
|
||||
@@ -194,6 +201,36 @@ 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).
|
||||
*******************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user