mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Add arc llm chat subcommand for interactive multi-turn conversations
Adds a new `chat` subcommand under `arc llm` that reads user input in a loop, maintains conversation history, streams LLM responses, and supports `--model` and `--system` options. Includes an e2e test verifying multi-turn context and system prompt behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6dd422186e
commit
095dd5ff3c
3 changed files with 109 additions and 1 deletions
|
|
@ -54,6 +54,8 @@ enum RunCommand {
|
|||
enum LlmCommand {
|
||||
/// Execute a prompt
|
||||
Prompt(arc_llm::cli::PromptArgs),
|
||||
/// Interactive multi-turn chat
|
||||
Chat(arc_llm::cli::ChatArgs),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -80,6 +82,7 @@ async fn main() -> Result<()> {
|
|||
match cli.command {
|
||||
Command::Llm { command } => match command {
|
||||
LlmCommand::Prompt(args) => arc_llm::cli::run_prompt(args).await?,
|
||||
LlmCommand::Chat(args) => arc_llm::cli::run_chat(args).await?,
|
||||
},
|
||||
Command::Agent(args) => arc_agent::cli::run_with_args(args).await?,
|
||||
Command::Run { command } => match command {
|
||||
|
|
|
|||
|
|
@ -248,6 +248,48 @@ fn prompt_schema_stream_generates_json() {
|
|||
);
|
||||
}
|
||||
|
||||
// == LLM: chat ================================================================
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires API key"]
|
||||
fn chat_multi_turn_with_system_prompt() {
|
||||
let assert = arc()
|
||||
.args([
|
||||
"llm",
|
||||
"chat",
|
||||
"-m",
|
||||
"claude-haiku-4-5",
|
||||
"-s",
|
||||
"You are a pilot. End every response with 'Roger that.'",
|
||||
])
|
||||
.write_stdin("What is your profession?\nWhat did I just ask you?\n")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
|
||||
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
|
||||
|
||||
// Verify model info printed to stderr
|
||||
assert!(
|
||||
stderr.contains("Using model:"),
|
||||
"stderr should show model info"
|
||||
);
|
||||
|
||||
// Verify the system prompt influenced the output
|
||||
assert!(
|
||||
stdout.to_lowercase().contains("roger that"),
|
||||
"response should follow pilot system prompt, got: {stdout}"
|
||||
);
|
||||
|
||||
// Verify multi-turn: the second response should reference the first question
|
||||
assert!(
|
||||
stdout.to_lowercase().contains("profession")
|
||||
|| stdout.to_lowercase().contains("asked")
|
||||
|| stdout.to_lowercase().contains("pilot"),
|
||||
"second response should show multi-turn context, got: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
// == Agent ====================================================================
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::io::{self, IsTerminal, Read};
|
||||
use std::io::{self, BufRead, IsTerminal, Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
|
@ -7,6 +7,7 @@ use futures::StreamExt;
|
|||
|
||||
use crate::catalog;
|
||||
use crate::generate::{self, GenerateParams};
|
||||
use crate::types::Message;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct PromptArgs {
|
||||
|
|
@ -169,6 +170,68 @@ fn print_usage(usage: &crate::types::Usage) {
|
|||
);
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ChatArgs {
|
||||
/// Model to use
|
||||
#[arg(short, long)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// System prompt
|
||||
#[arg(short, long)]
|
||||
pub system: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run_chat(args: ChatArgs) -> Result<()> {
|
||||
let (model_id, provider) = resolve_model(args.model);
|
||||
eprintln!("Using model: {model_id}");
|
||||
|
||||
let mut messages: Vec<Message> = Vec::new();
|
||||
let stdin = io::stdin();
|
||||
let mut lines = stdin.lock().lines();
|
||||
|
||||
loop {
|
||||
eprint!("> ");
|
||||
io::stderr().flush()?;
|
||||
|
||||
let line = match lines.next() {
|
||||
Some(Ok(line)) => line,
|
||||
Some(Err(e)) => return Err(e.into()),
|
||||
None => break, // EOF
|
||||
};
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.push(Message::user(trimmed));
|
||||
|
||||
let mut params = GenerateParams::new(&model_id)
|
||||
.messages(messages.clone())
|
||||
.max_tokens(4096);
|
||||
if let Some(ref p) = provider {
|
||||
params = params.provider(p);
|
||||
}
|
||||
if let Some(ref sys) = args.system {
|
||||
params = params.system(sys);
|
||||
}
|
||||
|
||||
let mut stream_result = generate::stream(params).await?;
|
||||
let mut full_text = String::new();
|
||||
while let Some(event) = stream_result.next().await {
|
||||
if let crate::types::StreamEvent::TextDelta { delta, .. } = event? {
|
||||
print!("{delta}");
|
||||
full_text.push_str(&delta);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
messages.push(Message::assistant(full_text));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_prompt(args: PromptArgs) -> Result<()> {
|
||||
let stdin_prompt = read_stdin_prompt();
|
||||
let prompt_text = resolve_prompt(args.prompt, stdin_prompt)?;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue