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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -17,6 +18,51 @@ type Forge struct {
|
|||||||
client *gitea.Client
|
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.
|
// CreateIssue implements port.Forge.
|
||||||
func (f *Forge) CreateIssue(ctx context.Context, rawProjectID string, title string, body string) (string, error) {
|
func (f *Forge) CreateIssue(ctx context.Context, rawProjectID string, title string, body string) (string, error) {
|
||||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
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.
|
// GetIssues implements port.Forge.
|
||||||
func (f *Forge) GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error) {
|
func (f *Forge) GetIssues(ctx context.Context, rawProjectID string, issueIDs ...string) ([]*model.Issue, error) {
|
||||||
panic("unimplemented")
|
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.
|
// GetAllProjects implements port.Forge.
|
||||||
func (f *Forge) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
func (f *Forge) GetAllProjects(ctx context.Context) ([]*model.Project, error) {
|
||||||
projects := make([]*model.Project, 0)
|
projects := make([]*model.Project, 0)
|
||||||
|
|
||||||
page := 1
|
page := 1
|
||||||
@ -93,14 +173,14 @@ func (f *Forge) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
|||||||
|
|
||||||
for _, r := range repositories {
|
for _, r := range repositories {
|
||||||
projects = append(projects, &model.Project{
|
projects = append(projects, &model.Project{
|
||||||
ID: strconv.FormatInt(r.ID, 10),
|
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 {
|
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
|
return projects, nil
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID string
|
ID string
|
||||||
Content string
|
Title string
|
||||||
|
Body string
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID string
|
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 {
|
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)
|
CreateIssue(ctx context.Context, projectID string, title string, body string) (string, error)
|
||||||
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
|
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
|
||||||
GetIssueTemplate(ctx context.Context, projectID string) (string, 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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ type IssueManager struct {
|
|||||||
forgeFactories []ForgeFactory
|
forgeFactories []ForgeFactory
|
||||||
llmClient llm.Client
|
llmClient llm.Client
|
||||||
projectCache *cache.Cache[[]*model.Project]
|
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) {
|
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)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshedProjects, err := forge.GetProjects(ctx)
|
refreshedProjects, err := forge.GetAllProjects(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -83,11 +85,15 @@ func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, proj
|
|||||||
return "", "", errors.WithStack(err)
|
return "", "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.DebugContext(ctx, "using system prompt", slog.String("systemPrompt", systemPrompt))
|
||||||
|
|
||||||
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
|
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", errors.WithStack(err)
|
return "", "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.DebugContext(ctx, "using user prompt", slog.String("userPrompt", userPrompt))
|
||||||
|
|
||||||
messages := []llm.Message{
|
messages := []llm.Message{
|
||||||
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||||
llm.NewMessage(llm.RoleUser, userPrompt),
|
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) {
|
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 {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userPrompt, err := llm.PromptTemplate(issueUserPromptRawTemplate, struct {
|
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 {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
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) {
|
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 {
|
for _, f := range m.forgeFactories {
|
||||||
if !f.Match(user) {
|
if !f.Match(user) {
|
||||||
continue
|
continue
|
||||||
@ -170,17 +202,66 @@ func (m *IssueManager) getUserForge(ctx context.Context, user *model.User) (port
|
|||||||
return nil, errors.WithStack(ErrForgeNotAvailable)
|
return nil, errors.WithStack(ErrForgeNotAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.forgeCache.Set(user.AccessToken, forge, cache.DefaultExpiration)
|
||||||
|
|
||||||
return forge, nil
|
return forge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("no forge matching user found")
|
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 {
|
func NewIssueManager(llmClient llm.Client, forgeFactories ...ForgeFactory) *IssueManager {
|
||||||
return &IssueManager{
|
return &IssueManager{
|
||||||
llmClient: llmClient,
|
llmClient: llmClient,
|
||||||
forgeFactories: forgeFactories,
|
forgeFactories: forgeFactories,
|
||||||
projectCache: cache.New[[]*model.Project](time.Minute*5, (time.Minute*5)/2),
|
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.
|
- Expected behavior.
|
||||||
- Actual behavior.
|
- Actual behavior.
|
||||||
- Any relevant error messages or logs.
|
- 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**:
|
2. **Additional Context**:
|
||||||
- Include any other relevant information that might help in understanding or resolving the issue/request.
|
- Include any other relevant information that might help in understanding or resolving the issue/request.
|
||||||
|
|
||||||
|
3. Let think step by step.
|
||||||
|
|
||||||
**Markdown Layout:**
|
**Markdown Layout:**
|
||||||
|
|
||||||
```markdown
|
```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="columns">
|
||||||
<div class="column is-4">
|
<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>
|
<h2 class="title is-size-2">Résumé de la demande</h2>
|
||||||
@common.FormSelect(
|
@common.FormSelect(
|
||||||
vmodel.SummaryForm, "issue-project", "project", "Projet",
|
vmodel.SummaryForm, "issue-project", "project", "Projet",
|
||||||
@ -179,7 +179,7 @@ templ IssuePage(vmodel IssuePageVModel) {
|
|||||||
func projectsToOptions(projects []*model.Project) []string {
|
func projectsToOptions(projects []*model.Project) []string {
|
||||||
options := make([]string, 0, len(projects)*2)
|
options := make([]string, 0, len(projects)*2)
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
options = append(options, p.Label, p.ID)
|
options = append(options, p.Name, p.ID)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
|
|||||||
func projectsToOptions(projects []*model.Project) []string {
|
func projectsToOptions(projects []*model.Project) []string {
|
||||||
options := make([]string, 0, len(projects)*2)
|
options := make([]string, 0, len(projects)*2)
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
options = append(options, p.Label, p.ID)
|
options = append(options, p.Name, p.ID)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user