internal/llm/capability_decision_client.go
Rama main · réplica de lectura del repositorio original.
package llm
import (
"context"
"errors"
"strings"
"time"
"github.com/yeomyeonggeori/blueprotocol/model"
)
type CapabilityDecisionClient struct {
CapabilityLLMClient
}
type capabilityDecisionRequestDocument struct {
Model string `json:"model,omitempty"`
State any `json:"state"`
Questions map[string]model.DecisionQuestion `json:"questions"`
SessionID string `json:"sessionID,omitempty"`
Context *RequestContext `json:"context,omitempty"`
}
type capabilityDecisionResponseDocument struct {
Answers map[string]model.DecisionAnswer `json:"answers"`
Usage capabilityDecisionUsageDocument `json:"usage"`
ModelName string `json:"modelName"`
ProviderName string `json:"providerName"`
UpstreamProvider string `json:"upstreamProvider"`
LatencyMS int64 `json:"latencyMs"`
reportedWireExchange
}
func (decisionClient CapabilityDecisionClient) Decide(responseContext context.Context, request model.DecisionRequest) (model.DecisionResponse, error) {
if decisionClient.CapabilityClient.HTTPClient == nil {
return model.DecisionResponse{}, errors.New("capability decision http client is not configured")
}
if len(request.Questions) == 0 {
return model.DecisionResponse{}, errors.New("a decision call carries no question")
}
wireState, errorValue := request.WireState()
if errorValue != nil {
return model.DecisionResponse{}, errorValue
}
requestDocument := capabilityDecisionRequestDocument{
Model: firstNonEmpty(request.Model, decisionClient.ModelName),
State: wireState,
Questions: request.Questions,
SessionID: request.SessionID,
Context: requestContextPointer(responseContext),
}
startedAt := time.Now()
var responseDocument capabilityDecisionResponseDocument
if errorValue := decisionClient.postJSONWithRetry(responseContext, "/v1/llm/decide", requestDocument, &responseDocument); errorValue != nil {
return model.DecisionResponse{}, errorValue
}
if len(responseDocument.Answers) == 0 {
return model.DecisionResponse{}, errors.New("the decision route answered no question")
}
responseDocument.record(responseContext)
return model.DecisionResponse{
Answers: typedDecisionAnswers(responseDocument.Answers, request.Questions),
Usage: decisionUsage(responseDocument.Usage),
ModelName: firstNonEmpty(responseDocument.ModelName, decisionClient.ModelName),
ProviderName: firstNonEmpty(responseDocument.ProviderName, "capabilityLLM"),
UpstreamProvider: responseDocument.UpstreamProvider,
LatencyMS: decisionLatency(responseDocument.LatencyMS, startedAt),
}, nil
}
func typedDecisionAnswers(answers map[string]model.DecisionAnswer, questions map[string]model.DecisionQuestion) map[string]model.DecisionAnswer {
typedAnswers := make(map[string]model.DecisionAnswer, len(answers))
for questionName, answer := range answers {
if answer.Type == "" {
answer.Type = questions[questionName].Type
}
answer.Choice = strings.TrimSpace(answer.Choice)
typedAnswers[questionName] = answer
}
return typedAnswers
}
type capabilityDecisionUsageDocument struct {
InputTokens int64 `json:"inputTokens"`
OutputTokens int64 `json:"outputTokens"`
CostUSD float64 `json:"costUSD"`
}
func decisionUsage(usage capabilityDecisionUsageDocument) model.Usage {
return model.Usage{
PromptTokens: usage.InputTokens,
CompletionTokens: usage.OutputTokens,
TotalTokens: usage.InputTokens + usage.OutputTokens,
CostUSD: usage.CostUSD,
}
}
func decisionLatency(reportedLatencyMS int64, startedAt time.Time) int64 {
if reportedLatencyMS > 0 {
return reportedLatencyMS
}
return time.Since(startedAt).Milliseconds()
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmedValue := strings.TrimSpace(value); trimmedValue != "" {
return trimmedValue
}
}
return ""
}