add support for multiple system prompt files
This commit is contained in:
@@ -30,14 +30,14 @@ pub struct ClaudeConfig {
|
||||
/// Path to file containing the Claude API key.
|
||||
#[conf(long, env)]
|
||||
pub api_key_file: PathBuf,
|
||||
/// Path to file containing the system prompt.
|
||||
#[conf(long, env)]
|
||||
pub system_prompt_file: PathBuf,
|
||||
/// Paths to files containing system prompt components (in order, last one is cached).
|
||||
#[conf(repeat, long, env)]
|
||||
pub system_prompt_files: Vec<PathBuf>,
|
||||
/// Claude API URL.
|
||||
#[conf(long, env, default_value = "https://api.anthropic.com/v1/messages")]
|
||||
pub claude_api_url: String,
|
||||
/// Claude model to use.
|
||||
#[conf(long, env, default_value = "claude-opus-4-20250514")]
|
||||
#[conf(long, env, default_value = "claude-sonnet-4-5-20250929")]
|
||||
pub claude_model: String,
|
||||
/// Maximum tokens in the response.
|
||||
#[conf(long, env, default_value = "1024")]
|
||||
@@ -54,8 +54,8 @@ pub enum ClaudeError {
|
||||
#[error("failed to read API key file: {0}")]
|
||||
ApiKeyRead(std::io::Error),
|
||||
/// Failed to read system prompt file.
|
||||
#[error("failed to read system prompt file: {0}")]
|
||||
SystemPromptRead(std::io::Error),
|
||||
#[error("failed to read system prompt file {0:?}: {1}")]
|
||||
SystemPromptRead(PathBuf, std::io::Error),
|
||||
/// HTTP request failed.
|
||||
#[error("HTTP request failed: {0}")]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
@@ -75,7 +75,7 @@ pub struct ClaudeWorker {
|
||||
config: ClaudeConfig,
|
||||
client: reqwest::Client,
|
||||
api_key: String,
|
||||
system_prompt: String,
|
||||
system_prompts: Vec<String>,
|
||||
// FIXME: use this and append to system prompt within <summary> </summary> tags
|
||||
#[allow(dead_code)]
|
||||
summary: String,
|
||||
@@ -88,7 +88,7 @@ pub struct ClaudeWorker {
|
||||
impl ClaudeWorker {
|
||||
/// Create a new Claude worker.
|
||||
///
|
||||
/// Reads the API key and system prompt from the configured files.
|
||||
/// Reads the API key and system prompts from the configured files.
|
||||
pub fn new(
|
||||
config: ClaudeConfig,
|
||||
tool_executor: Weak<dyn ToolExecutor>,
|
||||
@@ -100,14 +100,20 @@ impl ClaudeWorker {
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let system_prompt = std::fs::read_to_string(&config.system_prompt_file)
|
||||
.map_err(ClaudeError::SystemPromptRead)?;
|
||||
let system_prompts: Vec<String> = config
|
||||
.system_prompt_files
|
||||
.iter()
|
||||
.map(|path| {
|
||||
std::fs::read_to_string(path)
|
||||
.map_err(|e| ClaudeError::SystemPromptRead(path.clone(), e))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
client: reqwest::Client::new(),
|
||||
api_key,
|
||||
system_prompt,
|
||||
system_prompts,
|
||||
summary: String::new(),
|
||||
messages: Default::default(),
|
||||
tool_executor,
|
||||
@@ -211,6 +217,21 @@ impl ClaudeWorker {
|
||||
info!("Claude request: {}", text);
|
||||
}
|
||||
|
||||
// Build system content blocks, caching only the last one
|
||||
let system: Vec<SystemContent> = self
|
||||
.system_prompts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, text)| {
|
||||
let content = SystemContent::text(text);
|
||||
if i == self.system_prompts.len() - 1 {
|
||||
content.cached()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for iteration in 0..max_iterations {
|
||||
// Check for stop before making API call
|
||||
self.check_stop()?;
|
||||
@@ -218,7 +239,7 @@ impl ClaudeWorker {
|
||||
let request_body = MessagesRequest {
|
||||
model: &self.config.claude_model,
|
||||
max_tokens: self.config.claude_max_tokens,
|
||||
system: &self.system_prompt,
|
||||
system: &system,
|
||||
messages: &self.messages,
|
||||
tools: tools.clone(),
|
||||
};
|
||||
@@ -314,12 +335,52 @@ impl ClaudeWorker {
|
||||
struct MessagesRequest<'a> {
|
||||
model: &'a str,
|
||||
max_tokens: u32,
|
||||
system: &'a str,
|
||||
system: &'a [SystemContent],
|
||||
messages: &'a [MessageContent],
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
tools: Vec<Tool>,
|
||||
}
|
||||
|
||||
/// A content block in the system prompt array.
|
||||
#[derive(Clone, Serialize)]
|
||||
struct SystemContent {
|
||||
#[serde(rename = "type")]
|
||||
content_type: &'static str,
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
}
|
||||
|
||||
impl SystemContent {
|
||||
fn text(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
content_type: "text",
|
||||
text: text.into(),
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cached(mut self) -> Self {
|
||||
self.cache_control = Some(CacheControl::ephemeral());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache control directive for prompt caching.
|
||||
#[derive(Clone, Serialize)]
|
||||
struct CacheControl {
|
||||
#[serde(rename = "type")]
|
||||
cache_type: &'static str,
|
||||
}
|
||||
|
||||
impl CacheControl {
|
||||
fn ephemeral() -> Self {
|
||||
Self {
|
||||
cache_type: "ephemeral",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in the conversation (can have multiple content blocks).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageContent {
|
||||
|
||||
Reference in New Issue
Block a user