llm-worker-rs/README.md

13 KiB
Raw Blame History

worker

worker クレートは、大規模言語モデル (LLM) を利用したアプリケーションのバックエンド機能を提供するクレートです。LLM プロバイダーの抽象化、ツール利用、柔軟なプロンプト管理、フックシステムなど、高度な機能をカプセル化し、アプリケーション開発を簡素化します。

主な機能

  • マルチプロバイダー対応: Gemini, Claude, OpenAI, Ollama, XAI など、複数の LLM プロバイダーを統一されたインターフェースで利用できます。
  • プラグインシステム: カスタムプロバイダーをプラグインとして動的に追加できます。独自の LLM API や実験的なプロバイダーをサポートします。
  • ツール利用 (Function Calling): LLM が外部ツールを呼び出す機能をサポートします。独自のツールをマクロを用いて定義し、Worker に登録できます。
  • ストリーミング処理: LLM の応答やツール実行結果を StreamEvent として非同期に受け取ることができます。これにより、リアルタイムな UI 更新が可能になります。
  • フックシステム: Worker の処理フローの特定のタイミング(例: メッセージ送信前、ツール使用後)にカスタムロジックを介入させることができます。
  • セッション管理: 会話履歴やワークスペースの状態を管理し、永続化する機能を提供します。
  • 柔軟なプロンプト管理: 設定ファイルを用いて、ロールやコンテキストに応じたシステムプロンプトを動的に構築します。

主な概念

Worker

このクレートの中心的な構造体です。LLM との対話、ツールの登録と実行、セッション管理など、すべての主要な機能を担当します。

LlmProvider

サポートしている LLM プロバイダー(Gemini, Claude, OpenAI など)を表す enum です。

Tool トレイト

Worker が利用できるツールを定義するためのインターフェースです。このトレイトを実装することで、任意の機能をツールとして Worker に追加できます。

pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn parameters_schema(&self) -> serde_json::Value;
    async fn execute(&self, args: serde_json::Value) -> ToolResult<serde_json::Value>;
}

WorkerHook トレイト

Worker のライフサイクルイベントに介入するためのフックを定義するインターフェースです。特定のイベント(例: OnMessageSend, PostToolUse)に対して処理を追加できます。

StreamEvent

Worker の処理結果を非同期ストリームで受け取るための enum です。LLM の応答チャンク、ツール呼び出し、エラーなど、さまざまなイベントを表します。

アプリケーションへの組み込み方法

1. Worker の初期化

Builder patternを使用してWorkerを作成します。

use worker::{Worker, LlmProvider, Role};
use std::collections::HashMap;

// ロールを定義(必須)
let role = Role::new(
    "assistant",
    "AI Assistant",
    "You are a helpful AI assistant."
);

// APIキーを準備
let mut api_keys = HashMap::new();
api_keys.insert("openai".to_string(), "your_openai_api_key".to_string());
api_keys.insert("claude".to_string(), "your_claude_api_key".to_string());

// Workerを作成builder pattern
let mut worker = Worker::builder()
    .provider(LlmProvider::OpenAI)
    .model("gpt-4o")
    .api_keys(api_keys)
    .role(role)
    .build()
    .expect("Workerの作成に失敗しました");

// または、個別にAPIキーを設定
let worker = Worker::builder()
    .provider(LlmProvider::Claude)
    .model("claude-3-sonnet-20240229")
    .api_key("claude", "sk-ant-...")
    .role(role)
    .build()?;

2. ツールの定義と登録

Tool トレイトを実装してカスタムツールを作成し、Worker に登録します。

use worker::{Tool, ToolResult};
use worker::schemars::{self, JsonSchema};
use worker::serde_json::{self, json, Value};
use async_trait::async_trait;

// ツールの引数を定義
#[derive(Debug, serde::Deserialize, JsonSchema)]
struct FileSystemToolArgs {
    path: String,
}

// カスタムツールを定義
struct ListFilesTool;

#[async_trait]
impl Tool for ListFilesTool {
    fn name(&self) -> &str { "list_files" }
    fn description(&self) -> &str { "指定されたパスのファイル一覧を表示します" }

    fn parameters_schema(&self) -> Value {
        serde_json::to_value(schemars::schema_for!(FileSystemToolArgs)).unwrap()
    }

    async fn execute(&self, args: Value) -> ToolResult<Value> {
        let tool_args: FileSystemToolArgs = serde_json::from_value(args)?;
        // ここで実際のファイル一覧取得処理を実装
        let files = vec!["file1.txt", "file2.txt"];
        Ok(json!({ "files": files }))
    }
}

// 作成したツールをWorkerに登録
worker.register_tool(Box::new(ListFilesTool)).unwrap();

マクロを使ったツール定義(推奨)

worker-macros クレートの #[tool] マクロを使用すると、ツールの定義がより簡潔になります:

use worker_macros::tool;
use worker::ToolResult;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct ListFilesArgs {
    path: String,
}

#[tool]
async fn list_files(args: ListFilesArgs) -> ToolResult<serde_json::Value> {
    // ファイル一覧取得処理
    let files = vec!["file1.txt", "file2.txt"];
    Ok(serde_json::json!({ "files": files }))
}

// マクロで生成されたツールを登録
worker.register_tool(Box::new(ListFilesTool))?;

3. 対話処理の実行

process_task_with_history メソッドを呼び出して、ユーザーメッセージを処理します。このメソッドはイベントのストリームを返します。

use futures_util::StreamExt;

let user_message = "カレントディレクトリのファイルを教えて".to_string();

let mut stream = worker.process_task_with_history(user_message, None).await;

while let Some(event_result) = stream.next().await {
    match event_result {
        Ok(event) => {
            // StreamEventに応じた処理
            match event {
                worker::StreamEvent::Chunk(chunk) => {
                    print!("{}", chunk);
                }
                worker::StreamEvent::ToolCall(tool_call) => {
                    println!("\n[Tool Call: {} with args {}]", tool_call.name, tool_call.arguments);
                }
                worker::StreamEvent::ToolResult { tool_name, result } => {
                    println!("\n[Tool Result: {} -> {:?}]", tool_name, result);
                }
                _ => {}
            }
        }
        Err(e) => {
            eprintln!("\n[Error: {}]", e);
            break;
        }
    }
}

4. (オプション) フックの登録

WorkerHook トレイトを実装してカスタムフックを作成し、Worker に登録することで、処理フローをカスタマイズできます。

手動実装

use worker::{WorkerHook, HookContext, HookResult};
use async_trait::async_trait;

struct LoggingHook;

#[async_trait]
impl WorkerHook for LoggingHook {
    fn name(&self) -> &str { "logging_hook" }
    fn hook_type(&self) -> &str { "OnMessageSend" }
    fn matcher(&self) -> &str { "" }

    async fn execute(&self, context: HookContext) -> (HookContext, HookResult) {
        println!("User message: {}", context.content);
        (context, HookResult::Continue)
    }
}

// フックを登録
worker.register_hook(Box::new(LoggingHook));

マクロを使ったフック定義(推奨)

worker-macros クレートの #[hook] マクロを使用すると、フックの定義がより簡潔になります:

use worker_macros::hook;
use worker::{HookContext, HookResult};

#[hook(OnMessageSend)]
async fn logging_hook(context: HookContext) -> (HookContext, HookResult) {
    println!("User message: {}", context.content);
    (context, HookResult::Continue)
}

// マクロで生成されたフックを登録
worker.register_hook(Box::new(LoggingHook));

利用可能なフックタイプ:

  • OnMessageSend: ユーザーメッセージ送信前
  • PreToolUse: ツール実行前
  • PostToolUse: ツール実行後
  • OnTurnCompleted: ターン完了時

これで、アプリケーションの要件に応じて Worker を中心とした強力な LLM 連携機能を構築できます。

サンプルコード

完全な動作例は worker/examples/ ディレクトリを参照してください:

プラグインシステム

プラグインの作成

ProviderPlugin トレイトを実装してカスタムプロバイダーを作成できます:

use worker::plugin::{ProviderPlugin, PluginMetadata};
use async_trait::async_trait;

pub struct MyCustomProvider {
    // プロバイダーの状態
}

#[async_trait]
impl ProviderPlugin for MyCustomProvider {
    fn metadata(&self) -> PluginMetadata {
        PluginMetadata {
            id: "my-provider".to_string(),
            name: "My Custom Provider".to_string(),
            version: "1.0.0".to_string(),
            author: "Your Name".to_string(),
            description: "カスタムプロバイダーの説明".to_string(),
            supported_models: vec!["model-1".to_string()],
            requires_api_key: true,
            config_schema: None,
        }
    }

    async fn initialize(&mut self, config: HashMap<String, Value>) -> Result<(), WorkerError> {
        // プロバイダーの初期化
        Ok(())
    }

    fn create_client(
        &self,
        model_name: &str,
        api_key: Option<&str>,
        config: Option<HashMap<String, Value>>,
    ) -> Result<Box<dyn LlmClientTrait>, WorkerError> {
        // LLMクライアントを作成して返す
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

プラグインの使用

use worker::{Worker, Role, plugin::PluginRegistry};
use std::sync::{Arc, Mutex};

// プラグインレジストリを作成
let plugin_registry = Arc::new(Mutex::new(PluginRegistry::new()));

// プラグインを作成して登録
let my_plugin = Arc::new(MyCustomProvider::new());
{
    let mut registry = plugin_registry.lock().unwrap();
    registry.register(my_plugin)?;
}

// ロールを定義
let role = Role::new(
    "assistant",
    "AI Assistant",
    "You are a helpful AI assistant."
);

let worker = Worker::builder()
    .plugin("my-provider", plugin_registry.clone())
    .model("model-1")
    .api_key("__plugin__", "api-key")
    .role(role)
    .build()?;

動的プラグイン読み込み

dynamic-loading フィーチャーを有効にすることで、共有ライブラリからプラグインを動的に読み込むことができます:

[dependencies]
worker = { path = "../worker", features = ["dynamic-loading"] }
// ディレクトリからプラグインを読み込み
worker.load_plugins_from_directory(Path::new("./plugins")).await?;

完全な例は worker/src/plugin/example_provider.rsworker/examples/plugin_usage.rs を参照してください。

エラーハンドリング

構造化されたエラー型により、詳細なエラー情報を取得できます。

WorkerError の種類

use worker::WorkerError;

// ツール実行エラー
let error = WorkerError::tool_execution("my_tool", "Connection failed");
let error = WorkerError::tool_execution_with_source("my_tool", "Failed", source_error);

// 設定エラー
let error = WorkerError::config("Invalid configuration");
let error = WorkerError::config_with_context("Parse error", "config.yaml line 10");
let error = WorkerError::config_with_source("Failed to load", io_error);

// LLM APIエラー
let error = WorkerError::llm_api("openai", "Rate limit exceeded");
let error = WorkerError::llm_api_with_details("claude", "Invalid request", Some(400), None);

// モデルエラー
let error = WorkerError::model_not_found("openai", "gpt-5");

// ネットワークエラー
let error = WorkerError::network("Connection timeout");
let error = WorkerError::network_with_source("Request failed", reqwest_error);

エラーのパターンマッチング

match worker.build() {
    Ok(worker) => { /* ... */ },
    Err(WorkerError::ConfigurationError { message, context, .. }) => {
        eprintln!("Configuration error: {}", message);
        if let Some(ctx) = context {
            eprintln!("Context: {}", ctx);
        }
    },
    Err(WorkerError::ModelNotFound { provider, model_name }) => {
        eprintln!("Model '{}' not found for provider '{}'", model_name, provider);
    },
    Err(WorkerError::LlmApiError { provider, message, status_code, .. }) => {
        eprintln!("API error from {}: {}", provider, message);
        if let Some(code) = status_code {
            eprintln!("Status code: {}", code);
        }
    },
    Err(e) => {
        eprintln!("Error: {}", e);
    }
}