0.4.0: ワーカーの廃止
This commit is contained in:
parent
02667f5396
commit
cab8cd7f32
648
Cargo.lock
generated
648
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
34
README.md
34
README.md
|
|
@ -4,39 +4,37 @@
|
|||
|
||||
## 特徴
|
||||
- 主要 LLM プロバイダーを単一のインターフェースで利用
|
||||
- プロンプト/パーシャル読み込みを利用者実装の `ResourceLoader` へ委譲
|
||||
- システムプロンプトの生成を外部関数として注入可能
|
||||
- ツール連携とストリーミング応答、フックによるカスタマイズに対応
|
||||
|
||||
## 利用手順
|
||||
|
||||
`worker` のプロンプト/パーシャル解決は利用者側に委譲されています。以下の流れで組み込みます。
|
||||
`worker` はシステムプロンプトの生成を外部関数に完全委任します。以下の手順で組み込みます。
|
||||
|
||||
1. `ResourceLoader` を実装して、テンプレートやパーシャルが参照する識別子から文字列を返す。
|
||||
2. `Worker::builder()` にプロバイダー・モデル・ロールと合わせて `resource_loader` を渡し、`Worker` を生成。
|
||||
3. セッションを初期化し、`process_task_with_history` などの API でイベントストリームを処理。
|
||||
1. `fn(&PromptContext, &[Message]) -> Result<String, PromptError>` 形式の関数(またはクロージャ)でシステムプロンプトを返す。
|
||||
2. `Worker::builder()` にプロバイダー・モデル・APIキーと合わせて `system_prompt(...)` を渡し、`Worker` を生成。
|
||||
3. セッション初期化後、`process_task_with_history` などでイベントストリームを処理。
|
||||
|
||||
```rust
|
||||
use futures_util::StreamExt;
|
||||
use worker::{LlmProvider, PromptError, ResourceLoader, Role, Worker};
|
||||
|
||||
struct FsLoader;
|
||||
|
||||
impl ResourceLoader for FsLoader {
|
||||
fn load(&self, id: &str) -> Result<String, PromptError> {
|
||||
std::fs::read_to_string(id)
|
||||
.map_err(|e| PromptError::FileNotFound(format!("{}: {}", id, e)))
|
||||
}
|
||||
}
|
||||
use worker::{LlmProvider, PromptContext, PromptError, Worker};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let role = Role::new("assistant", "Helper", "You are a helpful assistant.");
|
||||
let system_prompt = |ctx: &PromptContext, _messages: &[worker_types::Message]| {
|
||||
Ok(format!(
|
||||
"You are assisting the project: {}",
|
||||
ctx.workspace
|
||||
.project_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
))
|
||||
};
|
||||
|
||||
let mut worker = Worker::builder()
|
||||
.provider(LlmProvider::Claude)
|
||||
.model("claude-3-sonnet-20240229")
|
||||
.resource_loader(FsLoader)
|
||||
.role(role)
|
||||
.system_prompt(system_prompt)
|
||||
.build()?;
|
||||
|
||||
worker.initialize_session()?;
|
||||
|
|
|
|||
33
docs/patch_note/v0.4.0.md
Normal file
33
docs/patch_note/v0.4.0.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Release Notes - v0.4.0
|
||||
|
||||
**Release Date**: 2025-??-??
|
||||
|
||||
v0.4.0 は Worker が `Role` や YAML 設定を扱わず、システムプロンプト生成を完全に利用者へ委譲する大規模リファクタです。これにより、任意のテンプレートエンジンやデータソースを組み合わせてプロンプトを構築できます。
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- `Role` / `ConfigParser` / `ResourceLoader` を削除。`WorkerBuilder` はシステムプロンプト生成関数 (`system_prompt(...)`) の指定が必須になりました。
|
||||
- `worker/src/config` の Role 関連コードとテストを削除。既存の YAML ベース設定は互換層なし。
|
||||
|
||||
## 新機能 / 仕様変更
|
||||
|
||||
- `PromptComposer` は `Arc<SystemPromptFn>` を受け取り、`PromptContext` と履歴メッセージからシステムプロンプト文字列を生成するシンプルなラッパーになりました。
|
||||
- `WorkerBuilder` は `.system_prompt(...)` で登録した関数を保持し、メッセージ送信時に毎回システムプロンプトを再生成します。
|
||||
- README/サンプルコードを刷新し、システムプロンプト関数・マクロベースのツール/フック登録手順のみを掲載。
|
||||
- 新しい `docs/prompt-composer.md` を追加し、`PromptComposer` の利用例をサマリー形式で紹介。
|
||||
|
||||
## 不具合修正
|
||||
|
||||
- `PromptComposer` が内部でファイルアクセスを行う経路を排除し、生成関数の失敗時は直近のキャッシュを利用するようにしました。
|
||||
- Worker から NIA 固有の設定コードを除去し、環境依存の副作用を縮小。
|
||||
|
||||
## 移行ガイド
|
||||
|
||||
1. 旧 `Role` / `ConfigParser` を利用していた場合、`PromptContext` と会話履歴を引数にシステムプロンプト文字列を返す関数を実装し、`.system_prompt(...)` に渡してください。
|
||||
2. `Worker::load_config` やリソースパス解決に依存していたコードは削除してください。必要であればアプリケーション側でファイル読み込みを行い、生成関数内で利用してください。
|
||||
3. ツール・フックは引き続き `#[worker::tool]` / `#[worker::hook]` マクロを推奨しています(API に変更はありません)。
|
||||
|
||||
## 開発者向けメモ
|
||||
|
||||
- README を簡潔化し、RustDocs で確認できる内容の重複を削除しました。
|
||||
- `worker/examples/` を更新し、システムプロンプト関数とマクロベースのツール登録のみを扱うよう整理しました。
|
||||
|
|
@ -1,106 +1,32 @@
|
|||
# PromptComposer
|
||||
|
||||
テンプレートベースのプロンプト構築システム。Handlebarsテンプレートエンジンによる動的プロンプト生成。
|
||||
|
||||
## 基本使用方法
|
||||
`PromptComposer` は、`PromptContext` と会話履歴からシステムプロンプト文字列を生成するクロージャをラップし、LLM へ送信するメッセージ列を構築します。
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use worker::prompt::{PromptComposer, PromptContext, PromptError, ResourceLoader};
|
||||
use worker::prompt::{PromptComposer, PromptContext, PromptError, SystemPromptFn};
|
||||
use worker_types::Message;
|
||||
|
||||
struct FsLoader;
|
||||
|
||||
impl ResourceLoader for FsLoader {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
std::fs::read_to_string(identifier)
|
||||
.map_err(|e| PromptError::FileNotFound(format!("{}: {}", identifier, e)))
|
||||
}
|
||||
fn build_context() -> PromptContext {
|
||||
// WorkspaceDetector などからアプリ固有の情報を収集して埋め込む
|
||||
todo!()
|
||||
}
|
||||
|
||||
// 初期化
|
||||
let loader = Arc::new(FsLoader);
|
||||
let mut composer = PromptComposer::from_config_file("role.yaml", context, loader.clone())?;
|
||||
composer.initialize_session(&messages)?;
|
||||
|
||||
// プロンプト構築
|
||||
let messages = composer.compose(&user_messages)?;
|
||||
```
|
||||
|
||||
## リソースローダー
|
||||
|
||||
`PromptComposer` はテンプレート内で参照されるパーシャルや `{{include_file}}` の解決をクレート利用者に委ねています。
|
||||
`ResourceLoader` トレイトを実装して、任意のストレージや命名規則に基づいて文字列を返してください。
|
||||
|
||||
```rust
|
||||
struct MyLoader;
|
||||
|
||||
impl ResourceLoader for MyLoader {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
match identifier.strip_prefix("#workspace/") {
|
||||
Some(rest) => {
|
||||
let path = std::env::current_dir()?.join(".nia").join(rest);
|
||||
std::fs::read_to_string(path).map_err(|e| PromptError::FileNotFound(e.to_string()))
|
||||
}
|
||||
None => std::fs::read_to_string(identifier)
|
||||
.map_err(|e| PromptError::FileNotFound(e.to_string())),
|
||||
}
|
||||
}
|
||||
fn generator(ctx: &PromptContext, messages: &[Message]) -> Result<String, PromptError> {
|
||||
Ok(format!(
|
||||
"Project {} has {} prior messages.",
|
||||
ctx.workspace
|
||||
.project_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| \"unknown\".into()),
|
||||
messages.len()
|
||||
))
|
||||
}
|
||||
|
||||
let context = build_context();
|
||||
let composer = PromptComposer::new(context, Arc::new(generator));
|
||||
let conversation = vec![Message::new(worker_types::Role::User, \"Hello\".into())];
|
||||
let final_messages = composer.compose(&conversation)?;
|
||||
```
|
||||
|
||||
## テンプレート構文
|
||||
|
||||
### 変数展開
|
||||
```handlebars
|
||||
{{workspace.project_name}} # プロジェクト名
|
||||
{{workspace.project_type}} # プロジェクト種別
|
||||
{{model.provider}}/{{model.model_name}} # モデル情報
|
||||
{{tools_schema}} # ツールスキーマ
|
||||
```
|
||||
|
||||
### 条件分岐
|
||||
```handlebars
|
||||
{{#if workspace.has_nia_md}}
|
||||
Project info: {{workspace_content}}
|
||||
{{/if}}
|
||||
|
||||
{{#eq workspace.project_type "Rust"}}
|
||||
Focus on memory safety and performance.
|
||||
{{/eq}}
|
||||
```
|
||||
|
||||
### 繰り返し処理
|
||||
```handlebars
|
||||
{{#each tools}}
|
||||
- **{{name}}**: {{description}}
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
### パーシャルテンプレート
|
||||
```handlebars
|
||||
{{> header}}
|
||||
{{> coding_guidelines}}
|
||||
{{> footer}}
|
||||
```
|
||||
|
||||
## カスタムヘルパー
|
||||
|
||||
### include_file
|
||||
外部ファイルを読み込み:
|
||||
```handlebars
|
||||
{{include_file "~/.config/nia/templates/guidelines.md"}}
|
||||
```
|
||||
|
||||
### workspace_content
|
||||
ワークスペースのnia.md内容を取得:
|
||||
```handlebars
|
||||
{{workspace_content}}
|
||||
```
|
||||
|
||||
## 利用可能なコンテキスト変数
|
||||
|
||||
- `workspace`: プロジェクト情報(root_path、project_type、git_info等)
|
||||
- `model`: LLMモデル情報(provider、model_name、capabilities)
|
||||
- `session`: セッション情報(conversation_id、message_count)
|
||||
- `user_input`: ユーザー入力内容
|
||||
- `tools_schema`: ツール定義JSON
|
||||
`compose_with_tools` を使うと、`tools_schema` をテンプレート変数として渡した上でシステムプロンプトを再生成できます。
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "worker"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -31,7 +31,6 @@ tracing = "0.1.40"
|
|||
eventsource-stream = "0.2.3"
|
||||
xdg = "3.0.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
handlebars = "5.1.2"
|
||||
regex = "1.10.2"
|
||||
uuid = { version = "1.10", features = ["v4", "serde"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
use std::collections::HashMap;
|
||||
use worker::{LlmProvider, PromptError, ResourceLoader, Role, Worker};
|
||||
|
||||
struct FsLoader;
|
||||
|
||||
impl ResourceLoader for FsLoader {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
std::fs::read_to_string(identifier)
|
||||
.map_err(|e| PromptError::FileNotFound(format!("{}: {}", identifier, e)))
|
||||
}
|
||||
}
|
||||
use worker::{LlmProvider, PromptContext, PromptError, Worker};
|
||||
use worker_types::Message;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Example 1: Basic role
|
||||
let role = Role::new(
|
||||
"assistant",
|
||||
"A helpful AI assistant",
|
||||
"You are a helpful, harmless, and honest AI assistant.",
|
||||
);
|
||||
// Example 1: Basic system prompt generator
|
||||
fn system_prompt(ctx: &PromptContext, _messages: &[Message]) -> Result<String, PromptError> {
|
||||
Ok(format!(
|
||||
"You are helping with the project: {}",
|
||||
ctx.workspace
|
||||
.project_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
))
|
||||
}
|
||||
|
||||
let mut api_keys = HashMap::new();
|
||||
api_keys.insert("claude".to_string(), std::env::var("ANTHROPIC_API_KEY")?);
|
||||
|
|
@ -26,27 +22,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.provider(LlmProvider::Claude)
|
||||
.model("claude-3-sonnet-20240229")
|
||||
.api_keys(api_keys)
|
||||
.resource_loader(FsLoader)
|
||||
.role(role)
|
||||
.system_prompt(system_prompt)
|
||||
.build()?;
|
||||
|
||||
println!("✅ Worker created with builder pattern");
|
||||
println!(" Provider: {:?}", worker.get_provider_name());
|
||||
println!(" Model: {}", worker.get_model_name());
|
||||
|
||||
// Example 2: Code reviewer role
|
||||
let code_reviewer_role = Role::new(
|
||||
"code-reviewer",
|
||||
"An AI that reviews code for best practices",
|
||||
"You are an expert code reviewer. Always provide constructive feedback.",
|
||||
);
|
||||
// Example 2: Different prompt generator
|
||||
fn reviewer_prompt(_ctx: &PromptContext, _messages: &[Message]) -> Result<String, PromptError> {
|
||||
Ok("You are an expert code reviewer. Always provide constructive feedback.".to_string())
|
||||
}
|
||||
|
||||
let _worker2 = Worker::builder()
|
||||
.provider(LlmProvider::Claude)
|
||||
.model("claude-3-sonnet-20240229")
|
||||
.api_key("claude", std::env::var("ANTHROPIC_API_KEY")?)
|
||||
.resource_loader(FsLoader)
|
||||
.role(code_reviewer_role)
|
||||
.system_prompt(reviewer_prompt)
|
||||
.build()?;
|
||||
|
||||
println!("✅ Worker created with custom role");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use worker::{
|
||||
PromptError, ResourceLoader, Role, Worker,
|
||||
PromptContext, PromptError, Worker,
|
||||
plugin::{PluginRegistry, ProviderPlugin, example_provider::CustomProviderPlugin},
|
||||
};
|
||||
|
||||
struct FsLoader;
|
||||
|
||||
impl ResourceLoader for FsLoader {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
std::fs::read_to_string(identifier)
|
||||
.map_err(|e| PromptError::FileNotFound(format!("{}: {}", identifier, e)))
|
||||
}
|
||||
}
|
||||
use worker_types::Message;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -58,18 +50,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
// Create a Worker instance using the plugin
|
||||
let role = Role::new(
|
||||
"assistant",
|
||||
"A helpful AI assistant",
|
||||
"You are a helpful, harmless, and honest AI assistant powered by a custom LLM provider.",
|
||||
);
|
||||
fn plugin_prompt(_ctx: &PromptContext, _messages: &[Message]) -> Result<String, PromptError> {
|
||||
Ok("You are a helpful assistant powered by a custom provider.".to_string())
|
||||
}
|
||||
|
||||
let worker = Worker::builder()
|
||||
.plugin("custom-provider", plugin_registry.clone())
|
||||
.model("custom-turbo")
|
||||
.api_key("__plugin__", "custom-1234567890abcdefghijklmnop")
|
||||
.resource_loader(FsLoader)
|
||||
.role(role)
|
||||
.system_prompt(plugin_prompt)
|
||||
.build()?;
|
||||
|
||||
println!("\nWorker created successfully with custom provider plugin!");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
use crate::Worker;
|
||||
use crate::prompt::{ResourceLoader, Role};
|
||||
use crate::prompt::{PromptComposer, PromptContext, PromptError, SystemPromptFn};
|
||||
use crate::types::WorkerError;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use worker_types::LlmProvider;
|
||||
use worker_types::{LlmProvider, Message};
|
||||
|
||||
// Type-state markers
|
||||
pub struct NoProvider;
|
||||
pub struct WithProvider;
|
||||
pub struct NoModel;
|
||||
pub struct WithModel;
|
||||
pub struct NoRole;
|
||||
pub struct WithRole;
|
||||
pub struct NoSystemPrompt;
|
||||
pub struct WithSystemPrompt;
|
||||
|
||||
/// WorkerBuilder with type-state pattern
|
||||
///
|
||||
|
|
@ -20,51 +20,40 @@ pub struct WithRole;
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use worker::{Worker, LlmProvider, Role, ResourceLoader, PromptError};
|
||||
/// use worker::{Worker, LlmProvider, PromptContext, PromptError};
|
||||
///
|
||||
/// struct FsLoader;
|
||||
///
|
||||
/// impl ResourceLoader for FsLoader {
|
||||
/// fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
/// std::fs::read_to_string(identifier)
|
||||
/// .map_err(|e| PromptError::FileNotFound(format!("{}: {}", identifier, e)))
|
||||
/// }
|
||||
/// fn system_prompt(
|
||||
/// _ctx: &PromptContext,
|
||||
/// _messages: &[worker_types::Message],
|
||||
/// ) -> Result<String, PromptError> {
|
||||
/// Ok("You are a helpful assistant.".to_string())
|
||||
/// }
|
||||
///
|
||||
/// let role = Role::new("assistant", "AI Assistant", "You are a helpful assistant.");
|
||||
/// let worker = Worker::builder()
|
||||
/// .provider(LlmProvider::Claude)
|
||||
/// .model("claude-3-sonnet-20240229")
|
||||
/// .api_key("claude", "sk-ant-...")
|
||||
/// .resource_loader(FsLoader)
|
||||
/// .role(role)
|
||||
/// .system_prompt(system_prompt)
|
||||
/// .build()?;
|
||||
/// # Ok::<(), worker::WorkerError>(())
|
||||
/// ```
|
||||
pub struct WorkerBuilder<P, M, R> {
|
||||
pub struct WorkerBuilder<P, M, S> {
|
||||
provider: Option<LlmProvider>,
|
||||
model_name: Option<String>,
|
||||
api_keys: HashMap<String, String>,
|
||||
|
||||
// Role configuration (required)
|
||||
role: Option<Role>,
|
||||
resource_loader: Option<Arc<dyn ResourceLoader>>,
|
||||
|
||||
// Plugin configuration
|
||||
system_prompt_fn: Option<Arc<SystemPromptFn>>,
|
||||
plugin_id: Option<String>,
|
||||
plugin_registry: Option<Arc<Mutex<crate::plugin::PluginRegistry>>>,
|
||||
|
||||
_phantom: PhantomData<(P, M, R)>,
|
||||
_phantom: PhantomData<(P, M, S)>,
|
||||
}
|
||||
|
||||
impl Default for WorkerBuilder<NoProvider, NoModel, NoRole> {
|
||||
impl Default for WorkerBuilder<NoProvider, NoModel, NoSystemPrompt> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: None,
|
||||
model_name: None,
|
||||
api_keys: HashMap::new(),
|
||||
role: None,
|
||||
resource_loader: None,
|
||||
system_prompt_fn: None,
|
||||
plugin_id: None,
|
||||
plugin_registry: None,
|
||||
_phantom: PhantomData,
|
||||
|
|
@ -72,23 +61,21 @@ impl Default for WorkerBuilder<NoProvider, NoModel, NoRole> {
|
|||
}
|
||||
}
|
||||
|
||||
impl WorkerBuilder<NoProvider, NoModel, NoRole> {
|
||||
/// Create a new WorkerBuilder
|
||||
impl WorkerBuilder<NoProvider, NoModel, NoSystemPrompt> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Set provider
|
||||
impl<M, R> WorkerBuilder<NoProvider, M, R> {
|
||||
pub fn provider(mut self, provider: LlmProvider) -> WorkerBuilder<WithProvider, M, R> {
|
||||
impl<M, S> WorkerBuilder<NoProvider, M, S> {
|
||||
pub fn provider(mut self, provider: LlmProvider) -> WorkerBuilder<WithProvider, M, S> {
|
||||
self.provider = Some(provider);
|
||||
WorkerBuilder {
|
||||
provider: self.provider,
|
||||
model_name: self.model_name,
|
||||
api_keys: self.api_keys,
|
||||
role: self.role,
|
||||
resource_loader: self.resource_loader,
|
||||
system_prompt_fn: self.system_prompt_fn,
|
||||
plugin_id: self.plugin_id,
|
||||
plugin_registry: self.plugin_registry,
|
||||
_phantom: PhantomData,
|
||||
|
|
@ -100,15 +87,14 @@ impl<M, R> WorkerBuilder<NoProvider, M, R> {
|
|||
mut self,
|
||||
plugin_id: impl Into<String>,
|
||||
registry: Arc<Mutex<crate::plugin::PluginRegistry>>,
|
||||
) -> WorkerBuilder<WithProvider, M, R> {
|
||||
) -> WorkerBuilder<WithProvider, M, S> {
|
||||
self.plugin_id = Some(plugin_id.into());
|
||||
self.plugin_registry = Some(registry);
|
||||
WorkerBuilder {
|
||||
provider: None,
|
||||
model_name: self.model_name,
|
||||
api_keys: self.api_keys,
|
||||
role: self.role,
|
||||
resource_loader: self.resource_loader,
|
||||
system_prompt_fn: self.system_prompt_fn,
|
||||
plugin_id: self.plugin_id,
|
||||
plugin_registry: self.plugin_registry,
|
||||
_phantom: PhantomData,
|
||||
|
|
@ -117,18 +103,17 @@ impl<M, R> WorkerBuilder<NoProvider, M, R> {
|
|||
}
|
||||
|
||||
// Step 2: Set model
|
||||
impl<R> WorkerBuilder<WithProvider, NoModel, R> {
|
||||
impl<S> WorkerBuilder<WithProvider, NoModel, S> {
|
||||
pub fn model(
|
||||
mut self,
|
||||
model_name: impl Into<String>,
|
||||
) -> WorkerBuilder<WithProvider, WithModel, R> {
|
||||
) -> WorkerBuilder<WithProvider, WithModel, S> {
|
||||
self.model_name = Some(model_name.into());
|
||||
WorkerBuilder {
|
||||
provider: self.provider,
|
||||
model_name: self.model_name,
|
||||
api_keys: self.api_keys,
|
||||
role: self.role,
|
||||
resource_loader: self.resource_loader,
|
||||
system_prompt_fn: self.system_prompt_fn,
|
||||
plugin_id: self.plugin_id,
|
||||
plugin_registry: self.plugin_registry,
|
||||
_phantom: PhantomData,
|
||||
|
|
@ -136,16 +121,21 @@ impl<R> WorkerBuilder<WithProvider, NoModel, R> {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 3: Set role
|
||||
impl WorkerBuilder<WithProvider, WithModel, NoRole> {
|
||||
pub fn role(mut self, role: Role) -> WorkerBuilder<WithProvider, WithModel, WithRole> {
|
||||
self.role = Some(role);
|
||||
// Step 3: Register system prompt generator
|
||||
impl WorkerBuilder<WithProvider, WithModel, NoSystemPrompt> {
|
||||
pub fn system_prompt<F>(
|
||||
mut self,
|
||||
generator: F,
|
||||
) -> WorkerBuilder<WithProvider, WithModel, WithSystemPrompt>
|
||||
where
|
||||
F: Fn(&PromptContext, &[Message]) -> Result<String, PromptError> + Send + Sync + 'static,
|
||||
{
|
||||
self.system_prompt_fn = Some(Arc::new(generator));
|
||||
WorkerBuilder {
|
||||
provider: self.provider,
|
||||
model_name: self.model_name,
|
||||
api_keys: self.api_keys,
|
||||
role: self.role,
|
||||
resource_loader: self.resource_loader,
|
||||
system_prompt_fn: self.system_prompt_fn,
|
||||
plugin_id: self.plugin_id,
|
||||
plugin_registry: self.plugin_registry,
|
||||
_phantom: PhantomData,
|
||||
|
|
@ -153,45 +143,33 @@ impl WorkerBuilder<WithProvider, WithModel, NoRole> {
|
|||
}
|
||||
}
|
||||
|
||||
// Optional configurations (available at any stage)
|
||||
impl<P, M, R> WorkerBuilder<P, M, R> {
|
||||
/// Add API key for a provider
|
||||
// Optional configurations
|
||||
impl<P, M, S> WorkerBuilder<P, M, S> {
|
||||
pub fn api_key(mut self, provider: impl Into<String>, key: impl Into<String>) -> Self {
|
||||
self.api_keys.insert(provider.into(), key.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple API keys at once
|
||||
pub fn api_keys(mut self, keys: HashMap<String, String>) -> Self {
|
||||
self.api_keys = keys;
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a resource loader implementation for partial/include resolution
|
||||
pub fn resource_loader<L>(mut self, loader: L) -> Self
|
||||
where
|
||||
L: ResourceLoader + 'static,
|
||||
{
|
||||
self.resource_loader = Some(Arc::new(loader));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Build
|
||||
impl WorkerBuilder<WithProvider, WithModel, WithRole> {
|
||||
impl WorkerBuilder<WithProvider, WithModel, WithSystemPrompt> {
|
||||
pub fn build(self) -> Result<Worker, WorkerError> {
|
||||
use crate::{LlmProviderExt, PromptComposer, WorkspaceDetector, plugin};
|
||||
use crate::{LlmProviderExt, WorkspaceDetector, plugin};
|
||||
|
||||
let resource_loader = self.resource_loader.clone().ok_or_else(|| {
|
||||
let system_prompt_fn = self.system_prompt_fn.ok_or_else(|| {
|
||||
WorkerError::config(
|
||||
"Resource loader is required. Call resource_loader(...) before build.",
|
||||
"System prompt generator is required. Call system_prompt(...) before build.",
|
||||
)
|
||||
})?;
|
||||
|
||||
let role = self.role.unwrap();
|
||||
let model_name = self.model_name.unwrap();
|
||||
|
||||
// Plugin provider
|
||||
// Plugin-backed provider
|
||||
if let (Some(plugin_id), Some(plugin_registry)) = (self.plugin_id, self.plugin_registry) {
|
||||
let api_key_opt = self
|
||||
.api_keys
|
||||
|
|
@ -215,7 +193,6 @@ impl WorkerBuilder<WithProvider, WithModel, WithRole> {
|
|||
|
||||
let provider_str = plugin_id.clone();
|
||||
let api_key = api_key_opt.map(|s| s.to_string()).unwrap_or_default();
|
||||
|
||||
let workspace_context = WorkspaceDetector::detect_workspace().ok();
|
||||
|
||||
let prompt_context = Worker::create_prompt_context_static(
|
||||
|
|
@ -225,23 +202,15 @@ impl WorkerBuilder<WithProvider, WithModel, WithRole> {
|
|||
&[],
|
||||
);
|
||||
|
||||
tracing::info!("Creating worker with plugin and role: {}", role.name);
|
||||
|
||||
let composer =
|
||||
PromptComposer::from_config(role.clone(), prompt_context, resource_loader.clone())
|
||||
.map_err(|e| WorkerError::config(e.to_string()))?;
|
||||
|
||||
drop(registry);
|
||||
|
||||
let mut worker = Worker {
|
||||
llm_client: Box::new(llm_client),
|
||||
composer,
|
||||
resource_loader: resource_loader.clone(),
|
||||
composer: PromptComposer::new(prompt_context, system_prompt_fn.clone()),
|
||||
tools: Vec::new(),
|
||||
api_key,
|
||||
provider_str,
|
||||
model_name,
|
||||
role,
|
||||
workspace_context,
|
||||
message_history: Vec::new(),
|
||||
hook_manager: worker_types::HookManager::new(),
|
||||
|
|
@ -256,7 +225,7 @@ impl WorkerBuilder<WithProvider, WithModel, WithRole> {
|
|||
return Ok(worker);
|
||||
}
|
||||
|
||||
// Standard provider
|
||||
// Built-in provider
|
||||
let provider = self.provider.unwrap();
|
||||
let provider_str = provider.as_str();
|
||||
let api_key = self.api_keys.get(provider_str).cloned().unwrap_or_default();
|
||||
|
|
@ -272,21 +241,13 @@ impl WorkerBuilder<WithProvider, WithModel, WithRole> {
|
|||
&[],
|
||||
);
|
||||
|
||||
tracing::info!("Creating worker with role: {}", role.name);
|
||||
|
||||
let composer =
|
||||
PromptComposer::from_config(role.clone(), prompt_context, resource_loader.clone())
|
||||
.map_err(|e| WorkerError::config(e.to_string()))?;
|
||||
|
||||
let mut worker = Worker {
|
||||
llm_client: Box::new(llm_client),
|
||||
composer,
|
||||
resource_loader,
|
||||
composer: PromptComposer::new(prompt_context, system_prompt_fn.clone()),
|
||||
tools: Vec::new(),
|
||||
api_key,
|
||||
provider_str: provider_str.to_string(),
|
||||
model_name,
|
||||
role,
|
||||
workspace_context,
|
||||
message_history: Vec::new(),
|
||||
hook_manager: worker_types::HookManager::new(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
mod parser;
|
||||
mod url;
|
||||
|
||||
pub use parser::ConfigParser;
|
||||
pub use url::UrlConfig;
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
use crate::prompt::{PromptError, Role};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// 設定ファイルのパーサー
|
||||
pub struct ConfigParser;
|
||||
|
||||
impl ConfigParser {
|
||||
/// YAML設定ファイルを読み込んでパースする
|
||||
pub fn parse_from_file<P: AsRef<Path>>(path: P) -> Result<Role, PromptError> {
|
||||
let content = fs::read_to_string(path.as_ref()).map_err(|e| {
|
||||
PromptError::FileNotFound(format!("{}: {}", path.as_ref().display(), e))
|
||||
})?;
|
||||
|
||||
Self::parse_from_string(&content)
|
||||
}
|
||||
|
||||
/// YAML文字列をパースしてRoleに変換する
|
||||
pub fn parse_from_string(content: &str) -> Result<Role, PromptError> {
|
||||
let config: Role = serde_yaml::from_str(content)?;
|
||||
|
||||
// 基本的なバリデーション
|
||||
Self::validate_config(&config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 設定ファイルの基本的なバリデーション
|
||||
fn validate_config(config: &Role) -> Result<(), PromptError> {
|
||||
if config.name.is_empty() {
|
||||
return Err(PromptError::VariableResolution(
|
||||
"name field cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if config.template.is_empty() {
|
||||
return Err(PromptError::TemplateCompilation(
|
||||
"template field cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// パーシャルのパス検証
|
||||
if let Some(partials) = &config.partials {
|
||||
for (name, partial) in partials {
|
||||
if partial.path.is_empty() {
|
||||
return Err(PromptError::PartialLoading(format!(
|
||||
"partial '{}' has empty path",
|
||||
name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
use crate::prompt::{
|
||||
ModelCapabilities, ModelContext, PromptComposer, PromptContext, SessionContext,
|
||||
WorkspaceContext,
|
||||
};
|
||||
use crate::prompt::{ModelCapabilities, ModelContext, SessionContext, WorkspaceContext};
|
||||
use crate::workspace::WorkspaceDetector;
|
||||
use async_stream::stream;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
|
|
@ -33,17 +30,12 @@ pub mod prompt;
|
|||
pub mod types;
|
||||
pub mod workspace;
|
||||
|
||||
pub use crate::prompt::{PromptError, ResourceLoader, Role};
|
||||
pub use crate::prompt::{PromptComposer, PromptContext, PromptError, SystemPromptFn};
|
||||
pub use crate::types::WorkerError;
|
||||
pub use builder::WorkerBuilder;
|
||||
pub use client::LlmClient;
|
||||
pub use core::LlmClientTrait;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod config_tests;
|
||||
}
|
||||
|
||||
pub use schemars;
|
||||
pub use serde_json;
|
||||
|
||||
|
|
@ -442,13 +434,11 @@ pub async fn supports_native_tools(
|
|||
|
||||
pub struct Worker {
|
||||
pub(crate) llm_client: Box<dyn LlmClientTrait>,
|
||||
pub(crate) composer: PromptComposer,
|
||||
pub(crate) resource_loader: std::sync::Arc<dyn ResourceLoader>,
|
||||
pub(crate) composer: crate::prompt::PromptComposer,
|
||||
pub(crate) tools: Vec<Box<dyn Tool>>,
|
||||
pub(crate) api_key: String,
|
||||
pub(crate) provider_str: String,
|
||||
pub(crate) model_name: String,
|
||||
pub(crate) role: Role,
|
||||
pub(crate) workspace_context: Option<WorkspaceContext>,
|
||||
pub(crate) message_history: Vec<Message>,
|
||||
pub(crate) hook_manager: crate::types::HookManager,
|
||||
|
|
@ -461,29 +451,22 @@ impl Worker {
|
|||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use worker::{Worker, LlmProvider, Role, PromptError, ResourceLoader};
|
||||
/// use worker::{Worker, LlmProvider, PromptContext, PromptError};
|
||||
///
|
||||
/// struct FsLoader;
|
||||
///
|
||||
/// impl ResourceLoader for FsLoader {
|
||||
/// fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
/// std::fs::read_to_string(identifier)
|
||||
/// .map_err(|e| PromptError::FileNotFound(format!("{}: {}", identifier, e)))
|
||||
/// }
|
||||
/// fn system_prompt(_ctx: &PromptContext, _messages: &[worker_types::Message]) -> Result<String, PromptError> {
|
||||
/// Ok("You are a helpful assistant.".to_string())
|
||||
/// }
|
||||
///
|
||||
/// let role = Role::new("assistant", "AI Assistant", "You are a helpful assistant.");
|
||||
/// let worker = Worker::builder()
|
||||
/// .provider(LlmProvider::Claude)
|
||||
/// .model("claude-3-sonnet-20240229")
|
||||
/// .api_key("claude", "sk-ant-...")
|
||||
/// .resource_loader(FsLoader)
|
||||
/// .role(role)
|
||||
/// .system_prompt(system_prompt)
|
||||
/// .build()?;
|
||||
/// # Ok::<(), worker::WorkerError>(())
|
||||
/// ```
|
||||
pub fn builder()
|
||||
-> builder::WorkerBuilder<builder::NoProvider, builder::NoModel, builder::NoRole> {
|
||||
-> builder::WorkerBuilder<builder::NoProvider, builder::NoModel, builder::NoSystemPrompt> {
|
||||
builder::WorkerBuilder::new()
|
||||
}
|
||||
|
||||
|
|
@ -784,43 +767,13 @@ impl Worker {
|
|||
)
|
||||
}
|
||||
|
||||
/// 設定を読み込む
|
||||
pub fn load_config<P: AsRef<std::path::Path>>(
|
||||
&mut self,
|
||||
config_path: P,
|
||||
) -> Result<(), WorkerError> {
|
||||
use crate::config::ConfigParser;
|
||||
|
||||
// 設定ファイルを読み込み
|
||||
let role = ConfigParser::parse_from_file(config_path)
|
||||
.map_err(|e| WorkerError::config(e.to_string()))?;
|
||||
|
||||
// プロンプトコンテキストを構築
|
||||
let prompt_context = self.create_prompt_context()?;
|
||||
|
||||
// DynamicPromptComposerを作成
|
||||
let composer =
|
||||
PromptComposer::from_config(role.clone(), prompt_context, self.resource_loader.clone())
|
||||
.map_err(|e| WorkerError::config(e.to_string()))?;
|
||||
|
||||
self.role = role;
|
||||
self.composer = composer;
|
||||
|
||||
// 設定変更後にセッション再初期化
|
||||
self.initialize_session()
|
||||
.map_err(|e| WorkerError::config(e.to_string()))?;
|
||||
|
||||
tracing::info!("Dynamic configuration loaded successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 静的プロンプトコンテキストを作成(構築時用)
|
||||
fn create_prompt_context_static(
|
||||
workspace_context: &Option<WorkspaceContext>,
|
||||
provider: LlmProvider,
|
||||
model_name: &str,
|
||||
tools: &[String],
|
||||
) -> PromptContext {
|
||||
) -> crate::prompt::PromptContext {
|
||||
let supports_native_tools = match provider {
|
||||
LlmProvider::Claude => true,
|
||||
LlmProvider::OpenAI => !model_name.contains("gpt-3.5-turbo-instruct"),
|
||||
|
|
@ -853,7 +806,7 @@ impl Worker {
|
|||
|
||||
let workspace_context = workspace_context.clone().unwrap_or_default();
|
||||
|
||||
PromptContext {
|
||||
crate::prompt::PromptContext {
|
||||
workspace: workspace_context,
|
||||
model: model_context,
|
||||
session: session_context,
|
||||
|
|
@ -862,7 +815,7 @@ impl Worker {
|
|||
}
|
||||
|
||||
/// プロンプトコンテキストを作成
|
||||
fn create_prompt_context(&self) -> Result<PromptContext, WorkerError> {
|
||||
fn create_prompt_context(&self) -> Result<crate::prompt::PromptContext, WorkerError> {
|
||||
let provider = LlmProvider::from_str(&self.provider_str).ok_or_else(|| {
|
||||
WorkerError::config(format!("Unknown provider: {}", self.provider_str))
|
||||
})?;
|
||||
|
|
@ -902,7 +855,7 @@ impl Worker {
|
|||
|
||||
let workspace_context = self.workspace_context.clone().unwrap_or_default();
|
||||
|
||||
Ok(PromptContext {
|
||||
Ok(crate::prompt::PromptContext {
|
||||
workspace: workspace_context,
|
||||
model: model_context,
|
||||
session: session_context,
|
||||
|
|
@ -999,13 +952,8 @@ impl Worker {
|
|||
}
|
||||
|
||||
/// Get configuration information for task delegation
|
||||
pub fn get_config(&self) -> (LlmProvider, &str, &str, &Role) {
|
||||
(
|
||||
self.llm_client.provider(),
|
||||
&self.model_name,
|
||||
&self.api_key,
|
||||
&self.role,
|
||||
)
|
||||
pub fn get_config(&self) -> (LlmProvider, &str, &str) {
|
||||
(self.llm_client.provider(), &self.model_name, &self.api_key)
|
||||
}
|
||||
|
||||
/// Get tool names (used to filter out specific tools)
|
||||
|
|
|
|||
|
|
@ -1,342 +1,83 @@
|
|||
use super::types::*;
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext};
|
||||
use std::path::Path;
|
||||
use super::types::{PromptContext, PromptError};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Import Message and Role enum from worker_types
|
||||
use worker_types::{Message, Role as MessageRole};
|
||||
|
||||
/// プロンプト構築システム
|
||||
pub type SystemPromptFn =
|
||||
dyn Fn(&PromptContext, &[Message]) -> Result<String, PromptError> + Send + Sync;
|
||||
|
||||
/// シンプルなシステムプロンプト生成ラッパー
|
||||
#[derive(Clone)]
|
||||
pub struct PromptComposer {
|
||||
config: Role,
|
||||
handlebars: Handlebars<'static>,
|
||||
context: PromptContext,
|
||||
system_prompt: Option<String>,
|
||||
resource_loader: Arc<dyn ResourceLoader>,
|
||||
generator: Arc<SystemPromptFn>,
|
||||
cached_prompt: Arc<std::sync::Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl PromptComposer {
|
||||
/// 設定ファイルから新しいインスタンスを作成
|
||||
pub fn from_config_file<P: AsRef<Path>>(
|
||||
config_path: P,
|
||||
context: PromptContext,
|
||||
resource_loader: Arc<dyn ResourceLoader>,
|
||||
) -> Result<Self, PromptError> {
|
||||
let config = crate::config::ConfigParser::parse_from_file(config_path)?;
|
||||
Self::from_config(config, context, resource_loader)
|
||||
}
|
||||
|
||||
/// 設定オブジェクトから新しいインスタンスを作成
|
||||
pub fn from_config(
|
||||
config: Role,
|
||||
context: PromptContext,
|
||||
resource_loader: Arc<dyn ResourceLoader>,
|
||||
) -> Result<Self, PromptError> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
// カスタムヘルパー関数を登録
|
||||
Self::register_custom_helpers(&mut handlebars, resource_loader.clone())?;
|
||||
|
||||
let mut composer = Self {
|
||||
config,
|
||||
handlebars,
|
||||
pub fn new(context: PromptContext, generator: Arc<SystemPromptFn>) -> Self {
|
||||
Self {
|
||||
context,
|
||||
system_prompt: None,
|
||||
resource_loader,
|
||||
};
|
||||
|
||||
// パーシャルテンプレートを読み込み・登録
|
||||
composer.load_partials()?;
|
||||
|
||||
Ok(composer)
|
||||
generator,
|
||||
cached_prompt: Arc::new(std::sync::Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// セッション開始時にシステムプロンプトを事前構築
|
||||
/// 初期化時にシステムプロンプトを先行生成してキャッシュ
|
||||
pub fn initialize_session(&mut self, initial_messages: &[Message]) -> Result<(), PromptError> {
|
||||
let system_prompt = self.compose_system_prompt(initial_messages)?;
|
||||
self.system_prompt = Some(system_prompt);
|
||||
let prompt = (self.generator)(&self.context, initial_messages)?;
|
||||
if let Ok(mut guard) = self.cached_prompt.lock() {
|
||||
*guard = Some(prompt);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// メインのプロンプト構築メソッド
|
||||
pub fn compose(&self, messages: &[Message]) -> Result<Vec<Message>, PromptError> {
|
||||
if let Some(system_prompt) = &self.system_prompt {
|
||||
// システムプロンプトが既に構築済みの場合、それを使用
|
||||
let mut result_messages =
|
||||
vec![Message::new(MessageRole::System, system_prompt.clone())];
|
||||
|
||||
// ユーザーメッセージを追加
|
||||
for msg in messages {
|
||||
if msg.role != MessageRole::System {
|
||||
result_messages.push(msg.clone());
|
||||
}
|
||||
let system_prompt = self.generate_with_context(&self.context, messages)?;
|
||||
Ok(self.build_message_list(messages, system_prompt))
|
||||
}
|
||||
|
||||
Ok(result_messages)
|
||||
} else {
|
||||
// フォールバック: 従来の動的構築
|
||||
self.compose_with_context(messages, &self.context)
|
||||
}
|
||||
}
|
||||
|
||||
/// ツール情報を含むセッション初期化
|
||||
pub fn initialize_session_with_tools(
|
||||
&mut self,
|
||||
initial_messages: &[Message],
|
||||
tools_schema: &serde_json::Value,
|
||||
) -> Result<(), PromptError> {
|
||||
// 一時的にコンテキストをコピーしてツールスキーマを追加
|
||||
let mut temp_context = self.context.clone();
|
||||
temp_context
|
||||
.variables
|
||||
.insert("tools_schema".to_string(), tools_schema.clone());
|
||||
|
||||
let system_prompt =
|
||||
self.compose_system_prompt_with_context(initial_messages, &temp_context)?;
|
||||
self.system_prompt = Some(system_prompt);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ツール情報を含むプロンプト構築(後方互換性のため保持)
|
||||
pub fn compose_with_tools(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
tools_schema: &serde_json::Value,
|
||||
) -> Result<Vec<Message>, PromptError> {
|
||||
if let Some(system_prompt) = &self.system_prompt {
|
||||
// システムプロンプトが既に構築済みの場合、それを使用
|
||||
let mut result_messages =
|
||||
vec![Message::new(MessageRole::System, system_prompt.clone())];
|
||||
|
||||
// ユーザーメッセージを追加
|
||||
for msg in messages {
|
||||
if msg.role != MessageRole::System {
|
||||
result_messages.push(msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result_messages)
|
||||
} else {
|
||||
// フォールバック: 従来の動的構築
|
||||
let mut temp_context = self.context.clone();
|
||||
temp_context
|
||||
let mut context = self.context.clone();
|
||||
context
|
||||
.variables
|
||||
.insert("tools_schema".to_string(), tools_schema.clone());
|
||||
|
||||
self.compose_with_context(messages, &temp_context)
|
||||
}
|
||||
let system_prompt = self.generate_with_context(&context, messages)?;
|
||||
Ok(self.build_message_list(messages, system_prompt))
|
||||
}
|
||||
|
||||
/// システムプロンプトのみを構築(セッション初期化用)
|
||||
fn compose_system_prompt(&self, messages: &[Message]) -> Result<String, PromptError> {
|
||||
self.compose_system_prompt_with_context(messages, &self.context)
|
||||
}
|
||||
|
||||
/// コンテキストを指定してシステムプロンプトを構築
|
||||
fn compose_system_prompt_with_context(
|
||||
fn generate_with_context(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
context: &PromptContext,
|
||||
messages: &[Message],
|
||||
) -> Result<String, PromptError> {
|
||||
// コンテキスト変数を準備
|
||||
let mut template_data = self.prepare_template_data_with_context(messages, context)?;
|
||||
|
||||
// 条件評価と変数の動的設定
|
||||
self.apply_conditions(&mut template_data)?;
|
||||
|
||||
// メインテンプレートを実行
|
||||
let system_prompt = self
|
||||
.handlebars
|
||||
.render_template(&self.config.template, &template_data)
|
||||
.map_err(PromptError::Handlebars)?;
|
||||
|
||||
Ok(system_prompt)
|
||||
match (self.generator)(context, messages) {
|
||||
Ok(prompt) => {
|
||||
if let Ok(mut guard) = self.cached_prompt.lock() {
|
||||
*guard = Some(prompt.clone());
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
Err(err) => match self.cached_prompt.lock() {
|
||||
Ok(cache) => cache.as_ref().cloned().ok_or(err),
|
||||
Err(_) => Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// コンテキストを指定してプロンプトを構築(後方互換性のため保持)
|
||||
fn compose_with_context(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
context: &PromptContext,
|
||||
) -> Result<Vec<Message>, PromptError> {
|
||||
let system_prompt = self.compose_system_prompt_with_context(messages, context)?;
|
||||
fn build_message_list(&self, messages: &[Message], system_prompt: String) -> Vec<Message> {
|
||||
let mut result = Vec::with_capacity(messages.len() + 1);
|
||||
result.push(Message::new(MessageRole::System, system_prompt));
|
||||
|
||||
// システムメッセージとユーザーメッセージを結合
|
||||
let mut result_messages = vec![Message::new(MessageRole::System, system_prompt)];
|
||||
|
||||
// ユーザーメッセージを追加
|
||||
for msg in messages {
|
||||
if msg.role != MessageRole::System {
|
||||
result_messages.push(msg.clone());
|
||||
result.push(msg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result_messages)
|
||||
}
|
||||
|
||||
/// カスタムヘルパー関数を登録
|
||||
fn register_custom_helpers(
|
||||
handlebars: &mut Handlebars<'static>,
|
||||
resource_loader: Arc<dyn ResourceLoader>,
|
||||
) -> Result<(), PromptError> {
|
||||
handlebars.register_helper(
|
||||
"include_file",
|
||||
Box::new(IncludeFileHelper {
|
||||
loader: resource_loader.clone(),
|
||||
}),
|
||||
);
|
||||
handlebars.register_helper("workspace_content", Box::new(workspace_content_helper));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// パーシャルテンプレートを読み込み・登録
|
||||
fn load_partials(&mut self) -> Result<(), PromptError> {
|
||||
if let Some(partials) = &self.config.partials {
|
||||
for (name, partial_config) in partials {
|
||||
let content = self.load_partial_content(partial_config)?;
|
||||
self.handlebars
|
||||
.register_partial(name, content)
|
||||
.map_err(|e| PromptError::PartialLoading(e.to_string()))?;
|
||||
result
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// パーシャルの内容を読み込み(フォールバック対応)
|
||||
fn load_partial_content(&self, partial_config: &PartialConfig) -> Result<String, PromptError> {
|
||||
match self.resource_loader.load(&partial_config.path) {
|
||||
Ok(content) => Ok(content),
|
||||
Err(primary_err) => {
|
||||
if let Some(fallback) = &partial_config.fallback {
|
||||
match self.resource_loader.load(fallback) {
|
||||
Ok(content) => Ok(content),
|
||||
Err(fallback_err) => Err(PromptError::PartialLoading(format!(
|
||||
"Could not load partial '{}' (fallback: {:?}): primary error={}, fallback error={}",
|
||||
partial_config.path, partial_config.fallback, primary_err, fallback_err
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(primary_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// コンテキストを指定してテンプレート用のデータを準備
|
||||
fn prepare_template_data_with_context(
|
||||
&self,
|
||||
messages: &[Message],
|
||||
context: &PromptContext,
|
||||
) -> Result<serde_json::Value, PromptError> {
|
||||
let user_input = messages
|
||||
.iter()
|
||||
.filter(|m| m.role == MessageRole::User)
|
||||
.map(|m| m.content.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
|
||||
let mut data = serde_json::json!({
|
||||
"workspace": context.workspace,
|
||||
"model": context.model,
|
||||
"session": context.session,
|
||||
"user_input": user_input,
|
||||
"tools": context.variables.get("tools_schema").unwrap_or(&serde_json::Value::Null),
|
||||
"tools_schema": context.variables.get("tools_schema").unwrap_or(&serde_json::Value::Null),
|
||||
});
|
||||
|
||||
// 設定ファイルの変数を追加
|
||||
if let Some(variables) = &self.config.variables {
|
||||
for (key, value_template) in variables {
|
||||
// 変数値もHandlebarsテンプレートとして処理
|
||||
let resolved_value = self
|
||||
.handlebars
|
||||
.render_template(value_template, &data)
|
||||
.map_err(PromptError::Handlebars)?;
|
||||
data[key] = serde_json::Value::String(resolved_value);
|
||||
}
|
||||
}
|
||||
|
||||
// コンテキストの追加変数をマージ
|
||||
for (key, value) in &context.variables {
|
||||
data[key] = value.clone();
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 条件評価と動的変数設定
|
||||
fn apply_conditions(&self, data: &mut serde_json::Value) -> Result<(), PromptError> {
|
||||
if let Some(conditions) = &self.config.conditions {
|
||||
for (_condition_name, condition_config) in conditions {
|
||||
// 条件式を評価
|
||||
let condition_result = self
|
||||
.handlebars
|
||||
.render_template(&condition_config.when, data)
|
||||
.map_err(PromptError::Handlebars)?;
|
||||
|
||||
// 条件が真の場合、変数を適用
|
||||
if condition_result.trim() == "true" {
|
||||
if let Some(variables) = &condition_config.variables {
|
||||
for (key, value_template) in variables {
|
||||
let resolved_value = self
|
||||
.handlebars
|
||||
.render_template(value_template, data)
|
||||
.map_err(PromptError::Handlebars)?;
|
||||
data[key] = serde_json::Value::String(resolved_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// カスタムヘルパー関数の実装
|
||||
|
||||
struct IncludeFileHelper {
|
||||
loader: Arc<dyn ResourceLoader>,
|
||||
}
|
||||
|
||||
impl HelperDef for IncludeFileHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'rc>,
|
||||
_handlebars: &Handlebars<'reg>,
|
||||
_context: &Context,
|
||||
_rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> HelperResult {
|
||||
let file_path = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
|
||||
|
||||
match self.loader.load(file_path) {
|
||||
Ok(content) => {
|
||||
out.write(&content)?;
|
||||
}
|
||||
Err(_) => {
|
||||
// ファイルが見つからない場合は空文字を出力
|
||||
out.write("")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_content_helper(
|
||||
_h: &Helper,
|
||||
_hbs: &Handlebars,
|
||||
ctx: &Context,
|
||||
_rc: &mut RenderContext,
|
||||
out: &mut dyn Output,
|
||||
) -> HelperResult {
|
||||
if let Some(workspace) = ctx.data().get("workspace") {
|
||||
if let Some(content) = workspace.get("nia_md_content") {
|
||||
if let Some(content_str) = content.as_str() {
|
||||
out.write(content_str)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
mod composer;
|
||||
mod types;
|
||||
|
||||
pub use composer::PromptComposer;
|
||||
pub use composer::{PromptComposer, SystemPromptFn};
|
||||
pub use types::{
|
||||
ConditionConfig, GitInfo, ModelCapabilities, ModelContext, PartialConfig, ProjectType,
|
||||
PromptContext, PromptError, ResourceLoader, Role, SessionContext, SystemInfo, WorkspaceContext,
|
||||
GitInfo, ModelCapabilities, ModelContext, ProjectType, PromptContext, PromptError,
|
||||
SessionContext, SystemInfo, WorkspaceContext,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,37 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Role configuration - defines the system instructions for the LLM
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Role {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version: Option<String>,
|
||||
pub template: String,
|
||||
pub partials: Option<HashMap<String, PartialConfig>>,
|
||||
pub variables: Option<HashMap<String, String>>,
|
||||
pub conditions: Option<HashMap<String, ConditionConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PartialConfig {
|
||||
pub path: String,
|
||||
pub fallback: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// External resource loader used to resolve template includes/partials
|
||||
pub trait ResourceLoader: Send + Sync {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConditionConfig {
|
||||
pub when: String,
|
||||
pub variables: Option<HashMap<String, String>>,
|
||||
pub template_override: Option<String>,
|
||||
}
|
||||
|
||||
/// システム情報
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemInfo {
|
||||
|
|
@ -116,35 +85,17 @@ pub struct PromptContext {
|
|||
pub variables: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// プロンプト構築エラー
|
||||
/// システムプロンプト生成時のエラー
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PromptError {
|
||||
#[error("Template compilation error: {0}")]
|
||||
TemplateCompilation(String),
|
||||
|
||||
#[error("Variable resolution error: {0}")]
|
||||
VariableResolution(String),
|
||||
|
||||
#[error("Partial loading error: {0}")]
|
||||
PartialLoading(String),
|
||||
|
||||
#[error("File not found: {0}")]
|
||||
FileNotFound(String),
|
||||
#[error("System prompt generation error: {0}")]
|
||||
Generation(String),
|
||||
|
||||
#[error("Workspace detection error: {0}")]
|
||||
WorkspaceDetection(String),
|
||||
|
||||
#[error("Git information error: {0}")]
|
||||
GitInfo(String),
|
||||
|
||||
#[error("Handlebars error: {0}")]
|
||||
Handlebars(#[from] handlebars::RenderError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("YAML parsing error: {0}")]
|
||||
YamlParsing(#[from] serde_yaml::Error),
|
||||
}
|
||||
|
||||
impl SystemInfo {
|
||||
|
|
@ -356,22 +307,3 @@ impl Default for SessionContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
/// Create a new Role with name, description, and template
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
template: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
version: Some("1.0.0".to_string()),
|
||||
template: template.into(),
|
||||
partials: None,
|
||||
variables: None,
|
||||
conditions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,334 +0,0 @@
|
|||
use crate::config::ConfigParser;
|
||||
use crate::prompt::{
|
||||
ModelCapabilities, ModelContext, PromptComposer, PromptContext, PromptError, ResourceLoader,
|
||||
SessionContext, SystemInfo, WorkspaceContext,
|
||||
};
|
||||
use crate::types::LlmProvider;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_parse_basic_config() {
|
||||
let yaml_content = r##"
|
||||
name: "Test Assistant"
|
||||
description: "A test configuration"
|
||||
version: "1.0"
|
||||
template: |
|
||||
# Test Role
|
||||
{{>role_header}}
|
||||
|
||||
{{#if workspace.has_nia_md}}
|
||||
# Project Context
|
||||
{{workspace.nia_md_content}}
|
||||
{{/if}}
|
||||
|
||||
partials:
|
||||
role_header:
|
||||
path: "#nia/prompts/headers/role.md"
|
||||
description: "Basic role definition"
|
||||
|
||||
variables:
|
||||
max_context_length: "{{model.context_length}}"
|
||||
project_name: "{{workspace.project_name}}"
|
||||
"##;
|
||||
|
||||
let config =
|
||||
ConfigParser::parse_from_string(yaml_content).expect("Failed to parse basic config");
|
||||
|
||||
assert_eq!(config.name, "Test Assistant");
|
||||
assert_eq!(config.description, "A test configuration");
|
||||
assert_eq!(config.version, Some("1.0".to_string()));
|
||||
assert!(!config.template.is_empty());
|
||||
|
||||
// パーシャルのテスト
|
||||
let partials = config.partials.expect("Partials should be present");
|
||||
assert!(partials.contains_key("role_header"));
|
||||
let role_header = &partials["role_header"];
|
||||
assert_eq!(role_header.path, "#nia/prompts/headers/role.md");
|
||||
assert_eq!(
|
||||
role_header.description,
|
||||
Some("Basic role definition".to_string())
|
||||
);
|
||||
|
||||
// 変数のテスト
|
||||
let variables = config.variables.expect("Variables should be present");
|
||||
assert!(variables.contains_key("max_context_length"));
|
||||
assert!(variables.contains_key("project_name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_minimal_config() {
|
||||
let yaml_content = r##"
|
||||
name: "Minimal Assistant"
|
||||
description: "A minimal configuration"
|
||||
template: "Hello {{user_input}}"
|
||||
"##;
|
||||
|
||||
let config =
|
||||
ConfigParser::parse_from_string(yaml_content).expect("Failed to parse minimal config");
|
||||
|
||||
assert_eq!(config.name, "Minimal Assistant");
|
||||
assert_eq!(config.description, "A minimal configuration");
|
||||
assert_eq!(config.template, "Hello {{user_input}}");
|
||||
assert!(config.partials.is_none());
|
||||
assert!(config.variables.is_none());
|
||||
assert!(config.conditions.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_with_conditions() {
|
||||
let yaml_content = r##"
|
||||
name: "Conditional Assistant"
|
||||
description: "Configuration with conditions"
|
||||
template: "Base template"
|
||||
|
||||
conditions:
|
||||
native_tools_enabled:
|
||||
when: "{{model.supports_native_tools}}"
|
||||
variables:
|
||||
tool_format: "native"
|
||||
include_tool_schemas: false
|
||||
|
||||
xml_tools_enabled:
|
||||
when: "{{not model.supports_native_tools}}"
|
||||
variables:
|
||||
tool_format: "xml"
|
||||
include_tool_schemas: true
|
||||
"##;
|
||||
|
||||
let config = ConfigParser::parse_from_string(yaml_content)
|
||||
.expect("Failed to parse config with conditions");
|
||||
|
||||
let conditions = config.conditions.expect("Conditions should be present");
|
||||
assert!(conditions.contains_key("native_tools_enabled"));
|
||||
assert!(conditions.contains_key("xml_tools_enabled"));
|
||||
|
||||
let native_condition = &conditions["native_tools_enabled"];
|
||||
assert_eq!(native_condition.when, "{{model.supports_native_tools}}");
|
||||
|
||||
let variables = native_condition
|
||||
.variables
|
||||
.as_ref()
|
||||
.expect("Variables should be present");
|
||||
assert_eq!(variables.get("tool_format"), Some(&"native".to_string()));
|
||||
assert_eq!(
|
||||
variables.get("include_tool_schemas"),
|
||||
Some(&"false".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_errors() {
|
||||
// 空の名前
|
||||
let invalid_yaml = r##"
|
||||
name: ""
|
||||
description: "Test"
|
||||
template: "Test template"
|
||||
"##;
|
||||
let result = ConfigParser::parse_from_string(invalid_yaml);
|
||||
assert!(result.is_err());
|
||||
|
||||
// 空のテンプレート
|
||||
let invalid_yaml = r##"
|
||||
name: "Test"
|
||||
description: "Test"
|
||||
template: ""
|
||||
"##;
|
||||
let result = ConfigParser::parse_from_string(invalid_yaml);
|
||||
assert!(result.is_err());
|
||||
|
||||
// 空のパーシャルパス
|
||||
let invalid_yaml = r##"
|
||||
name: "Test"
|
||||
description: "Test"
|
||||
template: "Test template"
|
||||
partials:
|
||||
empty_path:
|
||||
path: ""
|
||||
"##;
|
||||
let result = ConfigParser::parse_from_string(invalid_yaml);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_from_file() {
|
||||
let yaml_content = r##"
|
||||
name: "File Test Assistant"
|
||||
description: "Testing file parsing"
|
||||
template: "File content {{user_input}}"
|
||||
"##;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
|
||||
temp_file
|
||||
.write_all(yaml_content.as_bytes())
|
||||
.expect("Failed to write to temp file");
|
||||
|
||||
let config =
|
||||
ConfigParser::parse_from_file(temp_file.path()).expect("Failed to parse config from file");
|
||||
|
||||
assert_eq!(config.name, "File Test Assistant");
|
||||
assert_eq!(config.description, "Testing file parsing");
|
||||
assert_eq!(config.template, "File content {{user_input}}");
|
||||
}
|
||||
|
||||
struct InMemoryLoader {
|
||||
data: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl InMemoryLoader {
|
||||
fn new(data: HashMap<String, String>) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceLoader for InMemoryLoader {
|
||||
fn load(&self, identifier: &str) -> Result<String, PromptError> {
|
||||
self.data
|
||||
.get(identifier)
|
||||
.cloned()
|
||||
.ok_or_else(|| PromptError::FileNotFound(format!("not found: {}", identifier)))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_prompt_context() -> PromptContext {
|
||||
let workspace = WorkspaceContext {
|
||||
root_path: PathBuf::from("."),
|
||||
nia_md_content: None,
|
||||
project_type: None,
|
||||
git_info: None,
|
||||
has_nia_md: false,
|
||||
project_name: None,
|
||||
system_info: SystemInfo::collect(),
|
||||
};
|
||||
|
||||
let capabilities = ModelCapabilities {
|
||||
supports_tools: false,
|
||||
supports_function_calling: false,
|
||||
supports_vision: false,
|
||||
supports_multimodal: None,
|
||||
context_length: None,
|
||||
capabilities: vec![],
|
||||
needs_verification: None,
|
||||
};
|
||||
|
||||
let model_context = ModelContext {
|
||||
provider: LlmProvider::Claude,
|
||||
model_name: "test-model".to_string(),
|
||||
capabilities,
|
||||
supports_native_tools: false,
|
||||
};
|
||||
|
||||
let session_context = SessionContext {
|
||||
conversation_id: None,
|
||||
message_count: 0,
|
||||
active_tools: vec![],
|
||||
user_preferences: None,
|
||||
};
|
||||
|
||||
PromptContext {
|
||||
workspace,
|
||||
model: model_context,
|
||||
session: session_context,
|
||||
variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prompt_composer_uses_resource_loader() {
|
||||
let yaml_content = r##"
|
||||
name: "Loader Test"
|
||||
description: "Ensure resource loader is used"
|
||||
template: |
|
||||
{{>header}}
|
||||
{{include_file "include.md"}}
|
||||
|
||||
partials:
|
||||
header:
|
||||
path: "missing.md"
|
||||
fallback: "fallback.md"
|
||||
"##;
|
||||
|
||||
let role =
|
||||
ConfigParser::parse_from_string(yaml_content).expect("Failed to parse loader test config");
|
||||
|
||||
let loader = Arc::new(InMemoryLoader::new(HashMap::from([
|
||||
("fallback.md".to_string(), "Fallback Partial".to_string()),
|
||||
("include.md".to_string(), "Included Content".to_string()),
|
||||
])));
|
||||
|
||||
let prompt_context = build_prompt_context();
|
||||
|
||||
let composer = PromptComposer::from_config(role, prompt_context, loader)
|
||||
.expect("Composer should use provided loader");
|
||||
|
||||
let messages = composer
|
||||
.compose(&[])
|
||||
.expect("Composer should build system prompt");
|
||||
|
||||
assert!(!messages.is_empty());
|
||||
let system_message = &messages[0];
|
||||
assert!(system_message.content.contains("Fallback Partial"));
|
||||
assert!(system_message.content.contains("Included Content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_template_syntax() {
|
||||
let yaml_content = r##"
|
||||
name: "Complex Template Assistant"
|
||||
description: "Testing complex Handlebars syntax"
|
||||
template: |
|
||||
# Dynamic Role
|
||||
{{>role_header}}
|
||||
|
||||
{{#if workspace.has_nia_md}}
|
||||
# Project: {{workspace.project_name}}
|
||||
{{workspace.nia_md_content}}
|
||||
{{/if}}
|
||||
|
||||
{{#if_native_tools model.supports_native_tools}}
|
||||
Native tools are supported.
|
||||
{{else}}
|
||||
Using XML-based tool calls.
|
||||
Available tools:
|
||||
```json
|
||||
{{tools_schema}}
|
||||
```
|
||||
{{/if_native_tools}}
|
||||
|
||||
{{#model_specific model.provider}}
|
||||
{{#case "Claude"}}
|
||||
Claude-specific instructions here.
|
||||
{{/case}}
|
||||
{{#case "Gemini"}}
|
||||
Gemini-specific instructions here.
|
||||
{{/case}}
|
||||
{{#default}}
|
||||
Generic model instructions.
|
||||
{{/default}}
|
||||
{{/model_specific}}
|
||||
|
||||
partials:
|
||||
role_header:
|
||||
path: "#nia/prompts/headers/role.md"
|
||||
"##;
|
||||
|
||||
let config =
|
||||
ConfigParser::parse_from_string(yaml_content).expect("Failed to parse complex template");
|
||||
|
||||
assert!(!config.template.is_empty());
|
||||
assert!(config.template.contains("{{>role_header}}"));
|
||||
assert!(config.template.contains("{{#if workspace.has_nia_md}}"));
|
||||
assert!(
|
||||
config
|
||||
.template
|
||||
.contains("{{#if_native_tools model.supports_native_tools}}")
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.template
|
||||
.contains("{{#model_specific model.provider}}")
|
||||
);
|
||||
}
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
use crate::types::{LlmProvider, Message, Role};
|
||||
use crate::workspace_detector::WorkspaceDetector;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
#[ignore] // Temporarily disabled due to missing dependencies
|
||||
fn test_full_dynamic_prompt_composition() {
|
||||
// テスト用の一時ディレクトリを作成
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let temp_path = temp_dir.path();
|
||||
|
||||
// テスト用のNIA.mdファイルを作成
|
||||
let nia_md_content = r#"# Test Project
|
||||
|
||||
This is a test project for dynamic prompt composition.
|
||||
|
||||
## Features
|
||||
- Dynamic prompt generation
|
||||
- Workspace detection
|
||||
- Model-specific optimizations
|
||||
"#;
|
||||
fs::write(temp_path.join("NIA.md"), nia_md_content).expect("Failed to write NIA.md");
|
||||
|
||||
// テスト用のCargoファイルを作成(Rustプロジェクトとして認識させる)
|
||||
let cargo_toml = r#"[package]
|
||||
name = "test-project"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
"#;
|
||||
fs::write(temp_path.join("Cargo.toml"), cargo_toml).expect("Failed to write Cargo.toml");
|
||||
|
||||
// ワークスペースコンテキストを取得
|
||||
let workspace = WorkspaceDetector::detect_workspace_from_path(temp_path)
|
||||
.expect("Failed to detect workspace");
|
||||
|
||||
assert!(workspace.has_nia_md);
|
||||
assert_eq!(workspace.project_type, Some(ProjectType::Rust));
|
||||
assert!(workspace.nia_md_content.is_some());
|
||||
assert_eq!(workspace.project_name, Some("test-project".to_string()));
|
||||
|
||||
// モデルコンテキストを作成
|
||||
let model_context = ModelContext {
|
||||
provider: LlmProvider::Claude,
|
||||
model_name: "claude-3-sonnet".to_string(),
|
||||
capabilities: ModelCapabilities {
|
||||
supports_tools: true,
|
||||
supports_function_calling: true,
|
||||
..Default::default()
|
||||
},
|
||||
supports_native_tools: true,
|
||||
};
|
||||
|
||||
// セッションコンテキストを作成
|
||||
let session_context = SessionContext {
|
||||
conversation_id: Some("test-conv".to_string()),
|
||||
message_count: 1,
|
||||
active_tools: vec!["file_read".to_string(), "file_write".to_string()],
|
||||
user_preferences: None,
|
||||
};
|
||||
|
||||
// 全体的なプロンプトコンテキストを作成
|
||||
let prompt_context = PromptContext {
|
||||
workspace,
|
||||
model: model_context,
|
||||
session: session_context,
|
||||
variables: HashMap::new(),
|
||||
};
|
||||
|
||||
// 動的設定を作成
|
||||
let config = create_test_dynamic_config();
|
||||
|
||||
// DynamicPromptComposerを作成
|
||||
let mut composer = DynamicPromptComposer::from_config(config, prompt_context)
|
||||
.expect("Failed to create composer");
|
||||
|
||||
// テストメッセージ
|
||||
let messages = vec![Message {
|
||||
role: Role::User,
|
||||
content: "Please help me understand the project structure".to_string(),
|
||||
}];
|
||||
|
||||
// プロンプトを構築
|
||||
let result = composer
|
||||
.compose(&messages)
|
||||
.expect("Failed to compose prompt");
|
||||
|
||||
assert!(!result.is_empty());
|
||||
assert_eq!(result[0].role, Role::System);
|
||||
|
||||
// 生成されたプロンプトにワークスペース情報が含まれていることを確認
|
||||
let system_prompt = &result[0].content;
|
||||
assert!(system_prompt.contains("Test Project"));
|
||||
assert!(system_prompt.contains("dynamic prompt generation"));
|
||||
assert!(system_prompt.contains("test-project"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Temporarily disabled due to missing dependencies
|
||||
fn test_native_tools_vs_xml_tools() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let workspace = WorkspaceDetector::detect_workspace_from_path(temp_dir.path())
|
||||
.expect("Failed to detect workspace");
|
||||
|
||||
// ネイティブツールサポートありのモデル
|
||||
let native_model = ModelContext {
|
||||
provider: LlmProvider::Claude,
|
||||
model_name: "claude-3-sonnet".to_string(),
|
||||
capabilities: ModelCapabilities {
|
||||
supports_tools: true,
|
||||
supports_function_calling: true,
|
||||
..Default::default()
|
||||
},
|
||||
supports_native_tools: true,
|
||||
};
|
||||
|
||||
// XMLツールのみのモデル
|
||||
let xml_model = ModelContext {
|
||||
provider: LlmProvider::Ollama,
|
||||
model_name: "llama3".to_string(),
|
||||
capabilities: ModelCapabilities {
|
||||
supports_tools: false,
|
||||
supports_function_calling: false,
|
||||
..Default::default()
|
||||
},
|
||||
supports_native_tools: false,
|
||||
};
|
||||
|
||||
let session = SessionContext::default();
|
||||
|
||||
// 両方のモデルでプロンプトを生成
|
||||
let native_context = PromptContext {
|
||||
workspace: workspace.clone(),
|
||||
model: native_model,
|
||||
session: session.clone(),
|
||||
variables: HashMap::new(),
|
||||
};
|
||||
|
||||
let xml_context = PromptContext {
|
||||
workspace: workspace.clone(),
|
||||
model: xml_model,
|
||||
session: session.clone(),
|
||||
variables: HashMap::new(),
|
||||
};
|
||||
|
||||
let config = create_test_dynamic_config();
|
||||
|
||||
let mut native_composer = DynamicPromptComposer::from_config(config.clone(), native_context)
|
||||
.expect("Failed to create native composer");
|
||||
|
||||
let mut xml_composer = DynamicPromptComposer::from_config(config, xml_context)
|
||||
.expect("Failed to create xml composer");
|
||||
|
||||
let messages = vec![Message {
|
||||
role: Role::User,
|
||||
content: "Test message".to_string(),
|
||||
}];
|
||||
|
||||
let native_result = native_composer
|
||||
.compose(&messages)
|
||||
.expect("Failed to compose native prompt");
|
||||
|
||||
let xml_result = xml_composer
|
||||
.compose(&messages)
|
||||
.expect("Failed to compose xml prompt");
|
||||
|
||||
// 両方のプロンプトが生成されることを確認
|
||||
assert!(!native_result.is_empty());
|
||||
assert!(!xml_result.is_empty());
|
||||
|
||||
// ネイティブツール用プロンプトとXMLツール用プロンプトが異なることを確認
|
||||
assert_ne!(native_result[0].content, xml_result[0].content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Temporarily disabled due to missing dependencies
|
||||
fn test_workspace_detection_without_nia_md() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
|
||||
// .nia ディレクトリのみ作成(NIA.mdなし)
|
||||
fs::create_dir(temp_dir.path().join(".nia")).expect("Failed to create .nia dir");
|
||||
|
||||
let workspace = WorkspaceDetector::detect_workspace_from_path(temp_dir.path())
|
||||
.expect("Failed to detect workspace");
|
||||
|
||||
assert!(!workspace.has_nia_md);
|
||||
assert!(workspace.nia_md_content.is_none());
|
||||
assert_eq!(workspace.project_type, Some(ProjectType::Unknown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Temporarily disabled due to missing dependencies
|
||||
fn test_project_type_detection() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let temp_path = temp_dir.path();
|
||||
|
||||
// TypeScriptプロジェクト
|
||||
fs::write(temp_path.join("package.json"), r#"{"name": "test"}"#)
|
||||
.expect("Failed to write package.json");
|
||||
fs::write(temp_path.join("tsconfig.json"), "{}").expect("Failed to write tsconfig.json");
|
||||
|
||||
let workspace = WorkspaceDetector::detect_workspace_from_path(temp_path)
|
||||
.expect("Failed to detect workspace");
|
||||
|
||||
assert_eq!(workspace.project_type, Some(ProjectType::TypeScript));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Temporarily disabled due to missing dependencies
|
||||
fn test_tools_schema_integration() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let workspace = WorkspaceDetector::detect_workspace_from_path(temp_dir.path())
|
||||
.expect("Failed to detect workspace");
|
||||
|
||||
let model_context = ModelContext {
|
||||
provider: LlmProvider::Gemini,
|
||||
model_name: "gemini-1.5-flash".to_string(),
|
||||
capabilities: ModelCapabilities {
|
||||
supports_tools: false,
|
||||
supports_function_calling: false,
|
||||
..Default::default()
|
||||
},
|
||||
supports_native_tools: false,
|
||||
};
|
||||
|
||||
let session_context = SessionContext::default();
|
||||
let prompt_context = PromptContext {
|
||||
workspace,
|
||||
model: model_context,
|
||||
session: session_context,
|
||||
variables: HashMap::new(),
|
||||
};
|
||||
|
||||
let config = create_test_dynamic_config();
|
||||
let mut composer = DynamicPromptComposer::from_config(config, prompt_context)
|
||||
.expect("Failed to create composer");
|
||||
|
||||
// ツールスキーマを作成
|
||||
let tools_schema = serde_json::json!([
|
||||
{
|
||||
"name": "file_read",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let messages = vec![Message {
|
||||
role: Role::User,
|
||||
content: "Read a file for me".to_string(),
|
||||
}];
|
||||
|
||||
// ツール情報付きでプロンプトを構築
|
||||
let result = composer
|
||||
.compose_with_tools(&messages, &tools_schema)
|
||||
.expect("Failed to compose with tools");
|
||||
|
||||
assert!(!result.is_empty());
|
||||
|
||||
// XMLツールモデルなので、ツール情報がプロンプトに含まれるはず
|
||||
let system_prompt = &result[0].content;
|
||||
assert!(system_prompt.contains("file_read"));
|
||||
}
|
||||
|
||||
// テスト用の動的設定を作成
|
||||
fn create_test_dynamic_config() -> DynamicRoleConfig {
|
||||
let mut variables = HashMap::new();
|
||||
variables.insert(
|
||||
"project_name".to_string(),
|
||||
"{{workspace.project_name}}".to_string(),
|
||||
);
|
||||
variables.insert("model_name".to_string(), "{{model.model_name}}".to_string());
|
||||
|
||||
let mut conditions = HashMap::new();
|
||||
|
||||
// ネイティブツール条件
|
||||
let mut native_vars = HashMap::new();
|
||||
native_vars.insert("tool_format".to_string(), "native".to_string());
|
||||
conditions.insert(
|
||||
"native_tools".to_string(),
|
||||
ConditionConfig {
|
||||
when: "{{model.supports_native_tools}}".to_string(),
|
||||
variables: Some(native_vars),
|
||||
template_override: None,
|
||||
},
|
||||
);
|
||||
|
||||
// XMLツール条件
|
||||
let mut xml_vars = HashMap::new();
|
||||
xml_vars.insert("tool_format".to_string(), "xml".to_string());
|
||||
conditions.insert(
|
||||
"xml_tools".to_string(),
|
||||
ConditionConfig {
|
||||
when: "{{not model.supports_native_tools}}".to_string(),
|
||||
variables: Some(xml_vars),
|
||||
template_override: None,
|
||||
},
|
||||
);
|
||||
|
||||
DynamicRoleConfig {
|
||||
name: "Test Assistant".to_string(),
|
||||
description: "A test configuration".to_string(),
|
||||
version: Some("1.0".to_string()),
|
||||
template: r#"# Test Role
|
||||
|
||||
{{#if workspace.has_nia_md}}
|
||||
# Project Context
|
||||
Project: {{workspace.project_name}}
|
||||
{{workspace.nia_md_content}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.supports_native_tools}}
|
||||
Native tools are supported for {{model.model_name}}.
|
||||
{{else}}
|
||||
Using XML-based tool calls for {{model.model_name}}.
|
||||
{{#if tools_schema}}
|
||||
Available tools: {{tools_schema}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
User request: {{user_input}}
|
||||
"#
|
||||
.to_string(),
|
||||
partials: None, // パーシャルを使わない
|
||||
variables: Some(variables),
|
||||
conditions: Some(conditions),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user