mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
fix(studio): do not report a draft chat as not selected
A chat that is not created yet has no add-on bound to it, so its execution status cannot be read. The host now says so with the reason draft, and the Home composer shows no commands warning for it. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
7 files changed
+37
-5
No files matched your search
@@ -21,6 +21,8 @@ export function commandsState(res) {
|
||||
return { state: 'host', reason: '' }
|
||||
}
|
||||
const observation = res?.execution_status
|
||||
// A chat that is not created yet has no status to read: nothing to warn of.
|
||||
if (observation?.unknown_reason === 'draft' && (state === 'available' || state === 'none')) return { state: state === 'none' ? 'none' : 'draft', reason: '' }
|
||||
if (state === 'available') {
|
||||
const isolation = observation?.isolation || res?.commands_isolation
|
||||
if (observation && observation.readiness !== 'available') return { state: 'unknown', reason: plainExplanation(observation.explanation), code: reasonCode(observation) }
|
||||
|
||||
@@ -320,3 +320,12 @@ test('unknown status: each reason code has its own text key, and a code from a n
|
||||
assert.equal(unknownTextKey(''), 'unknown')
|
||||
assert.equal(commandsState({ commands: 'available', commands_provider: 'sandbox', execution_status: { readiness: 'unknown', isolation: 'unknown', unknown_reason: 'x' } }).code, '')
|
||||
})
|
||||
|
||||
test('a draft chat has no execution status to warn about', async () => {
|
||||
const { commandsState } = await import('../../src/utils/chatCommands.js')
|
||||
const draft = { readiness: 'unknown', isolation: 'unknown', unknown_reason: 'draft' }
|
||||
const available = commandsState({ commands: 'available', commands_provider: 'sandbox', execution_status: draft })
|
||||
assert.equal(available.state, 'draft')
|
||||
assert.equal(commandsNotice(available.state), null)
|
||||
assert.equal(commandsState({ commands: 'none', execution_status: draft }).state, 'none')
|
||||
})
|
||||
@@ -1756,8 +1756,8 @@ type ChatsExecutionStatus struct {
|
||||
Isolation string `json:"isolation"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
// UnknownReason says why the status is unknown: owner_unreachable,
|
||||
// not_reported, stale, invalid_status, not_selected, no_answer or
|
||||
// reported_unknown. Absent when the status is known.
|
||||
// not_reported, stale, invalid_status, not_selected, no_answer,
|
||||
// reported_unknown or draft (the chat does not exist yet). Absent when the status is known.
|
||||
UnknownReason string `json:"unknown_reason,omitempty" example:"owner_unreachable"`
|
||||
Generation string `json:"generation,omitempty"`
|
||||
Incarnation string `json:"incarnation,omitempty"`
|
||||
|
||||
@@ -283,6 +283,17 @@ var _ = Describe("execution presentation DTO", func() {
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("the execution status of a chat that does not exist yet", func() {
|
||||
It("says the chat is a draft, not that the add-on is not selected", func() {
|
||||
broker := presentationAddons{fakeAddons: &fakeAddons{tools: map[string][]studio.AddonTool{alice: runAddon()}}, bound: true}
|
||||
svc, _, _ := newCapsChat(broker, nil)
|
||||
_, c, err := svc.DraftAddons(context.Background(), alice, "m", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(c.ExecutionStatus).NotTo(BeNil())
|
||||
Expect(c.ExecutionStatus.UnknownReason).To(Equal("draft"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("a second provider of commands", func() {
|
||||
runbox := func() []studio.AddonTool {
|
||||
bash := addonTool("runbox", "bash")
|
||||
|
||||
@@ -121,7 +121,7 @@ var _ = Describe("document provider matrix", func() {
|
||||
}
|
||||
b.sets = append(b.sets, set)
|
||||
}
|
||||
ctx, closeScope := svc.executionScope(ctx, "alice", "")
|
||||
ctx, closeScope := svc.executionScope(ctx, "alice", "c1")
|
||||
defer closeScope()
|
||||
list, _, _, err := svc.chatSkillSet(ctx, "alice", nil, off, sc, command)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@@ -403,7 +403,7 @@ func documentCommands(s *Service, ctx context.Context, p string, rec session.Cha
|
||||
// chatSkillNotes resolves the skills of a chat as the turn does and returns
|
||||
// the notes it gets.
|
||||
func chatSkillNotes(svc *Service, ctx context.Context, off []string, command Commands) ([]skills.Skill, []string, error) {
|
||||
ctx, closeScope := svc.executionScope(ctx, "alice", "")
|
||||
ctx, closeScope := svc.executionScope(ctx, "alice", "c1")
|
||||
defer closeScope()
|
||||
list, notes, _, err := svc.chatSkillSet(ctx, "alice", nil, off, nil, command)
|
||||
return list, notes, err
|
||||
|
||||
@@ -41,6 +41,8 @@ const (
|
||||
UnknownNoAnswer UnknownReason = "no_answer"
|
||||
// UnknownReportedUnknown: the add-on answered that it cannot tell.
|
||||
UnknownReportedUnknown UnknownReason = "reported_unknown"
|
||||
// UnknownDraft: the chat does not exist yet, so no add-on is bound to it.
|
||||
UnknownDraft UnknownReason = "draft"
|
||||
)
|
||||
|
||||
// ExecutionDetail is what the provider said beside the typed value, plus why
|
||||
@@ -127,6 +129,9 @@ func ObserveExecution(ctx context.Context, user, addon string) (model.Value, Exe
|
||||
if user != scope.user {
|
||||
return model.Unknown(), ExecutionDetail{Failure: UnknownNotSelected}
|
||||
}
|
||||
if scope.conversation == "" {
|
||||
return model.Unknown(), ExecutionDetail{Failure: UnknownDraft}
|
||||
}
|
||||
scope.mu.Lock()
|
||||
if !scope.closed && scope.provider != nil && (!scope.started || (scope.retriable && time.Since(scope.attempted) >= observationRetryAfter)) {
|
||||
// The first read of a scope stands. A turn can outlive a restart of the
|
||||
@@ -201,6 +206,8 @@ func (r UnknownReason) Text() string {
|
||||
return "the add-on did not answer in time"
|
||||
case UnknownReportedUnknown:
|
||||
return "the add-on reported that it cannot tell"
|
||||
case UnknownDraft:
|
||||
return "the chat has not been created yet"
|
||||
}
|
||||
return "the reason is not known"
|
||||
}
|
||||
@@ -223,6 +230,9 @@ type ExecutionStatusView struct {
|
||||
|
||||
func executionStatusView(ctx context.Context, user, addon string) *ExecutionStatusView {
|
||||
v, d := model.Unknown(), ExecutionDetail{Failure: UnknownNotSelected}
|
||||
if scope, ok := ctx.Value(executionObservationKey{}).(*executionObservation); ok && scope.user == user && scope.conversation == "" {
|
||||
addon, d.Failure = "", UnknownDraft // a draft has no add-on bound yet
|
||||
}
|
||||
if addon != "" {
|
||||
v, d = ObserveExecution(ctx, user, addon)
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ not an addon ID or a private settings payload. Prompt construction, command
|
||||
status and nested egress reads share one observation within a turn. Priming and
|
||||
standalone command-status or egress requests have separate, bounded scopes.
|
||||
Consumer cancellation stops that wait; turn completion cancels the observation.
|
||||
A failed read settles as unknown with a reason (`unknown_reason`: `owner_unreachable`, `not_reported`, `stale`, `invalid_status`, `not_selected`, `no_answer` or `reported_unknown`), which the Tools menu shows. It is not retried at once. After a failure that a restart of the add-on owner can cause (`owner_unreachable`, `no_answer`, `stale`), a scope that is still open (a long turn) binds and reads again, at most once every five seconds. A refusal of the binding or of the payload is not retried. Each
|
||||
A failed read settles as unknown with a reason (`unknown_reason`: `owner_unreachable`, `not_reported`, `stale`, `invalid_status`, `not_selected`, `no_answer`, `reported_unknown` or `draft`, for a chat that is not created yet), which the Tools menu shows. It is not retried at once. After a failure that a restart of the add-on owner can cause (`owner_unreachable`, `no_answer`, `stale`), a scope that is still open (a long turn) binds and reads again, at most once every five seconds. A refusal of the binding or of the payload is not retried. Each
|
||||
consumption revalidates the complete owner binding, including conversation,
|
||||
current selection identity and runtime incarnation.
|
||||
|
||||
|
||||
Reference in new issue
Block a user