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 }