first commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
type AIProviderClient interface {
|
||||
Invoke(provider domain.AIProvider, request domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error)
|
||||
}
|
||||
|
||||
type MockAIProviderClient struct{}
|
||||
|
||||
func (MockAIProviderClient) Invoke(provider domain.AIProvider, request domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error) {
|
||||
model := request.Model
|
||||
if model == "" {
|
||||
model = provider.DefaultModel
|
||||
}
|
||||
if model == "" && len(provider.Models) > 0 {
|
||||
model = provider.Models[0]
|
||||
}
|
||||
recommendation := "Mock AI recommendation for " + request.Purpose + ": review the proposed change before dispatch."
|
||||
result := domain.AIProviderInvocationResult{
|
||||
Recommendation: recommendation,
|
||||
Usage: domain.AIInvocationUsage{
|
||||
ProviderID: provider.ID,
|
||||
Model: model,
|
||||
InputTokens: boundedTokenEstimate(request.Prompt + request.CurrentConfig),
|
||||
OutputTokens: boundedTokenEstimate(recommendation),
|
||||
Mocked: true,
|
||||
},
|
||||
}
|
||||
if request.Purpose == "config.suggest" || request.Purpose == "config.generate" {
|
||||
result.SuggestedConfig = buildSuggestedConfig(request.CurrentConfig, request.Prompt)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) InvokeAIForSession(sessionID string, request domain.AIInvocationRequest) (domain.AIInvocationResponse, error) {
|
||||
request = domain.CopyAIInvocationRequest(request)
|
||||
if err := validator.ValidateAIInvocationRequest(request); err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
if _, err := svc.GetCurrentUser(sessionID); err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
if request.ServerInstanceID != "" {
|
||||
instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
if request.PluginID != "" && instance.PluginID != request.PluginID {
|
||||
return safeAIDenial(request, "plugin scope does not match server instance"), nil
|
||||
}
|
||||
}
|
||||
if request.PluginID != "" {
|
||||
plugin, err := svc.store.GamePlugins().Get(request.PluginID)
|
||||
if err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
authorization, err := validator.AuthorizePluginBridgeAction(plugin, domain.PluginBridgeAuthorizeRequest{
|
||||
PluginID: request.PluginID,
|
||||
RouteKey: request.RouteKey,
|
||||
ServerInstanceID: request.ServerInstanceID,
|
||||
Action: domain.PluginBridgeActionAIInvoke,
|
||||
AIPurpose: request.Purpose,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
if !authorization.Allowed {
|
||||
return safeAIDenial(request, authorization.Reason), nil
|
||||
}
|
||||
}
|
||||
provider, err := svc.selectAIProvider(request.ProviderID)
|
||||
if err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
result, err := svc.aiProviderClient.Invoke(provider, request)
|
||||
if err != nil {
|
||||
return domain.CopyAIInvocationResponse(domain.AIInvocationResponse{
|
||||
RequestID: request.RequestID,
|
||||
Purpose: request.Purpose,
|
||||
ProviderID: provider.ID,
|
||||
Model: safeModel(request.Model, provider),
|
||||
Status: "error",
|
||||
Usage: domain.AIInvocationUsage{ProviderID: provider.ID, Model: safeModel(request.Model, provider), Mocked: true},
|
||||
Error: &domain.AIInvocationSafeError{Code: "provider_failed", Message: safeBridgeReason(err.Error())},
|
||||
}), nil
|
||||
}
|
||||
response := domain.AIInvocationResponse{
|
||||
RequestID: request.RequestID,
|
||||
Purpose: request.Purpose,
|
||||
ProviderID: provider.ID,
|
||||
Model: result.Usage.Model,
|
||||
Status: "ok",
|
||||
Recommendation: result.Recommendation,
|
||||
Usage: result.Usage,
|
||||
}
|
||||
if result.SuggestedConfig != "" {
|
||||
response.ConfigRecommendation = &domain.AIConfigRecommendation{Key: "server.properties", SuggestedConfig: result.SuggestedConfig, DiffSummary: "review required before config write dispatch"}
|
||||
}
|
||||
if err := validator.ValidateAIInvocationResponse(response); err != nil {
|
||||
return domain.AIInvocationResponse{}, err
|
||||
}
|
||||
return domain.CopyAIInvocationResponse(response), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) selectAIProvider(id string) (domain.AIProvider, error) {
|
||||
if id != "" {
|
||||
provider, err := svc.store.AIProviders().Get(id)
|
||||
if err != nil {
|
||||
return domain.AIProvider{}, err
|
||||
}
|
||||
if provider.Status != domain.AIProviderStatusActive {
|
||||
return domain.AIProvider{}, ErrForbidden
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
providers, err := svc.store.AIProviders().List(domain.AIProviderFilter{Status: domain.AIProviderStatusActive})
|
||||
if err != nil {
|
||||
return domain.AIProvider{}, err
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
return domain.AIProvider{}, repo.ErrNotFound
|
||||
}
|
||||
return providers[0], nil
|
||||
}
|
||||
|
||||
func safeAIDenial(request domain.AIInvocationRequest, reason string) domain.AIInvocationResponse {
|
||||
return domain.CopyAIInvocationResponse(domain.AIInvocationResponse{
|
||||
RequestID: request.RequestID,
|
||||
Purpose: request.Purpose,
|
||||
Status: "denied",
|
||||
Error: &domain.AIInvocationSafeError{Code: "permission_denied", Message: safeBridgeReason(reason)},
|
||||
})
|
||||
}
|
||||
|
||||
func safeModel(model string, provider domain.AIProvider) string {
|
||||
if strings.TrimSpace(model) != "" {
|
||||
return model
|
||||
}
|
||||
if provider.DefaultModel != "" {
|
||||
return provider.DefaultModel
|
||||
}
|
||||
if len(provider.Models) > 0 {
|
||||
return provider.Models[0]
|
||||
}
|
||||
return "mock-model"
|
||||
}
|
||||
|
||||
func buildSuggestedConfig(currentConfig string, prompt string) string {
|
||||
base := strings.TrimRight(currentConfig, "\n")
|
||||
if base == "" {
|
||||
base = "# generated server config"
|
||||
}
|
||||
if strings.Contains(strings.ToLower(prompt), "pvp") && !strings.Contains(base, "pvp=") {
|
||||
base += "\npvp=false"
|
||||
}
|
||||
return base + "\n# ai.recommendation=review-required\n"
|
||||
}
|
||||
|
||||
func boundedTokenEstimate(value string) int {
|
||||
count := len([]rune(value)) / 4
|
||||
if count < 1 {
|
||||
return 1
|
||||
}
|
||||
if count > 4096 {
|
||||
return 4096
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
type failingAIProviderClient struct{ err error }
|
||||
|
||||
func (client failingAIProviderClient) Invoke(domain.AIProvider, domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error) {
|
||||
if client.err == nil {
|
||||
return domain.AIProviderInvocationResult{}, errors.New("provider failed")
|
||||
}
|
||||
return domain.AIProviderInvocationResult{}, client.err
|
||||
}
|
||||
Reference in New Issue
Block a user