Skip to content

Commit 515d22a

Browse files
committed
Add conversation history support to chat modes
The interactive chat modes now maintain conversation context across exchanges by tracking user inputs and assistant responses. This allows for more natural multi-turn conversations where the assistant can reference previous messages in the current session. The changes introduce a new API method ChatWithMessagesContext that accepts conversation history and returns the assistant's response for history tracking. Both the readline-based and basic interactive modes now use this enhanced functionality. Signed-off-by: Eric Curtin <[email protected]>
1 parent fb80c6d commit 515d22a

File tree

2 files changed

+66
-108
lines changed

2 files changed

+66
-108
lines changed

cmd/cli/commands/run.go

Lines changed: 35 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
140140
AltPlaceholder: `Use """ to end multi-line input`,
141141
})
142142
if err != nil {
143-
// Fall back to basic input mode if readline initialization fails
144-
return generateInteractiveBasic(cmd, desktopClient, model)
143+
return err
145144
}
146145

147146
// Disable history if the environment variable is set
@@ -154,6 +153,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
154153

155154
var sb strings.Builder
156155
var multiline bool
156+
var conversationHistory []desktop.OpenAIChatMessage
157157

158158
// Add a helper function to handle file inclusion when @ is pressed
159159
// We'll implement a basic version here that shows a message when @ is pressed
@@ -245,7 +245,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
245245
}
246246
}()
247247

248-
err := chatWithMarkdownContext(chatCtx, cmd, desktopClient, model, userInput)
248+
assistantResponse, err := chatWithMarkdownContext(chatCtx, cmd, desktopClient, model, userInput, conversationHistory)
249249

250250
// Clean up signal handler
251251
signal.Stop(sigChan)
@@ -263,70 +263,22 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
263263
continue
264264
}
265265

266+
// Add the user message and assistant response to conversation history
267+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
268+
Role: "user",
269+
Content: userInput,
270+
})
271+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
272+
Role: "assistant",
273+
Content: assistantResponse,
274+
})
275+
266276
cmd.Println()
267277
sb.Reset()
268278
}
269279
}
270280
}
271281

272-
// generateInteractiveBasic provides a basic interactive mode (fallback)
273-
func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client, model string) error {
274-
scanner := bufio.NewScanner(os.Stdin)
275-
for {
276-
userInput, err := readMultilineInput(cmd, scanner)
277-
if err != nil {
278-
if err.Error() == "EOF" {
279-
break
280-
}
281-
return fmt.Errorf("Error reading input: %w", err)
282-
}
283-
284-
if strings.ToLower(strings.TrimSpace(userInput)) == "/bye" {
285-
break
286-
}
287-
288-
if strings.TrimSpace(userInput) == "" {
289-
continue
290-
}
291-
292-
// Create a cancellable context for the chat request
293-
// This allows us to cancel the request if the user presses Ctrl+C during response generation
294-
chatCtx, cancelChat := context.WithCancel(cmd.Context())
295-
296-
// Set up signal handler to cancel the context on Ctrl+C
297-
sigChan := make(chan os.Signal, 1)
298-
signal.Notify(sigChan, syscall.SIGINT)
299-
go func() {
300-
select {
301-
case <-sigChan:
302-
cancelChat()
303-
case <-chatCtx.Done():
304-
// Context cancelled, exit goroutine
305-
// Context cancelled, exit goroutine
306-
}
307-
}()
308-
309-
err = chatWithMarkdownContext(chatCtx, cmd, desktopClient, model, userInput)
310-
311-
cancelChat()
312-
signal.Stop(sigChan)
313-
cancelChat()
314-
315-
if err != nil {
316-
// Check if the error is due to context cancellation (Ctrl+C during response)
317-
if errors.Is(err, context.Canceled) {
318-
fmt.Println("\nUse Ctrl + d or /bye to exit.")
319-
} else {
320-
cmd.PrintErrln(handleClientError(err, "Failed to generate a response"))
321-
}
322-
continue
323-
}
324-
325-
cmd.Println()
326-
}
327-
return nil
328-
}
329-
330282
var (
331283
markdownRenderer *glamour.TermRenderer
332284
lastWidth int
@@ -509,40 +461,42 @@ func renderMarkdown(content string) (string, error) {
509461

510462
// chatWithMarkdown performs chat and streams the response with selective markdown rendering.
511463
func chatWithMarkdown(cmd *cobra.Command, client *desktop.Client, model, prompt string) error {
512-
return chatWithMarkdownContext(cmd.Context(), cmd, client, model, prompt)
464+
_, err := chatWithMarkdownContext(cmd.Context(), cmd, client, model, prompt, nil)
465+
return err
513466
}
514467

515468
// chatWithMarkdownContext performs chat with context support and streams the response with selective markdown rendering.
516-
func chatWithMarkdownContext(ctx context.Context, cmd *cobra.Command, client *desktop.Client, model, prompt string) error {
469+
// It accepts an optional conversation history and returns the assistant's response for history tracking.
470+
func chatWithMarkdownContext(ctx context.Context, cmd *cobra.Command, client *desktop.Client, model, prompt string, conversationHistory []desktop.OpenAIChatMessage) (string, error) {
517471
colorMode, _ := cmd.Flags().GetString("color")
518472
useMarkdown := shouldUseMarkdown(colorMode)
519473
debug, _ := cmd.Flags().GetBool("debug")
520474

521475
// Process file inclusions first (files referenced with @ symbol)
522476
prompt, err := processFileInclusions(prompt)
523477
if err != nil {
524-
return fmt.Errorf("failed to process file inclusions: %w", err)
478+
return "", fmt.Errorf("failed to process file inclusions: %w", err)
525479
}
526480

527481
var imageURLs []string
528482
cleanedPrompt, imgs, err := processImagesInPrompt(prompt)
529483
if err != nil {
530-
return fmt.Errorf("failed to process images: %w", err)
484+
return "", fmt.Errorf("failed to process images: %w", err)
531485
}
532486
prompt = cleanedPrompt
533487
imageURLs = imgs
534488

535489
if !useMarkdown {
536490
// Simple case: just stream as plain text
537-
return client.ChatWithContext(ctx, model, prompt, imageURLs, func(content string) {
491+
return client.ChatWithMessagesContext(ctx, model, conversationHistory, prompt, imageURLs, func(content string) {
538492
cmd.Print(content)
539493
}, false)
540494
}
541495

542496
// For markdown: use streaming buffer to render code blocks as they complete
543497
markdownBuffer := NewStreamingMarkdownBuffer()
544498

545-
err = client.ChatWithContext(ctx, model, prompt, imageURLs, func(content string) {
499+
assistantResponse, err := client.ChatWithMessagesContext(ctx, model, conversationHistory, prompt, imageURLs, func(content string) {
546500
// Use the streaming markdown buffer to intelligently render content
547501
rendered, err := markdownBuffer.AddContent(content, true)
548502
if err != nil {
@@ -556,15 +510,15 @@ func chatWithMarkdownContext(ctx context.Context, cmd *cobra.Command, client *de
556510
}
557511
}, true)
558512
if err != nil {
559-
return err
513+
return assistantResponse, err
560514
}
561515

562516
// Flush any remaining content from the markdown buffer
563517
if remaining, flushErr := markdownBuffer.Flush(true); flushErr == nil && remaining != "" {
564518
cmd.Print(remaining)
565519
}
566520

567-
return nil
521+
return assistantResponse, nil
568522
}
569523

570524
func newRunCmd() *cobra.Command {
@@ -641,14 +595,10 @@ func newRunCmd() *cobra.Command {
641595
return nil
642596
}
643597

644-
// Interactive mode for external OpenAI endpoint
645-
if term.IsTerminal(int(os.Stdin.Fd())) {
646-
termenv.SetDefaultOutput(
647-
termenv.NewOutput(asPrinter(cmd), termenv.WithColorCache(true)),
648-
)
649-
return generateInteractiveWithReadline(cmd, openaiClient, model)
650-
}
651-
return generateInteractiveBasic(cmd, openaiClient, model)
598+
termenv.SetDefaultOutput(
599+
termenv.NewOutput(asPrinter(cmd), termenv.WithColorCache(true)),
600+
)
601+
return generateInteractiveWithReadline(cmd, openaiClient, model)
652602
}
653603

654604
if _, err := ensureStandaloneRunnerAvailable(cmd.Context(), asPrinter(cmd), debug); err != nil {
@@ -746,19 +696,15 @@ func newRunCmd() *cobra.Command {
746696
return nil
747697
}
748698

749-
// Use enhanced readline-based interactive mode when terminal is available
750-
if term.IsTerminal(int(os.Stdin.Fd())) {
751-
// Initialize termenv with color caching before starting interactive session.
752-
// This queries the terminal background color once and caches it, preventing
753-
// OSC response sequences from appearing in stdin during the interactive loop.
754-
termenv.SetDefaultOutput(
755-
termenv.NewOutput(asPrinter(cmd), termenv.WithColorCache(true)),
756-
)
757-
return generateInteractiveWithReadline(cmd, desktopClient, model)
758-
}
699+
// Initialize termenv with color caching before starting interactive session.
700+
// This queries the terminal background color once and caches it, preventing
701+
// OSC response sequences from appearing in stdin during the interactive loop.
702+
termenv.SetDefaultOutput(
703+
termenv.NewOutput(asPrinter(cmd), termenv.WithColorCache(true)),
704+
)
705+
706+
return generateInteractiveWithReadline(cmd, desktopClient, model)
759707

760-
// Fall back to basic mode if not a terminal
761-
return generateInteractiveBasic(cmd, desktopClient, model)
762708
},
763709
ValidArgsFunction: completion.ModelNames(getDesktopClient, 1),
764710
}

cmd/cli/desktop/desktop.go

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -350,13 +350,14 @@ func (c *Client) Chat(model, prompt string, imageURLs []string, outputFunc func(
350350
return c.ChatWithContext(context.Background(), model, prompt, imageURLs, outputFunc, shouldUseMarkdown)
351351
}
352352

353-
// ChatWithContext performs a chat request with context support for cancellation and streams the response content with selective markdown rendering.
354-
func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imageURLs []string, outputFunc func(string), shouldUseMarkdown bool) error {
355-
// Build the message content - either simple string or multimodal array
353+
// ChatWithMessagesContext performs a chat request with conversation history and returns the assistant's response.
354+
// This allows maintaining conversation context across multiple exchanges.
355+
func (c *Client) ChatWithMessagesContext(ctx context.Context, model string, conversationHistory []OpenAIChatMessage, prompt string, imageURLs []string, outputFunc func(string), shouldUseMarkdown bool) (string, error) {
356+
// Build the current user message content - either simple string or multimodal array
356357
var messageContent interface{}
357358
if len(imageURLs) > 0 {
358359
// Multimodal message with images
359-
contentParts := make([]ContentPart, 0, len(imageURLs))
360+
contentParts := make([]ContentPart, 0, len(imageURLs)+1)
360361

361362
// Add all images first
362363
for _, imageURL := range imageURLs {
@@ -382,20 +383,23 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
382383
messageContent = prompt
383384
}
384385

386+
// Build messages array with conversation history plus current message
387+
messages := make([]OpenAIChatMessage, 0, len(conversationHistory)+1)
388+
messages = append(messages, conversationHistory...)
389+
messages = append(messages, OpenAIChatMessage{
390+
Role: "user",
391+
Content: messageContent,
392+
})
393+
385394
reqBody := OpenAIChatRequest{
386-
Model: model,
387-
Messages: []OpenAIChatMessage{
388-
{
389-
Role: "user",
390-
Content: messageContent,
391-
},
392-
},
393-
Stream: true,
395+
Model: model,
396+
Messages: messages,
397+
Stream: true,
394398
}
395399

396400
jsonData, err := json.Marshal(reqBody)
397401
if err != nil {
398-
return fmt.Errorf("error marshaling request: %w", err)
402+
return "", fmt.Errorf("error marshaling request: %w", err)
399403
}
400404

401405
completionsPath := c.modelRunner.OpenAIPathPrefix() + "/chat/completions"
@@ -407,13 +411,13 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
407411
bytes.NewReader(jsonData),
408412
)
409413
if err != nil {
410-
return c.handleQueryError(err, completionsPath)
414+
return "", c.handleQueryError(err, completionsPath)
411415
}
412416
defer resp.Body.Close()
413417

414418
if resp.StatusCode != http.StatusOK {
415419
body, _ := io.ReadAll(resp.Body)
416-
return fmt.Errorf("error response: status=%d body=%s", resp.StatusCode, body)
420+
return "", fmt.Errorf("error response: status=%d body=%s", resp.StatusCode, body)
417421
}
418422

419423
type chatPrinterState int
@@ -426,6 +430,7 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
426430
printerState := chatPrinterNone
427431
reasoningFmt := color.New().Add(color.Italic)
428432

433+
var assistantResponse strings.Builder
429434
var finalUsage *struct {
430435
CompletionTokens int `json:"completion_tokens"`
431436
PromptTokens int `json:"prompt_tokens"`
@@ -437,7 +442,7 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
437442
// Check if context was cancelled
438443
select {
439444
case <-ctx.Done():
440-
return ctx.Err()
445+
return assistantResponse.String(), ctx.Err()
441446
default:
442447
}
443448

@@ -458,7 +463,7 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
458463

459464
var streamResp OpenAIChatResponse
460465
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
461-
return fmt.Errorf("error parsing stream response: %w", err)
466+
return assistantResponse.String(), fmt.Errorf("error parsing stream response: %w", err)
462467
}
463468

464469
if streamResp.Usage != nil {
@@ -493,12 +498,13 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
493498
}
494499
printerState = chatPrinterContent
495500
outputFunc(chunk)
501+
assistantResponse.WriteString(chunk)
496502
}
497503
}
498504
}
499505

500506
if err := scanner.Err(); err != nil {
501-
return fmt.Errorf("error reading response stream: %w", err)
507+
return assistantResponse.String(), fmt.Errorf("error reading response stream: %w", err)
502508
}
503509

504510
if finalUsage != nil {
@@ -514,7 +520,13 @@ func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imag
514520
outputFunc(usageFmt.Sprint(usageInfo))
515521
}
516522

517-
return nil
523+
return assistantResponse.String(), nil
524+
}
525+
526+
// ChatWithContext performs a chat request with context support for cancellation and streams the response content with selective markdown rendering.
527+
func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imageURLs []string, outputFunc func(string), shouldUseMarkdown bool) error {
528+
_, err := c.ChatWithMessagesContext(ctx, model, nil, prompt, imageURLs, outputFunc, shouldUseMarkdown)
529+
return err
518530
}
519531

520532
func (c *Client) Remove(modelArgs []string, force bool) (string, error) {

0 commit comments

Comments
 (0)