feat: Add Tool trait definition and tool_registry macro skeleton
This commit is contained in:
parent
9a7acb74c8
commit
a4e2795e56
65
Cargo.lock
generated
65
Cargo.lock
generated
|
|
@ -157,6 +157,12 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
|
@ -872,6 +878,26 @@ dependencies = [
|
|||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.1"
|
||||
|
|
@ -1047,6 +1073,31 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
|
|
@ -1100,6 +1151,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.148"
|
||||
|
|
@ -1869,8 +1931,11 @@ dependencies = [
|
|||
name = "worker-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ worker
|
|||
│ ├── scheme
|
||||
│ │ └── openai, anthropic, gemini // APIスキーマ
|
||||
│ ├── events
|
||||
│ ├── providers
|
||||
│ │ └── anthropic, googleai, ollama, etc... // プロバイダ
|
||||
│ └── timeline
|
||||
│ └── providers
|
||||
│ └── anthropic, googleai, ollama, etc... // プロバイダ
|
||||
└── timeline
|
||||
```
|
||||
|
||||
OpenAI互換のプロバイダでスキーマを使い回せるよう、schemeとプロバイダモジュールは分離されている
|
||||
|
|
|
|||
231
docs/spec/worker_design.md
Normal file
231
docs/spec/worker_design.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# Worker & Tool/Hook 設計
|
||||
|
||||
## 概要
|
||||
|
||||
`Worker`はアプリケーションの「ターン」を制御する高レベルコンポーネントです。
|
||||
`LlmClient`と`Timeline`を内包し、ユーザー定義の`Tool`と`Hook`を用いて自律的なインタラクションを行います。
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[Application / User] -->|1. Run| Worker
|
||||
Worker -->|2. Event Loop| Timeline
|
||||
Timeline -->|3. Dispatch| Handler[Handlers (inc. ToolExecutor)]
|
||||
|
||||
subgraph "Worker Layer"
|
||||
Worker
|
||||
Hook[Hooks]
|
||||
end
|
||||
|
||||
subgraph "Core Layer"
|
||||
Timeline
|
||||
LlmClient
|
||||
end
|
||||
|
||||
Worker -.->|Intervene| Hook
|
||||
Handler -.->|Execute| Tool[User Defined Tools]
|
||||
```
|
||||
|
||||
## ライフサイクル (ターン制御)
|
||||
|
||||
Workerは以下のループ(ターン)を実行します。
|
||||
|
||||
1. **Start Turn**: `Worker::run(messages)` 呼び出し
|
||||
2. **Hook: OnMessageSend**:
|
||||
* ユーザーメッセージの改変、バリデーション、キャンセルが可能。
|
||||
* コンテキストへのシステムプロンプト注入などもここで行う想定。
|
||||
3. **Request & Stream**:
|
||||
* LLMへリクエスト送信。イベントストリーム開始。
|
||||
* `Timeline`によるイベント処理。
|
||||
4. **Tool Handling (Parallel)**:
|
||||
* レスポンス内に含まれる全てのTool Callを収集。
|
||||
* 各Toolに対して **Hook: BeforeToolCall** を実行(実行可否、引数改変)。
|
||||
* 許可されたToolを**並列実行 (`join_all`)**。
|
||||
* 各Tool実行後に **Hook: AfterToolCall** を実行(結果の確認、加工)。
|
||||
5. **Next Request Decision**:
|
||||
* Tool実行結果がある場合 -> 結果をMessageとしてContextに追加し、**Step 3へ戻る** (自動ループ)。
|
||||
* Tool実行がない場合 -> Step 6へ。
|
||||
6. **Hook: OnTurnEnd**:
|
||||
* 最終的な応答に対するチェック(Lint/Fmt)。
|
||||
* エラーがある場合、エラーメッセージをContextに追加して **Step 3へ戻る** ことで自己修正を促せる。
|
||||
* 問題なければターン終了。
|
||||
|
||||
## Tool 設計
|
||||
|
||||
### アーキテクチャ概要
|
||||
|
||||
Rustの静的型付けシステムとLLMの動的なツール呼び出し(文字列による指定)を、**Trait Object** と **動的ディスパッチ** を用いて接続します。
|
||||
|
||||
1. **共通インターフェース (`Tool` Trait)**: 全てのツールが実装すべき共通の振る舞い(メタデータ取得と実行)を定義します。
|
||||
2. **ラッパー生成 (`#[tool]` Macro)**: ユーザー定義のメソッドをラップし、`Tool` Traitを実装した構造体を自動生成します。
|
||||
3. **レジストリ (`HashMap`)**: Workerは動的ディスパッチ用に `HashMap<String, Box<dyn Tool>>` でツールを管理します。
|
||||
|
||||
この仕組みにより、「名前からツールを探し、JSON引数を型変換して関数を実行する」フローを安全に実現します。
|
||||
|
||||
### 1. Tool Trait 定義
|
||||
|
||||
ツールが最低限持つべきインターフェースです。`Send + Sync` を必須とし、マルチスレッド(並列実行)に対応します。
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
/// ツール名 (LLMが識別に使用)
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// ツールの説明 (LLMへのプロンプトに含まれる)
|
||||
fn description(&self) -> &str;
|
||||
|
||||
/// 引数のJSON Schema (schemars等で生成)
|
||||
fn input_schema(&self) -> serde_json::Value;
|
||||
|
||||
/// 実行関数
|
||||
/// JSON文字列を受け取り、デシリアライズして元のメソッドを実行し、結果を返す
|
||||
async fn execute(&self, input_json: &str) -> Result<String, ToolError>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. マクロと実装モデル
|
||||
|
||||
ユーザーは「状態を持つ構造体」とその「メソッド」としてツールを定義します。
|
||||
|
||||
**ユーザーコード:**
|
||||
|
||||
```rust
|
||||
#[derive(Clone)] // 状態はClone (Arc推奨) で共有される想定
|
||||
struct MyApp {
|
||||
db: Arc<Database>,
|
||||
}
|
||||
|
||||
impl MyApp {
|
||||
/// ユーザー情報を取得する
|
||||
/// 指定されたIDのユーザーをDBから検索します。
|
||||
#[tool]
|
||||
async fn get_user(
|
||||
&self,
|
||||
#[description = "取得したいユーザーのID"] user_id: String
|
||||
) -> Result<User, Error> {
|
||||
let user = self.db.find(&user_id).await?;
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**マクロ展開後のイメージ (擬似コード):**
|
||||
|
||||
マクロは、元のメソッドに対応する**ラッパー構造体**を生成します。このラッパーが `Tool` Trait を実装します。
|
||||
|
||||
```rust
|
||||
// 1. 引数をデシリアライズ用の中間構造体に変換
|
||||
#[derive(serde::Deserialize, schemars::JsonSchema)]
|
||||
struct GetUserArgs {
|
||||
/// 取得したいユーザーのID
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
// 2. ラッパー構造体 (元のコンテキストを持つ)
|
||||
struct GetUserTool {
|
||||
ctx: MyApp, // コンテキストを保持 (Clone)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for GetUserTool {
|
||||
fn name(&self) -> &str { "get_user" }
|
||||
|
||||
fn description(&self) -> &str { "ユーザー情報を取得する\n指定されたIDのユーザーをDBから検索します。" }
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
schemars::schema_for!(GetUserArgs)
|
||||
}
|
||||
|
||||
async fn execute(&self, input_json: &str) -> Result<String, ToolError> {
|
||||
// A. JSONを引数構造体に変換
|
||||
let args: GetUserArgs = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(e.to_string()))?;
|
||||
|
||||
// B. 元のメソッド呼び出し (self.ctx 経由)
|
||||
let result = self.ctx.get_user(args.user_id).await
|
||||
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
|
||||
|
||||
// C. 結果を文字列化
|
||||
Ok(format!("{:?}", result)) // または serde_json::to_string(&result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Workerによる実行フロー
|
||||
|
||||
Workerは生成されたラッパー構造体を `Box<dyn Tool>` として保持し、以下のフローで実行します。
|
||||
|
||||
1. **登録**: アプリケーション開始時、コンテキスト(`MyApp`)から各ツールのラッパー(`GetUserTool`)を生成し、WorkerのMapに登録。
|
||||
2. **解決**: LLMからのレスポンスに含まれる `ToolUse { name: "get_user", ... }` を受け取る。
|
||||
3. **検索**: `name` をキーに Map から `Box<dyn Tool>` を取得。
|
||||
4. **実行**:
|
||||
* `tool.execute(json)` を呼び出す。
|
||||
* 内部で `serde_json` による型変換とメソッド実行が行われる。
|
||||
* 結果が返る。
|
||||
|
||||
これにより、型安全性を保ちつつ、動的なツール実行が可能になります。
|
||||
|
||||
## Hook 設計
|
||||
|
||||
### コンセプト
|
||||
|
||||
* **制御の介入**: ターンの進行、メッセージの内容、ツールの実行に対して介入します。
|
||||
* **Contextへのアクセス**: メッセージ履歴(Context)を読み書きできます。
|
||||
|
||||
### Hook Trait
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait WorkerHook: Send + Sync {
|
||||
/// メッセージ送信前。
|
||||
/// リクエストに含まれるメッセージリストを改変できる。
|
||||
async fn on_message_send(&self, context: &mut Vec<Message>) -> Result<ControlFlow, Error> {
|
||||
Ok(ControlFlow::Continue)
|
||||
}
|
||||
|
||||
/// ツール実行前。
|
||||
/// 実行をキャンセルしたり、引数を書き換えることができる。
|
||||
async fn before_tool_call(&self, tool_call: &mut ToolCall) -> Result<ControlFlow, Error> {
|
||||
Ok(ControlFlow::Continue)
|
||||
}
|
||||
|
||||
/// ツール実行後。
|
||||
/// 結果を書き換えたり、隠蔽したりできる。
|
||||
async fn after_tool_call(&self, tool_result: &mut ToolResult) -> Result<ControlFlow, Error> {
|
||||
Ok(ControlFlow::Continue)
|
||||
}
|
||||
|
||||
/// ターン終了時。
|
||||
/// 生成されたメッセージを検査し、必要ならリトライ(ContinueWithMessages)を指示できる。
|
||||
async fn on_turn_end(&self, messages: &[Message]) -> Result<TurnResult, Error> {
|
||||
Ok(TurnResult::Finish)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ControlFlow {
|
||||
Continue,
|
||||
Skip, // Tool実行などをスキップ
|
||||
Abort(String), // 処理中断
|
||||
}
|
||||
|
||||
pub enum TurnResult {
|
||||
Finish,
|
||||
ContinueWithMessages(Vec<Message>), // メッセージを追加してターン継続(自己修正など)
|
||||
}
|
||||
```
|
||||
|
||||
## 実装方針
|
||||
|
||||
1. **Worker Struct**:
|
||||
* `Timeline`を所有。
|
||||
* `Handler`として「ToolCallCollector」をTimelineに登録。
|
||||
* `stream`終了後に収集したToolCallを処理するロジックを持つ。
|
||||
|
||||
2. **Tool Executor Handler**:
|
||||
* Timeline上ではツール実行を行わず、あくまで「ToolCallブロックの収集」に徹する(Toolの実行は非同期かつ並列で、ストリーム終了後あるいはブロック確定後に行うため)。
|
||||
* ただし、リアルタイム性を重視する場合(ストリーミング中にToolを実行開始等)は将来的な拡張とするが、現状は「結果が揃うのを待って」という要件に従い、収集フェーズと実行フェーズを分ける。
|
||||
|
||||
3. **worker-macros**:
|
||||
* `syn`, `quote` を用いて、関数定義から `Tool` トレイト実装と `InputInputSchema` (schemars利用) を生成。
|
||||
|
|
@ -1,41 +1,114 @@
|
|||
//! worker-macros - LLMワーカー用のProcedural Macros
|
||||
//!
|
||||
//! このクレートはTools/Hooksを定義するためのマクロを提供する予定です。
|
||||
//!
|
||||
//! TODO: Tool定義マクロの実装
|
||||
//! TODO: Hook定義マクロの実装
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, ImplItem, ItemImpl};
|
||||
|
||||
/// ツール定義マクロ(未実装)
|
||||
/// `impl` ブロックに付与し、内部の `#[tool]` 属性がついたメソッドからツールを生成するマクロ。
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// #[tool(
|
||||
/// name = "get_weather",
|
||||
/// description = "Get weather information for a city"
|
||||
/// )]
|
||||
/// fn get_weather(city: String) -> Result<WeatherInfo, ToolError> {
|
||||
/// // ...
|
||||
/// #[tool_registry]
|
||||
/// impl MyApp {
|
||||
/// #[tool]
|
||||
/// async fn my_function(&self, arg: String) -> Result<String, Error> { ... }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn tool_registry(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut impl_block = parse_macro_input!(item as ItemImpl);
|
||||
let self_ty = &impl_block.self_ty;
|
||||
|
||||
let mut generated_items = Vec::new();
|
||||
|
||||
for item in &mut impl_block.items {
|
||||
if let ImplItem::Fn(method) = item {
|
||||
// #[tool] 属性を探す
|
||||
let mut is_tool = false;
|
||||
let mut _description = String::new();
|
||||
|
||||
// 属性を走査してtoolがあるか確認し、削除する
|
||||
// 同時にドキュメントコメントから説明を取得
|
||||
method.attrs.retain(|attr| {
|
||||
if attr.path().is_ident("tool") {
|
||||
is_tool = true;
|
||||
false // 属性を削除
|
||||
} else if attr.path().is_ident("doc") {
|
||||
// TODO: docコメントのパース
|
||||
true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if is_tool {
|
||||
let sig = &method.sig;
|
||||
let method_name = &sig.ident;
|
||||
let tool_name = method_name.to_string();
|
||||
let tool_struct_name = syn::Ident::new(
|
||||
&format!("Tool_{}", method_name),
|
||||
method_name.span(),
|
||||
);
|
||||
|
||||
let factory_name = syn::Ident::new(
|
||||
&format!("{}_tool", method_name),
|
||||
method_name.span(),
|
||||
);
|
||||
|
||||
// TODO: 引数の解析とArgs構造体の生成
|
||||
// TODO: descriptionの取得
|
||||
|
||||
// 仮の実装: Contextを抱えるTool構造体を作成
|
||||
let tool_impl = quote! {
|
||||
#[derive(Clone)]
|
||||
pub struct #tool_struct_name {
|
||||
ctx: #self_ty,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl worker_types::Tool for #tool_struct_name {
|
||||
fn name(&self) -> &str {
|
||||
#tool_name
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"TODO: description from doc comments"
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
serde_json::json!({}) // TODO: schemars
|
||||
}
|
||||
|
||||
async fn execute(&self, input_json: &str) -> Result<String, worker_types::ToolError> {
|
||||
// TODO: Deserialize args and call check
|
||||
// self.ctx.#method_name(...)
|
||||
Ok("Not implemented yet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl #self_ty {
|
||||
pub fn #factory_name(&self) -> #tool_struct_name {
|
||||
#tool_struct_name {
|
||||
ctx: self.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generated_items.push(tool_impl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expanded = quote! {
|
||||
#impl_block
|
||||
|
||||
#(#generated_items)*
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
/// マーカー属性。`tool_registry` によって処理されるため、ここでは何もしない。
|
||||
#[proc_macro_attribute]
|
||||
pub fn tool(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// TODO: 実装
|
||||
item
|
||||
}
|
||||
|
||||
/// フック定義マクロ(未実装)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// #[hook(on = "before_tool_call")]
|
||||
/// fn log_tool_call(tool_name: &str) {
|
||||
/// println!("Calling tool: {}", tool_name);
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn hook(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// TODO: 実装
|
||||
item
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,5 +4,8 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
schemars = "1.2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "2.0.17"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
mod event;
|
||||
mod handler;
|
||||
mod tool;
|
||||
|
||||
pub use event::*;
|
||||
pub use handler::*;
|
||||
pub use tool::*;
|
||||
|
|
|
|||
33
worker-types/src/tool.rs
Normal file
33
worker-types/src/tool.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ToolError {
|
||||
#[error("Invalid argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
#[error("Execution failed: {0}")]
|
||||
ExecutionFailed(String),
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// ツール定義トレイト
|
||||
///
|
||||
/// ユーザー定義のツールはこれを実装し、Workerに登録される。
|
||||
/// 通常は `#[tool]` マクロによって自動生成される。
|
||||
#[async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
/// ツール名 (LLMが識別に使用)
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// ツールの説明 (LLMへのプロンプトに含まれる)
|
||||
fn description(&self) -> &str;
|
||||
|
||||
/// 引数のJSON Schema
|
||||
fn input_schema(&self) -> Value;
|
||||
|
||||
/// 実行関数
|
||||
/// JSON文字列を受け取り、デシリアライズして元のメソッドを実行し、結果を返す
|
||||
async fn execute(&self, input_json: &str) -> Result<String, ToolError>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user