feat: parse and include referenced issues as prompt context (#12)
This commit is contained in:
parent
3a18c25b3c
commit
a49254c9ed
@ -2,6 +2,7 @@ package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
@ -17,6 +18,51 @@ type Forge struct {
|
||||
client *gitea.Client
|
||||
}
|
||||
|
||||
// GetProject implements port.Forge.
|
||||
func (f *Forge) GetProject(ctx context.Context, rawProjectID string) (*model.Project, error) {
|
||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
project, _, err := f.client.GetRepoByID(projectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &model.Project{
|
||||
ID: rawProjectID,
|
||||
Name: project.FullName,
|
||||
Description: project.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProjectLanguages implements port.Forge.
|
||||
func (f *Forge) GetProjectLanguages(ctx context.Context, rawProjectID string) ([]string, error) {
|
||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
project, _, err := f.client.GetRepoByID(projectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
mappedLanguages, _, err := f.client.GetRepoLanguages(project.Owner.UserName, project.Name)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
languages := make([]string, 0, len(mappedLanguages))
|
||||
|
||||
for l := range mappedLanguages {
|
||||
languages = append(languages, l)
|
||||
}
|
||||
|
||||
return languages, nil
|
||||
}
|
||||
|
||||
// CreateIssue implements port.Forge.
|
||||
func (f *Forge) CreateIssue(ctx context.Context, rawProjectID string, title string, body string) (string, error) {
|
||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||
@ -65,12 +111,46 @@ func (f *Forge) GetIssueTemplate(ctx context.Context, rawProjectID string) (stri
|
||||
}
|
||||
|
||||
// GetIssues implements port.Forge.
|
||||
func (f *Forge) GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error) {
|
||||
panic("unimplemented")
|
||||
func (f *Forge) GetIssues(ctx context.Context, rawProjectID string, issueIDs ...string) ([]*model.Issue, error) {
|
||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
project, _, err := f.client.GetRepoByID(projectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
issues := make([]*model.Issue, 0)
|
||||
|
||||
for _, rawIssueID := range issueIDs {
|
||||
issueID, err := strconv.ParseInt(rawIssueID, 10, 64)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not parse issue id", slog.Any("error", errors.WithStack(err)))
|
||||
issues = append(issues, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
issue, _, err := f.client.GetIssue(project.Owner.UserName, project.Name, issueID)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not get issue", slog.String("project", project.FullName), slog.Int64("issueID", issueID), slog.Any("error", errors.WithStack(err)))
|
||||
issues = append(issues, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
issues = append(issues, &model.Issue{
|
||||
ID: rawIssueID,
|
||||
Title: issue.Title,
|
||||
Body: issue.Body,
|
||||
})
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// ListProjects implements port.Forge.
|
||||
func (f *Forge) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
||||
// GetAllProjects implements port.Forge.
|
||||
func (f *Forge) GetAllProjects(ctx context.Context) ([]*model.Project, error) {
|
||||
projects := make([]*model.Project, 0)
|
||||
|
||||
page := 1
|
||||
@ -94,13 +174,13 @@ func (f *Forge) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
||||
for _, r := range repositories {
|
||||
projects = append(projects, &model.Project{
|
||||
ID: strconv.FormatInt(r.ID, 10),
|
||||
Label: r.Owner.UserName + "/" + r.Name,
|
||||
Name: r.FullName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(projects, func(p1 *model.Project, p2 *model.Project) int {
|
||||
return strings.Compare(p1.Label, p2.Label)
|
||||
return strings.Compare(p1.Name, p2.Name)
|
||||
})
|
||||
|
||||
return projects, nil
|
||||
|
@ -2,5 +2,6 @@ package model
|
||||
|
||||
type Issue struct {
|
||||
ID string
|
||||
Content string
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
@ -2,5 +2,6 @@ package model
|
||||
|
||||
type Project struct {
|
||||
ID string
|
||||
Label string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
8
internal/core/model/resource.go
Normal file
8
internal/core/model/resource.go
Normal file
@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type Resource struct {
|
||||
Name string
|
||||
Type string
|
||||
Syntax string
|
||||
Content string
|
||||
}
|
@ -12,8 +12,10 @@ var (
|
||||
)
|
||||
|
||||
type Forge interface {
|
||||
GetProjects(ctx context.Context) ([]*model.Project, error)
|
||||
GetAllProjects(ctx context.Context) ([]*model.Project, error)
|
||||
CreateIssue(ctx context.Context, projectID string, title string, body string) (string, error)
|
||||
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
|
||||
GetIssueTemplate(ctx context.Context, projectID string) (string, error)
|
||||
GetProjectLanguages(ctx context.Context, projectID string) ([]string, error)
|
||||
GetProject(ctx context.Context, projectID string) (*model.Project, error)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -38,6 +39,7 @@ type IssueManager struct {
|
||||
forgeFactories []ForgeFactory
|
||||
llmClient llm.Client
|
||||
projectCache *cache.Cache[[]*model.Project]
|
||||
forgeCache *cache.Cache[port.Forge]
|
||||
}
|
||||
|
||||
func (m *IssueManager) CreateIssue(ctx context.Context, user *model.User, projectID string, title string, body string) (string, error) {
|
||||
@ -64,7 +66,7 @@ func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
refreshedProjects, err := forge.GetProjects(ctx)
|
||||
refreshedProjects, err := forge.GetAllProjects(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -83,11 +85,15 @@ func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, proj
|
||||
return "", "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
slog.DebugContext(ctx, "using system prompt", slog.String("systemPrompt", systemPrompt))
|
||||
|
||||
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
|
||||
if err != nil {
|
||||
return "", "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
slog.DebugContext(ctx, "using user prompt", slog.String("userPrompt", userPrompt))
|
||||
|
||||
messages := []llm.Message{
|
||||
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||
llm.NewMessage(llm.RoleUser, userPrompt),
|
||||
@ -141,15 +147,36 @@ func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.Use
|
||||
}
|
||||
|
||||
func (m *IssueManager) getIssueUserPrompt(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
|
||||
_, err := m.getUserForge(ctx, user)
|
||||
forge, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
project, err := forge.GetProject(ctx, projectID)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
projectLanguages, err := forge.GetProjectLanguages(ctx, projectID)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources, err := m.extractResources(ctx, forge, projectID, issueSummary)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
userPrompt, err := llm.PromptTemplate(issueUserPromptRawTemplate, struct {
|
||||
Context string
|
||||
Summary string
|
||||
Project *model.Project
|
||||
ProjectLanguages []string
|
||||
Resources []*model.Resource
|
||||
}{
|
||||
Context: issueSummary,
|
||||
Summary: issueSummary,
|
||||
Project: project,
|
||||
ProjectLanguages: projectLanguages,
|
||||
Resources: resources,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
@ -159,6 +186,11 @@ func (m *IssueManager) getIssueUserPrompt(ctx context.Context, user *model.User,
|
||||
}
|
||||
|
||||
func (m *IssueManager) getUserForge(ctx context.Context, user *model.User) (port.Forge, error) {
|
||||
forge, exists := m.forgeCache.Get(user.AccessToken)
|
||||
if exists {
|
||||
return forge, nil
|
||||
}
|
||||
|
||||
for _, f := range m.forgeFactories {
|
||||
if !f.Match(user) {
|
||||
continue
|
||||
@ -170,17 +202,66 @@ func (m *IssueManager) getUserForge(ctx context.Context, user *model.User) (port
|
||||
return nil, errors.WithStack(ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
m.forgeCache.Set(user.AccessToken, forge, cache.DefaultExpiration)
|
||||
|
||||
return forge, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("no forge matching user found")
|
||||
}
|
||||
|
||||
func (m *IssueManager) extractResources(ctx context.Context, forge port.Forge, projectID string, issueSummary string) ([]*model.Resource, error) {
|
||||
resources := make([]*model.Resource, 0)
|
||||
|
||||
issues, err := m.extractIssues(ctx, forge, projectID, issueSummary)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract issues")
|
||||
}
|
||||
|
||||
resources = append(resources, issues...)
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
var issueRefRegExp = regexp.MustCompile(`#([0-9]+)`)
|
||||
|
||||
func (m *IssueManager) extractIssues(ctx context.Context, forge port.Forge, projectID string, issueSummary string) ([]*model.Resource, error) {
|
||||
issueRefMatches := issueRefRegExp.FindAllStringSubmatch(issueSummary, -1)
|
||||
|
||||
issueIDs := make([]string, 0, len(issueRefMatches))
|
||||
for _, m := range issueRefMatches {
|
||||
issueIDs = append(issueIDs, m[1])
|
||||
}
|
||||
|
||||
issues, err := forge.GetIssues(ctx, projectID, issueIDs...)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resources := make([]*model.Resource, 0, len(issues))
|
||||
|
||||
for _, iss := range issues {
|
||||
if iss == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resources = append(resources, &model.Resource{
|
||||
Name: fmt.Sprintf("#%s - %s", iss.ID, iss.Title),
|
||||
Type: "Issue",
|
||||
Syntax: "",
|
||||
Content: iss.Body,
|
||||
})
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func NewIssueManager(llmClient llm.Client, forgeFactories ...ForgeFactory) *IssueManager {
|
||||
return &IssueManager{
|
||||
llmClient: llmClient,
|
||||
forgeFactories: forgeFactories,
|
||||
projectCache: cache.New[[]*model.Project](time.Minute*5, (time.Minute*5)/2),
|
||||
forgeCache: cache.New[port.Forge](time.Minute, time.Minute/2),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,13 @@ You are an expert software developer with extensive experience in writing clear
|
||||
- Expected behavior.
|
||||
- Actual behavior.
|
||||
- Any relevant error messages or logs.
|
||||
- Always use the user prompt context main language.
|
||||
- Always use the user prompt summary main language.
|
||||
|
||||
2. **Additional Context**:
|
||||
- Include any other relevant information that might help in understanding or resolving the issue/request.
|
||||
|
||||
3. Let think step by step.
|
||||
|
||||
**Markdown Layout:**
|
||||
|
||||
```markdown
|
||||
|
@ -1,3 +1,32 @@
|
||||
Write a formatted issue/request based on theses contextual informations:
|
||||
Write a formatted issue/request based on the following informations:
|
||||
|
||||
{{ .Context }}
|
||||
## Request Summary
|
||||
|
||||
{{ .Summary }}
|
||||
|
||||
## General Project Informations
|
||||
|
||||
- Name: {{ .Project.Name }}
|
||||
- Description: {{ .Project.Description }}
|
||||
- Languages:
|
||||
{{- range .ProjectLanguages }}
|
||||
- {{ . -}}
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .Resources) 0 }}
|
||||
## Additional Resources
|
||||
|
||||
{{ range .Resources }}
|
||||
### {{ .Name }}
|
||||
|
||||
**Type:**
|
||||
|
||||
{{ .Type }}
|
||||
|
||||
**Content:**
|
||||
|
||||
```{{ .Syntax }}
|
||||
{{ .Content }}
|
||||
```
|
||||
{{end}}
|
||||
{{end}}
|
@ -84,7 +84,7 @@ templ IssuePage(vmodel IssuePageVModel) {
|
||||
}
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<form id="summary-form" action={ common.CurrentURL(ctx) } method="put" hx-disabled-elt="#summary-form textarea, #summary-form select, #summary-form button" hx-on:htmx:before-send="savePreferredProject()" hx-indicator="#generation-progress">
|
||||
<form id="summary-form" action={ common.CurrentURL(ctx) } method="put" hx-disabled-elt="textarea, input, select, button" hx-on:htmx:before-send="savePreferredProject()" hx-indicator="#generation-progress">
|
||||
<h2 class="title is-size-2">Résumé de la demande</h2>
|
||||
@common.FormSelect(
|
||||
vmodel.SummaryForm, "issue-project", "project", "Projet",
|
||||
@ -179,7 +179,7 @@ templ IssuePage(vmodel IssuePageVModel) {
|
||||
func projectsToOptions(projects []*model.Project) []string {
|
||||
options := make([]string, 0, len(projects)*2)
|
||||
for _, p := range projects {
|
||||
options = append(options, p.Label, p.ID)
|
||||
options = append(options, p.Name, p.ID)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" method=\"put\" hx-disabled-elt=\"#summary-form textarea, #summary-form select, #summary-form button\" hx-on:htmx:before-send=\"savePreferredProject()\" hx-indicator=\"#generation-progress\"><h2 class=\"title is-size-2\">Résumé de la demande</h2>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" method=\"put\" hx-disabled-elt=\"textarea, input, select, button\" hx-on:htmx:before-send=\"savePreferredProject()\" hx-indicator=\"#generation-progress\"><h2 class=\"title is-size-2\">Résumé de la demande</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -219,7 +219,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
||||
func projectsToOptions(projects []*model.Project) []string {
|
||||
options := make([]string, 0, len(projects)*2)
|
||||
for _, p := range projects {
|
||||
options = append(options, p.Label, p.ID)
|
||||
options = append(options, p.Name, p.ID)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user