220 lines
7.8 KiB
Go
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] + "..."
|
|
}
|