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

# QA

> Ask questions about a workspace as a conversation

`search.qa` asks one question about a workspace as one turn of a conversation.
It returns a `Job` whose result is an `AutomaticAgentResult`. Every turn runs
the automatic search agent on its own: it opens with what a search of the
workspace finds, answers from that when it can, and otherwise reads documents,
queries tables, and selects documents from the metadata catalog. Files must
already be [ingested](/guides/ingest-reconcile).

***

## A conversation

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import AutomaticAgentResult

with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    first = client.jobs.wait(
        client.search.qa(workspace_id, query="What was total freight spend in FY2024?", wait_seconds=30).job_id,
        timeout=600,
    )
    result = first.result
    if isinstance(result, AutomaticAgentResult):
        print(result.answer.answer)
        follow_up = client.jobs.wait(
            client.search.qa(
                workspace_id,
                query="and for FY2023?",
                session_id=result.answer.session_id,
                wait_seconds=30,
            ).job_id,
            timeout=600,
        )
        # Returns: succeeded Job; follow_up.result is AutomaticAgentResult
```

***

## Clarifying questions

On a genuinely ambiguous question the agent may ask back instead of answering:
`answer.clarification_questions` lists the questions and `answer.answer` is
`None`. Reply in the same conversation:

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import AutomaticAgentResult
from ndi_sdk.models.tools import ClarificationAnswer

with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    query = "What did we pay the carrier?"
    job = client.jobs.wait(client.search.qa(workspace_id, query=query, wait_seconds=30).job_id, timeout=600)
    result = job.result
    if isinstance(result, AutomaticAgentResult) and result.answer.clarification_questions:
        answers = [
            ClarificationAnswer(question=asked.question, answer=asked.options[0] if asked.options else None)
            for asked in result.answer.clarification_questions
        ]
        reply = client.search.qa(
            workspace_id,
            query=query,
            session_id=result.answer.session_id,
            clarification_answers=answers,
        )
        print(reply.job_id)
```

Pass `allow_clarification=False` to have every turn answer under the agent's
best reading, stated in the answer.

***

## Structured answers

Pass `output_schema` (a JSON Schema dict or a Pydantic model class, under the
same rules as [automatic search's](/sdks/python/automatic-search#structured-answers))
to get a turn's answer as JSON. On success `answer.answer` is the JSON text and
`answer.structured_answer` the parsed value, `None` if the answer still missed
the schema after the run's corrections. A field the source does not state comes back `None`, whatever type the schema gives
it, so declare such fields optional in a Pydantic model you validate into.

The schema applies to the turn that sends it, not to the conversation. To
keep structured answers across a conversation, send it on every turn,
including the one that carries `clarification_answers`. A turn that asks
clarifying questions returns them with `structured_answer` set to `None`.

```python theme={"dark"}
from pydantic import BaseModel
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import AutomaticAgentResult


class Liabilities(BaseModel):
    total: float
    currency: str


with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    job = client.jobs.wait(
        client.search.qa(
            workspace_id,
            query="What were total liabilities at year end?",
            output_schema=Liabilities,
            wait_seconds=30,
        ).job_id,
        timeout=600,
    )
    if isinstance(job.result, AutomaticAgentResult) and job.result.answer.structured_answer is not None:
        print(Liabilities.model_validate(job.result.answer.structured_answer).total)
```

***

## Method signature

```text theme={"dark"}
def qa(
    workspace_id: UUID | str,
    *,
    query: str,
    context: str | None = None,
    path_prefix: str | None = None,
    paths: list[str] | None = None,
    session_id: UUID | None = None,
    allow_clarification: bool | None = None,
    clarification_answers: list[ClarificationAnswer] | None = None,
    output_schema: dict | type[BaseModel] | None = None,
    wait_seconds: int = 0,
    idempotency_key: str | None = None,
) -> Job
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `workspace_id` | `UUID` or `str` | Yes | Workspace that already holds ingested files |
| `query` | `str` | Yes | The question, in the conversation's own words |
| `context` | `str` | No | Extra background the model should treat as given. Omit it to use the workspace's `global_context`; `""` runs with none |
| `path_prefix` | `str` | No | Scope the conversation to this workspace subtree |
| `paths` | `list[str]` | No | Scope the conversation to exactly these uploaded files. Adds to `path_prefix` |
| `session_id` | `UUID` | No | Continue a conversation: an earlier result's `answer.session_id` |
| `allow_clarification` | `bool` | No | Let the agent ask back on an ambiguous question. On when omitted |
| `clarification_answers` | `list[ClarificationAnswer]` | No | Answers to the last turn's `clarification_questions`, one per question, in order |
| `output_schema` | `dict` or Pydantic model class | No | JSON Schema this turn's answer must follow. Send it on every turn that should answer in that shape. See [Structured answers](#structured-answers) |
| `wait_seconds` | `int` | No | Hold the submission request open (max 300) |
| `idempotency_key` | `str` | No | Override the key the SDK normally mints |

### Returns

A `Job`. On success, `job.result` is an `AutomaticAgentResult`
(`result_type="automatic_agent"`): `answer`, a `DeepSearchV2Result` with the
answer, citations, SQL receipts, `session_id` and any `clarification_questions`;
and `catalog`, a `FilteredSearchResult` present when the agent answered from the
metadata catalog. A catalog table's `next_cursor` pages through
`client.search.filtered_page`.

***

## Error handling

| Code | When |
| - | - |
| `invalid_request` (422) | Blank query, or `clarification_answers` without `session_id` |
| `session_not_found` (404) | `session_id` names no QA conversation in this workspace |
| `session_busy` (409) | The conversation's previous turn is still running |
| `access_denied` (403) | Another API key started the conversation |

***

## Related search methods

| Need | Method |
| - | - |
| One query, service picks the route | `client.search.automatic` |
| A fact lookup without a conversation | `client.search.automatic(mode="fact")` |
| Page a catalog table past its first 200 rows | `client.search.filtered_page` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.