sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"ai-operator/internal/dialogue/state"
|
||||
"ai-operator/internal/tools"
|
||||
)
|
||||
|
||||
type Decision struct {
|
||||
Allowed bool
|
||||
ToolName string
|
||||
State state.ConversationState
|
||||
ReasonCode string
|
||||
UserMessageKey string
|
||||
RequiredNextAction string
|
||||
}
|
||||
|
||||
func AuthorizeTool(session state.ConversationSession, toolName string) Decision {
|
||||
d := Decision{ToolName: toolName, State: session.State, ReasonCode: "ok"}
|
||||
if !tools.Known(toolName) {
|
||||
return deny(d, "unknown_tool", "denied.not_ready", "none")
|
||||
}
|
||||
if session.State == state.StateEnded {
|
||||
return deny(d, "call_ended", "closing.started", "none")
|
||||
}
|
||||
if toolName == tools.SearchKnowledgeBase {
|
||||
return authorizeSearch(session, d)
|
||||
}
|
||||
allowed := allowedByState(session.State, toolName)
|
||||
if !allowed {
|
||||
return deny(d, "tool_not_allowed_in_state", "denied.not_ready", nextAction(session))
|
||||
}
|
||||
d.Allowed = true
|
||||
return d
|
||||
}
|
||||
|
||||
func authorizeSearch(session state.ConversationSession, d Decision) Decision {
|
||||
if session.State == state.StateEnded || session.State == state.StateClosing || session.State == state.StateHandoff {
|
||||
return deny(d, "state_not_ready", "denied.not_ready", nextAction(session))
|
||||
}
|
||||
d.Allowed = true
|
||||
return d
|
||||
}
|
||||
|
||||
func allowedByState(s state.ConversationState, tool string) bool {
|
||||
switch s {
|
||||
case state.StateLanguageSelection:
|
||||
return tool == tools.SearchKnowledgeBase || tool == tools.SetLanguage || tool == tools.SetRegion || tool == tools.RequestHumanHandoff || tool == tools.EndCall
|
||||
case state.StateRegionSelection:
|
||||
return tool == tools.SearchKnowledgeBase || tool == tools.SetRegion || tool == tools.SetLanguage || tool == tools.RequestHumanHandoff || tool == tools.EndCall
|
||||
case state.StateReadyToHelp:
|
||||
return tool == tools.SearchKnowledgeBase || tool == tools.SetLanguage || tool == tools.SetRegion || tool == tools.RequestHumanHandoff || tool == tools.EndCall
|
||||
case state.StateQuestionAnswering:
|
||||
return tool == tools.SearchKnowledgeBase || tool == tools.SetLanguage || tool == tools.SetRegion || tool == tools.RequestHumanHandoff || tool == tools.EndCall
|
||||
case state.StateHandoff:
|
||||
return tool == tools.RequestHumanHandoff || tool == tools.EndCall
|
||||
case state.StateClosing:
|
||||
return tool == tools.EndCall
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deny(d Decision, reason, key, action string) Decision {
|
||||
d.Allowed = false
|
||||
d.ReasonCode = reason
|
||||
d.UserMessageKey = key
|
||||
d.RequiredNextAction = action
|
||||
return d
|
||||
}
|
||||
|
||||
func nextAction(session state.ConversationSession) string {
|
||||
if session.Language != state.LanguageRU && session.Language != state.LanguageKK {
|
||||
return "infer_language"
|
||||
}
|
||||
if session.Region.Status != state.RegionSelected || session.Region.Code == "" {
|
||||
return "use_global_or_ask_region_if_needed"
|
||||
}
|
||||
return "wait_for_question"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"ai-operator/internal/dialogue/state"
|
||||
"ai-operator/internal/tools"
|
||||
)
|
||||
|
||||
func TestToolPolicyAndGuardrail(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
session state.ConversationSession
|
||||
tool string
|
||||
allowed bool
|
||||
reason string
|
||||
}{
|
||||
{"search call started", state.ConversationSession{State: state.StateCallStarted}, tools.SearchKnowledgeBase, true, "ok"},
|
||||
{"search language selection", state.ConversationSession{State: state.StateLanguageSelection}, tools.SearchKnowledgeBase, true, "ok"},
|
||||
{"set language allowed", state.ConversationSession{State: state.StateLanguageSelection}, tools.SetLanguage, true, "ok"},
|
||||
{"search region selection", state.ConversationSession{State: state.StateRegionSelection, Language: state.LanguageRU}, tools.SearchKnowledgeBase, true, "ok"},
|
||||
{"set region allowed", state.ConversationSession{State: state.StateRegionSelection, Language: state.LanguageRU}, tools.SetRegion, true, "ok"},
|
||||
{"search ready", state.ConversationSession{State: state.StateReadyToHelp, Language: state.LanguageRU, Region: state.RegionSelection{Code: "global", Status: state.RegionSelected}}, tools.SearchKnowledgeBase, true, "ok"},
|
||||
{"search ended", state.ConversationSession{State: state.StateEnded, Language: state.LanguageRU, Region: state.RegionSelection{Code: "global", Status: state.RegionSelected}}, tools.SearchKnowledgeBase, false, "call_ended"},
|
||||
{"unknown", state.ConversationSession{State: state.StateReadyToHelp}, "bad_tool", false, "unknown_tool"},
|
||||
{"handoff non-ended", state.ConversationSession{State: state.StateRegionSelection}, tools.RequestHumanHandoff, true, "ok"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := AuthorizeTool(tc.session, tc.tool)
|
||||
if got.Allowed != tc.allowed || got.ReasonCode != tc.reason {
|
||||
t.Fatalf("decision=%+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHardGuardrail(t *testing.T) {
|
||||
base := state.ConversationSession{State: state.StateReadyToHelp}
|
||||
if !AuthorizeTool(base, tools.SearchKnowledgeBase).Allowed {
|
||||
t.Fatal("search denied without explicit language")
|
||||
}
|
||||
base.Language = state.LanguageRU
|
||||
if !AuthorizeTool(base, tools.SearchKnowledgeBase).Allowed {
|
||||
t.Fatal("search denied without region")
|
||||
}
|
||||
base.Region = state.RegionSelection{Code: "almaty_city", Status: state.RegionSelected}
|
||||
if !AuthorizeTool(base, tools.SearchKnowledgeBase).Allowed {
|
||||
t.Fatal("search denied after language and region")
|
||||
}
|
||||
base.State = state.StateQuestionAnswering
|
||||
if !AuthorizeTool(base, tools.SearchKnowledgeBase).Allowed {
|
||||
t.Fatal("search denied in question answering")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user