package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "git.cin.su/m/Assistant/adapters/models" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/platform/ids" ) const creatorAdvisoryCapabilityID capability.CapabilityID = "creator-advisory" type bindingResolver interface { Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) Observe(context.Context, capability.CognitiveUsageObservation) error RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) } type admissionClient interface { Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) Release(context.Context, capability.AdmissionLease) error } type providerRegistration struct { ProviderInstanceID capability.ProviderInstanceID Kind capability.ProviderKind EndpointRef capability.EndpointBindingRef CredentialRef capability.CredentialRef AuthMode capability.AuthMode BillingMode capability.BillingMode DataPolicy capability.DataPolicy Provider advisoryProvider Discovery models.LocalProvider } type cognitiveGateway struct { resolver bindingResolver providers map[capability.ProviderKind][]providerRegistration builtinLocal *providerRegistration externalRecovery []providerRegistration now func() time.Time allowCanary bool canaryMaxOutputBytes int admission admissionClient } func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) { var request cognitive.GatewayAdvisoryPolicyRequest if !decodeJSON(w, r, 8<<10, &request) || request.PrincipalID == "" { return } requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, MaxOutputBytes: 1} selected, err := g.resolver.Resolve(r.Context(), requirement) policy := cognitive.GatewayAdvisoryPolicy{} if err == nil && selected.Binding.DataPolicy != capability.DataPolicyLocalOnly { policy.ExternalEgress = true policy.AcknowledgementRequired = true policy.Disclosure = "Текст вопроса будет передан внешнему когнитивному провайдеру; секреты и внутренний контекст не передаются." } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(policy) } func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request) { var request cognitive.GatewayAdvisoryRequest if !decodeJSON(w, r, 80<<10, &request) { return } if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.Message == "" || request.MaxOutputBytes < 1 { http.Error(w, "invalid advisory request", http.StatusBadRequest) return } maxOutputBytes := request.MaxOutputBytes if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes { maxOutputBytes = g.canaryMaxOutputBytes } requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)} selected, err := g.resolver.Resolve(r.Context(), requirement) if err != nil { writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged { http.Error(w, "external egress acknowledgement required", http.StatusBadRequest) return } provider := g.provider(selected) if provider == nil { writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID} started := g.now() response, invokeErr := g.invokeAdmitted(r.Context(), string(request.RequestID), provider, invocation, maxOutputBytes) if invokeErr != nil { response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable} } g.observe(r.Context(), selected, response, started) if fallbackEligible(response.FinishReason) { requirement.ExcludedBindingIDs = append(requirement.ExcludedBindingIDs, selected.Binding.ID) fallback, resolveErr := g.resolver.Resolve(r.Context(), requirement) fallbackProvider := g.provider(fallback) if resolveErr == nil && fallbackProvider != nil && (fallback.Binding.DataPolicy == capability.DataPolicyLocalOnly || request.EgressAcknowledged) { fallbackRequest := invocation fallbackRequest.ProviderBindingID, fallbackRequest.ProviderBindingRevision = fallback.Binding.ID, fallback.Binding.Revision fallbackRequest.ProviderInstanceID, fallbackRequest.ModelDescriptorID, fallbackRequest.ProviderModelID = fallback.Provider.ID, fallback.Model.ID, fallback.Model.ProviderModelID fallbackStarted := g.now() fallbackResponse, fallbackErr := g.invokeAdmitted(r.Context(), string(request.RequestID)+":fallback", fallbackProvider, fallbackRequest, maxOutputBytes) if fallbackErr != nil { fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable} } g.observe(r.Context(), fallback, fallbackResponse, fallbackStarted) response = fallbackResponse } } if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) { writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes)) return } writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}) } func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) { var lease capability.AdmissionLease if g.admission != nil { decision, admittedLease, err := g.admission.Admit(ctx, leaseID, capability.AdmissionRequest{InputBytes: len(invocation.Message), MaxOutputBytes: maxOutputBytes, DeadlineMillis: 0}) if err != nil { return cognitive.AdvisoryResponse{}, err } if decision.State != capability.AdmissionAdmitted { return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil } lease = admittedLease defer func() { _ = g.admission.Release(ctx, lease) }() } return provider.Advise(ctx, invocation) } func selectionObjectiveForMessage(message string) capability.SelectionObjective { lower := strings.ToLower(message) if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") { return capability.SelectionEconomy } if strings.Contains(lower, "быстр") || strings.Contains(lower, "срочно") || strings.Contains(lower, "статус") || strings.Contains(lower, "quick") || strings.Contains(lower, "fast") { return capability.SelectionSpeed } if strings.Contains(lower, "сложн") || strings.Contains(lower, "анализ") || strings.Contains(lower, "код") || strings.Contains(lower, "план") || strings.Contains(lower, "глубок") || strings.Contains(lower, "reason") || strings.Contains(lower, "code") { return capability.SelectionQuality } return capability.SelectionBalanced } func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) capability.SelectionObjective { if meaning.CapabilityKind != "" || len(meaning.CapabilityHints) > 0 || meaning.SpeechAct != "" || meaning.RequestedOutcome != "" { kinds := append([]capability.CapabilityKind{meaning.CapabilityKind}, meaning.CapabilityHints...) for _, kind := range kinds { switch kind { case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs: return capability.SelectionQuality case capability.CapabilityKindScheduleReminder: return capability.SelectionSpeed } } if meaning.RequestedOutcome != "" || meaning.SpeechAct == "REQUEST_TO_INVESTIGATE" || meaning.SpeechAct == "DIRECTIVE" { return capability.SelectionQuality } if meaning.ConfidenceMillis > 0 { return capability.SelectionBalanced } } return selectionObjectiveForMessage(message) } func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse { text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности." switch request.Meaning.InformationRequestKind { case interaction.InformationRequestCognitiveModelInventory: text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны." case interaction.InformationRequestCapabilityInventory: text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap." case interaction.InformationRequestCognitiveChannelStatus: text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели." case interaction.InformationRequestCognitiveRecovery: text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг." default: switch request.Meaning.SpeechAct { case "REQUEST_TO_INVESTIGATE": text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг." case "QUESTION": text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально." } } return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted} } func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse { if g.builtinLocal != nil && g.builtinLocal.Provider != nil { invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"} started := g.now() response, err := g.builtinLocal.Provider.Advise(ctx, invocation) if response.RequestID == "" { response.RequestID = request.RequestID } if response.Version == "" { response.Version = request.Version } if err != nil { response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse} } g.observeBuiltin(ctx, g.builtinLocal, response, started) if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" { return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage} } } if request.EgressAcknowledged { if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok { return response } } return builtinAdvisory(request) } func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) { for _, candidate := range g.externalRecovery { if candidate.Provider == nil || candidate.Discovery == nil { continue } probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second) modelsFound, probe := candidate.Discovery.Discover(probeCtx) cancel() if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 { continue } model := modelsFound[0] invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID} response, err := candidate.Provider.Advise(ctx, invocation) if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" { return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true } } return cognitive.GatewayAdvisoryResponse{}, false } func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) { usage := response.Usage observationID, idErr := ids.New() if idErr == nil { observation := capability.CognitiveUsageObservation{ObservationID: observationID, RequestID: ids.ID(response.RequestID), BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ObservedAtUnixMillis: g.now().UnixMilli(), LatencyMillis: g.now().Sub(started).Milliseconds(), ResultClass: capability.CognitiveResultClass(response.FinishReason)} if usage != nil { observation.InputUnits, observation.OutputUnits = usage.InputUnits, usage.OutputUnits } if response.FinishReason != cognitive.FinishCompleted { observation.FailureType = capability.CognitiveFailureType(response.FinishReason) } if selected.Provider.Kind == "openai-compatible-chat-v1" || selected.Provider.Kind == "ollama-v1" { zero := int64(0) observation.ActualCostMicros = &zero } _ = g.resolver.Observe(ctx, observation) } } // observeBuiltin gives the always-available local provider the same evidence // path as a resolved binding. The identity is stable and contains references // only; no endpoint, credential, prompt, or response text is persisted here. func (g cognitiveGateway) observeBuiltin(ctx context.Context, registration *providerRegistration, response cognitive.AdvisoryResponse, started time.Time) { if registration == nil { return } selected := capability.ResolvedCognitiveBinding{ Binding: capability.CognitiveBinding{ID: "builtin-local-binding-v1", Revision: 1}, Provider: capability.ProviderInstance{ID: "builtin-local-lifeboat-v1", Revision: 1, Kind: registration.Kind}, Model: capability.ModelDescriptor{ID: "builtin-local-model-v1", Revision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ProviderModelID: "domovoy-lifeboat"}, } g.observe(ctx, selected, response, started) } func fallbackEligible(reason cognitive.FinishReason) bool { switch reason { case cognitive.FinishProviderUnavailable, cognitive.FinishModelUnavailable, cognitive.FinishTimeout, cognitive.FinishDegraded: return true } return false } func (g cognitiveGateway) provider(selected capability.ResolvedCognitiveBinding) advisoryProvider { for _, registration := range g.providers[selected.Provider.Kind] { if registration.EndpointRef == selected.Provider.EndpointRef && registration.CredentialRef == selected.Provider.CredentialRef { return registration.Provider } } return nil } func writeGatewayResponse(w http.ResponseWriter, response cognitive.GatewayAdvisoryResponse) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) } func decodeJSON(w http.ResponseWriter, r *http.Request, limit int64, destination any) bool { r.Body = http.MaxBytesReader(w, r.Body, limit) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(destination); err != nil { http.Error(w, "invalid request", http.StatusBadRequest) return false } if err := decoder.Decode(&struct{}{}); err != io.EOF { http.Error(w, "invalid request", http.StatusBadRequest) return false } return true } type httpBindingResolver struct { base *url.URL client *http.Client } func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolver, error) { base, err := url.Parse(raw) if err != nil || base.Scheme == "" || base.Host == "" { return nil, fmt.Errorf("invalid Core Runtime URL") } return &httpBindingResolver{base: base, client: client}, nil } func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) { var result capability.ResolvedCognitiveBinding err := r.post(ctx, "/v1/cognitive-bindings/resolve", value, &result) return result, err } func (r *httpBindingResolver) Observe(ctx context.Context, value capability.CognitiveUsageObservation) error { return r.post(ctx, "/v1/cognitive-usage-observations", value, nil) } func (r *httpBindingResolver) RegisterDiscovery(ctx context.Context, value capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) { var result capability.CognitiveDiscoveryRegistration err := r.post(ctx, "/v1/cognitive-registry/discoveries", value, &result) return result, err } func (r *httpBindingResolver) post(ctx context.Context, path string, value, destination any) error { payload, err := json.Marshal(value) if err != nil { return err } target := *r.base target.Path = path request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) if err != nil { return err } request.Header.Set("Content-Type", "application/json") response, err := r.client.Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { _, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096)) return fmt.Errorf("registry status %d", response.StatusCode) } if destination != nil { return json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(destination) } return nil }