Route model inventory by semantic intent #90

Merged
m merged 3 commits from codex/creator-chat-model-status-v1 into main 2026-08-21 23:31:32 +00:00
6 changed files with 179 additions and 13 deletions
+83
View File
@@ -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{"локальн", "модель", "когнитивн", "канал мышления", "проверь канал", "настройк"}
+33
View File
@@ -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"}`))
+2 -2
View File
@@ -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"]
}
+18
View File
@@ -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 {
+17 -11
View File
@@ -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)