Files
Assistant/cmd/cognitive-worker/interpretation_live_trace_test.go
m ce666bbc43
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
DEV-000072 semantic interpretation deferral boundary
2026-08-17 23:02:00 +03:00

220 lines
7.8 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"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/contracts/situation"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestLiveInterpretationTrace(t *testing.T) {
if os.Getenv("ASSISTANT_LIVE_SEMANTIC_TRACE") != "1" {
t.Skip("set ASSISTANT_LIVE_SEMANTIC_TRACE=1 for live semantic/gateway/raw trace")
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
rawBase := envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1")
rawModelID, err := firstRawModel(ctx, rawBase)
if err != nil {
t.Fatal(err)
}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(rawBase, nil)
if err != nil {
t.Fatal(err)
}
discovered, probe := cpuAdapter.Discover(ctx)
if probe.Health != capability.HealthAvailable || len(discovered) == 0 {
t.Fatalf("cpu discovery unavailable: probe=%+v models=%+v", probe, discovered)
}
selected := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{
ID: "trace-binding",
Revision: 1,
CapabilityID: "creator-advisory",
OwnerPrincipalID: "domovoy-creator-principal",
DataPolicy: capability.DataPolicyLocalOnly,
},
Provider: capability.ProviderInstance{
ID: "local-cpu-primary",
Revision: 1,
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
},
Model: capability.ModelDescriptor{
ID: capability.ModelDescriptorID("trace-model"),
Revision: 1,
ProviderModelID: discovered[0].ProviderModelID,
},
}
gateway := cognitiveGateway{
resolver: traceResolver{selected: selected},
providers: map[capability.ProviderKind][]providerRegistration{
"openai-compatible-chat-v1": {{
ProviderInstanceID: "local-cpu-primary",
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
Provider: cpuAdapter,
}},
},
now: time.Now,
allowCanary: true,
}
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
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 {
t.Fatal(err)
}
semantic, err := interactiondomain.InterpretUtterance(utterance, []situation.ProjectDescriptor{{ID: "assistant", DisplayName: "Assistant"}}, nil, time.Now())
if err != nil {
t.Fatal(err)
}
rawResponse, err := rawCompletion(ctx, rawBase, rawModelID, message)
if err != nil {
t.Fatal(err)
}
request := cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "trace-request",
ActorID: identity.ActorID("trace-actor"),
PrincipalID: identity.PrincipalID("domovoy-creator-principal"),
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: message,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
httpRequest := httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload))
gateway.interpretationHandler(recorder, httpRequest.WithContext(ctx))
if recorder.Code != http.StatusOK {
t.Fatalf("message=%q gateway status=%d body=%s", message, recorder.Code, recorder.Body.String())
}
var gatewayResponse cognitive.GatewayInterpretationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &gatewayResponse); err != nil {
t.Fatal(err)
}
if gatewayResponse.FinishReason != cognitive.FinishCompleted {
t.Fatalf("message=%q gateway finish=%s body=%s", message, gatewayResponse.FinishReason, recorder.Body.String())
}
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)
}
}
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)
}
}
type traceResolver struct {
selected capability.ResolvedCognitiveBinding
}
func (r traceResolver) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
return r.selected, nil
}
func (r traceResolver) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (r traceResolver) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
func firstRawModel(ctx context.Context, base string) (string, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/models", nil)
if err != nil {
return "", err
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&payload); err != nil {
return "", err
}
if len(payload.Data) == 0 || payload.Data[0].ID == "" {
return "", io.EOF
}
return payload.Data[0].ID, nil
}
func rawCompletion(ctx context.Context, base, model, message string) (string, error) {
payload, err := json.Marshal(map[string]any{
"model": model,
"max_tokens": 128,
"messages": []map[string]string{
{"role": "user", "content": message},
},
"stream": false,
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/chat/completions", bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", io.EOF
}
return result.Choices[0].Message.Content, nil
}
func truncate(value string, max int) string {
if len(value) <= max {
return value
}
return value[:max] + "..."
}