feat: Implement Worker for LLM turn management/tool call/hooks
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user