// SPDX-License-Identifier: MIT package tools import ( "bytes" "context" "crypto/sha256" "encoding/binary" "encoding/json" "errors" "net/url" "regexp" "strconv" "strings" "time" "unicode" "unicode/utf8" "github.com/mudler/LocalAI/addons/github/internal/ghapi" "github.com/mudler/LocalAI/addons/internal/protocol" "github.com/mudler/LocalAI/pkg/addons/contract" ) // The write tools of the manifest. All of them are write tools: the host // decides, from the write mode of the person's binding, whether each one is // refused, stored as a proposal for a person, or applied at once. const ( ToolCommentIssue = "comment-issue" ToolCommentPR = "comment-pr" ToolCreateIssue = "create-issue" ToolEditIssue = "edit-issue" ToolAddLabels = "add-labels" ToolReviewCommentPR = "review-comment-pr" maxLabels = 10 maxLabelRunes = 50 maxTitleRunes = 256 previewBodyRunes = 800 previewTargetRunes = 120 operationMarkerStart = "" } func withMarker(body, op string) string { if strings.TrimSpace(body) == "" { return marker(op) } return body + "\n\n" + marker(op) } // link keeps a URL only when it points at github.com over https. func link(u string) string { if strings.HasPrefix(u, "https://github.com/") && len(u) <= 300 { return u } return "" } type created struct { ID int64 `json:"id"` Number int `json:"number"` HTMLURL string `json:"html_url"` } func outOf(cr created) map[string]any { out := map[string]any{} if cr.ID != 0 { out["id"] = cr.ID } if cr.Number != 0 { out["number"] = cr.Number } if u := link(cr.HTMLURL); u != "" { out["url"] = u } return out } // send makes the request for a checked write. func (c call) send(w write, op string) (map[string]any, error) { api, base := c.r.API, "/repos/"+w.Repo var cr created var err error switch w.tool { case ToolCommentIssue, ToolCommentPR: _, err = api.SendJSON(c.ctx, "POST", c.token, base+"/issues/"+strconv.Itoa(w.Number)+"/comments", map[string]any{"body": withMarker(w.Body, op)}, &cr) case ToolCreateIssue: in := map[string]any{"title": w.Title, "body": withMarker(w.Body, op)} if len(w.Labels) > 0 { in["labels"] = w.Labels } _, err = api.SendJSON(c.ctx, "POST", c.token, base+"/issues", in, &cr) case ToolEditIssue: in := map[string]any{} if w.Title != "" { in["title"] = w.Title } if w.State != "" { in["state"] = w.State if w.Reason != "" { in["state_reason"] = w.Reason } } _, err = api.SendJSON(c.ctx, "PATCH", c.token, base+"/issues/"+strconv.Itoa(w.Number), in, &cr) case ToolAddLabels: _, err = api.SendJSON(c.ctx, "POST", c.token, base+"/issues/"+strconv.Itoa(w.Number)+"/labels", map[string]any{"labels": w.Labels}, nil) cr.Number = w.Number case ToolReviewCommentPR: sha, _, e := c.pullHead(w.Repo, w.Number) if e != nil { return nil, e } side := w.Side if side == "" { side = "RIGHT" } _, err = api.SendJSON(c.ctx, "POST", c.token, base+"/pulls/"+strconv.Itoa(w.Number)+"/comments", map[string]any{"body": withMarker(w.Body, op), "commit_id": sha, "path": w.Path, "line": w.Line, "side": side}, &cr) } if err != nil { return nil, err } return outOf(cr), nil } // find looks on GitHub for text carrying the marker of an operation, written // since the operation was started. It is read-only. func (c call) find(w write, op string, since time.Time) (map[string]any, bool) { mark := marker(op) q := url.Values{"since": {since.Add(-time.Minute).UTC().Format(time.RFC3339)}, "per_page": {"100"}} var path string switch w.tool { case ToolCommentIssue, ToolCommentPR: path = "/repos/" + w.Repo + "/issues/" + strconv.Itoa(w.Number) + "/comments" case ToolReviewCommentPR: path = "/repos/" + w.Repo + "/pulls/" + strconv.Itoa(w.Number) + "/comments" case ToolCreateIssue: path = "/repos/" + w.Repo + "/issues" q.Set("state", "all") q.Set("sort", "created") q.Set("direction", "desc") default: return nil, false } var list []struct { ID int64 `json:"id"` Number int `json:"number"` Body string `json:"body"` HTMLURL string `json:"html_url"` } if _, err := c.r.API.GetJSON(c.ctx, c.token, path, q, &list); err != nil { return nil, false } for _, e := range list { if strings.Contains(e.Body, mark) { return outOf(created{ID: e.ID, Number: e.Number, HTMLURL: e.HTMLURL}), true } } return nil, false } var _ = protocol.ErrBadRequest