feat: add Google Gemini LLM client integration

This commit is contained in:
2026-01-07 00:54:58 +09:00
parent 89b12d277a
commit a26d43c52d
14 changed files with 1155 additions and 3 deletions
+176
View File
@@ -0,0 +1,176 @@
//! LLMクライアント + Timeline統合サンプル (Gemini)
//!
//! Google Gemini APIにリクエストを送信し、Timelineでイベントを処理するサンプル
//!
//! ## 使用方法
//!
//! ```bash
//! # .envファイルにAPIキーを設定
//! echo "GEMINI_API_KEY=your-api-key" > .env
//!
//! # 実行
//! cargo run --example llm_client_gemini
//! ```
use std::sync::{Arc, Mutex};
use futures::StreamExt;
use worker::{
Handler, TextBlockEvent, TextBlockKind, Timeline, ToolUseBlockEvent, ToolUseBlockKind,
UsageEvent, UsageKind,
llm_client::{LlmClient, Request, providers::gemini::GeminiClient},
};
/// テキスト出力をリアルタイムで表示するハンドラー
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("GEMINI_API_KEY")
.expect("GEMINI_API_KEY environment variable must be set");
println!("=== Gemini LLM Client + Timeline Integration Example ===\n");
// クライアントを作成
let client = GeminiClient::new(api_key, "gemini-2.0-flash");
// 共有状態
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(())
}
@@ -24,6 +24,7 @@ mod scenarios;
use clap::{Parser, ValueEnum};
use worker::llm_client::providers::anthropic::AnthropicClient;
use worker::llm_client::providers::gemini::GeminiClient;
use worker::llm_client::providers::openai::OpenAIClient;
#[derive(Parser, Debug)]
@@ -49,6 +50,7 @@ struct Args {
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
enum ClientType {
Anthropic,
Gemini,
Openai,
Ollama,
}
@@ -118,6 +120,28 @@ async fn run_scenario_with_ollama(
Ok(())
}
async fn run_scenario_with_gemini(
scenario: &scenarios::TestScenario,
subdir: &str,
model: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("GEMINI_API_KEY")
.expect("GEMINI_API_KEY environment variable must be set");
let model = model.as_deref().unwrap_or("gemini-2.0-flash");
let client = GeminiClient::new(&api_key, model);
recorder::record_request(
&client,
scenario.request.clone(),
scenario.name,
scenario.output_name,
subdir,
model,
)
.await?;
Ok(())
}
@@ -169,6 +193,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let subdir = match args.client {
ClientType::Anthropic => "anthropic",
ClientType::Gemini => "gemini",
ClientType::Openai => "openai",
ClientType::Ollama => "ollama",
};
@@ -178,6 +203,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
for scenario in scenarios_to_run {
match args.client {
ClientType::Anthropic => run_scenario_with_anthropic(&scenario, subdir, args.model.clone()).await?,
ClientType::Gemini => run_scenario_with_gemini(&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?,
}