feat: initial commit

This commit is contained in:
2025-02-22 09:42:15 +01:00
parent ee4a65b345
commit e6e5c9b04d
43 changed files with 1191 additions and 247 deletions

View File

@ -0,0 +1,6 @@
package model
type Issue struct {
ID string
Content string
}

View File

@ -0,0 +1,6 @@
package model
type Project struct {
ID string
Label string
}

View File

@ -0,0 +1,8 @@
package model
type User struct {
ID string
Provider string
AccessToken string
IDToken string
}

View 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)
}

View 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),
}
}

View 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 }}
```

View File

@ -0,0 +1,3 @@
Write a formatted issue/request based on theses contextual informations:
{{ .Context }}