feat: Implement AnthropicClient
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
//! LLMクライアント + Timeline統合サンプル
|
||||
//!
|
||||
//! Anthropic Claude APIにリクエストを送信し、Timelineでイベントを処理するサンプル
|
||||
//!
|
||||
//! ## 使用方法
|
||||
//!
|
||||
//! ```bash
|
||||
//! # .envファイルにAPIキーを設定
|
||||
//! echo "ANTHROPIC_API_KEY=your-api-key" > .env
|
||||
//!
|
||||
//! # 実行
|
||||
//! cargo run --example llm_client_anthropic
|
||||
//! ```
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use futures::StreamExt;
|
||||
use worker::{
|
||||
Handler, TextBlockEvent, TextBlockKind, Timeline, ToolUseBlockEvent, ToolUseBlockKind,
|
||||
UsageEvent, UsageKind,
|
||||
llm_client::{LlmClient, Request, providers::anthropic::AnthropicClient},
|
||||
};
|
||||
|
||||
/// テキスト出力をリアルタイムで表示するハンドラー
|
||||
struct PrintHandler;
|
||||
|
||||
impl Handler<TextBlockKind> for PrintHandler {
|
||||
type Scope = ();
|
||||
|
||||
fn on_event(&mut self, _scope: &mut (), event: &TextBlockEvent) {
|
||||
match event {
|
||||
TextBlockEvent::Start(_) => {
|
||||
print!("\n🤖 Assistant: ");
|
||||
}
|
||||
TextBlockEvent::Delta(text) => {
|
||||
print!("{}", text);
|
||||
// 即時出力をフラッシュ
|
||||
use std::io::Write;
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
TextBlockEvent::Stop(_) => {
|
||||
println!("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// テキストを蓄積するハンドラー
|
||||
struct TextCollector {
|
||||
texts: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Handler<TextBlockKind> for TextCollector {
|
||||
type Scope = String;
|
||||
|
||||
fn on_event(&mut self, buffer: &mut String, event: &TextBlockEvent) {
|
||||
match event {
|
||||
TextBlockEvent::Start(_) => {}
|
||||
TextBlockEvent::Delta(text) => {
|
||||
buffer.push_str(text);
|
||||
}
|
||||
TextBlockEvent::Stop(_) => {
|
||||
let text = std::mem::take(buffer);
|
||||
self.texts.lock().unwrap().push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ツール使用を検出するハンドラー
|
||||
struct ToolUseDetector;
|
||||
|
||||
impl Handler<ToolUseBlockKind> for ToolUseDetector {
|
||||
type Scope = String; // JSON accumulator
|
||||
|
||||
fn on_event(&mut self, json_buffer: &mut String, event: &ToolUseBlockEvent) {
|
||||
match event {
|
||||
ToolUseBlockEvent::Start(start) => {
|
||||
println!("\n🔧 Tool Call: {} (id: {})", start.name, start.id);
|
||||
}
|
||||
ToolUseBlockEvent::InputJsonDelta(json) => {
|
||||
json_buffer.push_str(json);
|
||||
}
|
||||
ToolUseBlockEvent::Stop(stop) => {
|
||||
println!(" Arguments: {}", json_buffer);
|
||||
println!(" Tool {} completed\n", stop.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用量を追跡するハンドラー
|
||||
struct UsageTracker {
|
||||
total_input: Arc<Mutex<u64>>,
|
||||
total_output: Arc<Mutex<u64>>,
|
||||
}
|
||||
|
||||
impl Handler<UsageKind> for UsageTracker {
|
||||
type Scope = ();
|
||||
|
||||
fn on_event(&mut self, _scope: &mut (), event: &UsageEvent) {
|
||||
if let Some(input) = event.input_tokens {
|
||||
*self.total_input.lock().unwrap() += input;
|
||||
}
|
||||
if let Some(output) = event.output_tokens {
|
||||
*self.total_output.lock().unwrap() += output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// APIキーを環境変数から取得
|
||||
let api_key = std::env::var("ANTHROPIC_API_KEY")
|
||||
.expect("ANTHROPIC_API_KEY environment variable must be set");
|
||||
|
||||
println!("=== LLM Client + Timeline Integration Example ===\n");
|
||||
|
||||
// クライアントを作成
|
||||
let client = AnthropicClient::new(api_key, "claude-sonnet-4-20250514");
|
||||
|
||||
// 共有状態
|
||||
let collected_texts = Arc::new(Mutex::new(Vec::new()));
|
||||
let total_input = Arc::new(Mutex::new(0u64));
|
||||
let total_output = Arc::new(Mutex::new(0u64));
|
||||
|
||||
// タイムラインを構築
|
||||
let mut timeline = Timeline::new();
|
||||
timeline
|
||||
.on_text_block(PrintHandler)
|
||||
.on_text_block(TextCollector {
|
||||
texts: collected_texts.clone(),
|
||||
})
|
||||
.on_tool_use_block(ToolUseDetector)
|
||||
.on_usage(UsageTracker {
|
||||
total_input: total_input.clone(),
|
||||
total_output: total_output.clone(),
|
||||
});
|
||||
|
||||
// リクエストを作成
|
||||
let request = Request::new()
|
||||
.system("You are a helpful assistant. Be concise.")
|
||||
.user("What is the capital of Japan? Answer in one sentence.")
|
||||
.max_tokens(100);
|
||||
|
||||
println!("📤 Sending request...\n");
|
||||
|
||||
// ストリーミングリクエストを送信
|
||||
let mut stream = client.stream(request).await?;
|
||||
|
||||
// イベントを処理
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(event) => {
|
||||
timeline.dispatch(&event);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 結果を表示
|
||||
println!("=== Summary ===");
|
||||
println!(
|
||||
"📊 Token Usage: {} input, {} output",
|
||||
total_input.lock().unwrap(),
|
||||
total_output.lock().unwrap()
|
||||
);
|
||||
|
||||
let texts = collected_texts.lock().unwrap();
|
||||
println!("📝 Collected {} text block(s)", texts.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! 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(())
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
//! 設計ドキュメントに基づいたTimelineの使用パターンを示すサンプル
|
||||
|
||||
use worker::{
|
||||
Event, Handler, TextBlockEvent, TextBlockKind, Timeline,
|
||||
ToolUseBlockEvent, ToolUseBlockKind, UsageEvent, UsageKind,
|
||||
Event, Handler, TextBlockEvent, TextBlockKind, Timeline, ToolUseBlockEvent, ToolUseBlockKind,
|
||||
UsageEvent, UsageKind,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -81,7 +81,9 @@ struct TextCollector {
|
||||
|
||||
impl TextCollector {
|
||||
fn new() -> Self {
|
||||
Self { results: Vec::new() }
|
||||
Self {
|
||||
results: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user