Route model inventory by semantic intent #90
@@ -394,6 +394,11 @@ func main() {
|
||||
}
|
||||
needsCognition = false
|
||||
}
|
||||
if isModelInventoryQuestion(semantic) {
|
||||
candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID)
|
||||
planningReply = humanCognitiveModelInventory(candidates, inventoryErr)
|
||||
needsCognition = false
|
||||
}
|
||||
if isCognitiveChannelQuestion(utterance.Content) {
|
||||
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
|
||||
planningReply = humanCognitiveChannelReply(utterance.Content, health, healthErr)
|
||||
@@ -1153,6 +1158,84 @@ func isCapabilityQuestion(message string) bool {
|
||||
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
|
||||
}
|
||||
|
||||
func isModelInventoryQuestion(result interactiondomain.SemanticResult) bool {
|
||||
return result.InformationRequestKind == interactiondomain.InformationRequestCognitiveModelInventory
|
||||
}
|
||||
|
||||
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
|
||||
if inventoryErr != nil {
|
||||
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
|
||||
}
|
||||
lines := make([]string, 0, len(candidates)+1)
|
||||
lines = append(lines, "Подключённые модели из реестра:")
|
||||
for _, candidate := range candidates {
|
||||
modelName := string(candidate.Model.ProviderModelID)
|
||||
if modelName == "" {
|
||||
modelName = "имя модели не подтверждено"
|
||||
}
|
||||
status := cognitiveInventoryStatus(candidate)
|
||||
pricing := cognitiveInventoryPricing(candidate.Provider)
|
||||
details := []string{status, "тип " + string(candidate.Provider.Kind), "политика " + string(candidate.Binding.DataPolicy)}
|
||||
if pricing != "" {
|
||||
details = append(details, pricing)
|
||||
}
|
||||
if candidate.Provider.AuthMode != "" && candidate.Provider.AuthMode != capability.AuthNone {
|
||||
details = append(details, "доступ "+humanAuthMode(candidate.Provider.AuthMode))
|
||||
}
|
||||
lines = append(lines, "- "+modelName+" ("+strings.Join(details, "; ")+")")
|
||||
}
|
||||
lines = append(lines, "Секреты, ключи и пароли намеренно не показываются.")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func cognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
|
||||
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
|
||||
if candidate.Binding.Activation == capability.ActivationActive {
|
||||
return "активна"
|
||||
}
|
||||
return "доступна, не активирована для автоматического выбора"
|
||||
}
|
||||
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
|
||||
return "работает с ограничениями"
|
||||
}
|
||||
return "недоступна или не подтверждена"
|
||||
}
|
||||
|
||||
func cognitiveInventoryPricing(provider capability.ProviderInstance) string {
|
||||
switch provider.BillingMode {
|
||||
case capability.BillingFree:
|
||||
return "бесплатная"
|
||||
case capability.BillingPerInput:
|
||||
return "платная: стоимость входа указана в реестре"
|
||||
case capability.BillingPerOutput:
|
||||
return "платная: стоимость выхода указана в реестре"
|
||||
case capability.BillingPerRequest:
|
||||
return "платная: стоимость запроса указана в реестре"
|
||||
default:
|
||||
return "стоимость не подтверждена"
|
||||
}
|
||||
}
|
||||
|
||||
func humanAuthMode(mode capability.AuthMode) string {
|
||||
switch mode {
|
||||
case capability.AuthKey:
|
||||
return "API-ключ"
|
||||
case capability.AuthBearer:
|
||||
return "Bearer-токен"
|
||||
case capability.AuthUsernamePassword:
|
||||
return "логин и пароль"
|
||||
case capability.AuthOAuth:
|
||||
return "OAuth"
|
||||
case capability.AuthSecretFile:
|
||||
return "защищённый файл секрета"
|
||||
default:
|
||||
return "защищённый способ"
|
||||
}
|
||||
}
|
||||
|
||||
func isCognitiveChannelQuestion(message string) bool {
|
||||
lower := strings.ToLower(message)
|
||||
markers := []string{"локальн", "модель", "когнитивн", "канал мышления", "проверь канал", "настройк"}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
@@ -49,6 +51,37 @@ func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
|
||||
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
|
||||
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("interpretation failed for %q: %v", message, err)
|
||||
}
|
||||
if !isModelInventoryQuestion(result) {
|
||||
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
|
||||
}
|
||||
}
|
||||
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isModelInventoryQuestion(ordinary) {
|
||||
t.Fatal("configuration mutation must not use read-only model inventory path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
|
||||
cost := int64(25)
|
||||
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
|
||||
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
|
||||
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
|
||||
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
|
||||
}}, nil)
|
||||
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
|
||||
t.Fatalf("unsafe or incomplete inventory: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
||||
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000091-builtin-local-chat-routing.json",
|
||||
"task_id": "DEV-000091",
|
||||
"task_file": "dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json",
|
||||
"task_id": "DEV-000092",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"task_id": "DEV-000092",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "2306c8d6b31fbf5ac0933ecc72b4f76bec7218f2",
|
||||
"risk_tier": "E",
|
||||
"intent": "Answer semantically identified model and external-AI inventory requests from the capability registry through a deterministic read-only chat fast path instead of treating them as unavailable cognition.",
|
||||
"requirement_ids": ["Model inventory is readable from chat", "No secret material is shown", "Inventory does not require model reasoning"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["core-runtime", "interaction", "development-governance"],
|
||||
"paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/main_test.go", "modules/interaction/domain/chat.go", "modules/interaction/domain/chat_test.go", "modules/interaction/domain/semantics.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json"]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["capability owner read-only LoadCognitiveCandidates port"], "may_change": ["human-facing chat fast-path text"]},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["model inventory question -> read-only capability registry query"],
|
||||
"side_effects_and_privileges": ["No mutation", "CredentialRef and endpoint secret material are never rendered"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Registry read failure produces an honest unavailable-settings explanation", "No cognitive retry is required"],
|
||||
"assumptions": ["CapabilityRepository.LoadCognitiveCandidates returns provider-neutral metadata and opaque secret references"],
|
||||
"acceptance_scenarios": ["Какие модели доступны? returns the registered inventory", "Напиши список настроек ИИ returns safe settings", "Empty or unavailable registry does not expose credentials or internal identifiers"],
|
||||
"required_tests": ["go test ./cmd/core-runtime", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -194,6 +194,9 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
|
||||
if hasQuestionMark {
|
||||
scores[interaction.SpeechActQuestion]++
|
||||
}
|
||||
if containsMeaningfulInterrogative(tokens) {
|
||||
scores[interaction.SpeechActQuestion] += 2
|
||||
}
|
||||
if explicitCorrection {
|
||||
scores[interaction.SpeechActCorrection] += 6
|
||||
}
|
||||
@@ -432,6 +435,9 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
||||
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") {
|
||||
add("VERIFY_FACT")
|
||||
}
|
||||
if containsStemInTokens(tokens, "модел", "ии") && containsStemInTokens(tokens, "настро", "список", "доступ", "подключ") {
|
||||
add("VERIFY_FACT")
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
for kind := range seen {
|
||||
values = append(values, kind)
|
||||
@@ -439,6 +445,18 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
||||
return values
|
||||
}
|
||||
|
||||
func informationRequestKind(message string) InformationRequestKind {
|
||||
tokens := semanticTokens(message)
|
||||
if len(tokens) == 0 || containsStem(tokens[0], "настро") || !containsStemInTokens(tokens, "модел", "ии") {
|
||||
return ""
|
||||
}
|
||||
inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро"))
|
||||
if !inventoryCue {
|
||||
return ""
|
||||
}
|
||||
return InformationRequestCognitiveModelInventory
|
||||
}
|
||||
|
||||
func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 {
|
||||
total := 0
|
||||
for _, score := range scores {
|
||||
|
||||
@@ -11,16 +11,21 @@ import (
|
||||
)
|
||||
|
||||
type SemanticResult struct {
|
||||
Interpretation interaction.UtteranceInterpretation
|
||||
Candidates []interaction.SemanticCandidate
|
||||
Projects []interaction.ProjectReference
|
||||
Relations []interaction.InteractionRelation
|
||||
ActiveFocus *situation.ActiveProjectFocus
|
||||
Reply string
|
||||
NeedsCognition bool
|
||||
CapabilityHints []string
|
||||
Interpretation interaction.UtteranceInterpretation
|
||||
Candidates []interaction.SemanticCandidate
|
||||
Projects []interaction.ProjectReference
|
||||
Relations []interaction.InteractionRelation
|
||||
ActiveFocus *situation.ActiveProjectFocus
|
||||
Reply string
|
||||
NeedsCognition bool
|
||||
CapabilityHints []string
|
||||
InformationRequestKind InformationRequestKind
|
||||
}
|
||||
|
||||
type InformationRequestKind string
|
||||
|
||||
const InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
|
||||
|
||||
func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) {
|
||||
interpretationID, err := ids.New()
|
||||
if err != nil {
|
||||
@@ -46,9 +51,10 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
|
||||
SelectionReason: selectedCandidate.SelectionReason,
|
||||
RecordedAtUnixMillis: millis,
|
||||
},
|
||||
Candidates: []interaction.SemanticCandidate{deterministicCandidate},
|
||||
NeedsCognition: selectedCandidate.NeedsCognition,
|
||||
CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...),
|
||||
Candidates: []interaction.SemanticCandidate{deterministicCandidate},
|
||||
NeedsCognition: selectedCandidate.NeedsCognition,
|
||||
CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...),
|
||||
InformationRequestKind: informationRequestKind(utterance.Content),
|
||||
}
|
||||
explicitFocus := explicitProjectFocus(utterance.Content, projects)
|
||||
projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)
|
||||
|
||||
Reference in New Issue
Block a user