Prompt improvements (#9)

improve prompt templates to get response in yaml format and make parsing easier
also add debug file functionality so that exact input, prompts, and output can be easily seen for every request
This commit is contained in:
Maximillian von Briesen 2023-09-04 16:44:59 -04:00 committed by GitHub
parent dfef07a1c0
commit d92efcb7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 134 additions and 148 deletions

View File

@ -33,6 +33,7 @@ type config struct {
usersToListenTo []string usersToListenTo []string
requiredIssueLabels []string requiredIssueLabels []string
waitDuration time.Duration waitDuration time.Duration
debugDir string
} }
func getConfig() config { func getConfig() config {
@ -49,6 +50,7 @@ func getConfig() config {
usersToListenTo: viper.GetStringSlice("users-to-listen-to"), usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
requiredIssueLabels: viper.GetStringSlice("required-issue-labels"), requiredIssueLabels: viper.GetStringSlice("required-issue-labels"),
waitDuration: viper.GetDuration("wait-duration"), waitDuration: viper.GetDuration("wait-duration"),
debugDir: viper.GetString("debug-dir"),
} }
} }
@ -79,6 +81,7 @@ func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
// TODO configurable model // TODO configurable model
Model: openai.GPT4, Model: openai.GPT4,
OpenAIToken: cfg.openAIToken, OpenAIToken: cfg.openAIToken,
DebugDir: cfg.debugDir,
} }
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), ppCfg) p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), ppCfg)
@ -135,6 +138,7 @@ func init() {
rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to") rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to")
rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue") rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue")
rootCmd.PersistentFlags().Duration("wait-time", 30*time.Second, "the amount of time Pull Pal should wait when no issues or comments are found to address") rootCmd.PersistentFlags().Duration("wait-time", 30*time.Second, "the amount of time Pull Pal should wait when no issues or comments are found to address")
rootCmd.PersistentFlags().StringP("debug-dir", "d", "", "the path to use for the pull pal debug directory")
viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle")) viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle"))
viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email")) viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email"))
@ -148,6 +152,7 @@ func init() {
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to")) viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels")) viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
viper.BindPFlag("wait-time", rootCmd.PersistentFlags().Lookup("wait-time")) viper.BindPFlag("wait-time", rootCmd.PersistentFlags().Lookup("wait-time"))
viper.BindPFlag("debug-dir", rootCmd.PersistentFlags().Lookup("debug-dir"))
} }
func initConfig() { func initConfig() {

2
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
go.uber.org/zap v1.24.0 go.uber.org/zap v1.24.0
golang.org/x/oauth2 v0.7.0 golang.org/x/oauth2 v0.7.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
@ -53,5 +54,4 @@ require (
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -1,13 +1,9 @@
package llm package llm
import (
"strings"
)
// File represents a file in a git repository. // File represents a file in a git repository.
type File struct { type File struct {
Path string Path string `yaml:"path"`
Contents string Contents string `yaml:"contents"`
} }
type ResponseType int type ResponseType int
@ -28,8 +24,8 @@ type CodeChangeRequest struct {
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest. // CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.
type CodeChangeResponse struct { type CodeChangeResponse struct {
Files []File Files []File `yaml:"files"`
Notes string Notes string `yaml:"notes"`
} }
// TODO support threads // TODO support threads
@ -37,45 +33,11 @@ type DiffCommentRequest struct {
File File File File
Contents string Contents string
Diff string Diff string
PRNumber int
} }
type DiffCommentResponse struct { type DiffCommentResponse struct {
Type ResponseType Type ResponseType `yaml:"responseType"`
Answer string Response string `yaml:"response"`
File File File File `yaml:"file"`
}
// parseFiles process the "files" subsection of the LLM's response. It is a helper for GetCodeChangeResponse.
func parseFiles(filesSection string) []File {
fileStringList := strings.Split(filesSection, "ppname:")
if len(fileStringList) < 2 {
return []File{}
}
// first item in the list is just gonna be "Files:"
fileStringList = fileStringList[1:]
replacer := strings.NewReplacer(
"\\n", "\n",
"\\\"", "\"",
"```", "",
)
fileList := make([]File, len(fileStringList))
for i, f := range fileStringList {
fileParts := strings.Split(f, "ppcontents:")
if len(fileParts) < 2 {
continue
}
path := replacer.Replace(fileParts[0])
path = strings.TrimSpace(path)
contents := replacer.Replace(fileParts[1])
contents = strings.TrimSpace(contents)
fileList[i] = File{
Path: path,
Contents: contents,
}
}
return fileList
} }

View File

@ -2,8 +2,9 @@ package llm
import ( import (
"bytes" "bytes"
"strings"
"text/template" "text/template"
"gopkg.in/yaml.v3"
) )
func (req DiffCommentRequest) String() string { func (req DiffCommentRequest) String() string {
@ -40,14 +41,14 @@ func (res DiffCommentResponse) String() string {
out := "" out := ""
if res.Type == ResponseAnswer { if res.Type == ResponseAnswer {
out += "Type: Answer\n" out += "Type: Answer\n"
out += res.Answer out += res.Response
return out return out
} }
out += "Type: Code Change\n" out += "Type: Code Change\n"
out += "Response:\n" out += "Response:\n"
out += res.Answer + "\n\n" out += res.Response + "\n\n"
out += "Files:\n" out += "Files:\n"
out += res.File.Path + ":\n```\n" out += res.File.Path + ":\n```\n"
out += res.File.Contents + "\n```\n" out += res.File.Contents + "\n```\n"
@ -55,36 +56,8 @@ func (res DiffCommentResponse) String() string {
return out return out
} }
func ParseDiffCommentResponse(llmResponse string) DiffCommentResponse { func ParseDiffCommentResponse(llmResponse string) (DiffCommentResponse, error) {
llmResponse = strings.TrimSpace(llmResponse) var response DiffCommentResponse
if llmResponse[0] == 'A' { err := yaml.Unmarshal([]byte(llmResponse), &response)
answer := strings.TrimSpace(llmResponse[1:]) return response, err
return DiffCommentResponse{
Type: ResponseAnswer,
Answer: answer,
}
}
parts := strings.Split(llmResponse, "ppresponse:")
filesSection := ""
if len(parts) > 0 {
filesSection = parts[0]
}
answer := ""
if len(parts) > 1 {
answer = strings.TrimSpace(parts[1])
}
files := parseFiles(filesSection)
f := File{}
if len(files) > 0 {
f = files[0]
}
return DiffCommentResponse{
Type: ResponseCodeChange,
Answer: answer,
File: f,
}
} }

View File

@ -2,8 +2,9 @@ package llm
import ( import (
"bytes" "bytes"
"strings"
"text/template" "text/template"
"gopkg.in/yaml.v3"
) )
// String is the string representation of a CodeChangeRequest. Functionally, it contains the LLM prompt. // String is the string representation of a CodeChangeRequest. Functionally, it contains the LLM prompt.
@ -50,22 +51,8 @@ func (res CodeChangeResponse) String() string {
} }
// ParseCodeChangeResponse parses the LLM's response to CodeChangeRequest (string) into a CodeChangeResponse. // ParseCodeChangeResponse parses the LLM's response to CodeChangeRequest (string) into a CodeChangeResponse.
func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse { func ParseCodeChangeResponse(llmResponse string) (CodeChangeResponse, error) {
sections := strings.Split(llmResponse, "ppnotes:") var response CodeChangeResponse
err := yaml.Unmarshal([]byte(llmResponse), &response)
filesSection := "" return response, err
if len(sections) > 0 {
filesSection = sections[0]
}
notes := ""
if len(sections) > 1 {
notes = strings.TrimSpace(sections[1])
}
files := parseFiles(filesSection)
return CodeChangeResponse{
Files: files,
Notes: notes,
}
} }

View File

@ -2,6 +2,11 @@ package llm
import ( import (
"context" "context"
"fmt"
"io/ioutil"
"os"
"path"
"time"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"go.uber.org/zap" "go.uber.org/zap"
@ -10,14 +15,16 @@ import (
type OpenAIClient struct { type OpenAIClient struct {
log *zap.Logger log *zap.Logger
client *openai.Client client *openai.Client
debugDir string
defaultModel string defaultModel string
} }
func NewOpenAIClient(log *zap.Logger, defaultModel, token string) *OpenAIClient { func NewOpenAIClient(log *zap.Logger, defaultModel, token, debugDir string) *OpenAIClient {
return &OpenAIClient{ return &OpenAIClient{
log: log, log: log,
client: openai.NewClient(token), client: openai.NewClient(token),
defaultModel: defaultModel, defaultModel: defaultModel,
debugDir: debugDir,
} }
} }
@ -44,10 +51,13 @@ func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, model string, req CodeC
choice := resp.Choices[0].Message.Content choice := resp.Choices[0].Message.Content
// TODO make debug log when I figure out how to config that oc.log.Info("got response from llm")
oc.log.Info("got response from llm", zap.String("output", choice))
return ParseCodeChangeResponse(choice), nil debugFilePrefix := fmt.Sprintf("%d-%d", req.IssueNumber, time.Now().Unix())
oc.writeDebug("codechangeresponse", debugFilePrefix+"-req.txt", req.String())
oc.writeDebug("codechangeresponse", debugFilePrefix+"-res.yaml", choice)
return ParseCodeChangeResponse(choice)
} }
func (oc *OpenAIClient) EvaluateDiffComment(ctx context.Context, model string, req DiffCommentRequest) (res DiffCommentResponse, err error) { func (oc *OpenAIClient) EvaluateDiffComment(ctx context.Context, model string, req DiffCommentRequest) (res DiffCommentResponse, err error) {
@ -73,8 +83,33 @@ func (oc *OpenAIClient) EvaluateDiffComment(ctx context.Context, model string, r
choice := resp.Choices[0].Message.Content choice := resp.Choices[0].Message.Content
// TODO make debug log when I figure out how to config that
oc.log.Info("got response from llm", zap.String("output", choice)) oc.log.Info("got response from llm", zap.String("output", choice))
return ParseDiffCommentResponse(choice), nil debugFilePrefix := fmt.Sprintf("%d-%d", req.PRNumber, time.Now().Unix())
oc.writeDebug("diffcommentresponse", debugFilePrefix+"-req.txt", req.String())
oc.writeDebug("diffcommentresponse", debugFilePrefix+"-res.yaml", choice)
return ParseDiffCommentResponse(choice)
}
func (oc *OpenAIClient) writeDebug(subdir, filename, contents string) {
if oc.debugDir == "" {
return
}
fullFolderPath := path.Join(oc.debugDir, subdir)
err := os.MkdirAll(fullFolderPath, os.ModePerm)
if err != nil {
oc.log.Error("failed to ensure debug directory existed", zap.String("folderpath", fullFolderPath), zap.Error(err))
return
}
fullPath := path.Join(fullFolderPath, filename)
err = ioutil.WriteFile(fullPath, []byte(contents), 0644)
if err != nil {
oc.log.Error("failed to write response to debug file", zap.String("filepath", fullPath), zap.Error(err))
return
}
oc.log.Info("response written to debug file", zap.String("filepath", fullPath))
} }

View File

@ -12,13 +12,13 @@ Subject: {{ .Subject }}
Body: Body:
{{ .Body }} {{ .Body }}
Respond in the exact format: Respond in a parseable YAML format based on the following template. Respond only with YAML, and nothing else:
Files: files:
{{ range $index, $file := .Files }} {{ range $index, $file := .Files }}
ppname: {{ $file.Path }} -
ppcontents: path: {{ $file.Path }}
contents: |
[new {{ $file.Path }} contents] [new {{ $file.Path }} contents]
{{ end }} {{ end }}
notes: |
ppnotes:
[additional context about your changes] [additional context about your changes]

View File

@ -16,19 +16,20 @@ Comment:
The above is information about a comment left on a file. The diff contains information about the precise location of the comment. The above is information about a comment left on a file. The diff contains information about the precise location of the comment.
First, determine if the comment is a question or a request for changes. First, determine if the comment is a question or a request for changes.
If the comment is a question, come up with an answer, and respond exactly as outlined directly below "Response Template A", starting with "Q". If the comment is a question, come up with an answer, and respond exactly as outlined directly below "Response Template A".
If the comment is a request, modify the file provided at the beginning of the message, and respond exactly as outlined directly below "Response Template B", starting with "R". If the comment is a request, modify the file provided at the beginning of the message, and respond exactly as outlined directly below "Response Template B".
For either response template, respond in a parseable YAML format. Respond only with YAML, and nothing else.
Response Template A: Response Template A:
Q responseType: 0
response: |
[your answer] [your answer]
Response Template B: Response Template B:
R responseType: 1
Files: file:
ppname: {{ .File.Path }} path: {{ .File.Path }}
ppcontents: contents: |
[new {{ .File.Path }} contents] [new {{ .File.Path }} contents]
response: |
ppresponse:
[additional context about your changes] [additional context about your changes]

View File

@ -26,6 +26,7 @@ type Config struct {
ListIssueOptions vc.ListIssueOptions ListIssueOptions vc.ListIssueOptions
Model string Model string
OpenAIToken string OpenAIToken string
DebugDir string
} }
// PullPal is the service responsible for: // PullPal is the service responsible for:
@ -54,11 +55,10 @@ type pullPalRepo struct {
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations. // NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, error) { func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, error) {
openAIClient := llm.NewOpenAIClient(log.Named("openaiClient"), cfg.Model, cfg.OpenAIToken) openAIClient := llm.NewOpenAIClient(log.Named("openaiClient"), cfg.Model, cfg.OpenAIToken, cfg.DebugDir)
ppRepos := []pullPalRepo{} ppRepos := []pullPalRepo{}
for _, r := range cfg.Repos { for _, r := range cfg.Repos {
fmt.Println(r)
parts := strings.Split(r, "/") parts := strings.Split(r, "/")
if len(parts) < 3 { if len(parts) < 3 {
continue continue
@ -66,9 +66,6 @@ func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, err
host := parts[0] host := parts[0]
owner := parts[1] owner := parts[1]
name := parts[2] name := parts[2]
fmt.Println(host)
fmt.Println(owner)
fmt.Println(name)
newRepo := vc.Repository{ newRepo := vc.Repository{
LocalPath: filepath.Join(cfg.LocalRepoPath, owner, name), LocalPath: filepath.Join(cfg.LocalRepoPath, owner, name),
HostDomain: host, HostDomain: host,
@ -81,7 +78,7 @@ func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
localGitClient, err := vc.NewLocalGitClient(log.Named("gitclient-"+r), cfg.Self, newRepo) localGitClient, err := vc.NewLocalGitClient(log.Named("gitclient-"+r), cfg.Self, newRepo, cfg.DebugDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -145,7 +142,6 @@ func (p pullPalRepo) checkIssuesAndComments() error {
issue := issues[0] issue := issues[0]
err = p.handleIssue(issue) err = p.handleIssue(issue)
if err != nil { if err != nil {
// TODO leave comment if error (make configurable)
p.log.Error("error handling issue", zap.Error(err)) p.log.Error("error handling issue", zap.Error(err))
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%s\n```", err.Error()) commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%s\n```", err.Error())
err = p.ghClient.CommentOnIssue(issue.Number, commentText) err = p.ghClient.CommentOnIssue(issue.Number, commentText)
@ -173,7 +169,6 @@ func (p pullPalRepo) checkIssuesAndComments() error {
comment := comments[0] comment := comments[0]
err = p.handleComment(comment) err = p.handleComment(comment)
if err != nil { if err != nil {
// TODO leave comment if error (make configurable)
p.log.Error("error handling comment", zap.Error(err)) p.log.Error("error handling comment", zap.Error(err))
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%s\n```", err.Error()) commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%s\n```", err.Error())
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, commentText) err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, commentText)
@ -187,11 +182,7 @@ func (p pullPalRepo) checkIssuesAndComments() error {
} }
func (p *pullPalRepo) handleIssue(issue vc.Issue) error { func (p *pullPalRepo) handleIssue(issue vc.Issue) error {
err := p.ghClient.CommentOnIssue(issue.Number, "working on it") // remove labels from issue so that it is not picked up again until labels are reapplied
if err != nil {
p.log.Error("error commenting on issue", zap.Error(err))
return err
}
for _, label := range p.listIssueOptions.Labels { for _, label := range p.listIssueOptions.Labels {
err = p.ghClient.RemoveLabelFromIssue(issue.Number, label) err = p.ghClient.RemoveLabelFromIssue(issue.Number, label)
if err != nil { if err != nil {
@ -236,7 +227,6 @@ func (p *pullPalRepo) handleIssue(issue vc.Issue) error {
} }
// open code change request // open code change request
// TODO don't hardcode main branch, make configurable
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName) _, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName)
if err != nil { if err != nil {
return err return err
@ -260,6 +250,7 @@ func (p *pullPalRepo) handleComment(comment vc.Comment) error {
File: file, File: file,
Contents: comment.Body, Contents: comment.Body,
Diff: comment.DiffHunk, Diff: comment.DiffHunk,
PRNumber: comment.PRNumber,
} }
p.log.Info("diff comment request", zap.String("req", diffCommentRequest.String())) p.log.Info("diff comment request", zap.String("req", diffCommentRequest.String()))
@ -298,7 +289,7 @@ func (p *pullPalRepo) handleComment(comment vc.Comment) error {
} }
} }
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, diffCommentResponse.Answer) err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, diffCommentResponse.Response)
if err != nil { if err != nil {
p.log.Error("error commenting on issue", zap.Error(err)) p.log.Error("error commenting on issue", zap.Error(err))
return err return err

View File

@ -93,6 +93,7 @@ func ParseIssueBody(body string) IssueBody {
issueBody := IssueBody{ issueBody := IssueBody{
BaseBranch: "main", BaseBranch: "main",
} }
// TODO get rid of parsing like this - "---" may occur in the normal issue body
divider := "---" divider := "---"
parts := strings.Split(body, divider) parts := strings.Split(body, divider)

View File

@ -6,6 +6,7 @@ import (
"go/format" "go/format"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -27,10 +28,11 @@ type LocalGitClient struct {
repo Repository repo Repository
worktree *git.Worktree worktree *git.Worktree
debugDir string
} }
// NewLocalGitClient initializes a local git client by checking out a repository locally. // NewLocalGitClient initializes a local git client by checking out a repository locally.
func NewLocalGitClient(log *zap.Logger, self Author, repo Repository) (*LocalGitClient, error) { func NewLocalGitClient(log *zap.Logger, self Author, repo Repository, debugDir string) (*LocalGitClient, error) {
log.Info("checking out local github repo", zap.String("repo name", repo.Name), zap.String("local path", repo.LocalPath)) log.Info("checking out local github repo", zap.String("repo name", repo.Name), zap.String("local path", repo.LocalPath))
// clone provided repository to local path // clone provided repository to local path
if repo.LocalPath == "" { if repo.LocalPath == "" {
@ -60,6 +62,7 @@ func NewLocalGitClient(log *zap.Logger, self Author, repo Repository) (*LocalGit
log: log, log: log,
self: self, self: self,
repo: repo, repo: repo,
debugDir: debugDir,
}, nil }, nil
} }
@ -239,6 +242,7 @@ func (gc *LocalGitClient) ParseIssueAndStartCommit(issue Issue) (llm.CodeChangeR
} }
issueBody := ParseIssueBody(issue.Body) issueBody := ParseIssueBody(issue.Body)
gc.log.Info("issue body info", zap.Any("files", issueBody.FilePaths))
// start a worktree // start a worktree
err := gc.StartCommit() err := gc.StartCommit()
@ -264,11 +268,38 @@ func (gc *LocalGitClient) ParseIssueAndStartCommit(issue Issue) (llm.CodeChangeR
files = append(files, nextFile) files = append(files, nextFile)
} }
return llm.CodeChangeRequest{ req := llm.CodeChangeRequest{
Subject: issue.Subject, Subject: issue.Subject,
Body: issueBody.PromptBody, Body: issueBody.PromptBody,
IssueNumber: issue.Number, IssueNumber: issue.Number,
Files: files, Files: files,
BaseBranch: issueBody.BaseBranch, BaseBranch: issueBody.BaseBranch,
}, nil }
debugFileNamePrefix := fmt.Sprintf("issue-%d-%d", issue.Number, time.Now().Unix())
gc.writeDebug("issues", debugFileNamePrefix+"-originalbody.txt", issue.Body)
gc.writeDebug("issues", debugFileNamePrefix+"-parsed-req.txt", req.String())
return req, nil
}
func (gc *LocalGitClient) writeDebug(subdir, filename, contents string) {
if gc.debugDir == "" {
return
}
fullFolderPath := path.Join(gc.debugDir, subdir)
err := os.MkdirAll(fullFolderPath, os.ModePerm)
if err != nil {
gc.log.Error("failed to ensure debug directory existed", zap.String("folderpath", fullFolderPath), zap.Error(err))
return
}
fullPath := path.Join(fullFolderPath, filename)
err = ioutil.WriteFile(fullPath, []byte(contents), 0644)
if err != nil {
gc.log.Error("failed to write response to debug file", zap.String("filepath", fullPath), zap.Error(err))
return
}
gc.log.Info("response written to debug file", zap.String("filepath", fullPath))
} }