feat: Implement openai/ollama client
This commit is contained in:
@@ -1,118 +0,0 @@
|
||||
//! APIレスポンス記録ツール
|
||||
//!
|
||||
//! 実際のAnthropicAPIからのレスポンスをファイルに記録する。
|
||||
//! 後でテストフィクスチャとして使用可能。
|
||||
//!
|
||||
//! ## 使用方法
|
||||
//!
|
||||
//! ```bash
|
||||
//! # 記録モード (APIを呼び出して記録)
|
||||
//! ANTHROPIC_API_KEY=your-key cargo run --example record_anthropic
|
||||
//!
|
||||
//! # 記録されたファイルは worker/tests/fixtures/ に保存される
|
||||
//! ```
|
||||
|
||||
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, providers::anthropic::AnthropicClient};
|
||||
|
||||
/// 記録されたSSEイベント
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct RecordedEvent {
|
||||
elapsed_ms: u64,
|
||||
event_type: String,
|
||||
data: String,
|
||||
}
|
||||
|
||||
/// セッションメタデータ
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct SessionMetadata {
|
||||
timestamp: u64,
|
||||
model: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_key = std::env::var("ANTHROPIC_API_KEY")
|
||||
.expect("ANTHROPIC_API_KEY environment variable must be set");
|
||||
|
||||
let model = "claude-sonnet-4-20250514";
|
||||
let description = "Simple greeting test";
|
||||
|
||||
println!("=== Anthropic API Response Recorder ===\n");
|
||||
println!("Model: {}", model);
|
||||
println!("Description: {}\n", description);
|
||||
|
||||
// クライアントを作成
|
||||
let client = AnthropicClient::new(&api_key, model);
|
||||
|
||||
// シンプルなリクエスト
|
||||
let request = Request::new()
|
||||
.system("You are a helpful assistant. Be very concise.")
|
||||
.user("Say hello in one word.")
|
||||
.max_tokens(50);
|
||||
|
||||
println!("📤 Sending request...\n");
|
||||
|
||||
// レスポンスを記録
|
||||
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) => {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n📊 Recorded {} events", events.len());
|
||||
|
||||
// ファイルに保存
|
||||
let fixtures_dir = Path::new("worker/tests/fixtures");
|
||||
fs::create_dir_all(fixtures_dir)?;
|
||||
|
||||
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let filename = format!("anthropic_{}.jsonl", timestamp);
|
||||
let filepath = fixtures_dir.join(&filename);
|
||||
|
||||
let file = File::create(&filepath)?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
// メタデータを書き込み
|
||||
let metadata = SessionMetadata {
|
||||
timestamp,
|
||||
model: model.to_string(),
|
||||
description: description.to_string(),
|
||||
};
|
||||
writeln!(writer, "{}", serde_json::to_string(&metadata)?)?;
|
||||
|
||||
// イベントを書き込み
|
||||
for event in &events {
|
||||
writeln!(writer, "{}", serde_json::to_string(event)?)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
println!("💾 Saved to: {}", filepath.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -16,80 +16,171 @@
|
||||
//! ANTHROPIC_API_KEY=your-key cargo run --example record_test_fixtures -- --all
|
||||
//! ```
|
||||
|
||||
|
||||
|
||||
|
||||
mod recorder;
|
||||
mod scenarios;
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use worker::llm_client::providers::anthropic::AnthropicClient;
|
||||
use worker::llm_client::providers::openai::OpenAIClient;
|
||||
|
||||
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");
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Scenario name
|
||||
#[arg(short, long)]
|
||||
scenario: Option<String>,
|
||||
|
||||
/// Run all scenarios
|
||||
#[arg(long, default_value_t = false)]
|
||||
all: bool,
|
||||
|
||||
/// Client to use
|
||||
#[arg(short, long, value_enum, default_value_t = ClientType::Anthropic)]
|
||||
client: ClientType,
|
||||
|
||||
/// Model to use (optional, defaults per client)
|
||||
#[arg(short, long)]
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
|
||||
enum ClientType {
|
||||
Anthropic,
|
||||
Openai,
|
||||
Ollama,
|
||||
}
|
||||
|
||||
async fn run_scenario_with_anthropic(
|
||||
scenario: &scenarios::TestScenario,
|
||||
subdir: &str,
|
||||
model: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_key = std::env::var("ANTHROPIC_API_KEY")
|
||||
.expect("ANTHROPIC_API_KEY environment variable must be set");
|
||||
let model = model.as_deref().unwrap_or("claude-sonnet-4-20250514");
|
||||
let client = AnthropicClient::new(&api_key, model);
|
||||
|
||||
recorder::record_request(
|
||||
&client,
|
||||
scenario.request.clone(),
|
||||
scenario.name,
|
||||
scenario.output_name,
|
||||
subdir,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_scenario_with_openai(
|
||||
scenario: &scenarios::TestScenario,
|
||||
subdir: &str,
|
||||
model: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY environment variable must be set");
|
||||
let model = model.as_deref().unwrap_or("gpt-4o");
|
||||
let client = OpenAIClient::new(&api_key, model);
|
||||
|
||||
recorder::record_request(
|
||||
&client,
|
||||
scenario.request.clone(),
|
||||
scenario.name,
|
||||
scenario.output_name,
|
||||
subdir,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_scenario_with_ollama(
|
||||
scenario: &scenarios::TestScenario,
|
||||
subdir: &str,
|
||||
model: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use worker::llm_client::providers::ollama::OllamaClient;
|
||||
// Ollama typically runs local, no key needed or placeholder
|
||||
let model = model.as_deref().unwrap_or("llama3"); // default example
|
||||
let client = OllamaClient::new(model); // base_url placeholder, handled by client default
|
||||
|
||||
recorder::record_request(
|
||||
&client,
|
||||
scenario.request.clone(),
|
||||
scenario.name,
|
||||
scenario.output_name,
|
||||
subdir,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
dotenv::dotenv().ok();
|
||||
let args = Args::parse();
|
||||
|
||||
// 引数がなければ使い方を表示
|
||||
if args.len() < 2 {
|
||||
print_usage();
|
||||
return Ok(());
|
||||
if !args.all && args.scenario.is_none() {
|
||||
use clap::CommandFactory;
|
||||
let mut cmd = Args::command();
|
||||
cmd.error(
|
||||
clap::error::ErrorKind::MissingRequiredArgument,
|
||||
"Either --all or --scenario <SCENARIO> must be provided",
|
||||
)
|
||||
.exit();
|
||||
}
|
||||
|
||||
let arg = &args[1];
|
||||
|
||||
// 全シナリオを取得
|
||||
let all_scenarios = scenarios::scenarios();
|
||||
|
||||
// 実行するシナリオを決定
|
||||
let scenarios_to_run: Vec<_> = if arg == "--all" {
|
||||
// Determine scenarios to run
|
||||
let scenarios_to_run: Vec<_> = if args.all {
|
||||
all_scenarios
|
||||
} else {
|
||||
// 指定されたシナリオを検索
|
||||
let scenario_name = args.scenario.as_ref().unwrap();
|
||||
let found: Vec<_> = all_scenarios
|
||||
.into_iter()
|
||||
.filter(|s| s.output_name == arg)
|
||||
.filter(|s| s.output_name == scenario_name)
|
||||
.collect();
|
||||
|
||||
if found.is_empty() {
|
||||
eprintln!("Error: Unknown scenario '{}'", arg);
|
||||
println!();
|
||||
print_usage();
|
||||
std::process::exit(1);
|
||||
eprintln!("Error: Unknown scenario '{}'", scenario_name);
|
||||
// Verify correct name by listing
|
||||
println!("Available scenarios:");
|
||||
for s in scenarios::scenarios() {
|
||||
println!(" {}", s.output_name);
|
||||
}
|
||||
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!("Client: {:?}", args.client);
|
||||
if let Some(ref m) = args.model {
|
||||
println!("Model: {}", m);
|
||||
}
|
||||
println!("Scenarios: {}\n", scenarios_to_run.len());
|
||||
|
||||
let client = AnthropicClient::new(&api_key, model);
|
||||
let subdir = match args.client {
|
||||
ClientType::Anthropic => "anthropic",
|
||||
ClientType::Openai => "openai",
|
||||
ClientType::Ollama => "ollama",
|
||||
};
|
||||
|
||||
// シナリオを記録
|
||||
// シナリオのフィルタリングは main.rs のロジックで実行済み
|
||||
// ここでは単純なループで実行
|
||||
for scenario in scenarios_to_run {
|
||||
recorder::record_request(
|
||||
&client,
|
||||
scenario.request,
|
||||
scenario.name,
|
||||
scenario.output_name,
|
||||
model,
|
||||
)
|
||||
.await?;
|
||||
match args.client {
|
||||
ClientType::Anthropic => run_scenario_with_anthropic(&scenario, subdir, args.model.clone()).await?,
|
||||
ClientType::Openai => run_scenario_with_openai(&scenario, subdir, args.model.clone()).await?,
|
||||
ClientType::Ollama => run_scenario_with_ollama(&scenario, subdir, args.model.clone()).await?,
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n✅ Done!");
|
||||
|
||||
@@ -49,6 +49,7 @@ pub async fn record_request<C: LlmClient>(
|
||||
request: Request,
|
||||
description: &str,
|
||||
output_name: &str,
|
||||
subdir: &str, // e.g. "anthropic", "openai"
|
||||
model: &str,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
println!("\n📝 Recording: {}", description);
|
||||
@@ -78,8 +79,8 @@ pub async fn record_request<C: LlmClient>(
|
||||
}
|
||||
|
||||
// 保存
|
||||
let fixtures_dir = Path::new("worker/tests/fixtures");
|
||||
fs::create_dir_all(fixtures_dir)?;
|
||||
let fixtures_dir = Path::new("worker/tests/fixtures").join(subdir);
|
||||
fs::create_dir_all(&fixtures_dir)?;
|
||||
|
||||
let filepath = fixtures_dir.join(format!("{}.jsonl", output_name));
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub fn scenarios() -> Vec<TestScenario> {
|
||||
vec![
|
||||
simple_text_scenario(),
|
||||
tool_call_scenario(),
|
||||
long_text_scenario(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -59,3 +60,15 @@ fn tool_call_scenario() -> TestScenario {
|
||||
.max_tokens(200),
|
||||
}
|
||||
}
|
||||
|
||||
/// 長文生成シナリオ
|
||||
fn long_text_scenario() -> TestScenario {
|
||||
TestScenario {
|
||||
name: "Long text response",
|
||||
output_name: "long_text",
|
||||
request: Request::new()
|
||||
.system("You are a creative writer.")
|
||||
.user("Write a short story about a robot discovering a garden. It should be at least 300 words.")
|
||||
.max_tokens(1000),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user