Files
dobotai-platform/internal/reasoningmode/service.go
T

155 lines
4.9 KiB
Go

package reasoningmode
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
const defaultIdleTimeout = 10 * time.Minute
type Scope struct {
TenantKey string
Channel string
ExternalActorID string
}
type TurnCompletion struct {
Scope Scope
TurnID string
Execution ExecutionMode
Answer string
Locale string
Expired bool
At time.Time
Idle time.Duration
}
type Repository interface {
ResolveState(context.Context, Scope, time.Time) (State, bool, error)
Enable(context.Context, Scope, time.Time, time.Duration) (State, error)
Disable(context.Context, Scope, time.Time) (State, bool, error)
SetMaxRounds(context.Context, Scope, int, time.Time) (State, error)
SetHintEnabled(context.Context, Scope, bool, time.Time) (State, error)
CompleteTurn(context.Context, TurnCompletion) (Completion, error)
}
type Decision struct {
Handled bool
Reply string
Query string
AgentID string
Execution ExecutionMode
Expired bool
}
type Service struct {
repo Repository
quickAgentID string
reasoningAgentID string
now func() time.Time
idleTimeout time.Duration
}
func NewService(repo Repository, quickAgentID, reasoningAgentID string) (*Service, error) {
quickAgentID = strings.TrimSpace(quickAgentID)
reasoningAgentID = strings.TrimSpace(reasoningAgentID)
if repo == nil {
return nil, errors.New("reasoning mode repository is required")
}
if quickAgentID == "" || reasoningAgentID == "" {
return nil, errors.New("quick and reasoning agent IDs are required")
}
if quickAgentID == reasoningAgentID {
return nil, errors.New("quick and reasoning agent IDs must differ")
}
return &Service{
repo: repo,
quickAgentID: quickAgentID,
reasoningAgentID: reasoningAgentID,
now: time.Now,
idleTimeout: defaultIdleTimeout,
}, nil
}
func (s *Service) Prepare(ctx context.Context, scope Scope, text string) (Decision, error) {
if err := validateScope(scope); err != nil {
return Decision{}, err
}
query := strings.TrimSpace(text)
if query == "" {
return Decision{}, errors.New("reasoning mode query is required")
}
if command, ok := ParseCommand(query); ok {
switch command.Kind {
case CommandEnable:
state, err := s.repo.Enable(ctx, scope, s.now(), s.idleTimeout)
if err != nil {
return Decision{}, err
}
return Decision{Handled: true, Reply: fmt.Sprintf(
"✅ 已开启【智能推理模式】(计费标准更高)\n持续模式将在%d轮对话后自动关闭,也可随时输入 /think off 手动关闭",
state.MaxRounds,
)}, nil
case CommandDisable:
_, wasReasoning, err := s.repo.Disable(ctx, scope, s.now())
if err != nil {
return Decision{}, err
}
if !wasReasoning {
return Decision{Handled: true, Reply: "当前未开启推理模式,默认是快速问答模式。"}, nil
}
return Decision{Handled: true, Reply: "✅ 已关闭智能推理模式,切回【快速问答模式】"}, nil
case CommandSingle:
return Decision{Query: command.Query, AgentID: s.reasoningAgentID, Execution: ExecutionReasoningSingle}, nil
case CommandHelp:
return Decision{Handled: true, Reply: helpReply()}, nil
default:
return Decision{Handled: true, Reply: "用法:/think <问题>、/think on、/think off。"}, nil
}
}
state, expired, err := s.repo.ResolveState(ctx, scope, s.now())
if err != nil {
return Decision{}, err
}
if state.Mode == ModeReasoning {
return Decision{Query: query, AgentID: s.reasoningAgentID, Execution: ExecutionReasoning}, nil
}
return Decision{Query: query, AgentID: s.quickAgentID, Execution: ExecutionQuick, Expired: expired}, nil
}
func helpReply() string {
return "一、模式说明\n" +
"默认模式:快速问答模式,基于知识库快速准确作答,响应快、成本低,适合信息查询和事实确认。\n" +
"智能推理模式:支持多步思考与工具调用,计费标准更高,适合复杂分析和推导。\n\n" +
"二、模式切换指令\n" +
"/think + 你的问题:仅当前问题使用智能推理\n" +
"/think on:开启持续智能推理模式\n" +
"/think off 或 /rag:关闭并切回快速问答模式\n\n" +
"三、其他常用指令\n" +
"/help:查看本帮助说明\n" +
"/setting 或 /设置:配置语言、自动关闭轮数和提示开关\n\n" +
"只有行首以 / 开头的完整指令会被识别。持续推理模式会在剩余2轮时提醒,到期自动切回快速问答模式。"
}
func (s *Service) Complete(ctx context.Context, input TurnCompletion) (Completion, error) {
if input.At.IsZero() {
input.At = s.now()
}
if input.Idle <= 0 {
input.Idle = s.idleTimeout
}
return s.repo.CompleteTurn(ctx, input)
}
func validateScope(scope Scope) error {
if strings.TrimSpace(scope.TenantKey) == "" || strings.TrimSpace(scope.Channel) == "" || strings.TrimSpace(scope.ExternalActorID) == "" {
return errors.New("tenant, channel and external actor are required")
}
return nil
}