feat: Implement Worker for LLM turn management/tool call/hooks

This commit is contained in:
2026-01-06 20:38:08 +09:00
parent a4e2795e56
commit e82e0a3ed9
13 changed files with 1330 additions and 71 deletions
@@ -0,0 +1,99 @@
//! テストフィクスチャ記録ツール
//!
//! 定義されたシナリオのAPIレスポンスを記録する。
//!
//! ## 使用方法
//!
//! ```bash
//! # 利用可能なシナリオを表示
//! cargo run --example record_test_fixtures
//!
//! # 特定のシナリオを記録
//! ANTHROPIC_API_KEY=your-key cargo run --example record_test_fixtures -- simple_text
//! ANTHROPIC_API_KEY=your-key cargo run --example record_test_fixtures -- tool_call
//!
//! # 全シナリオを記録
//! ANTHROPIC_API_KEY=your-key cargo run --example record_test_fixtures -- --all
//! ```
mod recorder;
mod scenarios;
use worker::llm_client::providers::anthropic::AnthropicClient;
fn print_usage() {
println!("Usage: cargo run --example record_test_fixtures -- <scenario_name>");
println!(" cargo run --example record_test_fixtures -- --all");
println!();
println!("Available scenarios:");
for scenario in scenarios::scenarios() {
println!(" {:20} - {}", scenario.output_name, scenario.name);
}
println!();
println!("Options:");
println!(" --all Record all scenarios");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
// 引数がなければ使い方を表示
if args.len() < 2 {
print_usage();
return Ok(());
}
let arg = &args[1];
// 全シナリオを取得
let all_scenarios = scenarios::scenarios();
// 実行するシナリオを決定
let scenarios_to_run: Vec<_> = if arg == "--all" {
all_scenarios
} else {
// 指定されたシナリオを検索
let found: Vec<_> = all_scenarios
.into_iter()
.filter(|s| s.output_name == arg)
.collect();
if found.is_empty() {
eprintln!("Error: Unknown scenario '{}'", arg);
println!();
print_usage();
std::process::exit(1);
}
found
};
// APIキーを取得
let api_key = std::env::var("ANTHROPIC_API_KEY")
.expect("ANTHROPIC_API_KEY environment variable must be set");
let model = "claude-sonnet-4-20250514";
println!("=== Test Fixture Generator ===");
println!("Model: {}", model);
println!("Scenarios: {}\n", scenarios_to_run.len());
let client = AnthropicClient::new(&api_key, model);
// シナリオを記録
for scenario in scenarios_to_run {
recorder::record_request(
&client,
scenario.request,
scenario.name,
scenario.output_name,
model,
)
.await?;
}
println!("\n✅ Done!");
println!("Run tests with: cargo test -p worker");
Ok(())
}
@@ -0,0 +1,100 @@
//! テストフィクスチャ記録機構
//!
//! イベントをJSONLフォーマットでファイルに保存する
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::Path;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use futures::StreamExt;
use worker::llm_client::{LlmClient, Request};
/// 記録されたイベント
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RecordedEvent {
pub elapsed_ms: u64,
pub event_type: String,
pub data: String,
}
/// セッションメタデータ
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SessionMetadata {
pub timestamp: u64,
pub model: String,
pub description: String,
}
/// イベントシーケンスをファイルに保存
pub fn save_fixture(
path: impl AsRef<Path>,
metadata: &SessionMetadata,
events: &[RecordedEvent],
) -> std::io::Result<()> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", serde_json::to_string(metadata)?)?;
for event in events {
writeln!(writer, "{}", serde_json::to_string(event)?)?;
}
writer.flush()?;
Ok(())
}
/// リクエストを送信してイベントを記録
pub async fn record_request<C: LlmClient>(
client: &C,
request: Request,
description: &str,
output_name: &str,
model: &str,
) -> Result<usize, Box<dyn std::error::Error>> {
println!("\n📝 Recording: {}", description);
let start_time = Instant::now();
let mut events: Vec<RecordedEvent> = Vec::new();
let mut stream = client.stream(request).await?;
while let Some(result) = stream.next().await {
let elapsed = start_time.elapsed().as_millis() as u64;
match result {
Ok(event) => {
let event_json = serde_json::to_string(&event)?;
println!(" [{:>6}ms] {:?}", elapsed, event);
events.push(RecordedEvent {
elapsed_ms: elapsed,
event_type: format!("{:?}", std::mem::discriminant(&event)),
data: event_json,
});
}
Err(e) => {
eprintln!(" Error: {}", e);
break;
}
}
}
// 保存
let fixtures_dir = Path::new("worker/tests/fixtures");
fs::create_dir_all(fixtures_dir)?;
let filepath = fixtures_dir.join(format!("{}.jsonl", output_name));
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let metadata = SessionMetadata {
timestamp,
model: model.to_string(),
description: description.to_string(),
};
save_fixture(&filepath, &metadata, &events)?;
let event_count = events.len();
println!(" 💾 Saved: {}", filepath.display());
println!(" 📊 {} events recorded", event_count);
Ok(event_count)
}
@@ -0,0 +1,61 @@
//! テストフィクスチャ用リクエスト定義
//!
//! 各シナリオのリクエストと出力ファイル名を定義
use worker::llm_client::{Request, ToolDefinition};
/// テストシナリオ
pub struct TestScenario {
/// シナリオ名(説明)
pub name: &'static str,
/// 出力ファイル名(拡張子なし)
pub output_name: &'static str,
/// リクエスト
pub request: Request,
}
/// 全てのテストシナリオを取得
pub fn scenarios() -> Vec<TestScenario> {
vec![
simple_text_scenario(),
tool_call_scenario(),
]
}
/// シンプルなテキストレスポンス
fn simple_text_scenario() -> TestScenario {
TestScenario {
name: "Simple text response",
output_name: "simple_text",
request: Request::new()
.system("You are a helpful assistant. Be very concise.")
.user("Say hello in one word.")
.max_tokens(50),
}
}
/// ツール呼び出しを含むレスポンス
fn tool_call_scenario() -> TestScenario {
let get_weather_tool = ToolDefinition::new("get_weather")
.description("Get the current weather for a city")
.input_schema(serde_json::json!({
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name"
}
},
"required": ["city"]
}));
TestScenario {
name: "Tool call response",
output_name: "tool_call",
request: Request::new()
.system("You are a helpful assistant. Use tools when appropriate.")
.user("What's the weather in Tokyo? Use the get_weather tool.")
.tool(get_weather_tool)
.max_tokens(200),
}
}