feat: done vibing
This commit is contained in:
27 files changed
+1922
-13
No files matched your search
@@ -0,0 +1,65 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ytshih/stonks/internal/report"
|
||||
)
|
||||
|
||||
// recommendationsMarker must match the marker text SystemPrompt instructs
|
||||
// Claude to emit before the trailing recommendations JSON block.
|
||||
const recommendationsMarker = "===RECOMMENDATIONS_JSON==="
|
||||
|
||||
type recommendationsPayload struct {
|
||||
Recommendations []report.Recommendation `json:"recommendations"`
|
||||
}
|
||||
|
||||
// SplitReport separates Claude's raw response into the long-form narrative
|
||||
// text and the short buy/sell recommendations list appended after
|
||||
// recommendationsMarker (see SystemPrompt).
|
||||
//
|
||||
// If the marker is missing or the trailing JSON fails to parse, the full raw
|
||||
// text is returned as-is with a nil recommendations slice, plus a non-nil
|
||||
// error — callers should log it as a warning and still send the text-only
|
||||
// report rather than treat this as fatal.
|
||||
func SplitReport(raw string) (report.Report, error) {
|
||||
idx := strings.Index(raw, recommendationsMarker)
|
||||
if idx == -1 {
|
||||
return report.Report{Text: strings.TrimSpace(raw)},
|
||||
fmt.Errorf("recommendations marker not found in response")
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(raw[:idx])
|
||||
jsonPart := extractJSONObject(raw[idx+len(recommendationsMarker):])
|
||||
|
||||
var payload recommendationsPayload
|
||||
if err := json.Unmarshal([]byte(jsonPart), &payload); err != nil {
|
||||
return report.Report{Text: text}, fmt.Errorf("parse recommendations json: %w", err)
|
||||
}
|
||||
|
||||
for _, r := range payload.Recommendations {
|
||||
if r.Action != "buy" && r.Action != "sell" {
|
||||
return report.Report{Text: text}, fmt.Errorf("recommendation for %q has invalid action %q", r.Symbol, r.Action)
|
||||
}
|
||||
}
|
||||
|
||||
return report.Report{Text: text, Recommendations: payload.Recommendations}, nil
|
||||
}
|
||||
|
||||
// extractJSONObject pulls out the substring from the first "{" to the last
|
||||
// "}" in s. This tolerates any decoration Claude adds around the JSON
|
||||
// despite being asked not to — a ```json fence, a stray leading backtick, a
|
||||
// trailing sentence — since none of that can contain a brace that comes
|
||||
// before the real opening one or after the real closing one. Returns s
|
||||
// trimmed, unchanged, if no braces are found (json.Unmarshal will then fail
|
||||
// with a clear error).
|
||||
func extractJSONObject(s string) string {
|
||||
start := strings.IndexByte(s, '{')
|
||||
end := strings.LastIndexByte(s, '}')
|
||||
if start == -1 || end == -1 || end < start {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
return s[start : end+1]
|
||||
}
|
||||
Reference in new issue
Block a user