feat: initial commit
This commit is contained in:
6
internal/core/model/issue.go
Normal file
6
internal/core/model/issue.go
Normal file
@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Issue struct {
|
||||
ID string
|
||||
Content string
|
||||
}
|
6
internal/core/model/project.go
Normal file
6
internal/core/model/project.go
Normal file
@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Project struct {
|
||||
ID string
|
||||
Label string
|
||||
}
|
8
internal/core/model/user.go
Normal file
8
internal/core/model/user.go
Normal file
@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Provider string
|
||||
AccessToken string
|
||||
IDToken string
|
||||
}
|
14
internal/core/port/forge.go
Normal file
14
internal/core/port/forge.go
Normal file
@ -0,0 +1,14 @@
|
||||
package port
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||
)
|
||||
|
||||
type Forge interface {
|
||||
GetProjects(ctx context.Context) ([]*model.Project, error)
|
||||
CreateIssue(ctx context.Context, projectID string, title string, content string) error
|
||||
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
|
||||
GetIssueTemplate(ctx context.Context, projectID string) (string, error)
|
||||
}
|
151
internal/core/service/issue_manager.go
Normal file
151
internal/core/service/issue_manager.go
Normal file
@ -0,0 +1,151 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||
"github.com/bornholm/genai/llm"
|
||||
"github.com/num30/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrForgeNotAvailable = errors.New("forge not available")
|
||||
)
|
||||
|
||||
//go:embed issue_system_prompt.gotmpl
|
||||
var issueSystemPromptRawTemplate string
|
||||
|
||||
//go:embed issue_user_prompt.gotmpl
|
||||
var issueUserPromptRawTemplate string
|
||||
|
||||
type ForgeFactory interface {
|
||||
Match(user *model.User) bool
|
||||
Create(ctx context.Context, user *model.User) (port.Forge, error)
|
||||
}
|
||||
|
||||
type IssueManager struct {
|
||||
forgeFactories []ForgeFactory
|
||||
llmClient llm.Client
|
||||
projectCache *cache.Cache[[]*model.Project]
|
||||
}
|
||||
|
||||
func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([]*model.Project, error) {
|
||||
cacheKey := fmt.Sprintf("%s/%s", user.Provider, user.ID)
|
||||
|
||||
projects, exists := m.projectCache.Get(cacheKey)
|
||||
if !exists {
|
||||
forge, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
refreshedProjects, err := forge.GetProjects(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
m.projectCache.Set(cacheKey, refreshedProjects, 0)
|
||||
|
||||
projects = refreshedProjects
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
|
||||
systemPrompt, err := m.getIssueSystemPrompt(ctx, user, projectID)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
messages := []llm.Message{
|
||||
llm.NewMessage(llm.RoleSystem, systemPrompt),
|
||||
llm.NewMessage(llm.RoleUser, userPrompt),
|
||||
}
|
||||
|
||||
res, err := m.llmClient.ChatCompletion(ctx, llm.WithMessages(messages...))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return res.Message().Content(), nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.User, projectID string) (string, error) {
|
||||
forge, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
issueTemplate, err := forge.GetIssueTemplate(ctx, projectID)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
systemPrompt, err := llm.PromptTemplate(issueSystemPromptRawTemplate, struct {
|
||||
IssueTemplate string
|
||||
}{
|
||||
IssueTemplate: issueTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return systemPrompt, nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) getIssueUserPrompt(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
|
||||
_, err := m.getUserForge(ctx, user)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
userPrompt, err := llm.PromptTemplate(issueUserPromptRawTemplate, struct {
|
||||
Context string
|
||||
}{
|
||||
Context: issueSummary,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return userPrompt, nil
|
||||
}
|
||||
|
||||
func (m *IssueManager) getUserForge(ctx context.Context, user *model.User) (port.Forge, error) {
|
||||
for _, f := range m.forgeFactories {
|
||||
if !f.Match(user) {
|
||||
continue
|
||||
}
|
||||
|
||||
forge, err := f.Create(ctx, user)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not retrieve user forge", slog.Any("error", errors.WithStack(err)))
|
||||
return nil, errors.WithStack(ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return forge, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("no forge matching user found")
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
20
internal/core/service/issue_system_prompt.gotmpl
Normal file
20
internal/core/service/issue_system_prompt.gotmpl
Normal file
@ -0,0 +1,20 @@
|
||||
You are an expert software developer with extensive experience in writing clear and comprehensive issues and requests for software forges. Your task is to create well-structured issues/requests based on the provided contextual information, following a predefined Markdown layout.
|
||||
|
||||
**Instructions:**
|
||||
|
||||
1. **Issue/request Description**:
|
||||
- Provide a detailed description of the issue/request, including:
|
||||
- Background information.
|
||||
- Steps to reproduce the issue.
|
||||
- Expected behavior.
|
||||
- Actual behavior.
|
||||
- Any relevant error messages or logs.
|
||||
|
||||
2. **Additional Context**:
|
||||
- Include any other relevant information that might help in understanding or resolving the issue/request.
|
||||
|
||||
**Markdown Layout:**
|
||||
|
||||
```markdown
|
||||
{{ .IssueTemplate }}
|
||||
```
|
3
internal/core/service/issue_user_prompt.gotmpl
Normal file
3
internal/core/service/issue_user_prompt.gotmpl
Normal file
@ -0,0 +1,3 @@
|
||||
Write a formatted issue/request based on theses contextual informations:
|
||||
|
||||
{{ .Context }}
|
Reference in New Issue
Block a user