/ go / internal / cli / chat.go
chat.go
  1  package cli
  2  
  3  import (
  4  	"bufio"
  5  	"context"
  6  	"fmt"
  7  	"os"
  8  	"strings"
  9  
 10  	"github.com/spf13/cobra"
 11  	"github.com/TransformerOS/kamaji-go/internal/config"
 12  	"github.com/TransformerOS/kamaji-go/internal/providers"
 13  	"github.com/TransformerOS/kamaji-go/internal/rag"
 14  	"github.com/TransformerOS/kamaji-go/internal/types"
 15  )
 16  
 17  var chatCmd = &cobra.Command{
 18  	Use:   "chat",
 19  	Short: "Start interactive chat with memory",
 20  	Long:  "Start an interactive chat session with conversation memory",
 21  	RunE:  runChat,
 22  }
 23  
 24  func init() {
 25  	chatCmd.Flags().Float64P("temperature", "t", 0.7, "Temperature for response generation (0.0 - 1.0)")
 26  	chatCmd.Flags().StringSliceP("files", "f", []string{}, "Files to load into context for RAG")
 27  }
 28  
 29  func runChat(cmd *cobra.Command, args []string) error {
 30  	files, _ := cmd.Flags().GetStringSlice("files")
 31  	
 32  	// Load config
 33  	cfg, err := config.Load()
 34  	if err != nil {
 35  		return fmt.Errorf("failed to load config: %w", err)
 36  	}
 37  
 38  	// Create LLM provider
 39  	var llm types.LLMProvider
 40  	switch cfg.Provider {
 41  	case "ollama":
 42  		llm, err = providers.NewOllamaProvider(cfg.BaseURL, cfg.Model)
 43  		if err != nil {
 44  			return fmt.Errorf("failed to create Ollama provider: %w", err)
 45  		}
 46  	case "anthropic":
 47  		apiKey := os.Getenv("ANTHROPIC_API_KEY")
 48  		if apiKey == "" {
 49  			return fmt.Errorf("ANTHROPIC_API_KEY not set")
 50  		}
 51  		llm, err = providers.NewAnthropicProvider(apiKey, cfg.Model)
 52  		if err != nil {
 53  			return fmt.Errorf("failed to create Anthropic provider: %w", err)
 54  		}
 55  	case "openai":
 56  		apiKey := os.Getenv("OPENAI_API_KEY")
 57  		if apiKey == "" {
 58  			return fmt.Errorf("OPENAI_API_KEY not set")
 59  		}
 60  		llm = providers.NewOpenAIProviderWrapper(apiKey, cfg.Model)
 61  	case "q":
 62  		llm, err = providers.GetQProvider()
 63  		if err != nil {
 64  			return fmt.Errorf("failed to create Q provider: %w", err)
 65  		}
 66  	default:
 67  		return fmt.Errorf("unsupported provider: %s", cfg.Provider)
 68  	}
 69  
 70  	// Load documents if files provided
 71  	var docStore *rag.DocumentStore
 72  	if len(files) > 0 {
 73  		docStore = rag.NewDocumentStore()
 74  		loadedCount := docStore.LoadDocuments(files)
 75  		if loadedCount > 0 {
 76  			fmt.Printf("\n๐Ÿ“‚ Loaded %d document(s) into context", loadedCount)
 77  		}
 78  	}
 79  
 80  	// Print header
 81  	fmt.Println("\n" + strings.Repeat("=", 60))
 82  	fmt.Println("๐Ÿ’ฌ Kamaji Interactive Chat")
 83  	fmt.Println(strings.Repeat("=", 60))
 84  	fmt.Printf("๐Ÿ“ Working directory: %s\n", mustGetwd())
 85  	if docStore != nil && docStore.HasDocuments() {
 86  		fmt.Printf("๐Ÿ“š Documents: %d loaded\n", len(files))
 87  	}
 88  	fmt.Println("\nCommands:")
 89  	fmt.Println("  exit, quit    - End conversation")
 90  	fmt.Println("  clear         - Clear conversation history")
 91  	if docStore != nil && docStore.HasDocuments() {
 92  		fmt.Println("  /docs         - List loaded documents")
 93  	}
 94  	fmt.Println(strings.Repeat("=", 60) + "\n")
 95  
 96  	// Conversation history
 97  	history := []string{}
 98  	scanner := bufio.NewScanner(os.Stdin)
 99  	ctx := context.Background()
100  
101  	for {
102  		fmt.Print("You: ")
103  		if !scanner.Scan() {
104  			break
105  		}
106  
107  		input := strings.TrimSpace(scanner.Text())
108  		if input == "" {
109  			continue
110  		}
111  
112  		if input == "exit" || input == "quit" || input == "q" {
113  			fmt.Println("\n๐Ÿ‘‹ Goodbye!\n")
114  			break
115  		}
116  
117  		if input == "clear" {
118  			history = []string{}
119  			fmt.Println("\n๐Ÿงน Conversation history cleared.\n")
120  			continue
121  		}
122  
123  		if input == "/docs" && docStore != nil && docStore.HasDocuments() {
124  			fmt.Println("\n" + docStore.ListDocuments() + "\n")
125  			continue
126  		}
127  
128  		// Add to history
129  		history = append(history, fmt.Sprintf("User: %s", input))
130  
131  		// Build context with history and documents
132  		prompt := buildChatPrompt(history, input, docStore)
133  
134  		fmt.Print("\nKamaji: ")
135  
136  		// Stream response
137  		responseChan, err := llm.CallStream(ctx, prompt)
138  		if err != nil {
139  			fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
140  			continue
141  		}
142  
143  		fullResponse := ""
144  		for chunk := range responseChan {
145  			if chunk.Error != nil {
146  				fmt.Fprintf(os.Stderr, "\nError: %v\n", chunk.Error)
147  				break
148  			}
149  			fmt.Print(chunk.Content)
150  			fullResponse += chunk.Content
151  		}
152  		fmt.Println("\n")
153  
154  		// Add assistant response to history
155  		history = append(history, fmt.Sprintf("Assistant: %s", fullResponse))
156  
157  		// Keep history manageable (last 10 exchanges)
158  		if len(history) > 20 {
159  			history = history[len(history)-20:]
160  		}
161  	}
162  
163  	if err := scanner.Err(); err != nil {
164  		return fmt.Errorf("scanner error: %w", err)
165  	}
166  
167  	return nil
168  }
169  
170  func buildChatPrompt(history []string, currentInput string, docStore *rag.DocumentStore) string {
171  	if len(history) == 0 {
172  		return currentInput
173  	}
174  
175  	// Build context from history
176  	var sb strings.Builder
177  	
178  	// Add document context if available
179  	if docStore != nil && docStore.HasDocuments() {
180  		docContext := docStore.Query(currentInput, 3)
181  		if docContext != "" {
182  			sb.WriteString("Context from documents:\n")
183  			sb.WriteString(docContext)
184  			sb.WriteString("\n\n")
185  		}
186  	}
187  	
188  	sb.WriteString("Previous conversation:\n")
189  	for _, msg := range history {
190  		sb.WriteString(msg)
191  		sb.WriteString("\n")
192  	}
193  	sb.WriteString("\nContinue the conversation naturally.\n")
194  	
195  	return sb.String()
196  }
197  
198  func mustGetwd() string {
199  	wd, err := os.Getwd()
200  	if err != nil {
201  		return "unknown"
202  	}
203  	return wd
204  }