返回 DeepSeek-Reasonix
decision.go
根目录 / internal / agent / decision.go
1 package agent
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "strings"
10 "unicode"
11
12 "reasonix/internal/event"
13 )
14
15 type acceptedDecision struct {
16 ID string
17 Question string
18 Answer string
19 Ambiguity decisionAmbiguity
20 }
21
22 type decisionAmbiguity struct {
23 Headers map[string]struct{}
24 Options map[string]struct{}
25 Terms map[string]struct{}
26 }
27
28 func decisionIDForQuestions(qs []event.AskQuestion) string {
29 h := sha256.New()
30 for _, q := range qs {
31 _, _ = h.Write([]byte(strings.ToLower(strings.TrimSpace(q.Prompt))))
32 _, _ = h.Write([]byte{0})
33 for _, opt := range q.Options {
34 _, _ = h.Write([]byte(strings.ToLower(strings.TrimSpace(opt.Label))))
35 _, _ = h.Write([]byte{0})
36 }
37 }
38 return "dec-" + hex.EncodeToString(h.Sum(nil)[:8])
39 }
40
41 type turnStateContextKey struct{}
42
43 func withTurnState(ctx context.Context, turn *turnRuntime) context.Context {
44 if turn == nil {
45 return ctx
46 }
47 return context.WithValue(ctx, turnStateContextKey{}, turn)
48 }
49
50 func turnStateFrom(ctx context.Context) *turnRuntime {
51 turn, _ := ctx.Value(turnStateContextKey{}).(*turnRuntime)
52 return turn
53 }
54
55 func rememberDecisionForQuestions(ctx context.Context, id, question, answer string, qs []event.AskQuestion) {
56 turn := turnStateFrom(ctx)
57 if turn == nil || id == "" {
58 return
59 }
60 turn.loop.rememberDecisionAmbiguity(id, question, answer, decisionAmbiguityForQuestions(qs))
61 }
62
63 func existingDecision(ctx context.Context, id string) (acceptedDecision, bool) {
64 turn := turnStateFrom(ctx)
65 if turn == nil || id == "" {
66 return acceptedDecision{}, false
67 }
68 return turn.loop.decision(id)
69 }
70
71 func firstExistingDecision(ctx context.Context) (acceptedDecision, bool) {
72 turn := turnStateFrom(ctx)
73 if turn == nil {
74 return acceptedDecision{}, false
75 }
76 decisions := turn.loop.snapshotDecisions()
77 if len(decisions) == 0 {
78 return acceptedDecision{}, false
79 }
80 return decisions[0], true
81 }
82
83 func matchingExistingDecision(ctx context.Context, qs []event.AskQuestion) (acceptedDecision, bool) {
84 turn := turnStateFrom(ctx)
85 if turn == nil {
86 return acceptedDecision{}, false
87 }
88 candidate := decisionAmbiguityForQuestions(qs)
89 for _, decision := range turn.loop.snapshotDecisions() {
90 if sameDecisionAmbiguity(decision.Ambiguity, candidate) {
91 return decision, true
92 }
93 }
94 return acceptedDecision{}, false
95 }
96
97 func decisionAmbiguityForQuestions(qs []event.AskQuestion) decisionAmbiguity {
98 out := decisionAmbiguity{
99 Headers: map[string]struct{}{},
100 Options: map[string]struct{}{},
101 Terms: map[string]struct{}{},
102 }
103 for _, q := range qs {
104 addDecisionPhrase(out.Headers, q.Header)
105 addDecisionTerms(out.Terms, q.Header)
106 addDecisionTerms(out.Terms, q.Prompt)
107 for _, option := range q.Options {
108 addDecisionPhrase(out.Options, option.Label)
109 addDecisionTerms(out.Terms, option.Label)
110 }
111 }
112 return out
113 }
114
115 func addDecisionPhrase(dst map[string]struct{}, value string) {
116 var b strings.Builder
117 for _, r := range strings.ToLower(strings.TrimSpace(value)) {
118 if unicode.IsLetter(r) || unicode.IsNumber(r) {
119 b.WriteRune(r)
120 }
121 }
122 if b.Len() > 0 {
123 dst[b.String()] = struct{}{}
124 }
125 }
126
127 func addDecisionTerms(dst map[string]struct{}, value string) {
128 var latin strings.Builder
129 flush := func() {
130 if latin.Len() > 1 {
131 dst[latin.String()] = struct{}{}
132 }
133 latin.Reset()
134 }
135 for _, r := range strings.ToLower(value) {
136 switch {
137 case unicode.In(r, unicode.Han, unicode.Hiragana, unicode.Katakana, unicode.Hangul):
138 flush()
139 dst[string(r)] = struct{}{}
140 case unicode.IsLetter(r) || unicode.IsNumber(r):
141 latin.WriteRune(r)
142 default:
143 flush()
144 }
145 }
146 flush()
147 }
148
149 func sameDecisionAmbiguity(left, right decisionAmbiguity) bool {
150 if len(left.Terms) == 0 || len(right.Terms) == 0 {
151 return false
152 }
153 headerMatch := setIntersection(left.Headers, right.Headers) > 0
154 optionSimilarity := setJaccard(left.Options, right.Options)
155 termSimilarity := setJaccard(left.Terms, right.Terms)
156 return (headerMatch && (optionSimilarity >= 0.5 || termSimilarity >= 0.45)) ||
157 (optionSimilarity >= 0.75 && termSimilarity >= 0.35) || termSimilarity >= 0.7
158 }
159
160 func setIntersection(left, right map[string]struct{}) int {
161 count := 0
162 for value := range left {
163 if _, ok := right[value]; ok {
164 count++
165 }
166 }
167 return count
168 }
169
170 func setJaccard(left, right map[string]struct{}) float64 {
171 union := len(left) + len(right)
172 if union == 0 {
173 return 0
174 }
175 intersection := setIntersection(left, right)
176 return float64(intersection) / float64(union-intersection)
177 }
178
179 type askArgs struct {
180 DecisionID string `json:"decision_id"`
181 Evidence string `json:"new_evidence"`
182 Questions []struct {
183 Header string `json:"header"`
184 Question string `json:"question"`
185 MultiSelect bool `json:"multiSelect"`
186 Options []struct {
187 Label string `json:"label"`
188 Description string `json:"description"`
189 } `json:"options"`
190 } `json:"questions"`
191 }
192
193 func parseAskArgs(raw json.RawMessage) (askArgs, error) {
194 var p askArgs
195 if err := json.Unmarshal(raw, &p); err != nil {
196 return askArgs{}, fmt.Errorf("invalid args: %w", err)
197 }
198 if len(p.Questions) == 0 {
199 return askArgs{}, fmt.Errorf("at least one question is required")
200 }
201 if len(p.Questions) > 3 {
202 return askArgs{}, fmt.Errorf("at most 3 questions may be asked in one clarification")
203 }
204 return p, nil
205 }
206
206 lines GO