ds-api-macros 0.1.1

Procedural macros for ds-api (provides the `tool` macro and related helpers)
Documentation
ds-api-workspace/ds-api/README.md
# ds-api — Rust client for DeepSeek (refactored)

This crate provides a layered, ergonomic Rust client for the DeepSeek chat API.

Quick Start
- Requirements: Rust toolchain and an environment variable `DEEPSEEK_API_KEY` with your API token.
- The crate exposes both low-level `raw` types and higher-level ergonomics via `ApiRequest`, `ApiClient`, `DeepseekConversation`, and `DeepseekAgent`.

Example: simple non-streaming request (async, requires Tokio)
```rust
use ds_api::{ApiClient, ApiRequest};
use ds_api::raw::request::message::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("DEEPSEEK_API_KEY")?;
    let client = ApiClient::new(token);

    let req = ApiRequest::deepseek_chat(vec![
        Message::new(ds_api::raw::request::message::Role::User, "Hello from Rust"),
    ])
    .max_tokens(150)
    .json();

    let resp = client.send(req).await?;
    // Print debug representation of the response; adapt to your needs.
    println!("Response: {:?}", resp);
    Ok(())
}
```

Example: minimal `DeepseekAgent` with a sample tool
```rust
use ds_api::{DeepseekAgent, tool};
use futures::StreamExt;
use serde_json::json;

struct EchoTool;

#[tool]
impl ds_api::Tool for EchoTool {
    async fn echo(&self, input: String) -> serde_json::Value {
        json!({ "echo": input })
    }
}

#[tokio::main]
async fn main() {
    let token = std::env::var("DEEPSEEK_API_KEY").unwrap();
    let agent = DeepseekAgent::new(token).add_tool(EchoTool);

    // AgentStream yields Result<AgentResponse, ApiError>
    let mut s = agent.chat("Please echo: hello");
    while let Some(event) = s.next().await {
        match event {
            Err(e) => { eprintln!("Error: {e}"); break; }
            Ok(ev) => {
                if let Some(content) = &ev.content {
                    println!("Assistant: {}", content);
                }
                for tc in &ev.tool_calls {
                    println!("Tool call: {} -> {}", tc.name, tc.result);
                }
            }
        }
    }
}
```

Example: streaming text with `.with_streaming()`
```rust
// Add .with_streaming() to receive text fragments as they arrive.
let agent = DeepseekAgent::new(token).with_streaming().add_tool(EchoTool);
let mut s = agent.chat("Please echo: hello");
while let Some(event) = s.next().await {
    match event {
        Err(e) => { eprintln!("Error: {e}"); break; }
        Ok(ev) => { if let Some(fragment) = &ev.content { print!("{}", fragment); } }
    }
}
```

High-level design
- raw — low-level types that mirror the API JSON (kept under `ds_api::raw` but not recommended).
- api — safe, chainable request builder and HTTP client (`ApiRequest`, `ApiClient`).
- conversation — session management and summarization (`DeepseekConversation`, `Summarizer`).
- agent — agent orchestration with tools and auto-summary (`DeepseekAgent`).

This README documents the new API, breaking changes, migration steps, and examples.

## Quick highlights (new API)

- ApiRequest — chainable, safe builder. Use `ApiRequest::builder()` or `ApiRequest::deepseek_chat(...)` / `ApiRequest::deepseek_reasoner(...)` to choose a model (Model enum is intentionally not exported).
- ApiClient — owns token/base_url/reqwest::Client; send blocking or streaming requests.
- DeepseekConversation — manages history and auto-summary via the `Summarizer` trait. Default summarizer is `TokenBasedSummarizer`.
- DeepseekAgent — high-level agent that combines a conversation, tools (via `tool` macro), and auto summary. `chat(...)` returns `AgentStream` which yields `Result<AgentResponse, ApiError>`. Tool calls produce two events: a preview (result = `null`) then execution results. Use `.with_streaming()` to receive text fragments as they arrive.
- raw module still available under `ds_api::raw` for advanced users, but not part of the primary recommended API.

## Breaking changes (important)

This refactor intentionally removes compatibility with older API shapes. Notable breaking items:

- `Request` and `DeepseekClient` (previous high-level types) have been removed. Use `ApiRequest` and `ApiClient` instead.
- `NormalChatter` and `SimpleChatter` were removed. Use `DeepseekConversation` and `DeepseekAgent`.
- `Model` enum is no longer exported directly as part of the public API. Use `ApiRequest::deepseek_chat(...)` or `ApiRequest::deepseek_reasoner(...)` to choose a model.
- Unsafe raw accessors (e.g. `from_raw_unchecked`, `get_raw_mut`) have been removed from the public API.
- Summarization is now pluggable via the `Summarizer` trait. Default `TokenBasedSummarizer` skips `system` messages when estimating tokens and triggers at the default threshold (100_000 tokens estimate).

If you're migrating from the old crate:
- Replace `Request` -> `ApiRequest`
- Replace `DeepseekClient` -> `ApiClient`
- Replace `NormalChatter` -> `DeepseekConversation` (or `DeepseekAgent` if you need tools)
- Replace `SimpleChatter` -> `DeepseekConversation` thin wrapper as needed

## Example: simple non-streaming request

```ds-api-workspace/ds-api/examples/agent_demo.rs#L1-40
// Build an ApiRequest and send a non-streaming call.
use ds_api::{ApiClient, ApiRequest};
use ds_api::raw::request::message::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("DEEPSEEK_API_KEY")?;
    let client = ApiClient::new(token);

    let req = ApiRequest::deepseek_chat(vec![
        Message::new(ds_api::raw::request::message::Role::User, "Hello from Rust"),
    ])
    .max_tokens(150)
    .json();

    let resp = client.send(req).await?;
    // Print debug representation of the response; adapt to your needs.
    println!("Response: {:?}", resp);
    Ok(())
}
```

## Example: streaming text from ApiClient

```ds-api-workspace/ds-api/examples/agent_demo.rs#L40-120
use ds_api::{ApiClient, ApiRequest};
use futures::StreamExt;

let token = std::env::var("DEEPSEEK_API_KEY")?;
let client = ApiClient::new(token);

let req = ApiRequest::deepseek_chat(vec![Message::new(Role::User, "Tell me a story")])
    .stream(true);

let mut stream = client.stream_text(req).await?;
while let Some(chunk_res) = stream.next().await {
    match chunk_res {
        Ok(text) => print!("{}", text),
        Err(err) => eprintln!("Stream error: {}", err),
    }
}
```

## Example: DeepseekConversation (auto summary)

```ds-api-workspace/ds-api/examples/agent_demo.rs#L120-200
use ds_api::{ApiClient, ApiRequest, DeepseekConversation, Message, Role};

let token = std::env::var("DEEPSEEK_API_KEY")?;
let client = ApiClient::new(token);

let mut conv = DeepseekConversation::new(client.clone())
    .with_history(vec![Message::new(Role::System, "You are a helpful assistant.")]);

conv.push_user_input("Hello! Tell me about Rust.".to_string());
let reply = conv.send_once().await?;
println!("Assistant: {:?}", reply);
```

## Example: DeepseekAgent with tools (preferred flow)

- Agent yields two-phase events when the model triggers tool calls:
  1. First yield: assistant `content` paired with `tool_calls` preview (result is `null`).
  2. Second yield: tool execution results (and these results are appended to conversation history as `Role::Tool` messages).

Tool functions are declared with the `#[tool]` macro.

```ds-api-workspace/ds-api/examples/agent_demo.rs#L1-200
use ds_api::{DeepseekAgent, tool};
use futures::StreamExt;
use serde_json::json;

struct WeatherTool {
    client: reqwest::Client,
}

#[tool]
impl Tool for WeatherTool {
    /// Get weather for a city
    async fn get_weather(&self, city: String, _unit: Option<String>) -> serde_json::Value {
        let url = format!("https://wttr.in/{}?format=3", city);
        let text = self.client.get(&url).send().await
            .and_then(|r| r.text().await)
            .unwrap_or_else(|e| e.to_string());
        json!({ "city": city, "weather": text })
    }
}

#[tokio::main]
async fn main() {
    let token = std::env::var("DEEPSEEK_API_KEY").unwrap();
    let agent = DeepseekAgent::new(token)
        .add_tool(WeatherTool { client: reqwest::Client::new() })
        .with_system_prompt("You are an assistant that can call tools.");

    let mut s = agent.chat("What's the weather in Beijing and Shanghai?");
    while let Some(event) = s.next().await {
        match event {
            Err(e) => { eprintln!("Error: {e}"); break; }
            Ok(ev) => {
                if let Some(content) = &ev.content {
                    println!("Assistant: {}", content);
                }
                for tc in &ev.tool_calls {
                    println!("Tool call preview/result: {} {} -> {}", tc.name, tc.args, tc.result);
                }
            }
        }
    }
}
```

## Summarizer and auto-summary behavior

- `Summarizer` trait: pluggable abstraction for summarization.
- Default: `TokenBasedSummarizer`:
  - Rough token estimate uses `chars / 4`.
  - SKIPS `system` messages when computing the estimate.
  - Default threshold: 100_000 estimated tokens (configurable).
  - When triggered, older messages are summarized into a single `system` message (prefixed/marked with `[auto-summary]` in `name`).

You can implement `Summarizer` to call an LLM for semantic summaries or to use a different heuristic.

## Migration guidance

- Replace previous usage of `Request::basic_query(...)` with `ApiRequest::deepseek_chat(...)` or `ApiRequest::builder()`.
- Use `ApiClient::new(token)` instead of `DeepseekClient`.
- Replace `NormalChatter` and `SimpleChatter` with `DeepseekConversation` or `DeepseekAgent`.
- If you previously relied on raw accessors, migrate carefully — raw module still exists at `ds_api::raw` but the recommended approach is through `ApiRequest` and `ApiClient`.

## Project layout (high level)

- `src/raw` — raw request/response types (kept for advanced use).
- `src/api.rs``ApiRequest`, `ApiClient`.
- `src/conversation/mod.rs``DeepseekConversation`, `Summarizer` + default summarizer.
- `src/agent.rs``DeepseekAgent` and streaming agent orchestration.
- `src/tool.rs` + `ds-api-macros` crate — tooling macro and Tool trait.

## Tests, linting, and build
- The project uses `cargo` for builds, and Clippy is enforced. After refactor we run `cargo clippy -p ds-api -- -D warnings` as part of CI.
- Run:
```ds-api-workspace/ds-api/README.md#L1-1
# in repo root
cargo build
cargo test
cargo clippy -p ds-api -- -D warnings
```

## Contributing & Release notes

- This refactor is breaking by design. A migration guide is in this README (above).
- If you maintain downstream consumers, notify them about:
  - Removed `Request`/`DeepseekClient`, `NormalChatter`, `SimpleChatter`.
  - New preferred entry points: `ApiRequest`, `ApiClient`, `DeepseekConversation`, `DeepseekAgent`.
- Future work:
  - Provide thin compatibility wrappers (if required).
  - Improve summarizer with a semantic LLM-backed default option (configurable).

## License
Check `Cargo.toml` for license information.