> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qredence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API and streaming

> Fleet Reasoner's FastAPI service — /seed, /engine, /chat/stream, and /config — plus the SSE frame shapes, session model, and normalized DSPy 3.3.0 error mapping.

Fleet Reasoner ships as a FastAPI service in `qlaw/serve.py`. Start it locally:

```bash theme={null}
uv run uvicorn qlaw.serve:app
```

Configuration is set once at startup with `configure_research()` — `dspy.configure` has an owner-thread rule and must not be called inside request handlers. Use `dspy.context` for per-request overrides.

## Endpoints

| Endpoint            | Purpose                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `POST /seed`        | Inception: prompt → graph with `ROOT` plus the first decomposition layer.                                               |
| `POST /engine`      | One step of the reasoning cycle: `(graph, active_node_id, action)` → expanded graph.                                    |
| `POST /chat/stream` | SSE from the Qlaw chat agent (`dspy.streamify`), status events plus a `done` frame with the answer and post-chat graph. |
| `GET /config`       | Runtime config: `{"model": "..."}` from `OPENAI_MODEL`. Never credentials.                                              |

## `POST /seed`

```json theme={null}
{ "prompt": "Launch a sustainable fashion brand" }
```

Returns the serialized `GraphState`. Errors map through the DSPy 3.3.0 exception hierarchy (see "Error mapping" below).

## `POST /engine`

```json theme={null}
{
  "nodes": { "root": { "...": "..." } },
  "root_id": "root",
  "active_node_id": "root",
  "action": null
}
```

Returns:

```json theme={null}
{
  "graph": { "...": "..." },
  "lens": "decompose",
  "outputs": { "sub_nodes": [ /* ... */ ] }
}
```

`outputs` uses `Prediction.toDict()` — DSPy 3.3.0 has no `Prediction.model_dump()`.

`max_depth` is **not** a request field. It is a constructor argument on `ReasoningEngine` and `ReasoningLoop`.

## `POST /chat/stream`

```json theme={null}
{
  "session_id": "<opaque per-tab id>",
  "nodes": { "root": { "...": "..." } },
  "root_id": "root",
  "question": "What matters most for the brand-identity component?",
  "history": [],
  "active_node_id": "root"
}
```

The server maps `session_id` to a bounded, independently locked `QlawChat` in `ChatSessionStore`. Missing or expired entries start a fresh conversation. `history` is used only to bootstrap a fresh instance for HTTP compatibility. Later turns use the instance-owned history.

### SSE frames

```text theme={null}
event: status
data: {"event": "status", "message": "run_lens.start"}

event: done
data: {
  "event": "done",
  "answer": "...",
  "termination_reason": "submit",
  "graph": { "...": "..." }
}
```

* Non-`Prediction` yields are `StatusMessage` and stream events. `ChatStatusProvider` turns tool start and end into compact status lines.
* ReActV2 emits its final answer as `submit` tool-call arguments, so token-level streaming of `answer` does not apply. Surface tool activity as `status` events and the complete answer in the `done` frame.
* The `done` frame carries the final graph. Chat tools expand the graph during the agent run, so the client must adopt `done.graph`.

## `GET /config`

Returns `{"model": "..."}` from `OPENAI_MODEL` (default `deepseek-v4-flash`). Credentials are never exposed. The web client reads this to display the active model id.

## Error mapping

DSPy 3.3.0 normalizes LM errors under `dspy.LMError`. Catch subclasses and map:

| Exception                         | HTTP status |
| --------------------------------- | ----------- |
| `dspy.LMRateLimitError`           | `429`       |
| `dspy.ContextWindowExceededError` | `413`       |
| `dspy.LMError` (base and others)  | `502`       |

For the streamed `/chat/stream` route, the response has already started when an error occurs mid-stream. `HTTPException` is not an option there — the generator catches the exception and emits:

```json theme={null}
{"event": "error", "status": 429, "message": "Rate limited: ..."}
```

## Streaming configuration

Relevant DSPy settings:

* `dspy.configure(allow_tool_async_sync_conversion=True)` — allow sync tool implementations in async paths.
* `async_max_workers=...` — control async concurrency.
* `dspy.asyncify(fn)` — wrap a sync function for the async path.

`dspy.stream` does **not** exist in 3.3.0. Streaming goes through `dspy.streamify(program, status_message_provider=...)`, which returns an async generator ending with the final `dspy.Prediction`.
