Repository navigation
feat: add an interactive chat command - #86
ConnorMoss02 wants to merge 10 commits into
Conversation
yarolegovich
left a comment
There was a problem hiding this comment.
thanks for the PR! overall looks good, but I'd encourage to polish the overall chat UX further before merging it
| Short: "Chat with an agent, carrying the conversation across turns", | ||
| Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " + | ||
| "across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " + | ||
| "Ctrl-D to exit; the context and task ids are printed so you can resume with send.\n\n" + |
There was a problem hiding this comment.
let's wrap the context so that a running turn gets cancelled through ctx ( signal.NotifyContext(cmd.Context(), os.Interrupt)). this will however require moving lines.Scan() blocking call into a background goroutine
There was a problem hiding this comment.
Done in 936c233. Ctrl-C now cancels the running turn and drops you back at the prompt. Ctrl-C at the prompt exits and prints the ids. Input reads on a goroutine so the interrupt gets noticed.
| defer cancel() | ||
|
|
||
| result, err := s.client.SendMessage(ctx, &a2a.SendMessageRequest{Message: msg, Tenant: s.cfg.tenant}) | ||
| if err != nil { |
There was a problem hiding this comment.
I'd probably keep the chat session open on send message errors
There was a problem hiding this comment.
Done in 7d78c4c. A failed turn prints the error like send does and the chat keeps going. Auth-required still ends it.
| const chatQuit = "/quit" | ||
|
|
||
| func newChatCmd(cfg *globalConfig, isTerminal func() bool) *cobra.Command { | ||
| return &cobra.Command{ |
There was a problem hiding this comment.
let's add support for --context-id and --task-id to allow resuming a task in chat mode
There was a problem hiding this comment.
Added in 2554dae. Same flags and help text as send, and the first message goes out with those ids.
| ctx, cancel := context.WithTimeout(ctx, s.cfg.timeout) | ||
| defer cancel() | ||
|
|
||
| result, err := s.client.SendMessage(ctx, &a2a.SendMessageRequest{Message: msg, Tenant: s.cfg.tenant}) |
There was a problem hiding this comment.
we're not in a rush with releasing chat support, so would be nice to have --stream option supported here
There was a problem hiding this comment.
Added in e5288f1. I pulled the streaming code out of send into sendStreaming so both commands use it.
|
I also took a stab at sending CancelTask when a turn is interrupted (663dd60), so the agent stops working on it too. Happy to roll it back if you'd rather keep that out of this PR. |
| Args: cobra.MaximumNArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| interactive := deps.isTerminal() | ||
| if !interactive && len(args) == 0 { |
There was a problem hiding this comment.
I would just always fail for non-interactive. otherwise it's a send but with less functionality
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "chat [message]", | ||
| Short: "Chat with an agent, carrying the conversation across turns", |
There was a problem hiding this comment.
| Short: "Chat with an agent, carrying the conversation across turns", | |
| Short: "Chat interactively with an agent", |
| "github.com/a2aproject/a2a-go/v2/a2aclient" | ||
| ) | ||
|
|
||
| const chatQuit = "/quit" |
There was a problem hiding this comment.
/exit feels more natural to me. Can we have both /exit & /quit ?
| Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " + | ||
| "across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " + | ||
| "Ctrl-D to exit; the context and task ids are printed so you can resume with send, or with chat --context-id and --task-id. " + | ||
| "Ctrl-C cancels a running turn and asks the agent to cancel its task, or exits at the prompt.\n\n" + | ||
| "When standard input or output is not a terminal, chat sends [message] once, like send, and reads no input.", |
There was a problem hiding this comment.
| Long: "Chat with an agent interactively. Each reply continues the same conversation: the context id is carried " + | |
| "across turns, and an input-required task is continued with your next message. Type " + chatQuit + " or press " + | |
| "Ctrl-D to exit; the context and task ids are printed so you can resume with send, or with chat --context-id and --task-id. " + | |
| "Ctrl-C cancels a running turn and asks the agent to cancel its task, or exits at the prompt.\n\n" + | |
| "When standard input or output is not a terminal, chat sends [message] once, like send, and reads no input.", | |
| Long: "Start an interactive chat session with an agent. Chat reuses the same context ID " + | |
| "across turns and continues tasks that require input.\n\n" + | |
| "Controls:\n" + | |
| " " + chatQuit + ", Ctrl-D Exit and print the context and task IDs to resume later\n" + | |
| " Ctrl-C Cancel an active turn (and its agent task), or exit at the prompt\n\n" + | |
| "Non-interactive mode sends [message] once and exits. Use resume --context-id and --task-id to resume a conversation.", |
| f.BoolVar(&flags.stream, "stream", false, "Use streaming response. Falls back to polling and synthetic events if a server does not support streaming.") | ||
| f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task") | ||
| f.StringVar(&flags.contextID, "context-id", "", "Context ID to group this turn under") | ||
| f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Duration between GetTask requests in polling fallback mode.") |
There was a problem hiding this comment.
| f.BoolVar(&flags.stream, "stream", false, "Use streaming response. Falls back to polling and synthetic events if a server does not support streaming.") | |
| f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task") | |
| f.StringVar(&flags.contextID, "context-id", "", "Context ID to group this turn under") | |
| f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Duration between GetTask requests in polling fallback mode.") | |
| f.BoolVar(&flags.stream, "stream", false, "Stream responses (falls back to polling if unsupported)") | |
| f.StringVar(&flags.taskID, "task-id", "", "Task ID to continue an existing task") | |
| f.StringVar(&flags.contextID, "context-id", "", "Context ID to continue an existing conversation") | |
| f.DurationVar(&flags.pollInterval, "poll-interval", 2*time.Second, "Interval between GetTask requests when polling") |
|
@ConnorMoss02 Thank you for working on this. We appreciate this work :) I have shared a few comment to help with the help messages. |
Adds
a2a chat. It reads a message, sends it, prints the reply and loops. The context id carries across turns and an input-required task continues with your next message.--streamworks like it does forsend, and--context-idand--task-idpick up an existing conversation. Ctrl-C cancels a running turn, and a failed turn prints the error without ending the chat. It stops on auth-required and prints the ids on/quit, EOF or Ctrl-C at the prompt. When stdin or stdout isn't a terminal it sends once and exits.Fixes #34