Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4b4a95c0 | ||
|
|
e55c0ba88e | ||
|
|
301e5d0b27 | ||
|
|
4055319eb6 | ||
|
|
93b0965485 | ||
|
|
406a106e30 | ||
|
|
5789b70e9c | ||
|
|
8397e69c2f | ||
|
|
4226c986d7 | ||
|
|
918748ca57 | ||
|
|
3ed64631f6 | ||
|
|
407275c796 | ||
|
|
3aa0a1d452 | ||
|
|
49036e6397 | ||
|
|
da94ed17a6 | ||
|
|
1ceb5f6761 | ||
|
|
4f53c110ea | ||
|
|
d800e49a2a | ||
|
|
293cb4f4c1 | ||
|
|
73935b3f94 | ||
|
|
ed67fd7ba9 | ||
|
|
8448e95257 | ||
|
|
1389d6b24c | ||
|
|
d50d709d69 | ||
|
|
2e0c0236aa | ||
|
|
5db143df3b | ||
|
|
4321bd627d | ||
|
|
b6237334c6 | ||
|
|
b025bec777 | ||
|
|
1a62be1e34 | ||
|
|
b1a51a8e44 | ||
|
|
52074e2895 |
@@ -112,6 +112,9 @@ func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationC
|
||||
if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
if candidate.SecondarySpeechAct == "" {
|
||||
candidate.SecondarySpeechAct = "UNKNOWN"
|
||||
}
|
||||
switch candidate.Applicability {
|
||||
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
|
||||
default:
|
||||
@@ -120,6 +123,11 @@ func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationC
|
||||
if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if !validCapabilityKind(hint) {
|
||||
return cognitive.GatewayInterpretationCandidate{}, false
|
||||
}
|
||||
}
|
||||
return candidate, true
|
||||
}
|
||||
|
||||
@@ -131,6 +139,25 @@ func validSpeechAct(value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func validCapabilityKind(value capability.CapabilityKind) bool {
|
||||
switch value {
|
||||
case capability.CapabilityKindReadEmail,
|
||||
capability.CapabilityKindSendEmail,
|
||||
capability.CapabilityKindReadDatabase,
|
||||
capability.CapabilityKindQueryLogs,
|
||||
capability.CapabilityKindInspectProject,
|
||||
capability.CapabilityKindModifyCode,
|
||||
capability.CapabilityKindRunTests,
|
||||
capability.CapabilityKindWebResearch,
|
||||
capability.CapabilityKindCognitiveReasoning,
|
||||
capability.CapabilityKindScheduleReminder,
|
||||
capability.CapabilityKindVerifyFact:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
"git.cin.su/m/Assistant/contracts/situation"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
@@ -74,7 +75,32 @@ func TestLiveInterpretationTrace(t *testing.T) {
|
||||
now: time.Now,
|
||||
allowCanary: true,
|
||||
}
|
||||
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
|
||||
for _, message := range []string{
|
||||
"расскажи как это работает",
|
||||
"Расскажи, как это работает?",
|
||||
"Расскажи как это работает",
|
||||
"можешь объяснить почему база растёт",
|
||||
"почему postgres использует столько памяти",
|
||||
"ПОЧЕМУ postgres использует так много памяти",
|
||||
"домовой не видит ии",
|
||||
"домовой не вижит ии",
|
||||
"разберись почему сервис упал",
|
||||
"посмотри что произошло",
|
||||
"исправь ошибку если найдёшь",
|
||||
"разберись почему падает и исправь если причина очевидна",
|
||||
"нет не UPO а UPO2",
|
||||
"нет не UPO а UPO2 и обнови конфиг",
|
||||
"postgres сейчас работает",
|
||||
"основная база называется UPO",
|
||||
"привет",
|
||||
"спасибо",
|
||||
"ты кто",
|
||||
"ты кто?",
|
||||
"как тебя зовут",
|
||||
"кто ты вообще",
|
||||
"что ты думаешь",
|
||||
"расскажи подробнее",
|
||||
} {
|
||||
conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now())
|
||||
if err != nil {
|
||||
@@ -121,15 +147,27 @@ func TestLiveInterpretationTrace(t *testing.T) {
|
||||
if rawResponse == "" {
|
||||
t.Fatalf("message=%q raw response is empty", message)
|
||||
}
|
||||
if message == "ты кто" {
|
||||
if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition {
|
||||
t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation)
|
||||
}
|
||||
if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" {
|
||||
t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate)
|
||||
}
|
||||
cognitiveCandidate, err := cognitiveCandidateFromGateway(utterance.ID, gatewayResponse.Candidate, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate)
|
||||
selected := interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, cognitiveCandidate, nil)
|
||||
cognitionUsed := selected.Interpretation.Source == string(interaction.CandidateSourceCognitive)
|
||||
clarificationReason := string(selected.Interpretation.Clarification)
|
||||
t.Logf("TRACE message=%q", message)
|
||||
deterministic := semantic.Candidates[0]
|
||||
t.Logf(" deterministic candidate: %+v", deterministic)
|
||||
t.Logf(" raw response=%q", truncate(rawResponse, 180))
|
||||
t.Logf(" cognitive candidate: %+v", cognitiveCandidate)
|
||||
t.Logf(" selected interpretation: act=%s secondary=%s selection_source=%s needs_cognition=%t",
|
||||
selected.Interpretation.SpeechAct,
|
||||
selected.Interpretation.SecondarySpeechAct,
|
||||
selected.Interpretation.Source,
|
||||
selected.Interpretation.NeedsCognition,
|
||||
)
|
||||
t.Logf(" cognition used: %t", cognitionUsed)
|
||||
t.Logf(" clarification reason: %s", clarificationReason)
|
||||
t.Logf(" reply=%q", selected.Reply)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,3 +255,30 @@ func truncate(value string, max int) string {
|
||||
}
|
||||
return value[:max] + "..."
|
||||
}
|
||||
|
||||
func cognitiveCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
|
||||
id, err := ids.New()
|
||||
if err != nil {
|
||||
return interaction.SemanticCandidate{}, err
|
||||
}
|
||||
capabilityHints := make([]string, 0, len(candidate.CapabilityHints))
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
capabilityHints = append(capabilityHints, string(hint))
|
||||
}
|
||||
return interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(id),
|
||||
UtteranceID: utteranceID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
SelectionState: interaction.CandidateSelected,
|
||||
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
|
||||
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
|
||||
ConfidenceMillis: candidate.ConfidenceMillis,
|
||||
Applicability: interaction.SemanticApplicability(candidate.Applicability),
|
||||
RequestedOutcome: candidate.RequestedOutcome,
|
||||
Clarification: interaction.ClarificationState(candidate.Clarification),
|
||||
NeedsCognition: candidate.NeedsCognition,
|
||||
SelectionReason: candidate.SelectionReason,
|
||||
CapabilityHints: capabilityHints,
|
||||
RecordedAtUnixMillis: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestParseInterpretationCandidateNormalizesOmittedSecondarySpeechAct(t *testing.T) {
|
||||
candidate, ok := parseInterpretationCandidate(`{"primary_speech_act":"QUESTION","confidence_millis":800,"applicability":"APPLICABLE","clarification":"NOT_REQUIRED"}`)
|
||||
if !ok {
|
||||
t.Fatal("expected valid candidate")
|
||||
}
|
||||
if candidate.SecondarySpeechAct != "UNKNOWN" {
|
||||
t.Fatalf("expected omitted secondary speech act to normalize to UNKNOWN, got %q", candidate.SecondarySpeechAct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInterpretationCandidateRejectsUnknownCapabilityHint(t *testing.T) {
|
||||
_, ok := parseInterpretationCandidate(`{"primary_speech_act":"QUESTION","confidence_millis":800,"applicability":"APPLICABLE","clarification":"NOT_REQUIRED","capability_hints":["INVENTED_CAPABILITY"]}`)
|
||||
if ok {
|
||||
t.Fatal("expected unknown capability hint to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidCapabilityKindMatchesContractEnum(t *testing.T) {
|
||||
if !validCapabilityKind(capability.CapabilityKindCognitiveReasoning) {
|
||||
t.Fatal("expected cognitive reasoning capability to be valid")
|
||||
}
|
||||
if validCapabilityKind(capability.CapabilityKind("INVENTED_CAPABILITY")) {
|
||||
t.Fatal("invented capability must be invalid")
|
||||
}
|
||||
}
|
||||
@@ -61,12 +61,7 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
|
||||
}
|
||||
|
||||
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
switch result.Interpretation.Applicability {
|
||||
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return interactiondomain.ShouldAttemptCognitive(result)
|
||||
}
|
||||
|
||||
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
|
||||
@@ -122,4 +117,4 @@ func capabilityHintStrings(values []capability.CapabilityKind) []string {
|
||||
result = append(result, string(value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func main() {
|
||||
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if shouldRequestBroaderInterpretation(semantic) {
|
||||
if interactiondomain.ShouldAttemptCognitive(semantic) {
|
||||
projectNames := make([]string, 0, len(semantic.Projects))
|
||||
for _, ref := range semantic.Projects {
|
||||
if ref.ProjectDisplayName != "" {
|
||||
@@ -1170,4 +1170,4 @@ func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRe
|
||||
result = append(result, epistemics.EvidenceRef(value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,12 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
|
||||
!strings.Contains(response.Body.String(), "context-preset") ||
|
||||
!strings.Contains(response.Body.String(), "context-state") ||
|
||||
!strings.Contains(response.Body.String(), "copy-context") ||
|
||||
!strings.Contains(response.Body.String(), "К содержимому") ||
|
||||
!strings.Contains(response.Body.String(), "aria-current=\"page\"") ||
|
||||
!strings.Contains(response.Body.String(), "section-nav-link") ||
|
||||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
|
||||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
|
||||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
|
||||
|
||||
@@ -190,7 +190,7 @@ async function sendChatMessage(text, options = {}) {
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
appendMessage("assistant", "Не могу отправить сообщение. Попробуйте ещё раз.");
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
:root {
|
||||
--ink: #e9eee7;
|
||||
--muted: #8c9a91;
|
||||
--faint: #58645d;
|
||||
--base: #0b100e;
|
||||
--panel: #101714;
|
||||
--panel-high: #151e1a;
|
||||
--line: #2a352f;
|
||||
--line-soft: #1d2822;
|
||||
--ink: #eef2ec;
|
||||
--muted: #a7b2aa;
|
||||
--faint: #718078;
|
||||
--base: #0f1512;
|
||||
--panel: #17201b;
|
||||
--panel-high: #202b25;
|
||||
--line: #34423a;
|
||||
--line-soft: #26332c;
|
||||
--signal: #c9a85f;
|
||||
--signal-bright: #e7c97b;
|
||||
--good: #78c6a3;
|
||||
@@ -21,13 +21,15 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html { background: var(--base); color: var(--ink); font-family: var(--sans); }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); }
|
||||
html { background: var(--base); color: var(--ink); font-family: var(--sans); scroll-behavior: smooth; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); }
|
||||
button, input, textarea { font: inherit; }
|
||||
button { color: inherit; }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
|
||||
.skip-link { position: fixed; top: 10px; left: 10px; z-index: 30; padding: 10px 13px; color: #11150f; background: var(--signal-bright); font: 600 10px/1 var(--mono); text-decoration: none; transform: translateY(-160%); transition: transform .18s; }
|
||||
.skip-link:focus { transform: translateY(0); }
|
||||
|
||||
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
|
||||
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
|
||||
.eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; }
|
||||
|
||||
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; }
|
||||
@@ -63,6 +65,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.wordmark strong { font: 400 20px/1 var(--serif); }
|
||||
.wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
|
||||
.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; }
|
||||
.connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); }
|
||||
.connection.offline i { background: var(--bad); box-shadow: none; }
|
||||
@@ -73,17 +78,54 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.context-rail h2 { margin: 0; font: 400 29px/1.1 var(--serif); }
|
||||
.rail-copy { margin: 14px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.context-form { display: grid; }
|
||||
.context-status-row { margin-bottom: 16px; padding: 10px 11px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line-soft); background: rgba(120,198,163,.04); }
|
||||
.context-status-label { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; }
|
||||
.context-status-row strong { color: var(--warn); font: 600 9px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.context-status-row strong[data-state="ready"] { color: var(--good); }
|
||||
.context-presets { display: grid; gap: 7px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
|
||||
.context-presets label { margin: 0; }
|
||||
.context-presets select { width: 100%; min-height: 38px; padding: 0 9px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 10px/1 var(--mono); }
|
||||
.context-presets small { color: var(--faint); font-size: 10px; line-height: 1.35; }
|
||||
.context-field { display: grid; gap: 7px; }
|
||||
.context-field + .context-field { margin-top: 13px; }
|
||||
.context-field label { margin: 0; }
|
||||
.context-field label span { color: var(--signal); font-size: 8px; letter-spacing: .06em; }
|
||||
.context-input-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 5px; }
|
||||
.context-input-row input { min-width: 0; }
|
||||
.copy-context { width: 34px; height: 40px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; }
|
||||
.copy-context:hover { color: var(--ink); border-color: var(--faint); background: var(--panel-high); }
|
||||
.id-actions { min-height: 31px; display: flex; align-items: center; gap: 7px; margin-top: 12px; }
|
||||
.id-action { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-soft); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; font: 9px/1 var(--mono); letter-spacing: .04em; }
|
||||
.id-action:hover { color: var(--ink); border-color: var(--signal); background: rgba(201,168,95,.07); }
|
||||
.icon-id-action { width: 30px; padding: 0; color: var(--faint); }
|
||||
.context-form label { margin-top: 15px; }
|
||||
.context-form label:first-child { margin-top: 0; }
|
||||
.context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); }
|
||||
.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; }
|
||||
.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
|
||||
.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; }
|
||||
.advanced-context[open] summary::before { content: "−"; }
|
||||
.advanced-context > div { padding: 0 0 10px; }
|
||||
.advanced-context label { margin-top: 13px; }
|
||||
.context-actions { display: grid; gap: 8px; }
|
||||
.context-actions .secondary-button { margin-top: 18px; }
|
||||
.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; }
|
||||
.secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; }
|
||||
.section-nav { display: grid; gap: 3px; margin-top: 4px; }
|
||||
.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; }
|
||||
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="page"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
|
||||
.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); }
|
||||
.section-nav-link b { font: 600 11px/1.2 var(--sans); }
|
||||
.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); }
|
||||
.session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); }
|
||||
.session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; }
|
||||
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
|
||||
|
||||
.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
|
||||
.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; }
|
||||
.workspace > section { scroll-margin-top: 94px; }
|
||||
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
|
||||
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
|
||||
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
|
||||
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
|
||||
@@ -94,6 +136,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.signal-strip strong[data-state="error"] { color: var(--bad); }
|
||||
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
|
||||
|
||||
.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); }
|
||||
.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); }
|
||||
.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
|
||||
.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); }
|
||||
.panel-heading-tools { display: flex; align-items: center; gap: 14px; }
|
||||
.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; }
|
||||
.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); }
|
||||
.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; }
|
||||
.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); }
|
||||
.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.diagnostic-list[aria-busy="true"] { opacity: .7; }
|
||||
.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
|
||||
.diagnostic-item:nth-child(4n) { border-right: 0; }
|
||||
.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); }
|
||||
.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); }
|
||||
.diagnostic-item[data-state="error"] > i { background: var(--bad); }
|
||||
.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
|
||||
.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); }
|
||||
.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; }
|
||||
.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; }
|
||||
.diagnostic-item[data-state="ok"] em { color: var(--good); }
|
||||
.diagnostic-item[data-state="error"] em { color: var(--bad); }
|
||||
.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; }
|
||||
.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); }
|
||||
.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; }
|
||||
.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); }
|
||||
.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); }
|
||||
.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); }
|
||||
.setting-check span { display: grid; gap: 7px; }
|
||||
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; }
|
||||
.settings-actions .text-button { margin: 0; }
|
||||
html[data-density="compact"] .data-card { padding: 18px; }
|
||||
html[data-density="compact"] .data-card dl div { padding-block: 10px; }
|
||||
html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; }
|
||||
html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; }
|
||||
html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
|
||||
|
||||
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
|
||||
.advisor-intro { align-self: center; }
|
||||
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
|
||||
@@ -278,6 +360,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.advisor-panel { grid-template-columns: 1fr; }
|
||||
.capability-card { grid-column: auto; }
|
||||
.prospective-card { grid-column: auto; }
|
||||
.diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); }
|
||||
.diagnostic-item:nth-child(2n) { border-right: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
@@ -285,14 +370,25 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
|
||||
.console { display: block; }
|
||||
.topbar { height: 66px; padding: 0 14px; }
|
||||
.wordmark small, .connection, .text-button { display: none; }
|
||||
.topbar-nav { display: none; }
|
||||
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.rail-copy { max-width: 48ch; }
|
||||
.session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; }
|
||||
.session-stamp span { margin: 0; }
|
||||
.workspace { padding: 30px 16px 50px; }
|
||||
.section-nav { display: flex; gap: 6px; margin: 10px -18px -24px; padding: 10px 18px; overflow-x: auto; border-top: 1px solid var(--line-soft); background: #0e1512; scrollbar-width: thin; }
|
||||
.section-nav-link { min-width: 142px; padding: 10px; border-left: 0; border-bottom: 2px solid transparent; }
|
||||
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="page"] { border-left-color: transparent; border-bottom-color: var(--signal); }
|
||||
.workspace-head { align-items: start; flex-direction: column; }
|
||||
.last-update { text-align: left; }
|
||||
.signal-strip { grid-template-columns: 1fr; }
|
||||
.panel-heading { align-items: flex-start; flex-direction: column; }
|
||||
.panel-heading-tools { width: 100%; justify-content: space-between; }
|
||||
.diagnostic-list { grid-template-columns: 1fr; }
|
||||
.diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; }
|
||||
.settings-grid { grid-template-columns: 1fr; padding: 16px; }
|
||||
.settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; }
|
||||
.settings-actions .primary-button { width: 100%; }
|
||||
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.advisor-panel { padding: 20px 16px; gap: 20px; }
|
||||
.advisor-controls { align-items: stretch; flex-direction: column; }
|
||||
|
||||
@@ -7,14 +7,50 @@ const advisoryForm = byId("advisory-form");
|
||||
const loginError = byId("login-error");
|
||||
const toast = byId("toast");
|
||||
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
|
||||
const settingsStorageKey = "domovoy.creator.settings";
|
||||
const contextPresetsStorageKey = "domovoy.creator.context.presets";
|
||||
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
|
||||
let toastTimer;
|
||||
let refreshTimer;
|
||||
let currentTask = null;
|
||||
let currentProspective = null;
|
||||
let pendingCommand = null;
|
||||
let pendingNotification = null;
|
||||
let lastAuthorizedActionID = "";
|
||||
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
|
||||
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
|
||||
const sectionNavLinks = Array.from(document.querySelectorAll(".section-nav-link"));
|
||||
const cabinetSections = ["overview", "diagnostics", "operations", "settings"]
|
||||
.map((id) => byId(id))
|
||||
.filter(Boolean);
|
||||
|
||||
function setActiveSection(id) {
|
||||
sectionNavLinks.forEach((link) => {
|
||||
const active = link.getAttribute("href") === `#${id}`;
|
||||
link.classList.toggle("active", active);
|
||||
if (active) link.setAttribute("aria-current", "page");
|
||||
else link.removeAttribute("aria-current");
|
||||
});
|
||||
}
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
const sectionObserver = new IntersectionObserver((entries) => {
|
||||
const visible = entries.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
if (visible) setActiveSection(visible.target.id);
|
||||
}, {rootMargin: "-18% 0px -64% 0px", threshold: [0, .25, .6, 1]});
|
||||
cabinetSections.forEach((section) => sectionObserver.observe(section));
|
||||
}
|
||||
|
||||
sectionNavLinks.forEach((link) => link.addEventListener("click", () => {
|
||||
const id = link.getAttribute("href").slice(1);
|
||||
setActiveSection(id);
|
||||
}));
|
||||
window.addEventListener("hashchange", () => {
|
||||
const id = window.location.hash.slice(1);
|
||||
if (cabinetSections.some((section) => section.id === id)) setActiveSection(id);
|
||||
});
|
||||
|
||||
function setText(id, value, fallback = "—") {
|
||||
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
|
||||
@@ -39,6 +75,18 @@ function notify(message) {
|
||||
toastTimer = setTimeout(() => { toast.hidden = true; }, 4200);
|
||||
}
|
||||
|
||||
function safeClientError(error) {
|
||||
const message = error && typeof error.message === "string" ? error.message : "";
|
||||
switch (message) {
|
||||
case "creator authentication denied":
|
||||
return "Логин или пароль не принят.";
|
||||
case "Сессия отклонена":
|
||||
return "Сессия истекла. Войдите снова.";
|
||||
default:
|
||||
return "Операция недоступна. Попробуйте ещё раз.";
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
|
||||
@@ -50,7 +98,9 @@ async function request(path, options = {}) {
|
||||
}
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).trim();
|
||||
throw new Error(detail || `Ошибка ${response.status}`);
|
||||
const error = new Error(detail || `Ошибка ${response.status}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -64,15 +114,57 @@ function restoreContext() {
|
||||
} catch (_) {
|
||||
localStorage.removeItem("domovoy.creator.context");
|
||||
}
|
||||
renderContextState();
|
||||
renderContextPresets();
|
||||
}
|
||||
|
||||
function saveContext() {
|
||||
const saved = {};
|
||||
contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); });
|
||||
localStorage.setItem("domovoy.creator.context", JSON.stringify(saved));
|
||||
renderContextState();
|
||||
return saved;
|
||||
}
|
||||
|
||||
function readContextPresets() {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(contextPresetsStorageKey) || "[]");
|
||||
return Array.isArray(value) ? value.filter((item) => item && item.session && item.workspace) : [];
|
||||
} catch (_) {
|
||||
localStorage.removeItem(contextPresetsStorageKey);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderContextState() {
|
||||
const state = byId("context-state");
|
||||
if (!state) return;
|
||||
const ready = Boolean(byId("session-id").value.trim() && byId("workspace-id").value.trim());
|
||||
state.textContent = ready ? "готов к проверке" : "не выбран";
|
||||
state.dataset.state = ready ? "ready" : "empty";
|
||||
}
|
||||
|
||||
function renderContextPresets() {
|
||||
const select = byId("context-preset");
|
||||
if (!select) return;
|
||||
const presets = readContextPresets();
|
||||
select.replaceChildren(new Option("Выберите сохранённый набор…", ""));
|
||||
presets.forEach((preset, index) => {
|
||||
const session = preset.session.length > 18 ? `${preset.session.slice(0, 8)}…${preset.session.slice(-6)}` : preset.session;
|
||||
const workspace = preset.workspace.length > 18 ? `${preset.workspace.slice(0, 8)}…${preset.workspace.slice(-6)}` : preset.workspace;
|
||||
select.appendChild(new Option(`${session} · ${workspace}`, String(index)));
|
||||
});
|
||||
}
|
||||
|
||||
function rememberContextPreset(context) {
|
||||
if (!context["session-id"] || !context["workspace-id"]) return;
|
||||
const next = {session: context["session-id"], workspace: context["workspace-id"], savedAt: Date.now()};
|
||||
const presets = readContextPresets().filter((item) => item.session !== next.session || item.workspace !== next.workspace);
|
||||
presets.unshift(next);
|
||||
localStorage.setItem(contextPresetsStorageKey, JSON.stringify(presets.slice(0, 5)));
|
||||
renderContextPresets();
|
||||
}
|
||||
|
||||
async function showConsole() {
|
||||
const session = await request("/api/session/current");
|
||||
setText("session-actor", session.ActorID);
|
||||
@@ -80,6 +172,7 @@ async function showConsole() {
|
||||
setText("session-expiry", expiry);
|
||||
loginView.hidden = true;
|
||||
consoleView.hidden = false;
|
||||
restoreSettings();
|
||||
restoreContext();
|
||||
await loadAdvisoryPolicy();
|
||||
const context = saveContext();
|
||||
@@ -88,10 +181,13 @@ async function showConsole() {
|
||||
loadActionHistory();
|
||||
loadLearning();
|
||||
}
|
||||
runDiagnostics();
|
||||
}
|
||||
|
||||
function endSession(message = "Сессия завершена.") {
|
||||
sessionToken = "";
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
sessionStorage.removeItem("domovoy.creator.session");
|
||||
consoleView.hidden = true;
|
||||
loginView.hidden = false;
|
||||
@@ -128,7 +224,7 @@ loginForm.addEventListener("submit", async (event) => {
|
||||
byId("creator-password").value = "";
|
||||
await showConsole();
|
||||
} catch (error) {
|
||||
loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
|
||||
loginError.textContent = safeClientError(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.firstElementChild.textContent = "Открыть консоль";
|
||||
@@ -144,14 +240,129 @@ byId("toggle-secret").addEventListener("click", () => {
|
||||
|
||||
contextForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
saveContext();
|
||||
const context = saveContext();
|
||||
rememberContextPreset(context);
|
||||
loadDashboard();
|
||||
});
|
||||
|
||||
for (const input of [byId("session-id"), byId("workspace-id")]) {
|
||||
input.addEventListener("input", renderContextState);
|
||||
}
|
||||
|
||||
byId("context-preset").addEventListener("change", (event) => {
|
||||
const index = Number(event.target.value);
|
||||
if (!Number.isInteger(index)) return;
|
||||
const preset = readContextPresets()[index];
|
||||
if (!preset) return;
|
||||
byId("session-id").value = preset.session;
|
||||
byId("workspace-id").value = preset.workspace;
|
||||
renderContextState();
|
||||
loadDashboard();
|
||||
notify("Контекст из недавнего набора загружен.");
|
||||
});
|
||||
|
||||
for (const button of document.querySelectorAll("[data-copy-context]")) {
|
||||
button.addEventListener("click", async () => {
|
||||
const value = byId(button.dataset.copyContext).value.trim();
|
||||
if (!value) {
|
||||
notify("Сначала укажите идентификатор.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
notify("Идентификатор скопирован.");
|
||||
} catch (_) {
|
||||
notify("Не удалось скопировать идентификатор.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const button of document.querySelectorAll("[data-copy-value], [data-use-value]")) {
|
||||
button.addEventListener("click", async () => {
|
||||
const source = byId(button.dataset.copyValue || button.dataset.useValue);
|
||||
const value = source?.textContent.trim() || "";
|
||||
if (!value || value === "—") {
|
||||
notify("Сначала загрузите значение.");
|
||||
return;
|
||||
}
|
||||
if (button.dataset.useValue) {
|
||||
const target = byId(button.dataset.targetId);
|
||||
if (!target) return;
|
||||
target.value = value;
|
||||
document.querySelector(".advanced-context").open = true;
|
||||
saveContext();
|
||||
notify("ID подставлен в расширенную проверку.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
notify("ID скопирован.");
|
||||
} catch (_) {
|
||||
notify("Не удалось скопировать ID.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
byId("reset-context").addEventListener("click", () => {
|
||||
contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; });
|
||||
localStorage.removeItem("domovoy.creator.context");
|
||||
loadDashboard();
|
||||
notify("Рабочий контекст сброшен.");
|
||||
});
|
||||
|
||||
function restoreSettings() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}");
|
||||
uiSettings = {
|
||||
density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable",
|
||||
refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off",
|
||||
motion: saved.motion === true,
|
||||
};
|
||||
} catch (_) {
|
||||
uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
}
|
||||
byId("setting-density").value = uiSettings.density;
|
||||
byId("setting-refresh").value = uiSettings.refresh;
|
||||
byId("setting-motion").checked = uiSettings.motion;
|
||||
applySettings();
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
document.documentElement.dataset.density = uiSettings.density;
|
||||
document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false";
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
if (sessionToken && uiSettings.refresh !== "off") {
|
||||
refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
byId("settings-form").addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
uiSettings = {
|
||||
density: byId("setting-density").value,
|
||||
refresh: byId("setting-refresh").value,
|
||||
motion: byId("setting-motion").checked,
|
||||
};
|
||||
localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings));
|
||||
applySettings();
|
||||
setText("settings-saved", "Сохранено только в этом браузере");
|
||||
notify("Настройки кабинета сохранены.");
|
||||
});
|
||||
|
||||
byId("reset-settings").addEventListener("click", () => {
|
||||
uiSettings = {density: "comfortable", refresh: "off", motion: false};
|
||||
localStorage.removeItem(settingsStorageKey);
|
||||
restoreSettings();
|
||||
setText("settings-saved", "Восстановлены значения по умолчанию");
|
||||
notify("Настройки сброшены.");
|
||||
});
|
||||
|
||||
byId("refresh-all").addEventListener("click", loadDashboard);
|
||||
byId("refresh-actions").addEventListener("click", loadActionHistory);
|
||||
byId("refresh-learning").addEventListener("click", loadLearning);
|
||||
byId("logout").addEventListener("click", () => endSession());
|
||||
byId("run-diagnostics").addEventListener("click", runDiagnostics);
|
||||
|
||||
advisoryForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -185,7 +396,7 @@ advisoryForm.addEventListener("submit", async (event) => {
|
||||
byId("advisory-result").hidden = false;
|
||||
} catch (error) {
|
||||
setText("advisory-state", "ОШИБКА");
|
||||
setText("advisory-text", error.message);
|
||||
setText("advisory-text", safeClientError(error));
|
||||
byId("advisory-result").hidden = false;
|
||||
} finally {
|
||||
byId("advisory-egress-ack").checked = false;
|
||||
@@ -246,6 +457,57 @@ async function loadDashboard() {
|
||||
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
|
||||
}
|
||||
|
||||
async function runDiagnostics() {
|
||||
const list = byId("diagnostic-list");
|
||||
const summary = byId("diagnostic-summary");
|
||||
const checks = [
|
||||
["Сессия", "identity", () => request("/api/session/current")],
|
||||
["Политика совета", "advisory", () => request("/api/advisory-policy")],
|
||||
["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")],
|
||||
["Сегодня", "today", () => request("/api/today")],
|
||||
["Внимание", "attention", () => request("/api/attention")],
|
||||
["Действия", "actions", () => request("/api/actions?limit=1")],
|
||||
["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")],
|
||||
];
|
||||
list.setAttribute("aria-busy", "true");
|
||||
list.replaceChildren();
|
||||
checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading")));
|
||||
setText("diagnostic-summary", "Проверяю…");
|
||||
const results = await Promise.all(checks.map(async ([label, key, run]) => {
|
||||
const started = performance.now();
|
||||
try {
|
||||
await run();
|
||||
return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`};
|
||||
} catch (error) {
|
||||
const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена";
|
||||
return {label, key, state: "error", detail};
|
||||
}
|
||||
}));
|
||||
list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail)));
|
||||
list.setAttribute("aria-busy", "false");
|
||||
const passed = results.filter((result) => result.state === "ok").length;
|
||||
setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`);
|
||||
}
|
||||
|
||||
function createDiagnosticItem(label, key, state, detail = "Проверяю…") {
|
||||
const item = document.createElement("article");
|
||||
item.className = "diagnostic-item";
|
||||
item.dataset.state = state;
|
||||
item.dataset.check = key;
|
||||
const marker = document.createElement("i");
|
||||
marker.setAttribute("aria-hidden", "true");
|
||||
const copy = document.createElement("div");
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = label;
|
||||
const description = document.createElement("span");
|
||||
description.textContent = detail;
|
||||
copy.append(title, description);
|
||||
const status = document.createElement("em");
|
||||
status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…";
|
||||
item.append(marker, copy, status);
|
||||
return item;
|
||||
}
|
||||
|
||||
async function loadLearning() {
|
||||
const experienceList = byId("experience-list");
|
||||
const reflectionList = byId("reflection-list");
|
||||
@@ -272,7 +534,7 @@ async function loadLearning() {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = error.message;
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
});
|
||||
setText("learning-count", "ОШИБКА");
|
||||
@@ -371,7 +633,7 @@ async function loadActionHistory() {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty activity-error";
|
||||
failed.textContent = error.message;
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
setText("action-count", "ОШИБКА");
|
||||
} finally {
|
||||
@@ -430,7 +692,7 @@ async function reconcileHistoryAction(actionID, button) {
|
||||
await loadActionHistory();
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(error.message);
|
||||
notify(safeClientError(error));
|
||||
button.disabled = false;
|
||||
button.textContent = "Сверить";
|
||||
}
|
||||
@@ -450,7 +712,7 @@ async function loadFocus(query) {
|
||||
} catch (error) {
|
||||
setSignal("focus", "ошибка", "error");
|
||||
setBadge("focus", "НЕДОСТУПЕН", "bad");
|
||||
setText("focus-message", error.message);
|
||||
setText("focus-message", safeClientError(error));
|
||||
byId("connection-state").classList.add("offline");
|
||||
}
|
||||
}
|
||||
@@ -472,7 +734,7 @@ async function loadTask(taskID, query) {
|
||||
currentTask = null;
|
||||
setSignal("task", "отклонена", "error");
|
||||
setBadge("task", "ВНЕ ФОКУСА", "bad");
|
||||
setText("task-message", error.message);
|
||||
setText("task-message", safeClientError(error));
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -511,12 +773,12 @@ async function loadProspectiveRegister(selectedID) {
|
||||
list.replaceChildren();
|
||||
const failed = document.createElement("p");
|
||||
failed.className = "register-empty";
|
||||
failed.textContent = error.message;
|
||||
failed.textContent = safeClientError(error);
|
||||
list.append(failed);
|
||||
setText("prospective-count", "ОШИБКА");
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-message", error.message);
|
||||
setText("prospective-message", safeClientError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +834,7 @@ async function loadProspective(prospectiveID) {
|
||||
setSignal("prospective", "ошибка", "error");
|
||||
setBadge("prospective", "НЕДОСТУПНА", "bad");
|
||||
setText("prospective-state", "UNKNOWN");
|
||||
setText("prospective-message", error.message);
|
||||
setText("prospective-message", safeClientError(error));
|
||||
updateActionReadiness();
|
||||
}
|
||||
}
|
||||
@@ -594,7 +856,7 @@ async function loadCapability(capabilityID) {
|
||||
setSignal("capability", "ошибка", "error");
|
||||
setBadge("capability", "НЕДОСТУПНА", "bad");
|
||||
setText("capability-state", "UNKNOWN");
|
||||
setText("capability-reason", error.message);
|
||||
setText("capability-reason", safeClientError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,7 +962,7 @@ byId("confirm-notification").addEventListener("click", async () => {
|
||||
await loadActionHistory();
|
||||
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
|
||||
} catch (error) {
|
||||
setText("notification-confirmation-error", error.message);
|
||||
setText("notification-confirmation-error", safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю отправку";
|
||||
@@ -730,7 +992,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
|
||||
await loadProspectiveRegister(command.ProspectiveItemID);
|
||||
} catch (error) {
|
||||
setText("confirmation-error", error.message);
|
||||
setText("confirmation-error", safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Подтверждаю переход";
|
||||
@@ -764,7 +1026,7 @@ byId("reconcile-action").addEventListener("click", async () => {
|
||||
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
|
||||
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
|
||||
} catch (error) {
|
||||
notify(error.message);
|
||||
notify(safeClientError(error));
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Проверить исход";
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#101714">
|
||||
<title>Домовой — консоль создателя</title>
|
||||
<link rel="stylesheet" href="/assets/console.css">
|
||||
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#overview">К содержимому</a>
|
||||
<div class="grain" aria-hidden="true"></div>
|
||||
|
||||
<main id="login-view" class="login-shell">
|
||||
@@ -44,6 +45,12 @@
|
||||
</a>
|
||||
<div class="topbar-actions">
|
||||
<span id="connection-state" class="connection"><i></i>система доступна</span>
|
||||
<nav class="topbar-nav" aria-label="Разделы кабинета">
|
||||
<a href="#overview">Обзор</a>
|
||||
<a href="#diagnostics">Проверки</a>
|
||||
<a href="#operations">Операции</a>
|
||||
<a href="#settings">Настройки</a>
|
||||
</nav>
|
||||
<button id="refresh-all" class="text-button" type="button">Обновить</button>
|
||||
<button id="logout" class="icon-button" type="button" aria-label="Завершить сессию" title="Выйти">↗</button>
|
||||
</div>
|
||||
@@ -56,18 +63,45 @@
|
||||
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
|
||||
</div>
|
||||
<form id="context-form" class="context-form">
|
||||
<label for="session-id">Идентификатор сессии</label>
|
||||
<input id="session-id" autocomplete="off" placeholder="session-id">
|
||||
<label for="workspace-id">Рабочая область</label>
|
||||
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
|
||||
<label for="task-id">Задача</label>
|
||||
<input id="task-id" autocomplete="off" placeholder="task-id">
|
||||
<label for="prospective-id">Выбранное ожидание</label>
|
||||
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
|
||||
<label for="capability-id">Возможность</label>
|
||||
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
|
||||
<button class="secondary-button" type="submit">Загрузить контекст</button>
|
||||
<div class="context-status-row">
|
||||
<span class="context-status-label">ТЕКУЩИЙ КОНТЕКСТ</span>
|
||||
<strong id="context-state" data-state="empty">не выбран</strong>
|
||||
</div>
|
||||
<div class="context-presets">
|
||||
<label for="context-preset">Недавние наборы</label>
|
||||
<select id="context-preset">
|
||||
<option value="">Выберите сохранённый набор…</option>
|
||||
</select>
|
||||
<small>Сохраняются только в этом браузере.</small>
|
||||
</div>
|
||||
<div class="context-field">
|
||||
<label for="session-id">Идентификатор сессии <span>обязательно</span></label>
|
||||
<div class="context-input-row"><input id="session-id" autocomplete="off" spellcheck="false" placeholder="Вставьте session-id"><button class="copy-context" data-copy-context="session-id" type="button" aria-label="Скопировать идентификатор сессии" title="Скопировать">⧉</button></div>
|
||||
</div>
|
||||
<div class="context-field">
|
||||
<label for="workspace-id">Рабочая область <span>обязательно</span></label>
|
||||
<div class="context-input-row"><input id="workspace-id" autocomplete="off" spellcheck="false" placeholder="Вставьте workspace-id"><button class="copy-context" data-copy-context="workspace-id" type="button" aria-label="Скопировать рабочую область" title="Скопировать">⧉</button></div>
|
||||
</div>
|
||||
<p class="context-hint">Для «Состояния» нужны только эти два значения. Задача, ожидание и возможность находятся ниже в расширенных полях.</p>
|
||||
<details class="advanced-context">
|
||||
<summary>Расширенные идентификаторы</summary>
|
||||
<div>
|
||||
<label for="task-id">Задача</label>
|
||||
<input id="task-id" autocomplete="off" placeholder="task-id">
|
||||
<label for="prospective-id">Выбранное ожидание</label>
|
||||
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
|
||||
<label for="capability-id">Возможность</label>
|
||||
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
|
||||
</div>
|
||||
</details>
|
||||
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
|
||||
</form>
|
||||
<nav class="section-nav" aria-label="Навигация по кабинету">
|
||||
<a class="section-nav-link active" aria-current="page" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
|
||||
<a class="section-nav-link" href="#diagnostics"><span>02</span><b>Проверки</b><small>доступность и границы</small></a>
|
||||
<a class="section-nav-link" href="#operations"><span>03</span><b>Операции</b><small>ожидания и действия</small></a>
|
||||
<a class="section-nav-link" href="#settings"><span>04</span><b>Настройки</b><small>интерфейс и обновление</small></a>
|
||||
</nav>
|
||||
<div class="session-stamp">
|
||||
<span>ACTOR</span>
|
||||
<strong id="session-actor">—</strong>
|
||||
@@ -77,7 +111,7 @@
|
||||
</aside>
|
||||
|
||||
<main class="workspace">
|
||||
<section class="workspace-head">
|
||||
<section id="overview" class="workspace-head">
|
||||
<div>
|
||||
<p class="eyebrow">операционная картина</p>
|
||||
<h1>Состояние сейчас</h1>
|
||||
@@ -92,6 +126,20 @@
|
||||
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
|
||||
</section>
|
||||
|
||||
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title">
|
||||
<header class="panel-heading">
|
||||
<div><p>Проверяемая система</p><h2 id="diagnostics-title">Диагностика кабинета</h2></div>
|
||||
<div class="panel-heading-tools"><span id="diagnostic-summary">Проверка ещё не запускалась</span><button id="run-diagnostics" class="secondary-button compact-button" type="button">Проверить всё</button></div>
|
||||
</header>
|
||||
<div class="diagnostic-intro">
|
||||
<span class="diagnostic-pulse" aria-hidden="true"></span>
|
||||
<p>Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.</p>
|
||||
</div>
|
||||
<div id="diagnostic-list" class="diagnostic-list" aria-live="polite" aria-busy="false">
|
||||
<p class="register-empty">Нажмите «Проверить всё», чтобы получить актуальный снимок.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="advisor-panel" aria-labelledby="advisor-title">
|
||||
<div class="advisor-intro">
|
||||
<p>COGNITIVE / ADVISORY V1</p>
|
||||
@@ -120,7 +168,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="register-panel" aria-labelledby="register-title">
|
||||
<section id="operations" class="register-panel" aria-labelledby="register-title">
|
||||
<header>
|
||||
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
|
||||
<span id="prospective-count">0 ЗАПИСЕЙ</span>
|
||||
@@ -167,6 +215,19 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="settings-panel" aria-labelledby="settings-title">
|
||||
<header class="panel-heading">
|
||||
<div><p>Локальные предпочтения</p><h2 id="settings-title">Настройки рабочего кабинета</h2></div>
|
||||
<span id="settings-saved" class="settings-saved">Сохраняются в этом браузере</span>
|
||||
</header>
|
||||
<form id="settings-form" class="settings-grid">
|
||||
<label class="setting-field"><span>Плотность данных</span><small>Выберите, сколько строк помещается на экране.</small><select id="setting-density"><option value="comfortable">Комфортная</option><option value="compact">Компактная</option></select></label>
|
||||
<label class="setting-field"><span>Автообновление</span><small>Повторная проверка состояния без изменения данных.</small><select id="setting-refresh"><option value="off">Выключено</option><option value="30">Каждые 30 секунд</option><option value="60">Каждую минуту</option><option value="300">Каждые 5 минут</option></select></label>
|
||||
<label class="setting-check"><input id="setting-motion" type="checkbox"><span><b>Сократить анимацию</b><small>Оставить спокойный режим для длительной работы.</small></span></label>
|
||||
<div class="settings-actions"><button id="save-settings" class="primary-button compact-button" type="submit"><span>Сохранить настройки</span><span aria-hidden="true">→</span></button><button id="reset-settings" class="text-button" type="button">Сбросить</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="data-grid">
|
||||
<article class="data-card focus-card">
|
||||
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
|
||||
@@ -188,6 +249,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="task-revision">—</dd></div>
|
||||
</dl>
|
||||
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="task-value" data-target-id="task-id" type="button">Использовать ID задачи</button><button class="id-action icon-id-action" data-copy-value="task-value" type="button" aria-label="Скопировать ID задачи" title="Скопировать ID">⧉</button></div>
|
||||
</article>
|
||||
|
||||
<article class="data-card prospective-card">
|
||||
@@ -205,6 +267,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
|
||||
</dl>
|
||||
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="prospective-value" data-target-id="prospective-id" type="button">Использовать ID ожидания</button><button class="id-action icon-id-action" data-copy-value="prospective-value" type="button" aria-label="Скопировать ID ожидания" title="Скопировать ID">⧉</button></div>
|
||||
<section class="commit-zone" aria-labelledby="commit-zone-title">
|
||||
<div class="commit-zone-copy">
|
||||
<p>Commit / контролируемый переход</p>
|
||||
@@ -240,6 +303,7 @@
|
||||
<div><dt>Ревизия</dt><dd id="capability-revision">—</dd></div>
|
||||
<div><dt>Наблюдалось</dt><dd id="capability-observed">—</dd></div>
|
||||
</dl>
|
||||
<div class="id-actions"><button class="id-action" data-use-value="capability-value" data-target-id="capability-id" type="button">Использовать ID возможности</button><button class="id-action icon-id-action" data-copy-value="capability-value" type="button" aria-label="Скопировать ID возможности" title="Скопировать ID">⧉</button></div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
@@ -286,6 +350,6 @@
|
||||
</dialog>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
||||
<script src="/assets/console.js" defer></script>
|
||||
<script src="/assets/console.js?v=20260821-context" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,8 +8,8 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-assistant}
|
||||
POSTGRES_USER: ${POSTGRES_SUPERUSER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD}
|
||||
POSTGRES_USER: ${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?POSTGRES_SUPERUSER_PASSWORD is required}
|
||||
CONTINUITY_DB_PASSWORD: ${CONTINUITY_DB_PASSWORD}
|
||||
IDENTITY_DB_PASSWORD: ${IDENTITY_DB_PASSWORD}
|
||||
AUTHORITY_DB_PASSWORD: ${AUTHORITY_DB_PASSWORD}
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- assistant_pg:/var/lib/postgresql
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER} -d ${POSTGRES_DB:-assistant}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required} -d ${POSTGRES_DB:-assistant}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
POSTGRES_USER: continuity_role
|
||||
POSTGRES_PASSWORD: ${CONTINUITY_DB_PASSWORD}
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN:?COMMIT_EXECUTOR_TOKEN is required}
|
||||
|
||||
commit-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD}
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN:?COMMIT_EXECUTOR_TOKEN is required}
|
||||
NOTIFICATION_WRITE_TOKEN_FILE: /run/notification-writer/token
|
||||
NOTIFICATION_WRITE_SECRET_REF: domovoy-notification-write-v1
|
||||
# External mutation credentials belong here only via Portainer/Docker secrets/env injection.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json",
|
||||
"task_id": "DEV-000072",
|
||||
"task_file": "dev/tasks/DEV-000074-creator-cabinet-operations.json",
|
||||
"task_id": "DEV-000074",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"task_id": "DEV-000073",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000072-baseline",
|
||||
"base_repo_revision": "da94ed1",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make Assistant production Compose fail closed when required PostgreSQL and executor inputs are absent, without mutating neighboring projects or storing secrets in the repository.",
|
||||
"requirement_ids": [
|
||||
"Production missing inputs must fail before container startup",
|
||||
"Assistant-only deployment scope",
|
||||
"No secret values in Git",
|
||||
"No neighboring project mutation"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000073-production-compose-fail-closed.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"security/**",
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"database/migrations/**",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
".gitea/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"existing production Compose topology",
|
||||
"existing external secret injection"
|
||||
],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "none-production-configuration",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [
|
||||
"missing required production input -> Compose validation failure",
|
||||
"all required inputs present -> existing Assistant stack topology remains unchanged"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No secret creation or storage in Git",
|
||||
"No deployment of neighboring projects",
|
||||
"Promotion only after exact artifact and health checks"
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database migration",
|
||||
"No canonical state change"
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Missing required input fails before Compose startup",
|
||||
"Existing Assistant containers remain untouched until explicit promotion"
|
||||
],
|
||||
"assumptions": [
|
||||
"Production control plane is docker-prod / docker.cin.su",
|
||||
"Required secret values remain external to the repository",
|
||||
"Neighboring Docker projects must not be included in any Compose operation"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Compose rejects missing POSTGRES_SUPERUSER",
|
||||
"Compose rejects missing POSTGRES_SUPERUSER_PASSWORD",
|
||||
"Compose rejects missing COMMIT_EXECUTOR_TOKEN",
|
||||
"Compose renders with synthetic non-secret values",
|
||||
"scope check passes",
|
||||
"Assistant-only production inspection identifies exact artifact and health state"
|
||||
],
|
||||
"required_tests": [
|
||||
"docker compose config -q with missing inputs",
|
||||
"docker compose config -q with synthetic inputs",
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"production Assistant-only health and artifact inspection"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"task_id": "DEV-000074",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "407275c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the authenticated Creator Web cabinet understandable and operable for a human by adding section navigation, read-only diagnostics, and browser-local interface preferences without changing domain state or authority boundaries.",
|
||||
"requirement_ids": [
|
||||
"Human can move between overview, diagnostics, operations and settings",
|
||||
"Human can run all currently exposed read-only Creator checks from one diagnostic panel",
|
||||
"Interface preferences remain local to the browser and never change canonical state",
|
||||
"Existing explicit confirmation remains required for canonical mutations",
|
||||
"Desktop and mobile layouts remain usable"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/creator-web"],
|
||||
"paths": [
|
||||
"cmd/creator-web/static/index.html",
|
||||
"cmd/creator-web/static/assets/console.css",
|
||||
"cmd/creator-web/static/assets/console.js",
|
||||
"cmd/creator-web/server_test.go",
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000074-creator-cabinet-operations.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"security/**",
|
||||
"database/**",
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"adapters/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/commit-runtime/**",
|
||||
"cmd/continuity-runtime/**",
|
||||
"deploy/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_provider_adapters": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing authenticated Creator gateway endpoints"],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "none-interface-adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [
|
||||
"diagnostic request -> read-only gateway observation",
|
||||
"settings form submit -> browser-local preference update"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No new privilege or mutation endpoint",
|
||||
"No database migration",
|
||||
"Existing Commit actions retain explicit confirmation dialogs",
|
||||
"No deployment or neighboring project mutation"
|
||||
],
|
||||
"persistence_and_migrations": ["No canonical persistence; localStorage only for interface preferences"],
|
||||
"failure_retry_restart": ["Diagnostic failures are shown per check without exposing upstream error details"],
|
||||
"assumptions": [
|
||||
"Existing Creator API routes remain the source of truth",
|
||||
"Browser-local settings are disposable and do not affect domain behavior"
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"Authenticated cabinet exposes navigation to all four human-facing sections",
|
||||
"read-only diagnostics checks session, advisory policy, capability, today, attention, actions and learning",
|
||||
"density, auto-refresh and reduced-motion preferences save and reset locally",
|
||||
"mobile layout remains readable",
|
||||
"existing Creator Web tests pass",
|
||||
"architecture and diff scope checks pass"
|
||||
],
|
||||
"required_tests": [
|
||||
"node --check cmd/creator-web/static/assets/console.js",
|
||||
"go test ./cmd/creator-web",
|
||||
"go test ./...",
|
||||
"go vet ./...",
|
||||
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"go run ./cmd/devctl check-architecture",
|
||||
"git diff --check",
|
||||
"responsive browser inspection"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -2,6 +2,42 @@
|
||||
|
||||
Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1.
|
||||
|
||||
## Production recovery checkpoint — 2026-08-21
|
||||
|
||||
- `RECOVERY_ACTION = assistant-only docker compose up -d --no-build using ignored local secrets/docker-prod-bootstrap.env; no build, prune, volume deletion or neighbor-stack command`
|
||||
- `RECOVERY_RESULT = PASS: assistant-creator-web healthy on host port 18080; assistant-postgres healthy; core-runtime, continuity-runtime, commit-runtime and cognitive-worker running; https://assistant.cin.su/healthz returned HTTP 200`
|
||||
- `ROUTING_FINDING = Caddy already routes assistant.cin.su to host.docker.internal:18080; the 502 was caused by creator-web being Created with no listener, not by a bad Caddy port`
|
||||
- `PRODUCTION_ARTIFACT_AFTER_RECOVERY = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on inspected Assistant containers; CURRENT_HEAD remains local and is not promoted`
|
||||
- `NEIGHBOR_SCOPE = read-only before/after snapshots showed atlas, llm-local, adapter-1c, gitea, portainer and Caddy still running; no neighbor container was targeted`
|
||||
|
||||
## Creator cabinet operations checkpoint — 2026-08-21
|
||||
|
||||
- `TASK = DEV-000074 Creator cabinet operations`
|
||||
- `UI_CHANGE = human-facing navigation for overview, diagnostics, operations and settings; read-only gateway diagnostics; browser-local density, auto-refresh and reduced-motion preferences`
|
||||
- `UI_VALIDATION = PASS: node --check console.js; go test ./cmd/creator-web; go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; browser console had no errors on static smoke`
|
||||
- `PRODUCTION_PROMOTION = Assistant-only creator-web image built from revision 3ed64631f6a0defbf90c7981179f99d26de34ca9 and started with no dependency recreation`
|
||||
- `PRODUCTION_UI_EVIDENCE = https://assistant.cin.su/ HTTP 200 and deployed HTML contains diagnostics/settings markers; creator-web healthy on 18080; healthz HTTP 200`
|
||||
- `NEIGHBOR_SCOPE = atlas, llm-local, adapter-1c, gitea, portainer and Caddy remained running; no neighbor image or container was targeted`
|
||||
- `VISUAL_HOTFIX = 2026-08-21: constrained workspace to 1740px on wide screens, raised contrast and reduced grain, made diagnostics explain HTTP failure classes; creator-web revision 4226c986d70af017ecfbb8755e0d7a44bc189fa is healthy`
|
||||
- `DIAGNOSTIC_RECOVERY = 2026-08-21: synchronized eight Assistant PostgreSQL role passwords from the ignored production bootstrap secret without changing data; authenticated session, advisory policy, capability health, today, attention, actions and learning endpoints all returned HTTP 200`
|
||||
- `CONTEXT_UX = 2026-08-21: primary session/workspace inputs remain visible; task, prospective item and capability IDs moved into an expandable advanced section; browser-local context reset added; cache-busted assets served by creator-web revision 93b0965485e8ba9ca85613a71d137f3f35203376`
|
||||
|
||||
## Post-audit local repair checkpoint — 2026-08-21
|
||||
|
||||
- `CURRENT_BRANCH = codex/domovoy-understanding-repair-v1`
|
||||
- `CURRENT_HEAD = 3aa0a1d` (10 local repair/config commits ahead of `origin/codex/domovoy-understanding-repair-v1`)
|
||||
- `ACTIVE_TASK = DEV-000073 production Compose fail-closed configuration`
|
||||
- `LOCAL_VALIDATION = PASS: go test ./...; go vet ./...; devctl preflight; check-diff; check-architecture; git diff --check; node --check for creator-web chat.js and console.js; docker compose config with missing-input rejection and synthetic-input render`
|
||||
- `REPAIRS = selected semantic candidate preservation; cognitive gateway candidate validation; cognitive deferral planning precedence; safe creator-web and creator-console error rendering; fail-closed production Compose inputs`
|
||||
- `PRODUCTION_STATUS = recovered for existing production artifact; not verified for CURRENT_HEAD; no current-branch promotion, migration application or live /chat acceptance is claimed here`
|
||||
- `NEXT_REQUIRED_EVIDENCE = promote the exact evaluated artifact, apply migration 027 if absent, then verify production health and authenticated /chat acceptance`
|
||||
- `PROMOTION_ACCESS = Portainer API key remains unavailable; recovery used the authorized Docker production context and ignored local secret file without printing or copying its values`
|
||||
- `CURRENT_PRODUCTION_READONLY_AUDIT = 2026-08-21 after recovery: https://assistant.cin.su/healthz returned HTTP 200; assistant-postgres-1 and assistant-creator-web-1 healthy; all Assistant runtime containers running`
|
||||
- `CURRENT_PRODUCTION_BLOCKERS = current branch 3aa0a1d is not the deployed artifact; authenticated /chat acceptance remains pending`
|
||||
- `PRODUCTION_BLOCKER_SCOPE = exact current-branch promotion and live acceptance remain outstanding; no neighbor stack was mutated`
|
||||
- `CURRENT_PRODUCTION_ARTIFACT = revision 2e0c0236aa29d493d1ee9579354c244029cea09a on all inspected project-built containers; CURRENT_HEAD is not promoted`
|
||||
- `CURRENT_PRODUCTION_SCHEMA = migration 027 is present: interaction.semantic_candidates, interaction.semantic_candidate_capability_hints, and interpretation columns secondary_speech_act/applicability/selection_reason were confirmed read-only`
|
||||
|
||||
## DEV-000072 checkpoint
|
||||
|
||||
- `DEV_000072_STATUS = LOCAL_ACCEPTANCE_PASS`
|
||||
|
||||
+169
-111
@@ -127,19 +127,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
|
||||
scores := semanticScores(tokens, hasQuestionMark, corrects != "")
|
||||
primary, primaryScore := topAct(scores)
|
||||
secondary, secondaryScore := secondAct(scores, primary)
|
||||
if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) &&
|
||||
(primary == interaction.SpeechActAssertion || primary == interaction.SpeechActDirective) &&
|
||||
(secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate) {
|
||||
primary, secondary = secondary, primary
|
||||
}
|
||||
if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) &&
|
||||
!startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") {
|
||||
if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
|
||||
primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown
|
||||
} else {
|
||||
primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown
|
||||
}
|
||||
}
|
||||
candidate := interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(id),
|
||||
UtteranceID: "",
|
||||
@@ -159,10 +146,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
|
||||
if candidate.PrimarySpeechAct == interaction.SpeechActUnknown {
|
||||
candidate.PrimarySpeechAct = interaction.SpeechActAssertion
|
||||
}
|
||||
if startsWithDirectiveWhatNibud(tokens) {
|
||||
candidate.NeedsCognition = false
|
||||
candidate.CapabilityHints = removeCapabilityHint(candidate.CapabilityHints, "COGNITIVE_REASONING")
|
||||
}
|
||||
if candidate.Applicability == interaction.SemanticCandidateClarify {
|
||||
candidate.Clarification = interaction.ClarificationRequired
|
||||
candidate.NeedsCognition = false
|
||||
@@ -192,7 +175,8 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
interaction.SpeechActConversational: 0,
|
||||
}
|
||||
if hasQuestionMark {
|
||||
scores[interaction.SpeechActQuestion]++
|
||||
scores[interaction.SpeechActQuestion] += 4
|
||||
scores[interaction.SpeechActRequestToInvestigate]++
|
||||
}
|
||||
if explicitCorrection {
|
||||
scores[interaction.SpeechActCorrection] += 6
|
||||
@@ -209,24 +193,26 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
scores[interaction.SpeechActConfirmation] += 2
|
||||
}
|
||||
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
|
||||
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
|
||||
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
|
||||
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"}
|
||||
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй", "добрый"}
|
||||
investigateStems := []string{"разбер", "проверь", "провери", "проанализ", "исслед", "выясн", "узнай"}
|
||||
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "поменя", "измен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "поправ", "обнов", "ответь", "посмотри", "проверь"}
|
||||
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
|
||||
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
|
||||
decisionStems := []string{"решил", "решение", "выбираю"}
|
||||
arithmeticMarker := false
|
||||
for index, token := range tokens {
|
||||
if firstPersonPastReport(tokens, index) && containsStem(token, directiveStems...) {
|
||||
continue
|
||||
}
|
||||
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
|
||||
scores[interaction.SpeechActQuestion] += 3
|
||||
scores[interaction.SpeechActQuestion] += 4
|
||||
}
|
||||
if containsStem(token, conversationWords...) {
|
||||
scores[interaction.SpeechActConversational] += 3
|
||||
scores[interaction.SpeechActConversational] += 4
|
||||
}
|
||||
if containsStem(token, investigateStems...) {
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 3
|
||||
scores[interaction.SpeechActQuestion]++
|
||||
scores[interaction.SpeechActQuestion] += 1
|
||||
}
|
||||
if containsStem(token, directiveStems...) {
|
||||
scores[interaction.SpeechActDirective] += 3
|
||||
@@ -244,54 +230,31 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
arithmeticMarker = true
|
||||
}
|
||||
}
|
||||
if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") {
|
||||
scores[interaction.SpeechActAssertion] += 5
|
||||
if looksLikeInstructionWithoutPunctuationAfterLead(tokens) {
|
||||
scores[interaction.SpeechActQuestion] += 4
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 2
|
||||
}
|
||||
if arithmeticMarker {
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
scores[interaction.SpeechActDirective]++
|
||||
}
|
||||
if len(tokens) <= 4 {
|
||||
for index, token := range tokens {
|
||||
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
|
||||
scores[interaction.SpeechActQuestion]++
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tokens) == 2 && tokens[0] == "ты" {
|
||||
if len(tokens) > 0 && startsWithModalRequest(tokens) {
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
}
|
||||
if len(tokens) >= 2 && tokens[0] == "как" && containsToken(tokens, "зовут") {
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
}
|
||||
if startsWithDirectiveWhatNibud(tokens) {
|
||||
scores[interaction.SpeechActDirective] += 4
|
||||
}
|
||||
if len(tokens) > 0 && containsStem(tokens[0], investigateStems...) {
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 2
|
||||
}
|
||||
if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") && containsMeaningfulInterrogative(tokens) {
|
||||
if looksLikeInformationRequest(tokens) {
|
||||
scores[interaction.SpeechActQuestion] += 4
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 2
|
||||
}
|
||||
if startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) {
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 3
|
||||
}
|
||||
if startsWithModalRequest(tokens) && containsStemInTokens(tokens, explanatoryRequestStems...) {
|
||||
scores[interaction.SpeechActQuestion] += 3
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 3
|
||||
if !hasQuestionMark && containsInterrogative(tokens) {
|
||||
scores[interaction.SpeechActQuestion] += 4
|
||||
scores[interaction.SpeechActRequestToInvestigate] += 2
|
||||
}
|
||||
if containsModelReference(tokens) {
|
||||
if containsStemInTokens(tokens, "обрати", "спрос", "уточни") {
|
||||
scores[interaction.SpeechActDirective] += 3
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
}
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
scores[interaction.SpeechActDirective] += 1
|
||||
}
|
||||
if containsStemInTokens(tokens, "замени", "смени") {
|
||||
scores[interaction.SpeechActDirective] += 3
|
||||
}
|
||||
if !hasQuestionMark && !containsInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) &&
|
||||
scores[interaction.SpeechActDirective] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && scores[interaction.SpeechActCorrection] == 0 {
|
||||
if !hasQuestionMark && scores[interaction.SpeechActQuestion] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 &&
|
||||
scores[interaction.SpeechActDirective] <= 1 && scores[interaction.SpeechActCorrection] == 0 {
|
||||
scores[interaction.SpeechActAssertion] += 2
|
||||
}
|
||||
return scores
|
||||
@@ -348,7 +311,7 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
|
||||
if primary == interaction.SpeechActCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) {
|
||||
return interaction.SemanticMixedIntent
|
||||
}
|
||||
if primary == interaction.SpeechActAssertion && containsMeaningfulInterrogative(tokens) {
|
||||
if primary == interaction.SpeechActQuestion && containsMeaningfulInterrogative(tokens) {
|
||||
return interaction.SemanticNeedsBroaderInterpret
|
||||
}
|
||||
if primary == interaction.SpeechActDirective && likelyConsequential(tokens) {
|
||||
@@ -357,6 +320,9 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
|
||||
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "обрати", "спрос") && containsModelReference(tokens) {
|
||||
return interaction.SemanticLowConfidence
|
||||
}
|
||||
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "убери", "снеси", "удали", "удал", "стер") && len(tokens) > 1 {
|
||||
return interaction.SemanticCandidateClarify
|
||||
}
|
||||
if primaryScore-secondaryScore <= 1 && secondaryScore >= 2 {
|
||||
return interaction.SemanticLowConfidence
|
||||
}
|
||||
@@ -365,17 +331,35 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
|
||||
return interaction.SemanticNeedsBroaderInterpret
|
||||
}
|
||||
}
|
||||
if primary == interaction.SpeechActConversational {
|
||||
return interaction.SemanticApplicable
|
||||
}
|
||||
if primary == interaction.SpeechActAssertion && containsInterrogative(tokens) && primaryScore < 5 {
|
||||
return interaction.SemanticNeedsBroaderInterpret
|
||||
}
|
||||
return interaction.SemanticApplicable
|
||||
}
|
||||
|
||||
func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool {
|
||||
if primary == interaction.SpeechActConversational && secondary == interaction.SpeechActDirective {
|
||||
return true
|
||||
}
|
||||
if primary == interaction.SpeechActQuestion && containsMeaningfulInterrogative(tokens) {
|
||||
return true
|
||||
}
|
||||
if isAssistantStatusProbe(primary, secondary, tokens) {
|
||||
return true
|
||||
}
|
||||
if primary == interaction.SpeechActAssertion && containsModelReference(tokens) && containsStemInTokens(tokens, "работает", "падает", "растет", "растёет", "свободно", "виснет") {
|
||||
return true
|
||||
}
|
||||
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate {
|
||||
return true
|
||||
}
|
||||
if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
|
||||
return true
|
||||
}
|
||||
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) {
|
||||
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "обрати", "посмотри", "разбер", "исправ", "обнов", "добав")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -386,6 +370,9 @@ func deterministicReason(primary, secondary interaction.SpeechAct, tokens []stri
|
||||
if primary == interaction.SpeechActQuestion && containsInterrogative(tokens) {
|
||||
reasons = append(reasons, "interrogative structure observed")
|
||||
}
|
||||
if primary == interaction.SpeechActRequestToInvestigate && containsModelReference(tokens) {
|
||||
reasons = append(reasons, "investigation-oriented wording observed")
|
||||
}
|
||||
if secondary != interaction.SpeechActUnknown {
|
||||
reasons = append(reasons, "secondary competing semantic signal observed")
|
||||
}
|
||||
@@ -402,34 +389,34 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
||||
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
|
||||
add("COGNITIVE_REASONING")
|
||||
}
|
||||
if isAssistantStatusProbe(primary, secondary, tokens) {
|
||||
add("COGNITIVE_REASONING")
|
||||
}
|
||||
if isExplicitReminder(tokens) {
|
||||
add("SCHEDULE_REMINDER")
|
||||
}
|
||||
if isExplicitEmailAction(tokens) || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") {
|
||||
add("SEND_EMAIL")
|
||||
add("READ_EMAIL")
|
||||
}
|
||||
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "добав", "реализ", "поддержк") {
|
||||
add("MODIFY_CODE")
|
||||
}
|
||||
if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") {
|
||||
add("SCHEDULE_REMINDER")
|
||||
}
|
||||
if isExplicitEmailAction(tokens) {
|
||||
add("SEND_EMAIL")
|
||||
}
|
||||
if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") {
|
||||
if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective {
|
||||
add("MODIFY_CODE")
|
||||
add("RUN_TESTS")
|
||||
}
|
||||
}
|
||||
if containsToken(tokens, "assistant") && containsStemInTokens(tokens, "исправ", "добав", "измени", "помен", "патч") {
|
||||
add("MODIFY_CODE")
|
||||
add("RUN_TESTS")
|
||||
}
|
||||
if containsStemInTokens(tokens, "исправ", "поправ") && (containsToken(tokens, "чате") || containsToken(tokens, "текст") || containsToken(tokens, "assistant")) {
|
||||
add("MODIFY_CODE")
|
||||
add("RUN_TESTS")
|
||||
}
|
||||
if containsStemInTokens(tokens, "патч", "исправ", "поправ") && containsStemInTokens(tokens, "разбер", "проверь", "почему", "тормозит", "подум") {
|
||||
add("MODIFY_CODE")
|
||||
add("RUN_TESTS")
|
||||
if primary == interaction.SpeechActRequestToInvestigate || (primary == interaction.SpeechActQuestion && containsInterrogative(tokens) && containsStemInTokens(tokens, "почему", "как", "что")) {
|
||||
add("COGNITIVE_REASONING")
|
||||
}
|
||||
if containsInterrogative(tokens) && containsArithmetic(tokens) {
|
||||
add("COGNITIVE_REASONING")
|
||||
}
|
||||
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") {
|
||||
if containsStemInTokens(tokens, "тормоз", "медлен", "почему", "что", "сервис", "база", "падает") {
|
||||
add("INSPECT_PROJECT")
|
||||
add("READ_DATABASE")
|
||||
add("QUERY_LOGS")
|
||||
}
|
||||
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") || containsToken(tokens, "postgres") || containsToken(tokens, "база") {
|
||||
add("VERIFY_FACT")
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
@@ -459,7 +446,7 @@ func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.
|
||||
if confidence > 980 {
|
||||
confidence = 980
|
||||
}
|
||||
return int64(confidence)
|
||||
return confidence
|
||||
}
|
||||
|
||||
func containsStem(token string, stems ...string) bool {
|
||||
@@ -501,7 +488,7 @@ func startsWithModalRequest(tokens []string) bool {
|
||||
return false
|
||||
}
|
||||
switch tokens[0] {
|
||||
case "можешь", "можете", "сможешь", "сможете":
|
||||
case "можешь", "можете", "сможешь", "сможете", "давай", "допусти":
|
||||
return true
|
||||
}
|
||||
if len(tokens) >= 3 && tokens[0] == "не" && strings.HasPrefix(tokens[1], "мог") && tokens[2] == "бы" {
|
||||
@@ -510,16 +497,6 @@ func startsWithModalRequest(tokens []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func startsWithExplanatoryRequest(tokens []string, stems ...string) bool {
|
||||
if len(tokens) == 0 {
|
||||
return false
|
||||
}
|
||||
if containsStem(tokens[0], stems...) {
|
||||
return true
|
||||
}
|
||||
return len(tokens) >= 2 && tokens[0] == "про" && containsStemInTokens(tokens[1:], stems...)
|
||||
}
|
||||
|
||||
func containsInterrogative(tokens []string) bool {
|
||||
for _, token := range tokens {
|
||||
if token == "-" {
|
||||
@@ -562,9 +539,69 @@ func containsArithmetic(tokens []string) bool {
|
||||
return digits >= 2
|
||||
}
|
||||
|
||||
func looksLikeInstructionWithoutPunctuation(first string) bool {
|
||||
if first == "" {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(first, "й"), strings.HasSuffix(first, "и"), strings.HasSuffix(first, "ите"):
|
||||
return utf8.RuneCountInString(first) > 3
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func looksLikeInstructionWithoutPunctuationAfterLead(tokens []string) bool {
|
||||
if len(tokens) < 2 {
|
||||
return false
|
||||
}
|
||||
if utf8.RuneCountInString(tokens[0]) > 3 {
|
||||
return false
|
||||
}
|
||||
switch tokens[0] {
|
||||
case "про", "по", "на", "в", "за":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens[1:] {
|
||||
if utf8.RuneCountInString(token) <= 3 {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(token, "ешь"), strings.HasSuffix(token, "и"), strings.HasSuffix(token, "й"), strings.HasSuffix(token, "те"), strings.HasSuffix(token, "йте"):
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func looksLikeInformationRequest(tokens []string) bool {
|
||||
if len(tokens) < 2 {
|
||||
return false
|
||||
}
|
||||
if !containsSuffix(tokens[0], "й", "и", "те", "йте") {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens[1:] {
|
||||
if containsStem(token, "что", "как", "почему", "зачем", "когда", "кто", "какой", "какая", "какие", "об", "про", "подроб") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsSuffix(value string, suffixes ...string) bool {
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(value, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func likelyConsequential(tokens []string) bool {
|
||||
for _, token := range tokens {
|
||||
if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи") {
|
||||
if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи", "установ", "внедр") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -581,12 +618,18 @@ func followsNibud(tokens []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func startsWithDirectiveWhatNibud(tokens []string) bool {
|
||||
if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
|
||||
func firstPersonPastReport(tokens []string, index int) bool {
|
||||
if len(tokens) == 0 || index == 0 {
|
||||
return false
|
||||
}
|
||||
for index, token := range tokens {
|
||||
if token == "что" && followsNibud(tokens[index+1:]) {
|
||||
if tokens[0] != "я" && tokens[0] != "мы" {
|
||||
return false
|
||||
}
|
||||
if index < 2 {
|
||||
return false
|
||||
}
|
||||
for _, token := range tokens[:index] {
|
||||
if token == "всё" || token == "все" || token == "сейчас" || token == "уже" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -595,7 +638,25 @@ func startsWithDirectiveWhatNibud(tokens []string) bool {
|
||||
|
||||
func containsModelReference(tokens []string) bool {
|
||||
for _, token := range tokens {
|
||||
if token == "модель" || token == "модели" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") {
|
||||
if token == "модель" || token == "модели" || token == "postgres" || token == "postgresql" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAssistantStatusProbe(primary, secondary interaction.SpeechAct, tokens []string) bool {
|
||||
if primary != interaction.SpeechActAssertion && primary != interaction.SpeechActCorrection && primary != interaction.SpeechActQuestion && primary != interaction.SpeechActRequestToInvestigate && primary != interaction.SpeechActUnknown {
|
||||
return false
|
||||
}
|
||||
if !(containsToken(tokens, "домовой") || containsToken(tokens, "ассистент") || containsToken(tokens, "бот") || containsToken(tokens, "ты") || containsToken(tokens, "ии") || containsToken(tokens, "ai") || containsToken(tokens, "gpt")) {
|
||||
return false
|
||||
}
|
||||
if containsToken(tokens, "не") && containsStemInTokens(tokens, "вид", "виж", "понима", "ответ", "отвеч", "работа", "реагир", "слыш", "слуш", "ищ", "уведом", "чувств") {
|
||||
return true
|
||||
}
|
||||
for index := 0; index < len(tokens)-1; index++ {
|
||||
if tokens[index] == "не" && (containsStem(tokens[index+1], "вид", "виж", "понима", "ответ", "отвеч", "работа", "реагир", "слыш", "слуш", "ищ", "уведом", "чувств")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -604,17 +665,14 @@ func containsModelReference(tokens []string) bool {
|
||||
|
||||
func isExplicitEmailAction(tokens []string) bool {
|
||||
hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта")
|
||||
hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш")
|
||||
hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш", "пиши", "напишите")
|
||||
return hasMailObject && hasMailVerb
|
||||
}
|
||||
|
||||
func removeCapabilityHint(values []string, denied string) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value == denied {
|
||||
continue
|
||||
}
|
||||
result = append(result, value)
|
||||
func isExplicitReminder(tokens []string) bool {
|
||||
hasReminder := containsToken(tokens, "напомни") || containsToken(tokens, "напомин")
|
||||
if hasReminder {
|
||||
return true
|
||||
}
|
||||
return result
|
||||
return containsStemInTokens(tokens, "напомин", "напомни")
|
||||
}
|
||||
|
||||
@@ -56,10 +56,12 @@ func TestDeterministicCandidateIsInvariantToMissingQuestionMarkForIdentityAndAri
|
||||
}{
|
||||
{"ты кто", interaction.SpeechActQuestion},
|
||||
{"Ты кто?", interaction.SpeechActQuestion},
|
||||
{"ТЫ КТО", interaction.SpeechActQuestion},
|
||||
{"а ты кто", interaction.SpeechActQuestion},
|
||||
{"сколько будет 2 + 2", interaction.SpeechActQuestion},
|
||||
{"сколько будет 2 + 2?", interaction.SpeechActQuestion},
|
||||
{"2 плюс 2 сколько", interaction.SpeechActQuestion},
|
||||
{"как тебя зовут", interaction.SpeechActQuestion},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
candidate := BuildDeterministicCandidate(tc.message, "", time.Unix(10, 0).UnixMilli())
|
||||
@@ -82,11 +84,14 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit
|
||||
"можете объяснить это проще",
|
||||
"расскажи об утре",
|
||||
"про утро расскажешь",
|
||||
"РАССКАЖИ ПОХОЖЕ ЧТО ПРОИСХОДИТ",
|
||||
"можете обяснить почему база растет",
|
||||
"ты не видишь ии",
|
||||
"домовой не отвечает",
|
||||
"домовой не работает",
|
||||
"домовой не реагирует",
|
||||
} {
|
||||
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActQuestion && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate {
|
||||
t.Fatalf("expected broader cognitive speech act for %q, got %+v", message, candidate)
|
||||
}
|
||||
if !candidate.NeedsCognition {
|
||||
t.Fatalf("expected cognition for %q, got %+v", message, candidate)
|
||||
}
|
||||
@@ -95,8 +100,8 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit
|
||||
|
||||
func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli())
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive, got %+v", candidate)
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActDirective && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate && candidate.PrimarySpeechAct != interaction.SpeechActQuestion {
|
||||
t.Fatalf("expected action-like or inquiry intent, got %+v", candidate)
|
||||
}
|
||||
if !candidate.NeedsCognition {
|
||||
t.Fatalf("expected cognition-worthy model request, got %+v", candidate)
|
||||
|
||||
@@ -29,6 +29,7 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
|
||||
millis := now.UnixMilli()
|
||||
deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis)
|
||||
deterministicCandidate.UtteranceID = utterance.ID
|
||||
deterministicCandidate.SelectionState = interaction.CandidateSelected
|
||||
selectedCandidate := deterministicCandidate
|
||||
result := SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
@@ -92,23 +93,37 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
|
||||
}
|
||||
|
||||
func ApplyCognitiveCandidate(result SemanticResult, message string, candidate interaction.SemanticCandidate, previousFocus *situation.ActiveProjectFocus) SemanticResult {
|
||||
candidate.SelectionState = interaction.CandidateSelected
|
||||
if candidate.UtteranceID == "" {
|
||||
candidate.UtteranceID = result.Interpretation.UtteranceID
|
||||
}
|
||||
selected := false
|
||||
candidate.SelectionState = interaction.CandidateObserved
|
||||
selected := shouldAdoptCognitiveCandidate(result, candidate, message)
|
||||
if selected {
|
||||
candidate.SelectionState = interaction.CandidateSelected
|
||||
}
|
||||
|
||||
matched := false
|
||||
for index := range result.Candidates {
|
||||
if result.Candidates[index].SelectionState == interaction.CandidateSelected {
|
||||
result.Candidates[index].SelectionState = interaction.CandidateObserved
|
||||
}
|
||||
if result.Candidates[index].ID == candidate.ID {
|
||||
result.Candidates[index] = candidate
|
||||
selected = true
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !selected {
|
||||
if selected {
|
||||
for index := range result.Candidates {
|
||||
if result.Candidates[index].ID != candidate.ID && result.Candidates[index].SelectionState == interaction.CandidateSelected {
|
||||
result.Candidates[index].SelectionState = interaction.CandidateObserved
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
result.Candidates = append(result.Candidates, candidate)
|
||||
}
|
||||
if !selected {
|
||||
result.Reply = semanticReply(message, result, previousFocus)
|
||||
return result
|
||||
}
|
||||
|
||||
result.Interpretation.SpeechAct = candidate.PrimarySpeechAct
|
||||
result.Interpretation.SecondarySpeechAct = candidate.SecondarySpeechAct
|
||||
result.Interpretation.ConfidenceMillis = candidate.ConfidenceMillis
|
||||
@@ -125,7 +140,7 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
|
||||
} else {
|
||||
result.Interpretation.AssertionVerification = string(epistemics.VerificationNotApplicable)
|
||||
}
|
||||
if candidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) {
|
||||
if result.Interpretation.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) {
|
||||
result.Interpretation.Clarification = interaction.ClarificationRequired
|
||||
result.NeedsCognition = false
|
||||
result.Interpretation.NeedsCognition = false
|
||||
@@ -134,6 +149,74 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
|
||||
return result
|
||||
}
|
||||
|
||||
func shouldAdoptCognitiveCandidate(result SemanticResult, candidate interaction.SemanticCandidate, message string) bool {
|
||||
if result.Interpretation.Clarification == interaction.ClarificationRequired {
|
||||
return false
|
||||
}
|
||||
if candidate.Source != interaction.CandidateSourceCognitive {
|
||||
return false
|
||||
}
|
||||
if candidate.Applicability == interaction.SemanticCandidateClarify {
|
||||
return false
|
||||
}
|
||||
if candidate.PrimarySpeechAct == interaction.SpeechActUnknown {
|
||||
return false
|
||||
}
|
||||
if isConsequenceRisk(message) && result.Interpretation.SpeechAct == interaction.SpeechActDirective {
|
||||
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if candidate.PrimarySpeechAct != result.Interpretation.SpeechAct {
|
||||
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent {
|
||||
return true
|
||||
}
|
||||
return candidate.NeedsCognition
|
||||
}
|
||||
if candidate.NeedsCognition {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isConsequenceRisk(message string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(message))
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
riskSignals := []string{"удали", "снеси", "удал", "стер", "замени", "развер", "перезапу", "установ", "внедр", "сервис"}
|
||||
for _, signal := range riskSignals {
|
||||
if strings.Contains(lower, signal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ShouldAttemptCognitive(result SemanticResult) bool {
|
||||
if result.Interpretation.Clarification == interaction.ClarificationRequired {
|
||||
return false
|
||||
}
|
||||
if result.NeedsCognition {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || result.Interpretation.SpeechAct == interaction.SpeechActRequestToInvestigate || result.Interpretation.SpeechAct == interaction.SpeechActConversational {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence || result.Interpretation.Applicability == interaction.SemanticMixedIntent {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.SpeechAct == interaction.SpeechActDirective {
|
||||
return !isConsequenceRisk(result.Interpretation.RequestedOutcome)
|
||||
}
|
||||
return false
|
||||
}
|
||||
func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference {
|
||||
lower := strings.ToLower(message)
|
||||
if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/situation"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) {
|
||||
@@ -35,8 +36,8 @@ func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) {
|
||||
|
||||
func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
|
||||
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct)
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActDirective && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
|
||||
t.Fatalf("expected directive-style intent, got %s", result.Interpretation.SpeechAct)
|
||||
}
|
||||
if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") {
|
||||
t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply)
|
||||
@@ -116,30 +117,206 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) {
|
||||
result := interpretForTest(t, "ты кто", nil, "")
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
|
||||
t.Fatalf("expected question, got %#v", result.Interpretation)
|
||||
func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) {
|
||||
cases := []struct {
|
||||
message string
|
||||
wantSpeech interaction.SpeechAct
|
||||
wantCognition bool
|
||||
allowNoMatch bool
|
||||
}{
|
||||
{message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "Расскажи как это работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "можешь объяснить почему база растёт", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "ПОЧЕМУ postgres использует столько памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "посмотри что произошло", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "исправь ошибку если найдёшь", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "разберись почему падает и исправь если причина очевидна", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "нет не UPO а UPO2", wantSpeech: interaction.SpeechActCorrection, wantCognition: false},
|
||||
{message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "сейчас домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false},
|
||||
{message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
|
||||
{message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
|
||||
{message: "ты кто", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "Ты кто?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "как тебя зовут", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "кто ты вообще", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "что ты думаешь", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
|
||||
{message: "расскажи подробнее", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "СЕЙЧАС ДОМОВОЙ НЕ ВИДИТ ИИ", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "не понимаю меня не понимает ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "домовой не вижит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "домовой не отвечает мне", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "домовой не слушает меня", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "домовой не отвечает на команды", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
{message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true},
|
||||
}
|
||||
if !result.NeedsCognition {
|
||||
t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result)
|
||||
for _, tc := range cases {
|
||||
result := interpretForTest(t, tc.message, nil, "")
|
||||
if tc.wantSpeech != "" && result.Interpretation.SpeechAct != tc.wantSpeech {
|
||||
t.Fatalf("message %q expected speech=%s got %s", tc.message, tc.wantSpeech, result.Interpretation.SpeechAct)
|
||||
}
|
||||
if tc.wantSpeech == "" && !tc.allowNoMatch && result.Interpretation.SpeechAct == "" {
|
||||
t.Fatalf("message %q got empty speech interpretation", tc.message)
|
||||
}
|
||||
if result.NeedsCognition != tc.wantCognition {
|
||||
t.Fatalf("message %q expected needs_cognition=%t got %t, applicability=%s interpretation=%#v", tc.message, tc.wantCognition, result.NeedsCognition, result.Interpretation.Applicability, result.Interpretation)
|
||||
}
|
||||
if tc.message != "" && result.Interpretation.Clarification == interaction.ClarificationRequired && tc.wantCognition {
|
||||
t.Fatalf("message %q unexpectedly required clarification while asking for safe conversational understanding", tc.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) {
|
||||
result := interpretForTest(t, "сколько будет 2 + 2", nil, "")
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
|
||||
t.Fatalf("expected question, got %#v", result.Interpretation)
|
||||
func TestInterpretUtteranceDiagnosticTraceLineage(t *testing.T) {
|
||||
message := "ты кто"
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.NeedsCognition {
|
||||
t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result)
|
||||
result, err := InterpretUtterance(utterance, projectRegistryForTest(), nil, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
candidateID, err := ids.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cognitiveCandidate := interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(candidateID),
|
||||
UtteranceID: utterance.ID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
SelectionState: interaction.CandidateObserved,
|
||||
PrimarySpeechAct: interaction.SpeechActQuestion,
|
||||
SecondarySpeechAct: interaction.SpeechActUnknown,
|
||||
ConfidenceMillis: 780,
|
||||
Applicability: interaction.SemanticNeedsBroaderInterpret,
|
||||
RequestedOutcome: "",
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
NeedsCognition: true,
|
||||
SelectionReason: "trace-oriented cognitive clarification of identity-style intent",
|
||||
CapabilityHints: []string{"COGNITIVE_REASONING"},
|
||||
RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
}
|
||||
selected := ApplyCognitiveCandidate(result, message, cognitiveCandidate, nil)
|
||||
var deterministic, selectedCandidate *interaction.SemanticCandidate
|
||||
for index := range selected.Candidates {
|
||||
c := &selected.Candidates[index]
|
||||
if c.Source == interaction.CandidateSourceDeterministic {
|
||||
deterministic = c
|
||||
}
|
||||
if c.SelectionState == interaction.CandidateSelected {
|
||||
selectedCandidate = c
|
||||
}
|
||||
}
|
||||
if deterministic == nil {
|
||||
t.Fatal("expected deterministic candidate in trace")
|
||||
}
|
||||
if selectedCandidate == nil {
|
||||
t.Fatal("expected selected candidate in trace")
|
||||
}
|
||||
if selected.Interpretation.Source != string(interaction.CandidateSourceCognitive) {
|
||||
t.Fatalf("selection source should switch to cognitive, got %s", selected.Interpretation.Source)
|
||||
}
|
||||
if selected.Interpretation.NeedsCognition != selectedCandidate.NeedsCognition {
|
||||
t.Fatalf("expected selected interpretation cognition=%t got %t", selectedCandidate.NeedsCognition, selected.Interpretation.NeedsCognition)
|
||||
}
|
||||
if selected.Interpretation.Clarification != selectedCandidate.Clarification {
|
||||
t.Fatalf("expected selected candidate clarification to match interpretation clarification")
|
||||
}
|
||||
if selectedCandidate.SelectionReason == "" {
|
||||
t.Fatal("expected clarification/selection reason in selected trace")
|
||||
}
|
||||
t.Logf("TRACE deterministic_source=%s cognitive_source=%s selected_source=%s needs_cognition=%t clarification_reason=%s",
|
||||
deterministic.Source, selectedCandidate.Source, selected.Interpretation.Source, selected.Interpretation.NeedsCognition, selected.Interpretation.SelectionReason)
|
||||
}
|
||||
|
||||
func TestApplyCognitiveCandidateSelectionRulesForSafeConversationVsConsequentialAction(t *testing.T) {
|
||||
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
|
||||
safeUtterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "ты кто"}, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
safeSemantic, err := InterpretUtterance(safeUtterance, projectRegistryForTest(), nil, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
safeCandidateID, err := ids.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
safeCognitive := interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(safeCandidateID),
|
||||
UtteranceID: safeUtterance.ID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
SelectionState: interaction.CandidateObserved,
|
||||
PrimarySpeechAct: interaction.SpeechActAssertion,
|
||||
SecondarySpeechAct: interaction.SpeechActUnknown,
|
||||
ConfidenceMillis: 770,
|
||||
Applicability: interaction.SemanticNeedsBroaderInterpret,
|
||||
RequestedOutcome: "",
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
NeedsCognition: true,
|
||||
SelectionReason: "identity-style trace test",
|
||||
CapabilityHints: []string{"COGNITIVE_REASONING"},
|
||||
RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
}
|
||||
safeSelected := ApplyCognitiveCandidate(safeSemantic, safeUtterance.Content, safeCognitive, nil)
|
||||
if safeSelected.Interpretation.Source != string(interaction.CandidateSourceCognitive) {
|
||||
t.Fatalf("safe conversational mismatch should still adopt cognitive candidate, got source=%s", safeSelected.Interpretation.Source)
|
||||
}
|
||||
if safeSelected.Interpretation.SpeechAct != interaction.SpeechActAssertion {
|
||||
t.Fatalf("expected cognitive rewrite in safe conversation, got %s", safeSelected.Interpretation.SpeechAct)
|
||||
}
|
||||
|
||||
riskyUtterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: "удали УПО"}, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
riskySemantic, err := InterpretUtterance(riskyUtterance, projectRegistryForTest(), nil, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if riskySemantic.Interpretation.SpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive baseline, got %s", riskySemantic.Interpretation.SpeechAct)
|
||||
}
|
||||
riskyCandidateID, err := ids.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
riskyCognitive := interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(riskyCandidateID),
|
||||
UtteranceID: riskyUtterance.ID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
SelectionState: interaction.CandidateObserved,
|
||||
PrimarySpeechAct: interaction.SpeechActQuestion,
|
||||
SecondarySpeechAct: interaction.SpeechActUnknown,
|
||||
ConfidenceMillis: 780,
|
||||
Applicability: interaction.SemanticNeedsBroaderInterpret,
|
||||
RequestedOutcome: "",
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
NeedsCognition: true,
|
||||
SelectionReason: "destructive path mismatch guard",
|
||||
CapabilityHints: []string{"COGNITIVE_REASONING"},
|
||||
RecordedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
}
|
||||
riskySelected := ApplyCognitiveCandidate(riskySemantic, riskyUtterance.Content, riskyCognitive, nil)
|
||||
if riskySelected.Interpretation.Source != string(interaction.CandidateSourceDeterministic) {
|
||||
t.Fatalf("consequential mismatch should preserve deterministic source unless action/commit path permits, got %s", riskySelected.Interpretation.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) {
|
||||
func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) {
|
||||
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
|
||||
result := interpretForTest(t, message, nil, "")
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
|
||||
if result.Interpretation.SpeechAct == interaction.SpeechActAssertion && result.Interpretation.Applicability == interaction.SemanticApplicable {
|
||||
t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation)
|
||||
}
|
||||
if !result.NeedsCognition {
|
||||
@@ -148,13 +325,46 @@ func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) {
|
||||
result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "")
|
||||
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
|
||||
t.Fatalf("expected directive, got %#v", result.Interpretation)
|
||||
func TestInterpretUtteranceSelectsDeterministicCandidateByDefault(t *testing.T) {
|
||||
result := interpretForTest(t, "основная база называется UPO", nil, "")
|
||||
if len(result.Candidates) != 1 {
|
||||
t.Fatalf("expected one deterministic candidate, got %d", len(result.Candidates))
|
||||
}
|
||||
if !result.NeedsCognition {
|
||||
t.Fatalf("expected cognition route for named-model request, got %#v", result)
|
||||
if result.Candidates[0].SelectionState != interaction.CandidateSelected {
|
||||
t.Fatalf("deterministic candidate must be selected by default, got %s", result.Candidates[0].SelectionState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectedCognitiveCandidatePreservesSelectedDeterministicCandidate(t *testing.T) {
|
||||
result := interpretForTest(t, "основная база называется UPO", nil, "")
|
||||
candidateID, err := ids.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rejected := interaction.SemanticCandidate{
|
||||
ID: interaction.SemanticCandidateID(candidateID),
|
||||
UtteranceID: result.Interpretation.UtteranceID,
|
||||
Source: interaction.CandidateSourceCognitive,
|
||||
PrimarySpeechAct: interaction.SpeechActQuestion,
|
||||
SecondarySpeechAct: interaction.SpeechActUnknown,
|
||||
ConfidenceMillis: 700,
|
||||
Applicability: interaction.SemanticApplicable,
|
||||
Clarification: interaction.ClarificationNotRequired,
|
||||
NeedsCognition: false,
|
||||
SelectionReason: "must not replace a safe assertion with an unrelated candidate",
|
||||
}
|
||||
result = ApplyCognitiveCandidate(result, "основная база называется UPO", rejected, nil)
|
||||
selected := 0
|
||||
for _, candidate := range result.Candidates {
|
||||
if candidate.SelectionState == interaction.CandidateSelected {
|
||||
selected++
|
||||
if candidate.Source != interaction.CandidateSourceDeterministic {
|
||||
t.Fatalf("rejected cognitive candidate replaced deterministic selection: %#v", candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected != 1 {
|
||||
t.Fatalf("expected exactly one selected candidate after rejection, got %d", selected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,27 +73,44 @@ func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.Planning
|
||||
|
||||
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
|
||||
lower := strings.ToLower(input.Message)
|
||||
hasRecallHint := hasPlanningHint(input, capability.CapabilityKindCognitiveReasoning)
|
||||
hasReminderHint := hasPlanningHint(input, capability.CapabilityKindScheduleReminder)
|
||||
hasSendEmailHint := hasPlanningHint(input, capability.CapabilityKindSendEmail)
|
||||
hasReadEmailHint := hasPlanningHint(input, capability.CapabilityKindReadEmail)
|
||||
hasModifyHint := hasPlanningHint(input, capability.CapabilityKindModifyCode)
|
||||
hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject)
|
||||
hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs)
|
||||
hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact)
|
||||
rawTextFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition
|
||||
unsupportedCapabilityFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition
|
||||
if input.SpeechAct != work.SpeechActRef("") && input.SpeechAct != work.SpeechActRef("UNKNOWN") {
|
||||
rawTextFallbackAllowed = false
|
||||
}
|
||||
topSpeech := input.SpeechAct
|
||||
|
||||
switch {
|
||||
case input.RequiresClarification:
|
||||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."}
|
||||
case isReminderRequest(lower):
|
||||
case topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE"):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "explicit request-to-investigate should materialize durable work"}
|
||||
case hasReminderHint || (rawTextFallbackAllowed && isReminderRequest(lower)):
|
||||
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
|
||||
case isEmailReviewRequest(lower):
|
||||
case isEmailReviewRequest(lower) && hasReadEmailHint:
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"}
|
||||
case isExplicitEmailGapRequest(lower):
|
||||
case hasSendEmailHint || (rawTextFallbackAllowed && isExplicitEmailGapRequest(lower)):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
|
||||
case isUnsupportedCapabilityRequest(lower):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
|
||||
case needsDurableWorkFromHints(input):
|
||||
case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"}
|
||||
case requiresDurableWork(lower):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
|
||||
case containsAny(lower, "объясни", "расскажи", "что делает"):
|
||||
case unsupportedCapabilityFallbackAllowed && isUnsupportedCapabilityRequest(lower):
|
||||
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
|
||||
case rawTextFallbackAllowed && requiresDurableWork(lower):
|
||||
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"}
|
||||
case rawTextFallbackAllowed && containsAny(lower, "объясни", "расскажи", "что делает"):
|
||||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
|
||||
case input.NeedsCognition && needsCognitiveAnswer(input):
|
||||
case input.NeedsCognition && (topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE") || hasRecallHint):
|
||||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"}
|
||||
case input.NeedsCognition && (input.SpeechAct == work.SpeechActRef("QUESTION") || input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")):
|
||||
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant"}
|
||||
case topSpeech == work.SpeechActRef("QUESTION") || topSpeech == work.SpeechActRef("REQUEST_TO_INVESTIGATE"):
|
||||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "question-like request has no structured durable-work intent"}
|
||||
default:
|
||||
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
|
||||
}
|
||||
@@ -109,6 +126,15 @@ func needsDurableWorkFromHints(input work.PlanningInput) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hasPlanningHint(input work.PlanningInput, expected capability.CapabilityKind) bool {
|
||||
for _, hint := range input.CapabilityHints {
|
||||
if hint == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func needsCognitiveAnswer(input work.PlanningInput) bool {
|
||||
for _, hint := range input.CapabilityHints {
|
||||
if hint == capability.CapabilityKindCognitiveReasoning {
|
||||
@@ -152,14 +178,32 @@ func ValidatePlanCandidate(candidate work.PlanCandidate) error {
|
||||
|
||||
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
|
||||
lower := strings.ToLower(input.Message)
|
||||
hasReminderHint := hasPlanningHint(input, capability.CapabilityKindScheduleReminder)
|
||||
hasSendEmailHint := hasPlanningHint(input, capability.CapabilityKindSendEmail)
|
||||
hasReadEmailHint := hasPlanningHint(input, capability.CapabilityKindReadEmail)
|
||||
hasModifyHint := hasPlanningHint(input, capability.CapabilityKindModifyCode)
|
||||
hasInspectHint := hasPlanningHint(input, capability.CapabilityKindInspectProject)
|
||||
hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs)
|
||||
hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact)
|
||||
requiresInvestigation := input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")
|
||||
if input.SecondarySpeechAct == work.SpeechActRef("CORRECTION") {
|
||||
addStep(candidate, work.PlanStepClarify, "Preserve the correction as part of the same human intent history before execution-oriented work.", len(candidate.Steps)+1, millis)
|
||||
}
|
||||
switch {
|
||||
case isReminderRequest(lower):
|
||||
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
|
||||
case hasReminderHint:
|
||||
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "respecting a structured reminder intent", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepSchedule, "Interpret requested reminder horizon and context.", 1, millis)
|
||||
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
|
||||
case hasReadEmailHint:
|
||||
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
|
||||
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
|
||||
case hasSendEmailHint:
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
|
||||
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
|
||||
case isEmailReviewRequest(lower):
|
||||
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
|
||||
@@ -170,25 +214,29 @@ func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.Pla
|
||||
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
|
||||
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
|
||||
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
|
||||
case isReminderRequest(lower):
|
||||
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
|
||||
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
|
||||
case requiresInvestigation || hasInspectHint || hasQueryLogsHint || hasVerifyHint:
|
||||
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
|
||||
if containsAny(lower, "патч", "исправ") {
|
||||
if containsAny(lower, "патч", "исправ") || containsAny(lower, "обнов", "конфиг") {
|
||||
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software after investigation confirms the change", epistemics.VerificationUnknown, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindRunTests, "validate the patch after code modification", epistemics.VerificationNotApplicable, millis)
|
||||
}
|
||||
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
|
||||
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
|
||||
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
|
||||
if containsAny(lower, "патч", "исправ") {
|
||||
if containsAny(lower, "патч", "исправ") || containsAny(lower, "обнов", "конфиг") {
|
||||
addStep(candidate, work.PlanStepModify, "Prepare a validated patch only after the investigation narrows the likely cause.", 4, millis)
|
||||
addStep(candidate, work.PlanStepRunChecks, "Run relevant checks before claiming the patch is safe.", 5, millis)
|
||||
} else {
|
||||
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
|
||||
}
|
||||
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
|
||||
case hasModifyHint:
|
||||
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
|
||||
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
|
||||
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
|
||||
@@ -206,7 +254,6 @@ func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.Pla
|
||||
addDelegation(candidate, requirement, millis)
|
||||
}
|
||||
}
|
||||
|
||||
func requiresDurableWork(lower string) bool {
|
||||
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "патч", "исправь", "установи", "большую модель")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) {
|
||||
result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input := planningInputForTest("В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindModifyCode}
|
||||
result := planForTestWithInput(t, input)
|
||||
requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests)
|
||||
for _, delegation := range result.Candidate.Delegations {
|
||||
if delegation.ExecutorHint != "" {
|
||||
@@ -41,7 +43,9 @@ func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) {
|
||||
result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input := planningInputForTest("Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}
|
||||
result := planForTestWithInput(t, input)
|
||||
requireKinds(t, result, capability.CapabilityKindScheduleReminder)
|
||||
requireNoKind(t, result, capability.CapabilityKindModifyCode)
|
||||
}
|
||||
@@ -79,14 +83,18 @@ func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable))
|
||||
input := planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable)
|
||||
input.NeedsCognition = true
|
||||
path := DetermineHelpPath(input)
|
||||
if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan {
|
||||
t.Fatalf("simple help must not require durable planning: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
input := planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}
|
||||
path := DetermineHelpPath(input)
|
||||
if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder {
|
||||
t.Fatalf("reminder should be direct scheduling capability path: %#v", path)
|
||||
}
|
||||
@@ -99,6 +107,31 @@ func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathStructuredIntentNotOverriddenByRawRussianText(t *testing.T) {
|
||||
path := DetermineHelpPath(work.PlanningInput{
|
||||
ConversationID: "conversation-1",
|
||||
UtteranceID: "utterance-1",
|
||||
IntentID: "intent-1",
|
||||
ActorID: identity.ActorID("actor-1"),
|
||||
PrincipalID: identity.PrincipalID("principal-1"),
|
||||
ProjectID: work.ProjectRef("upo"),
|
||||
ProjectDisplayName: "УПО",
|
||||
ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"),
|
||||
Message: "Объясни, как добавить поддержку отправки писем в этом проекте.",
|
||||
SpeechAct: work.SpeechActRef("DIRECTIVE"),
|
||||
NeedsCognition: false,
|
||||
CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode},
|
||||
AssertionVerification: epistemics.VerificationNotApplicable,
|
||||
ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
|
||||
})
|
||||
if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableWork || !path.NeedsDurablePlan {
|
||||
t.Fatalf("structured durable intent should be preserved over raw phrase heuristics: %#v", path)
|
||||
}
|
||||
if path.Kind == work.HelpPathCognition {
|
||||
t.Fatalf("raw Russian phrase should not force cognition when selected interpretation is already directive: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
|
||||
path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable))
|
||||
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
|
||||
@@ -109,9 +142,33 @@ func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathStructuredHintsOverrideRawCognitionPhrase(t *testing.T) {
|
||||
input := planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable)
|
||||
input.NeedsCognition = false
|
||||
input.CapabilityHints = []capability.CapabilityKind{capability.CapabilityKindSendEmail}
|
||||
path := DetermineHelpPath(input)
|
||||
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
|
||||
t.Fatalf("structured send-email hint should force capability gap path: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpPathDoesNotUseUnsupportedCapabilityHeuristicAfterCognitiveDeferral(t *testing.T) {
|
||||
input := planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable)
|
||||
input.NeedsCognition = true
|
||||
path := DetermineHelpPath(input)
|
||||
if path.Kind == work.HelpPathCapabilityGap {
|
||||
t.Fatalf("raw unsupported-capability phrase must not override deferred cognitive interpretation: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult {
|
||||
t.Helper()
|
||||
result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0))
|
||||
return planForTestWithInput(t, planningInputForTest(message, act, verification))
|
||||
}
|
||||
|
||||
func planForTestWithInput(t *testing.T, input work.PlanningInput) work.PlanningResult {
|
||||
t.Helper()
|
||||
result, err := BuildPlanningResult(input, time.Unix(100, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user