# C5 — SDK Chat (`POST /api/chat`)

> **Summary — what this page covers**
> This is the **first Day 2 lab**. You add the **official Anthropic C# SDK** to BookTracker and build a
> working **`POST /api/chat`** endpoint: a BookTracker-scoped assistant with multi-turn history and
> **prompt caching**. Core stays SDK-free — only `BookTracker.Api` references the package.
>
> **Time:** ~45 minutes · **Format:** hands-on, solo · **You start from:** `checkpoint/c4-reading-progress` · **You end at:** `checkpoint/c5-sdk-chat`

Day 1 used **Claude Code** to *build* BookTracker. Day 2 calls the **Anthropic API** *from* BookTracker.
C5 is the foundation every later Day 2 lab builds on (C6 streaming + tools, C7 RAG, C10 hardening). You
start from the full Day 1 solution and finish with a chat endpoint that talks to Claude, remembers the
conversation across requests, and caches a large system prompt to save tokens.

**No Docker** in this lab. You **do** need an **Anthropic API key** (a separate credential from the
paid Claude plan you used on Day 1). The key goes in **user-secrets** — never commit it.

---

## 1. Start from the C4 checkpoint

Each lab starts from the previous checkpoint; the matching tag is the answer key. If you're continuing
your own branch from Day 1, make sure it's at the C4 state. Otherwise, branch from the tag:

```bash
git switch -c my-c5 checkpoint/c4-reading-progress

# everything below runs inside the solution folder
cd src/BookTracker
```

> The full as-built solution lives at `checkpoint/c5-sdk-chat`. Use it as the answer key if you get
> stuck, and see `SOLUTION-C5-BUILD-SHEET.md` for the file-by-file spec.

---

## 2. Add the Anthropic SDK

Add the official Claude C# SDK to the **Api** project only:

```bash
dotnet add BookTracker.Api package Anthropic
```

The as-built solution uses **`Anthropic` v12.31.0**. Confirm it lands in `BookTracker.Api.csproj` and
**not** in Core or Data — keeping the SDK out of Core is a hard rule for this lab.

> The as-built solution does **not** add a separate resilience package. The Anthropic SDK already
> retries transient `429`/`5xx` failures with exponential backoff, so an explicit Polly /
> `Microsoft.Extensions.Http.Resilience` pipeline is an **optional** teaching add-on, not required here.

---

## 3. Add the chat DTOs and interface (Core stays pure)

These live in `BookTracker.Core` and use **only** Core/primitive types — no Anthropic types — so the
contract stays SDK-free.

Create in `BookTracker.Core/Dtos`:

```csharp
public record ChatTurn(string Role, string Content);              // "user" / "assistant"
public record ChatRequest(string SessionId, string Message);
public record ChatResponse(string Reply, int InputTokens, int OutputTokens, int CacheReadInputTokens);
```

Create the interface in `BookTracker.Core/Services`:

```csharp
public interface IClaudeService
{
    Task<ChatResponse> ChatAsync(string userMessage, IReadOnlyList<ChatTurn> history, CancellationToken ct);
}
```

`ChatResponse` surfaces token usage on purpose — that's how you'll *see* prompt caching work in step 8.

---

## 4. Add `AnthropicOptions` (Api)

Create `BookTracker.Api/Options/AnthropicOptions.cs` bound from the `"Anthropic"` config section:

```csharp
public class AnthropicOptions
{
    public string ApiKey { get; set; } = "";
    public string Model { get; set; } = "claude-sonnet-4-6";
    public int MaxTokens { get; set; } = 1024;
    public string SystemPrompt { get; set; } = "";
}
```

The `ApiKey` is filled from user-secrets (step 6); `Model`, `MaxTokens`, and `SystemPrompt` come from
`appsettings.json` (step 7).

---

## 5. Implement `ClaudeService` (the SDK wrapper)

Create `BookTracker.Api/Services/ClaudeService.cs` implementing `IClaudeService`. It maps stored
history → SDK messages, appends the new user turn, sends a **cached** system prompt, calls
`Messages.Create`, and returns the reply text plus usage. Illustrative shape (full code in the answer
key):

```csharp
using Anthropic;
using Anthropic.Models.Messages;

var messages = history
    .Select(t => new MessageParam {
        Role = t.Role == "assistant" ? Role.Assistant : Role.User,
        Content = t.Content,
    })
    .Append(new MessageParam { Role = Role.User, Content = userMessage })
    .ToList();

var response = await _client.Messages.Create(new MessageCreateParams {
    Model = _opts.Model,
    MaxTokens = _opts.MaxTokens,
    System = new List<TextBlockParam> {
        new() { Text = _opts.SystemPrompt, CacheControl = new CacheControlEphemeral() },
    },
    Messages = messages,
}, ct);

var reply = string.Concat(response.Content.Select(b => b.Value).OfType<TextBlock>().Select(t => t.Text));
return new ChatResponse(reply,
    response.Usage.InputTokens, response.Usage.OutputTokens, response.Usage.CacheReadInputTokens);
```

Two things to internalize:

- **The API is stateless.** The full history is sent on every call — that's why the endpoint persists
  it (step 7).
- **Mark the system prompt with `CacheControlEphemeral`.** That's what enables prompt caching — but it
  only fires above a minimum size (step 8's gotcha).

---

## 6. Set the API key in user-secrets

The key is a secret — keep it out of `appsettings.json` and out of git:

```bash
dotnet user-secrets init --project BookTracker.Api
dotnet user-secrets set "Anthropic:ApiKey" "sk-ant-..." --project BookTracker.Api
```

User-secrets bind into the same `"Anthropic"` config section, so `AnthropicOptions.ApiKey` is populated
at startup without ever touching a committed file.

---

## 7. Wire config, DI, and the endpoint

**`appsettings.json`** — add an `"Anthropic"` section. The `SystemPrompt` must be **substantial** (see
step 8):

```json
{
  "Anthropic": {
    "Model": "claude-sonnet-4-6",
    "MaxTokens": 1024,
    "SystemPrompt": "You are the BookTracker assistant. Answer only about books, authors, reviews, and reading progress in this app. ...(pad this — see step 8)"
  }
}
```

**`Program.cs`** — register options, the client, the service, and the cache:

```csharp
builder.Services.Configure<AnthropicOptions>(builder.Configuration.GetSection("Anthropic"));
builder.Services.AddSingleton(sp =>
    new AnthropicClient { ApiKey = sp.GetRequiredService<IOptions<AnthropicOptions>>().Value.ApiKey });
builder.Services.AddScoped<IClaudeService, ClaudeService>();
builder.Services.AddDistributedMemoryCache();
// ...
app.MapChatEndpoints();
```

> **`AnthropicClient` is a Singleton on purpose** — it holds an `HttpClient`, so registering it Scoped
> or Transient risks socket exhaustion. `IClaudeService` is Scoped.

**`Endpoints/ChatEndpoints.cs`** — a thin `POST /api/chat` handler. It loads history from
`IDistributedCache` keyed by `SessionId`, calls `IClaudeService.ChatAsync`, appends the new user +
assistant turns, saves the history back, and returns the `ChatResponse`. Keep it async and thread the
`CancellationToken`. History is a JSON-serialized `List<ChatTurn>`, so the conversation survives across
HTTP requests even though the API itself is stateless.

---

## 8. Make prompt caching actually fire ⚠️

Marking the system prompt with `CacheControlEphemeral` (step 5) isn't enough on its own. **Caching has
a model-dependent minimum prefix:**

- **Sonnet 4.6 ≈ 2,048 tokens**
- **Haiku 4.5 / Opus ≈ 4,096 tokens**

A short BookTracker system prompt (a few hundred tokens) **silently won't cache** —
`CacheCreationInputTokens` stays `0` and `CacheReadInputTokens` never climbs. The demo falls flat with
no error.

**Fix:** pad the `SystemPrompt` past **~2,048 tokens** with real BookTracker domain context — a schema
summary (books, authors, reviews, reading-progress), the answer style you want, and a few example
Q&A pairs. This is genuine grounding *and* it crosses the caching threshold.

```bash
# rebuild and run
dotnet build BookTracker.sln
dotnet run --project BookTracker.Api   # → Now listening on: http://localhost:5255
```

In a second terminal, call the endpoint twice with the **same `SessionId`**:

```bash
curl -X POST http://localhost:5255/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"sessionId":"s1","message":"How many books are in my library?"}'

curl -X POST http://localhost:5255/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"sessionId":"s1","message":"Which one has the most pages?"}'
```

- The **first** call writes the cache (`CacheCreationInputTokens` > 0 on the server side).
- The **second** call should return `cacheReadInputTokens` **> 0** — the cached prefix bills at ~10%.
- Because both calls share `sessionId: "s1"`, the second reply should reflect the **first** turn's
  context — multi-turn history is working.

---

## ✅ Checkpoint — you're done when:

- [ ] `dotnet build` is green and the **`Anthropic`** package restores.
- [ ] `Anthropic:ApiKey` is set in **user-secrets** and is **not** committed anywhere.
- [ ] `POST /api/chat` returns a Claude reply **scoped to BookTracker** (it declines off-topic asks).
- [ ] Multi-turn history **persists** across requests that share a `SessionId`.
- [ ] With a sufficiently large system prompt, the **second** call shows `cacheReadInputTokens > 0`.
- [ ] `dotnet test` is still green — the Day 1 tests are unaffected.
- [ ] Core has **no** reference to the `Anthropic` package — only `BookTracker.Api` does.

Tag this state `checkpoint/c5-sdk-chat` once the above passes.

---

## What's next

**C6 (Day 2, Lab 2):** you'll add **streaming responses** (server-sent events) and a **tool-calling
agent** on top of this same `AnthropicClient` + `ClaudeService` foundation — including a tool that
updates reading progress through the C4 service.
