Skip to content

Commit 03d7d68

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 03d7d68

File tree

2 files changed

+208
-10
lines changed

2 files changed

+208
-10
lines changed

cmd/cli/commands/run.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
154154

155155
var sb strings.Builder
156156
var multiline bool
157+
var conversationHistory []desktop.OpenAIChatMessage
157158

158159
// Add a helper function to handle file inclusion when @ is pressed
159160
// We'll implement a basic version here that shows a message when @ is pressed
@@ -245,7 +246,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
245246
}
246247
}()
247248

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

250251
// Clean up signal handler
251252
signal.Stop(sigChan)
@@ -263,6 +264,16 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
263264
continue
264265
}
265266

267+
// Add the user message and assistant response to conversation history
268+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
269+
Role: "user",
270+
Content: userInput,
271+
})
272+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
273+
Role: "assistant",
274+
Content: assistantResponse,
275+
})
276+
266277
cmd.Println()
267278
sb.Reset()
268279
}
@@ -272,6 +283,8 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
272283
// generateInteractiveBasic provides a basic interactive mode (fallback)
273284
func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client, model string) error {
274285
scanner := bufio.NewScanner(os.Stdin)
286+
var conversationHistory []desktop.OpenAIChatMessage
287+
275288
for {
276289
userInput, err := readMultilineInput(cmd, scanner)
277290
if err != nil {
@@ -306,7 +319,7 @@ func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client,
306319
}
307320
}()
308321

309-
err = chatWithMarkdownContext(chatCtx, cmd, desktopClient, model, userInput)
322+
assistantResponse, err := chatWithMarkdownContext(chatCtx, cmd, desktopClient, model, userInput, conversationHistory)
310323

311324
cancelChat()
312325
signal.Stop(sigChan)
@@ -322,6 +335,16 @@ func generateInteractiveBasic(cmd *cobra.Command, desktopClient *desktop.Client,
322335
continue
323336
}
324337

338+
// Add the user message and assistant response to conversation history
339+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
340+
Role: "user",
341+
Content: userInput,
342+
})
343+
conversationHistory = append(conversationHistory, desktop.OpenAIChatMessage{
344+
Role: "assistant",
345+
Content: assistantResponse,
346+
})
347+
325348
cmd.Println()
326349
}
327350
return nil
@@ -509,40 +532,42 @@ func renderMarkdown(content string) (string, error) {
509532

510533
// chatWithMarkdown performs chat and streams the response with selective markdown rendering.
511534
func chatWithMarkdown(cmd *cobra.Command, client *desktop.Client, model, prompt string) error {
512-
return chatWithMarkdownContext(cmd.Context(), cmd, client, model, prompt)
535+
_, err := chatWithMarkdownContext(cmd.Context(), cmd, client, model, prompt, nil)
536+
return err
513537
}
514538

515539
// 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 {
540+
// It accepts an optional conversation history and returns the assistant's response for history tracking.
541+
func chatWithMarkdownContext(ctx context.Context, cmd *cobra.Command, client *desktop.Client, model, prompt string, conversationHistory []desktop.OpenAIChatMessage) (string, error) {
517542
colorMode, _ := cmd.Flags().GetString("color")
518543
useMarkdown := shouldUseMarkdown(colorMode)
519544
debug, _ := cmd.Flags().GetBool("debug")
520545

521546
// Process file inclusions first (files referenced with @ symbol)
522547
prompt, err := processFileInclusions(prompt)
523548
if err != nil {
524-
return fmt.Errorf("failed to process file inclusions: %w", err)
549+
return "", fmt.Errorf("failed to process file inclusions: %w", err)
525550
}
526551

527552
var imageURLs []string
528553
cleanedPrompt, imgs, err := processImagesInPrompt(prompt)
529554
if err != nil {
530-
return fmt.Errorf("failed to process images: %w", err)
555+
return "", fmt.Errorf("failed to process images: %w", err)
531556
}
532557
prompt = cleanedPrompt
533558
imageURLs = imgs
534559

535560
if !useMarkdown {
536561
// Simple case: just stream as plain text
537-
return client.ChatWithContext(ctx, model, prompt, imageURLs, func(content string) {
562+
return client.ChatWithMessagesContext(ctx, model, conversationHistory, prompt, imageURLs, func(content string) {
538563
cmd.Print(content)
539564
}, false)
540565
}
541566

542567
// For markdown: use streaming buffer to render code blocks as they complete
543568
markdownBuffer := NewStreamingMarkdownBuffer()
544569

545-
err = client.ChatWithContext(ctx, model, prompt, imageURLs, func(content string) {
570+
assistantResponse, err := client.ChatWithMessagesContext(ctx, model, conversationHistory, prompt, imageURLs, func(content string) {
546571
// Use the streaming markdown buffer to intelligently render content
547572
rendered, err := markdownBuffer.AddContent(content, true)
548573
if err != nil {
@@ -556,15 +581,15 @@ func chatWithMarkdownContext(ctx context.Context, cmd *cobra.Command, client *de
556581
}
557582
}, true)
558583
if err != nil {
559-
return err
584+
return assistantResponse, err
560585
}
561586

562587
// Flush any remaining content from the markdown buffer
563588
if remaining, flushErr := markdownBuffer.Flush(true); flushErr == nil && remaining != "" {
564589
cmd.Print(remaining)
565590
}
566591

567-
return nil
592+
return assistantResponse, nil
568593
}
569594

570595
func newRunCmd() *cobra.Command {

cmd/cli/desktop/desktop.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,179 @@ 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+
// 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
357+
var messageContent interface{}
358+
if len(imageURLs) > 0 {
359+
// Multimodal message with images
360+
contentParts := make([]ContentPart, 0, len(imageURLs))
361+
362+
// Add all images first
363+
for _, imageURL := range imageURLs {
364+
contentParts = append(contentParts, ContentPart{
365+
Type: "image_url",
366+
ImageURL: &ImageURL{
367+
URL: imageURL,
368+
},
369+
})
370+
}
371+
372+
// Add text prompt if present
373+
if prompt != "" {
374+
contentParts = append(contentParts, ContentPart{
375+
Type: "text",
376+
Text: prompt,
377+
})
378+
}
379+
380+
messageContent = contentParts
381+
} else {
382+
// Simple text-only message
383+
messageContent = prompt
384+
}
385+
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+
394+
reqBody := OpenAIChatRequest{
395+
Model: model,
396+
Messages: messages,
397+
Stream: true,
398+
}
399+
400+
jsonData, err := json.Marshal(reqBody)
401+
if err != nil {
402+
return "", fmt.Errorf("error marshaling request: %w", err)
403+
}
404+
405+
completionsPath := c.modelRunner.OpenAIPathPrefix() + "/chat/completions"
406+
407+
resp, err := c.doRequestWithAuthContext(
408+
ctx,
409+
http.MethodPost,
410+
completionsPath,
411+
bytes.NewReader(jsonData),
412+
)
413+
if err != nil {
414+
return "", c.handleQueryError(err, completionsPath)
415+
}
416+
defer resp.Body.Close()
417+
418+
if resp.StatusCode != http.StatusOK {
419+
body, _ := io.ReadAll(resp.Body)
420+
return "", fmt.Errorf("error response: status=%d body=%s", resp.StatusCode, body)
421+
}
422+
423+
type chatPrinterState int
424+
const (
425+
chatPrinterNone chatPrinterState = iota
426+
chatPrinterReasoning
427+
chatPrinterContent
428+
)
429+
430+
printerState := chatPrinterNone
431+
reasoningFmt := color.New().Add(color.Italic)
432+
433+
var assistantResponse strings.Builder
434+
var finalUsage *struct {
435+
CompletionTokens int `json:"completion_tokens"`
436+
PromptTokens int `json:"prompt_tokens"`
437+
TotalTokens int `json:"total_tokens"`
438+
}
439+
440+
scanner := bufio.NewScanner(resp.Body)
441+
for scanner.Scan() {
442+
// Check if context was cancelled
443+
select {
444+
case <-ctx.Done():
445+
return assistantResponse.String(), ctx.Err()
446+
default:
447+
}
448+
449+
line := scanner.Text()
450+
if line == "" {
451+
continue
452+
}
453+
454+
if !strings.HasPrefix(line, "data: ") {
455+
continue
456+
}
457+
458+
data := strings.TrimPrefix(line, "data: ")
459+
460+
if data == "[DONE]" {
461+
break
462+
}
463+
464+
var streamResp OpenAIChatResponse
465+
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
466+
return assistantResponse.String(), fmt.Errorf("error parsing stream response: %w", err)
467+
}
468+
469+
if streamResp.Usage != nil {
470+
finalUsage = streamResp.Usage
471+
}
472+
473+
if len(streamResp.Choices) > 0 {
474+
if streamResp.Choices[0].Delta.ReasoningContent != "" {
475+
chunk := streamResp.Choices[0].Delta.ReasoningContent
476+
if printerState == chatPrinterContent {
477+
outputFunc("\n\n")
478+
}
479+
if printerState != chatPrinterReasoning {
480+
const thinkingHeader = "Thinking:\n"
481+
if reasoningFmt != nil {
482+
reasoningFmt.Print(thinkingHeader)
483+
} else {
484+
outputFunc(thinkingHeader)
485+
}
486+
}
487+
printerState = chatPrinterReasoning
488+
if reasoningFmt != nil {
489+
reasoningFmt.Print(chunk)
490+
} else {
491+
outputFunc(chunk)
492+
}
493+
}
494+
if streamResp.Choices[0].Delta.Content != "" {
495+
chunk := streamResp.Choices[0].Delta.Content
496+
if printerState == chatPrinterReasoning {
497+
outputFunc("\n\n--\n\n")
498+
}
499+
printerState = chatPrinterContent
500+
outputFunc(chunk)
501+
assistantResponse.WriteString(chunk)
502+
}
503+
}
504+
}
505+
506+
if err := scanner.Err(); err != nil {
507+
return assistantResponse.String(), fmt.Errorf("error reading response stream: %w", err)
508+
}
509+
510+
if finalUsage != nil {
511+
usageInfo := fmt.Sprintf("\n\nToken usage: %d prompt + %d completion = %d total",
512+
finalUsage.PromptTokens,
513+
finalUsage.CompletionTokens,
514+
finalUsage.TotalTokens)
515+
516+
usageFmt := color.New(color.FgHiBlack)
517+
if !shouldUseMarkdown {
518+
usageFmt.DisableColor()
519+
}
520+
outputFunc(usageFmt.Sprint(usageInfo))
521+
}
522+
523+
return assistantResponse.String(), nil
524+
}
525+
353526
// ChatWithContext performs a chat request with context support for cancellation and streams the response content with selective markdown rendering.
354527
func (c *Client) ChatWithContext(ctx context.Context, model, prompt string, imageURLs []string, outputFunc func(string), shouldUseMarkdown bool) error {
355528
// Build the message content - either simple string or multimodal array

0 commit comments

Comments
 (0)