Files
Fabric/internal/core/chatter.go
Kayvan Sylvan ebc59ee82a refactor: move common package to domain and util packages for better organization
## CHANGES

- Move domain types from common to domain package
- Move utility functions from common to util package
- Update all import statements across codebase
- Reorganize OAuth storage functionality into util package
- Move file management functions to domain package
- Update test files to use new package structure
- Maintain backward compatibility for existing functionality
2025-07-08 23:26:11 -07:00

271 lines
7.6 KiB
Go

package core
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/danielmiessler/fabric/internal/chat"
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/plugins/ai"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
"github.com/danielmiessler/fabric/internal/plugins/strategy"
"github.com/danielmiessler/fabric/internal/plugins/template"
)
const NoSessionPatternUserMessages = "no session, pattern or user messages provided"
type Chatter struct {
db *fsdb.Db
Stream bool
DryRun bool
model string
modelContextLength int
vendor ai.Vendor
strategy string
}
// Send processes a chat request and applies file changes for create_coding_feature pattern
func (o *Chatter) Send(request *domain.ChatRequest, opts *domain.ChatOptions) (session *fsdb.Session, err error) {
modelToUse := opts.Model
if modelToUse == "" {
modelToUse = o.model
}
if o.vendor.NeedsRawMode(modelToUse) {
opts.Raw = true
}
if session, err = o.BuildSession(request, opts.Raw); err != nil {
return
}
vendorMessages := session.GetVendorMessages()
if len(vendorMessages) == 0 {
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
if err != nil {
return
}
}
err = fmt.Errorf("no messages provided")
return
}
if opts.Model == "" {
opts.Model = o.model
}
if opts.ModelContextLength == 0 {
opts.ModelContextLength = o.modelContextLength
}
message := ""
if o.Stream {
responseChan := make(chan string)
errChan := make(chan error, 1)
done := make(chan struct{})
go func() {
defer close(done)
if streamErr := o.vendor.SendStream(session.GetVendorMessages(), opts, responseChan); streamErr != nil {
errChan <- streamErr
}
}()
for response := range responseChan {
message += response
fmt.Print(response)
}
// Wait for goroutine to finish
<-done
// Check for errors in errChan
select {
case streamErr := <-errChan:
if streamErr != nil {
err = streamErr
return
}
default:
// No errors, continue
}
} else {
if message, err = o.vendor.Send(context.Background(), session.GetVendorMessages(), opts); err != nil {
return
}
}
if message == "" {
session = nil
err = fmt.Errorf("empty response")
return
}
// Process file changes for create_coding_feature pattern
if request.PatternName == "create_coding_feature" {
summary, fileChanges, parseErr := domain.ParseFileChanges(message)
if parseErr != nil {
fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr)
} else if len(fileChanges) > 0 {
projectRoot, err := os.Getwd()
if err != nil {
fmt.Printf("Warning: Failed to get current directory: %v\n", err)
} else {
if applyErr := domain.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil {
fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr)
} else {
fmt.Println("Successfully applied file changes.")
fmt.Printf("You can review the changes with 'git diff' if you're using git.\n\n")
}
}
}
message = summary
}
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleAssistant, Content: message})
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
}
return
}
func (o *Chatter) BuildSession(request *domain.ChatRequest, raw bool) (session *fsdb.Session, err error) {
if request.SessionName != "" {
var sess *fsdb.Session
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
err = fmt.Errorf("could not find session %s: %v", request.SessionName, err)
return
}
session = sess
} else {
session = &fsdb.Session{}
}
if request.Meta != "" {
session.Append(&chat.ChatCompletionMessage{Role: domain.ChatMessageRoleMeta, Content: request.Meta})
}
// if a context name is provided, retrieve it from the database
var contextContent string
if request.ContextName != "" {
var ctx *fsdb.Context
if ctx, err = o.db.Contexts.Get(request.ContextName); err != nil {
err = fmt.Errorf("could not find context %s: %v", request.ContextName, err)
return
}
contextContent = ctx.Content
}
// Process template variables in message content
// Double curly braces {{variable}} indicate template substitution
// Ensure we have a message before processing
if request.Message == nil {
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: " ",
}
}
// Now we know request.Message is not nil, process template variables
if request.InputHasVars {
request.Message.Content, err = template.ApplyTemplate(request.Message.Content, request.PatternVariables, "")
if err != nil {
return nil, err
}
}
var patternContent string
inputUsed := false
if request.PatternName != "" {
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
inputUsed = true
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
if request.StrategyName != "" {
strategy, err := strategy.LoadStrategy(request.StrategyName)
if err != nil {
return nil, fmt.Errorf("could not load strategy %s: %v", request.StrategyName, err)
}
if strategy != nil && strategy.Prompt != "" {
// prepend the strategy prompt to the system message
systemMessage = fmt.Sprintf("%s\n%s", strategy.Prompt, systemMessage)
}
}
// Apply refined language instruction if specified
if request.Language != "" && request.Language != "en" {
// Refined instruction: Execute pattern using user input, then translate the entire response.
systemMessage = fmt.Sprintf("%s\n\nIMPORTANT: First, execute the instructions provided in this prompt using the user's input. Second, ensure your entire final response, including any section headers or titles generated as part of executing the instructions, is written ONLY in the %s language.", systemMessage, request.Language)
}
if raw {
var finalContent string
if systemMessage != "" {
if request.PatternName != "" {
finalContent = systemMessage
} else {
finalContent = fmt.Sprintf("%s\n\n%s", systemMessage, request.Message.Content)
}
// Handle MultiContent properly in raw mode
if len(request.Message.MultiContent) > 0 {
// When we have attachments, add the text as a text part in MultiContent
newMultiContent := []chat.ChatMessagePart{
{
Type: chat.ChatMessagePartTypeText,
Text: finalContent,
},
}
// Add existing non-text parts (like images)
for _, part := range request.Message.MultiContent {
if part.Type != chat.ChatMessagePartTypeText {
newMultiContent = append(newMultiContent, part)
}
}
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
MultiContent: newMultiContent,
}
} else {
// No attachments, use regular Content field
request.Message = &chat.ChatCompletionMessage{
Role: chat.ChatMessageRoleUser,
Content: finalContent,
}
}
}
if request.Message != nil {
session.Append(request.Message)
}
} else {
if systemMessage != "" {
session.Append(&chat.ChatCompletionMessage{Role: chat.ChatMessageRoleSystem, Content: systemMessage})
}
// If multi-part content, it is in the user message, and should be added.
// Otherwise, we should only add it if we have not already used it in the systemMessage.
if len(request.Message.MultiContent) > 0 || (request.Message != nil && !inputUsed) {
session.Append(request.Message)
}
}
if session.IsEmpty() {
session = nil
err = errors.New(NoSessionPatternUserMessages)
}
return
}