# Noukai Integration

You are helping a developer integrate Noukai AI pipelines into their project.

## What Noukai Is

Noukai is an AI pipeline builder. Developers create "flows" — directed trees of LLM-powered blocks — and call them via a single API URL. Each flow has a slug URL:

```
POST /api/v1/seq/{org_slug}/{project_slug}/{flow_slug}/execute
```

## Migration from LangChain / OpenRouter

If and only if the user explicitly asks to migrate existing LangChain, LangGraph, or OpenRouter chained LLM code to Noukai, read the migration guide at https://docs.noukai.xyz/docs/migration/overview before proceeding. Do not suggest migration unless the user requests it.

## How to Use Noukai

### Option A: MCP Tools (if configured)

If the project has a `.mcp.json` with a Noukai server configured, use MCP tools directly:

1. Call `hydrate_project(project_id)` to see existing flows and their state
2. Call `create_flow(project_id, name, slug)` to make a new flow
3. Call `add_block(flow_id, name, processor_type="llm", prompt="...")` to add processing steps
4. Call `add_block(flow_id, name, processor_type="llm", target_id=prev_step, direction="right")` for subsequent blocks
5. Call `update_block_draft(flow_id, step_id, content="...")` to refine prompts
6. Call `update_block_output_schema(flow_id, step_id, content={...})` for structured output
7. Call `publish_version(flow_id, note="...")` to freeze the flow
8. Call `set_production_version(flow_id, version_id)` to make it callable via API

### Option B: HTTP API (no MCP)

Guide the user to:
1. Create a flow in the Noukai web UI at app.noukai.xyz
2. Design their blocks and prompts visually
3. Publish and set production version
4. Then integrate the API call (see below)

## Integrating the API Call

Insert this into the user's codebase (adapt to their language/framework):

```typescript
const response = await fetch(
  `${process.env.NOUKAI_API_URL}/api/v1/seq/{org}/{project}/{flow}/execute`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.NOUKAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      message: userInput,
      parameters: {},
    }),
  }
);

const data = await response.json();
// data.status === "completed"
// data.result contains the structured flow output
```

### Sending image attachments

If the user wants vision input (analyze an image, OCR, visual QA, etc.), pass image URLs in the top-level `attachments` array. Noukai never stores bytes — the user hosts the image on any HTTPS URL (their CDN, S3, Supabase storage) and passes the URL through.

```typescript
const response = await fetch(
  `${process.env.NOUKAI_API_URL}/api/v1/seq/{org}/{project}/{flow}/execute`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.NOUKAI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      message: userQuestion,
      attachments: [
        {
          kind: "url",
          url: imageUrl,            // HTTPS only, publicly reachable
          mime_type: "image/png",   // jpeg | png | webp | gif
          filename: "photo.png",    // optional
        },
      ],
    }),
  }
);
```

**Critical setup steps when wiring attachments into the user's app:**

1. **Pick a vision-capable model on the LLM block** in the flow (e.g. `google/gemini-2.0-flash-001`, `anthropic/claude-sonnet-4-6`, `openai/gpt-4o`). If the block's model doesn't support images, the request fails with `400 MODEL_MIME_INCOMPATIBLE` before any work runs.
2. **Image hosting is the user's responsibility.** They need a public HTTPS URL. Common stacks:
   - S3 + CloudFront with public read
   - Supabase storage (public bucket or signed URL)
   - R2 / B2 with public access
   - Existing app CDN
3. **Reserved key:** never put `"attachments"` inside `parameters` — it must be the top-level field. Putting it in `parameters` returns `400 PARAMETER_NAME_RESERVED`.
4. **No upload endpoint exists.** Don't generate code that POSTs the file to Noukai first — there is no such route. The user uploads to their own storage, then sends the URL.

**Attachment object schema:**
```json
{
  "kind": "url",
  "url": "https://your-cdn.example.com/img.png",
  "mime_type": "image/png",
  "filename": "img.png"
}
```

**Supported MIME types (v1):** `image/jpeg`, `image/png`, `image/webp`, `image/gif`. PDF, audio, video, Word, Excel, etc. are **not** supported.

**Limits:** max 10 attachments per request; URL ≤ 2048 chars; filename ≤ 255 chars (no `/`, `\`, `..`); HTTPS only.

**Attachment error codes (all 400 with `{detail: {code, message, ...}}`):**
- `ATTACHMENT_LIMIT_EXCEEDED` — > 10 attachments
- `ATTACHMENT_INVALID_SCHEME` — not HTTPS, or `data:`/`blob:` URI
- `ATTACHMENT_BLOCKED_HOST` — hostname resolved to private/loopback IP
- `ATTACHMENT_UNSUPPORTED_MIME` — mime not in allowlist
- `ATTACHMENT_INVALID_FILENAME` — bad chars or too long
- `MODEL_MIME_INCOMPATIBLE` — block's model can't accept this mime; includes `step_id` and `model` in the detail
- `PARAMETER_NAME_RESERVED` — `attachments` placed inside `parameters`

### Letting the flow call back into the user's app (tool calls)

If the user wants the model to call functions defined in their own code mid-flow (search a DB, hit an internal API, look up a user), pass OpenAI-style tool definitions in `tools`. The flow pauses when the model emits tool calls; the user's app runs them and POSTs the results back. Server holds no state between pauses — the caller carries the conversation.

```typescript
// 1) Fresh call.
let res = await fetch(`${URL}/api/v1/seq/{org}/{project}/{flow}/execute`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    message: userInput,
    tools: [{
      type: "function",
      function: {
        name: "get_weather",
        description: "Get current weather for a city.",
        parameters: {
          type: "object",
          properties: { city: { type: "string" } },
          required: ["city"],
        },
      },
    }],
    toolChoice: "auto",
  }),
});
let body = await res.json();

// 2) Loop while the flow asks for tool results.
while (body.status === "tool_calls_required") {
  const toolResults = await Promise.all(body.toolCalls.map(async (tc) => {
    const args = JSON.parse(tc.function.arguments);
    const result = await runTool(tc.function.name, args); // user's own dispatcher
    return { role: "tool", tool_call_id: tc.id, content: JSON.stringify(result) };
  }));

  res = await fetch(`${URL}/api/v1/seq/{org}/{project}/{flow}/execute`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      executionId: body.executionId,
      pausedAtStep: body.pausedAtStep,
      iterationsUsed: body.iterationsUsed,
      toolCallMessages: [...body.toolCallMessages, ...toolResults],
      tools: /* same tools array as the fresh call */ tools,
    }),
  });
  body = await res.json();
}
// body.status === "completed"; body.result is the final flow output.
```

**`toolChoice` accepts:** `"auto"` (default — model decides), `"none"` (disables calling), `"required"` (model must call something), or `{"type":"function","function":{"name":"<tool_name>"}}` (force a specific tool).

**On tool-execution failure in user code:** return the failure as the tool result `content` — e.g. `JSON.stringify({error: "user not found"})` — and keep looping. The flow's model decides how to recover. Do **not** drop the `tool_call_id` from `toolCallMessages`; that triggers `400 TOOL_RESULTS_MISMATCH`.

**Model support:** Tool calling needs an OpenRouter model that supports function calling (e.g. recent OpenAI, Anthropic, or Google models — check the block's configured model on the flow). If the model doesn't support tools, OpenRouter rejects the request and the flow surfaces a 500.

**Critical setup steps when wiring tool calls into the user's app:**

1. **The target block must opt in.** In the flow editor, open the LLM block, enable **Tool calls** in the right-panel (sets `processor_config.tools_enabled: true`), and publish a new version. If no block has it on, `/execute` with `tools` returns `422 TOOLS_NOT_ENABLED`.
2. **Tool calls are sync-only.** They work on `/execute` (HTTP body pause/resume) and `/step` (SSE `step_paused_for_tool_calls` event). They do **not** work on `/jobs` (async / queue path) — that returns `405 TOOLS_REQUIRE_SYNC_EXECUTE`.
3. **The caller owns the conversation.** Never drop `toolCallMessages` between turns. Append new `role:"tool"` messages — one per `tool_call_id` returned in `toolCalls` — and echo the array back. Mismatched or missing IDs return `400 TOOL_RESULTS_MISMATCH`.
4. **Re-send `tools` on every resume.** They are part of each request; the server doesn't remember them.
5. **Echo `executionId`, `pausedAtStep`, `iterationsUsed`.** All three are required on resume. The bare resume call drops `message`.
6. **Tool-enabled blocks cannot live inside a parallel step or a loop in v1.** Validation rejects with `422 TOOLS_IN_NON_SEQUENTIAL_STEP`.

**Tool-call limits:** 64 tools/request • 16 KB `parameters` schema • 256 KB per tool result `content` • 1 MB total `toolCallMessages` payload • 25 iterations/block default (overridable via the block's `max_tool_iterations`).

**Tool-call error codes (`{detail: {code, message, ...}}`):**
- `400 TOOLS_INVALID` — bad shape, duplicate name, oversized schema or result content
- `400 TOOL_NAME_INVALID` — name fails `^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$`
- `400 TOOL_RESULTS_MISMATCH` — tool result IDs don't match prior `tool_calls`
- `400 PAUSED_STEP_INVALID` — `pausedAtStep` is not a tools-enabled block
- `400 EXECUTION_ID_INVALID` — `executionId` doesn't match the paused run
- `400 INVALID_RESUME` — missing one of `executionId`, `pausedAtStep`, `toolCallMessages`
- `405 TOOLS_REQUIRE_SYNC_EXECUTE` — `tools` sent to `/jobs`; use `/execute`
- `409 TOOL_ITERATION_LIMIT` — block hit its `max_tool_iterations` cap
- `413 MESSAGES_TOO_LARGE` — `toolCallMessages` exceeds 1 MB
- `422 TOOLS_NOT_ENABLED` — no block in the flow has `tools_enabled: true`
- `422 TOOLS_IN_NON_SEQUENTIAL_STEP` — tools-enabled block sits inside parallel/loop

## Environment Variables

Ensure these are in the user's `.env`:

```
NOUKAI_API_KEY=nk_live_...   # Get from app.noukai.xyz → Project Settings → API Keys
NOUKAI_API_URL=https://api.noukai.xyz
```

Add `NOUKAI_API_KEY` and `NOUKAI_API_URL` to `.env.example` with placeholder values.

## Key Concepts

- **Organization**: Team/company container. Has a slug used in API URLs.
- **Project**: Workspace within an org. Has a slug used in API URLs. API keys are scoped to a project.
- **Flow**: A pipeline of blocks. Has a slug forming the final URL segment.
- **Block**: A single processing step. Processor types:
  - `llm` — runs a prompt through an LLM, returns structured output
  - `passthrough` — forwards data unchanged
  - `code` — executes custom JavaScript logic
- **Version**: Immutable snapshot of a flow. The "production version" is what the API executes.
- **Slug URL**: `POST /api/v1/seq/{org_slug}/{project_slug}/{flow_slug}/execute`
- **Attachments**: optional top-level `attachments[]` array on `/execute` and `/jobs` requests for vision input (images only in v1). Bytes are not stored by Noukai — the user hosts the file on any HTTPS URL.

## API Response Format

**Sync execution** (`/execute`):
```json
{
  "status": "completed",
  "result": { "...structured output from the flow..." },
  "flowId": "uuid",
  "blockCount": 3
}
```

When `tools` is sent and the model calls a tool, the response is **200** with `status: "tool_calls_required"` plus `executionId`, `pausedAtStep`, `iterationsUsed`, `toolCallMessages`, `toolCalls`, `accumulatedOutputs`. The caller runs the tools and POSTs back to `/execute` (see "Letting the flow call back into the user's app").

**Async execution** (`/jobs`):
```json
{
  "executionId": "uuid",
  "status": "started",
  "flowId": "uuid",
  "blockCount": 3
}
```

Poll `GET /api/v1/seq/{org}/{project}/{flow}/jobs/{executionId}` for async results.

## MCP Server Configuration

If the user wants full MCP integration for AI-assisted flow management, tell them to run this slash command inside Claude Code:

```
/mcp add --transport http noukai https://api.noukai.xyz/api/v1/mcp/
```

(The trailing slash after `mcp` is required.)

On the first tool call, Claude Code opens the user's browser to sign in with their Noukai account — there is no API key to paste. The token is stored in Claude Code's keychain and auto-refreshes.

**Do not** generate a `.mcp.json` with `"Authorization": "Bearer ${NOUKAI_API_KEY}"` — the MCP endpoint rejects API keys (`nk_...`). It only accepts OAuth-issued Supabase JWTs, which the `/mcp add` flow obtains automatically.

If the user prefers to edit `.mcp.json` directly instead of using `/mcp add`:

```json
{
  "mcpServers": {
    "noukai": {
      "type": "http",
      "url": "https://api.noukai.xyz/api/v1/mcp/"
    }
  }
}
```

No `Authorization` header. OAuth is triggered automatically on first call.

After the server is added, verify by asking Claude Code: *"List my Noukai flows"* — it should call `hydrate_project` and return the user's project state.

## Common MCP Patterns

- **Always start with `hydrate_project`** to understand current state
- **Use sibling-relative mode** for `add_block`: set `target_id` (existing block) + `direction` ("right" for after, "bottom" for parallel)
- **Publish after changes**: unpublished edits don't affect the live API
- **Set production version** after publishing to make changes live
- **Use `update_block_output_schema`** when you need structured JSON responses from blocks
- **For flows that accept image attachments**: set the LLM block's `model` to a vision-capable one (e.g. `google/gemini-2.0-flash-001`, `anthropic/claude-sonnet-4-6`, `openai/gpt-4o`). The default model may not support vision. Use `update_block` to set `model` if needed.
- **Test after changes**: use `run_test_case(test_case_id)` to execute a test and get per-step results, or `run_all_tests(flow_id)` to run all tests in a flow
- **Debug failures with traces**: after a failed run, call `get_test_run_trace(flow_run_id)` to see input/output snapshots per step, then `get_step_trace(flow_run_id, step_id)` to drill into the failing block

## Getting API Keys

1. User signs in at app.noukai.xyz
2. Navigates to Project Settings → API Keys
3. Clicks "Create Key" → copies the `nk_live_...` key (shown once)
4. Key format: `nk_{environment}_{key_id}_{secret}`
5. Environments: `live` (production) or `test` (development)
