hefa-core 0.1.0

Rust LLM foundation: provider abstraction, tools, structured output, tracing.
Documentation
# hefa-core

Rust 製 LLM 基盤ライブラリ。設計や詳細仕様は `docs/` を参照してください。

## 開発手順

1. `.env.example` をコピーして `.env` を作成し、必要な API キーを設定します。
2. `cargo fmt` / `cargo clippy` / `cargo test` で品質を維持します。
3. docs:
   - `docs/hefa-core-spec.md` 要件定義
   - `docs/AGENTS.md` Agent 層仕様
   - `docs/PLAN.md` 実装チェックリスト

## 実行環境に関する注記

- `OPENAI_API_KEY` は環境変数として既に投入済みなので `.env` には記載せず、そのまま利用してください。
- LM Studio / Ollama は 192.168.11.16 上で稼働しています。
  - LM Studio: `openai/gpt-oss-20b` モデルを OpenAI 互換エンドポイント経由で利用可能。
  - Ollama: `gpt-oss:20b` モデルを利用可能。

## トレース切替

Agent では `hefa_core::Tracer` を差し替えることでトレースの保存先を切り替えられます。

```rust
use hefa_core::{StdoutTracer, SqliteTracer};

let tracer = StdoutTracer::default(); // 開発時は標準出力
// もしくは永続化
let tracer = SqliteTracer::new("trace.db")?;
let agent = agent.with_tracer(std::sync::Arc::new(tracer));
```

`SqliteTracer` には `list_recent_spans` / `list_events` があり、SQLite 上に保存された span / event の検索が可能です。

## LLM クライアント

`hefa_core::LLMClient` はプロバイダ種別に応じて自動で OpenAI Responses API / OpenAI 互換 ChatCompletion API を呼び分けます。以下は「エンジニアの日報からリリースノートのドラフトを作る」例です。

```rust
use hefa_core::{
    llm::StructuredOutput, Agent, AgentConfig, ProviderKind, Tool, ToolError, ToolResult,
};
use async_trait::async_trait;
use serde_json::{json, Value};

/// Tool used when LLM asks for metadata to enrich the release note.
/// LLM が「リリースノートの下書きを作って」と要求した際に呼ばれる Hook。
struct ReleaseNoteTool;

#[async_trait]
impl Tool for ReleaseNoteTool {
    fn name(&self) -> &'static str {
        "release_note"
    }

    fn json_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "feature": { "type": "string" },
                "impact": { "type": "string" }
            },
            "required": ["feature", "impact"]
        })
    }

    async fn call(&self, args: Value) -> Result<ToolResult, ToolError> {
        let feature = args
            .get("feature")
            .and_then(Value::as_str)
            .ok_or_else(|| ToolError::InvalidInput("missing feature".into()))?;
        let impact = args
            .get("impact")
            .and_then(Value::as_str)
            .ok_or_else(|| ToolError::InvalidInput("missing impact".into()))?;
        Ok(ToolResult {
            content: json!({ "draft": format!("Feature: {feature} — Impact: {impact}") }),
        })
    }
}

let structured_output = StructuredOutput::new(json!({
    "type": "object",
    "properties": {
        "summary": { "type": "string" }
    }
}));

let mut agent = Agent::new(AgentConfig {
    instruction: "You are a release-note assistant. Respond in 1 sentence.".into(),
    provider: ProviderKind::OpenAi,
    model: "gpt-4o-mini".into(),
    structured_output: Some(structured_output),
    tools: vec![Box::new(ReleaseNoteTool)],
})?;

let prompt = "Update: Added offline search with 50% faster indexing.";
let result = agent.invoke(prompt).await?;
println!("Release note: {}", result.response.content);
```

出力例:

```
Release note: OK — Feature: Added offline search with 50% faster indexing.
```

## チュートリアル / Examples

1. **ステップ1 – LLM 選択 & Hello**  
   まずは Provider とモデルを決め、最小構成の Agent で “Hello” を返す例 (`examples/basic_agent.rs`) を実行します。  
   ```
   cargo run --example basic_agent
   ```

2. **ステップ2 – Tool 追加**  
   `examples/tool_agent.rs` では LLM の `tool_calls` を処理する Hook を登録し、ReleaseNoteTool で外部ロジックを呼び出す流れを体験します。
   ```
   cargo run --example tool_agent
   ```

3. **ステップ3 – 構造化出力**  
   JSON Schema を指定して LLM の最終回答を構造化する例は `examples/structured_agent.rs` で確認できます。README で紹介した `StructuredOutput::from_type::<T>()` も併用してください。
   ```
   cargo run --example structured_agent
   ```

4. **ステップ4 – トレース**  
   `examples/trace_agent.rs``SqliteTracer` を使って span / event を保存し、`list_recent_spans` で属性フィルタ(例: `instruction` に “release” が含まれる)をかけて検索する例です。`Tracer` には `start_trace` / `start_child_span` / `record_event` が用意されており、Agent はこれらを内部で呼び出します。
   ```
   cargo run --example trace_agent
   ```

5. **ステップ5 – Release Note Agent (統合)**  
   `examples/release_note.rs` で、Tool + 構造化出力 + トレースの統合例を確認できます。  
   ```
   cargo run --example release_note
   ```

## ライブ接続テスト

OpenAI / LM Studio / Ollama に接続する実機テストは `tests/live_llm.rs` にあります。
以下の環境変数をセットして個別に実行してください。

```
HEFA_LIVE_OPENAI=1 cargo test live_openai_responses
HEFA_LIVE_LMSTUDIO=1 LMSTUDIO_API_BASE=http://192.168.11.16:1234/v1 cargo test live_lmstudio_chat
HEFA_LIVE_OLLAMA=1 OLLAMA_API_BASE=http://192.168.11.16:11434 cargo test live_ollama_chat
```

## 構造化出力と `schemars` 連携

Feature `schema` を有効にすると、`schemars::JsonSchema` を derive した型から
`StructuredOutput::from_type::<T>()` で JSON Schema を生成できます。LLM へ構造化出力を要求する際に役立ちます。

```
cargo add schemars --features derive
cargo test --features schema
```

```rust
use hefa_core::llm::StructuredOutput;
use schemars::JsonSchema;
use serde::Serialize;

#[derive(Serialize, JsonSchema)]
struct MyAnswer {
    summary: String,
    tags: Vec<String>,
}

let output = StructuredOutput::from_type::<MyAnswer>();
```

## ライセンス

TBD