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

# Chat

> Ask questions about your documents using the Graphor SDK

The `ask` method allows you to ask questions about your ingested documents and receive answers grounded in your content. The SDK supports conversational memory, enabling follow-up questions that maintain context.

## Method Overview

<Tabs>
  <Tab title="Python">
    <CardGroup cols={2}>
      <Card title="Sync Method" icon="code">
        **`client.sources.ask()`**
      </Card>

      <Card title="Async Method" icon="code">
        **`await client.sources.ask()`** (using `AsyncGraphor`)
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="TypeScript">
    <CardGroup cols={2}>
      <Card title="Async Method" icon="code">
        **`await client.sources.ask()`**

        All TypeScript methods are async and return a `Promise`.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>

## Method Signature

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    client.sources.ask(
        question: str,                                # Required
        conversation_id: str | None = None,
        reset: bool | None = None,
        file_ids: list[str] | None = None,
        file_names: list[str] | None = None,          # Deprecated
        output_schema: dict | None = None,
        thinking_level: str | None = None,
        include_citation_images: bool | None = None,
        include_citation_markup: bool | None = None,
        timeout: float | None = None
    ) -> SourceAskResponse
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    await client.sources.ask({
      question: string,                                          // Required
      conversation_id?: string | null,
      reset?: boolean | null,
      file_ids?: string[] | null,
      file_names?: string[] | null,                              // Deprecated
      output_schema?: Record<string, unknown> | null,
      thinking_level?: 'fast' | 'balanced' | 'accurate' | null,
      include_citation_images?: boolean | null,
      include_citation_markup?: boolean | null,
    }): Promise<SourceAskResponse>
    ```
  </Tab>
</Tabs>

## Parameters

<Tabs>
  <Tab title="Python">
    | Parameter                 | Type        | Description                                                                                                                                                                                                                                                                                      | Required |
    | ------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
    | `question`                | `str`       | The question to ask about your documents                                                                                                                                                                                                                                                         | Yes      |
    | `conversation_id`         | `str`       | Conversation identifier to maintain memory context across questions                                                                                                                                                                                                                              | No       |
    | `reset`                   | `bool`      | When `True`, starts a new conversation and ignores previous history                                                                                                                                                                                                                              | No       |
    | `file_ids`                | `list[str]` | Restrict search to specific documents by file ID (preferred)                                                                                                                                                                                                                                     | No       |
    | `file_names`              | `list[str]` | Restrict search to specific documents by file name (deprecated, use `file_ids`)                                                                                                                                                                                                                  | No       |
    | `output_schema`           | `dict`      | JSON Schema to request structured output (see below)                                                                                                                                                                                                                                             | No       |
    | `thinking_level`          | `str`       | Controls model and thinking configuration: `"fast"`, `"balanced"`, `"accurate"` (default)                                                                                                                                                                                                        | No       |
    | `include_citation_images` | `bool`      | When `True`, populates `image_base64` (base64 PNG of the cited page) inside each `citations` entry. Default `False`. See [When to use `include_citation_images`](#when-to-use-include-citation-images).                                                                                          | No       |
    | `include_citation_markup` | `bool`      | When `True`, the `answer` field keeps the raw structured citation markup `[N](file_id\|pX\|sY\|eZ\|fNAME)` instead of stripping it down to plain `[N]` markers. Default `False`. The markup is an implementation detail — prefer parsing `citations`. Has no effect when `output_schema` is set. | No       |
    | `timeout`                 | `float`     | Request timeout in seconds                                                                                                                                                                                                                                                                       | No       |
  </Tab>

  <Tab title="TypeScript">
    | Parameter                 | Type                                         | Description                                                                                                                                                              | Required |
    | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
    | `question`                | `string`                                     | The question to ask about your documents                                                                                                                                 | Yes      |
    | `conversation_id`         | `string \| null`                             | Conversation identifier to maintain memory context across questions                                                                                                      | No       |
    | `reset`                   | `boolean \| null`                            | When `true`, starts a new conversation and ignores previous history                                                                                                      | No       |
    | `file_ids`                | `string[]  \| null`                          | Restrict search to specific documents by file ID (preferred)                                                                                                             | No       |
    | `file_names`              | `string[] \| null`                           | Restrict search to specific documents by file name (deprecated, use `file_ids`)                                                                                          | No       |
    | `output_schema`           | `Record<string, unknown> \| null`            | JSON Schema to request structured output (see below)                                                                                                                     | No       |
    | `thinking_level`          | `'fast' \| 'balanced' \| 'accurate' \| null` | Controls model and thinking configuration (default: `"accurate"`)                                                                                                        | No       |
    | `include_citation_images` | `boolean \| null`                            | When `true`, populates `image_base64` inside each `citations` entry. Default `false`. See [When to use `include_citation_images`](#when-to-use-include-citation-images). | No       |
    | `include_citation_markup` | `boolean \| null`                            | When `true`, the `answer` field keeps the raw structured citation markup `[N](file_id\|pX\|sY\|eZ\|fNAME)`. Default `false`. Has no effect when `output_schema` is set.  | No       |
  </Tab>
</Tabs>

### Thinking Level

The `thinking_level` parameter controls the model and thinking configuration used for answering questions:

| Value        | Description                                                                                                 |
| ------------ | ----------------------------------------------------------------------------------------------------------- |
| `"fast"`     | Uses a faster model without extended thinking. Best for simple questions where speed is prioritized.        |
| `"balanced"` | Uses a more capable model with low thinking. Good balance between quality and speed.                        |
| `"accurate"` | Default. Uses a more capable model with high thinking. Best for complex questions requiring deep reasoning. |

## Response Object

The method returns a `SourceAskResponse` object:

| Property            | Type                     | Description                                                                                                                                                                            |
| ------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `answer`            | `str`                    | The answer to your question, with inline `[N]` markers pointing to entries in `citations`. When `output_schema` is provided, this will be a short status message.                      |
| `conversation_id`   | `str \| None`            | Conversation identifier for follow-up questions                                                                                                                                        |
| `structured_output` | `dict \| None`           | Structured output validated against the requested `output_schema`. Present only when `output_schema` is provided.                                                                      |
| `raw_json`          | `str \| None`            | Raw JSON-text produced by the model before validation. Present only when `output_schema` is provided.                                                                                  |
| `citations`         | `list[Citation] \| None` | Structured references resolving each `[N]` marker in `answer`. May be empty/`None` when the agent did not ground its answer (e.g. small-talk follow-ups). See [Citations](#citations). |
| `usage`             | `Usage \| None`          | Token usage breakdown for the request                                                                                                                                                  |
| `elapsed_s`         | `float \| None`          | Wall-clock time in seconds                                                                                                                                                             |

### Citations

Each entry in `citations` corresponds to one `[N]` marker that appears in the `answer` text. Use `index` to map a marker to its citation.

| Field            | Type          | Description                                                                                                                                                                |
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index`          | `int`         | The 1-based citation number that appears as `[N]` in `answer`                                                                                                              |
| `file_id`        | `str`         | Unique identifier of the source file                                                                                                                                       |
| `file_name`      | `str`         | Display name of the source file                                                                                                                                            |
| `page_number`    | `int`         | 1-based page number where the cited content appears                                                                                                                        |
| `section_number` | `int \| None` | Optional section number within the page                                                                                                                                    |
| `element_id`     | `str \| None` | Optional element identifier (e.g. specific paragraph or table)                                                                                                             |
| `text_preview`   | `str \| None` | Short text excerpt around the cited content                                                                                                                                |
| `image_base64`   | `str \| None` | Base64-encoded PNG of the cited page. Populated only when the call used `include_citation_images=True`. May be `None` if the source is not visualizable (e.g. plain text). |

### When to use `include_citation_images`

The `include_citation_images` flag is convenient for quick prototyping or one-off requests where you want both the answer and the visual previews in a single round-trip.

For real applications — especially when answers commonly cite many pages — **prefer leaving the flag as `False` (the default) and lazy-load the screenshots on demand** via the dedicated [`get_page_screenshot`](/sdk/sources/page-screenshot) method. Reasons:

* **Payload size**: each base64 PNG is typically 100-400 KB. Five citations can push the JSON response above a megabyte.
* **Latency**: rendering screenshots adds seconds to the response. Without the flag, the answer comes back as soon as the model finishes.
* **Cache locality**: the screenshot endpoint is keyed by `(file_id, page_number)` and emits a `Cache-Control` hint. Lazy-loading lets browsers and CDNs cache the bytes; inlining base64 prevents that.

Rule of thumb: enable `include_citation_images=True` only when you control both ends and know the answer will cite at most 1-2 pages. Otherwise, ship the answer with structured `citations` and fetch images on hover/click.

## Code Examples

### Basic Question

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Ask a simple question
    response = client.sources.ask(
        question="What are the main findings in this report?"
    )

    print(f"Answer: {response.answer}")
    print(f"Conversation ID: {response.conversation_id}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Ask a simple question
    const response = await client.sources.ask({
      question: 'What are the main findings in this report?',
    });

    console.log(`Answer: ${response.answer}`);
    console.log(`Conversation ID: ${response.conversation_id}`);
    ```
  </Tab>
</Tabs>

### Conversation with Memory

Use `conversation_id` to maintain context across multiple questions:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # First question
    response = client.sources.ask(
        question="What products are mentioned in the catalog?"
    )

    print(f"Answer: {response.answer}")

    # Follow-up question using conversation memory
    follow_up = client.sources.ask(
        question="Which one is the most expensive?",
        conversation_id=response.conversation_id
    )

    print(f"Follow-up: {follow_up.answer}")

    # Another follow-up
    another = client.sources.ask(
        question="What are its specifications?",
        conversation_id=response.conversation_id
    )

    print(f"Answer: {another.answer}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // First question
    const response = await client.sources.ask({
      question: 'What products are mentioned in the catalog?',
    });

    console.log(`Answer: ${response.answer}`);

    // Follow-up question using conversation memory
    const followUp = await client.sources.ask({
      question: 'Which one is the most expensive?',
      conversation_id: response.conversation_id,
    });

    console.log(`Follow-up: ${followUp.answer}`);

    // Another follow-up
    const another = await client.sources.ask({
      question: 'What are its specifications?',
      conversation_id: response.conversation_id,
    });

    console.log(`Answer: ${another.answer}`);
    ```
  </Tab>
</Tabs>

### Reset Conversation

Start fresh by using the `reset` parameter:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Start a conversation
    response = client.sources.ask(
        question="What is the company's revenue?"
    )

    # Switch to a new topic - reset the conversation
    new_response = client.sources.ask(
        question="What are the safety guidelines?",
        conversation_id=response.conversation_id,
        reset=True  # Ignores previous conversation history
    )

    print(f"Answer: {new_response.answer}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Start a conversation
    const response = await client.sources.ask({
      question: "What is the company's revenue?",
    });

    // Switch to a new topic - reset the conversation
    const newResponse = await client.sources.ask({
      question: 'What are the safety guidelines?',
      conversation_id: response.conversation_id,
      reset: true, // Ignores previous conversation history
    });

    console.log(`Answer: ${newResponse.answer}`);
    ```
  </Tab>
</Tabs>

### Filter by Specific Documents

Restrict the search to specific files using `file_ids` (preferred):

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Ask about specific documents using file_ids (preferred)
    response = client.sources.ask(
        question="What is the total amount due?",
        file_ids=["file_abc123", "file_def456"]
    )

    print(f"Answer: {response.answer}")

    # Or using file_names (deprecated)
    response = client.sources.ask(
        question="What is the total amount due?",
        file_names=["invoice-2024.pdf", "invoice-2023.pdf"]
    )

    print(f"Answer: {response.answer}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Ask about specific documents using file_ids (preferred)
    const response = await client.sources.ask({
      question: 'What is the total amount due?',
      file_ids: ['file_abc123', 'file_def456'],
    });

    console.log(`Answer: ${response.answer}`);

    // Or using file_names (deprecated)
    const response2 = await client.sources.ask({
      question: 'What is the total amount due?',
      file_names: ['invoice-2024.pdf', 'invoice-2023.pdf'],
    });

    console.log(`Answer: ${response2.answer}`);
    ```
  </Tab>
</Tabs>

### Working with Citations

Every grounded answer comes back with a structured `citations` array. The default flow is **fetch screenshots on demand** — only render images for the citations the user actually inspects.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Default: lazy-load screenshots when needed
    response = client.sources.ask(
        question="What was the revenue in 2025?",
        file_ids=["file_abc123"]
    )

    print(response.answer)
    # -> "Revenue grew 23% year-over-year, reaching $4.2B [1]…"

    for c in (response.citations or []):
        print(f"[{c.index}] {c.file_name} p{c.page_number}")
        # When the user hovers/clicks on [N], fetch the page screenshot
        screenshot = client.sources.get_page_screenshot(
            page_number=c.page_number,
            file_id=c.file_id,
            max_width=900,
        )
        # screenshot.image_base64 is a ready-to-render PNG
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Default: lazy-load screenshots when needed
    const response = await client.sources.ask({
      question: 'What was the revenue in 2025?',
      file_ids: ['file_abc123'],
    });

    console.log(response.answer);
    // -> "Revenue grew 23% year-over-year, reaching $4.2B [1]…"

    for (const c of response.citations ?? []) {
      console.log(`[${c.index}] ${c.file_name} p${c.page_number}`);
      // When the user hovers/clicks on [N], fetch the page screenshot
      const screenshot = await client.sources.getPageScreenshot(
        c.page_number!,
        { file_id: c.file_id!, max_width: 900 },
      );
      // screenshot.image_base64 is a ready-to-render PNG
    }
    ```
  </Tab>
</Tabs>

#### Inlining citation images in the response

When you really do want the screenshots in the same round-trip — for example, a one-off batch job that won't be re-rendered — set `include_citation_images=true`. **Avoid this in interactive UIs** and any flow where the answer typically cites many pages.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    response = client.sources.ask(
        question="Confirm the invoice number and due date.",
        file_ids=["invoice_abc123"],
        include_citation_images=True,  # base64 PNG embedded per citation
    )

    for c in (response.citations or []):
        if c.image_base64:
            with open(f"page_{c.page_number}.png", "wb") as f:
                import base64
                f.write(base64.b64decode(c.image_base64))
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const response = await client.sources.ask({
      question: 'Confirm the invoice number and due date.',
      file_ids: ['invoice_abc123'],
      include_citation_images: true, // base64 PNG embedded per citation
    });

    for (const c of response.citations ?? []) {
      if (c.image_base64) {
        const bytes = Buffer.from(c.image_base64, 'base64');
        await fs.promises.writeFile(`page_${c.page_number}.png`, bytes);
      }
    }
    ```
  </Tab>
</Tabs>

### Using Thinking Level

Control the model's reasoning depth with `thinking_level`:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Fast mode for simple questions
    response = client.sources.ask(
        question="What is the document title?",
        thinking_level="fast"
    )

    print(f"Answer: {response.answer}")

    # Accurate mode for complex analysis
    response = client.sources.ask(
        question="Analyze the legal implications of the termination clause and identify potential risks.",
        file_names=["contract.pdf"],
        thinking_level="accurate"
    )

    print(f"Analysis: {response.answer}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Fast mode for simple questions
    const response = await client.sources.ask({
      question: 'What is the document title?',
      thinking_level: 'fast',
    });

    console.log(`Answer: ${response.answer}`);

    // Accurate mode for complex analysis
    const analysis = await client.sources.ask({
      question: 'Analyze the legal implications of the termination clause and identify potential risks.',
      file_names: ['contract.pdf'],
      thinking_level: 'accurate',
    });

    console.log(`Analysis: ${analysis.answer}`);
    ```
  </Tab>
</Tabs>

### Structured Output with JSON Schema

Request structured data by providing an `output_schema`:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Define the output schema
    invoice_schema = {
        "type": "object",
        "properties": {
            "invoice_number": {"type": ["string", "null"]},
            "total_amount_due": {"type": ["number", "null"]},
            "currency": {"type": ["string", "null"]},
            "due_date": {"type": ["string", "null"]}
        }
    }

    # Ask with structured output
    response = client.sources.ask(
        question="Extract the invoice number, total amount, currency, and due date.",
        file_names=["invoice-2024.pdf"],
        output_schema=invoice_schema
    )

    # Access the structured data
    if response.structured_output:
        data = response.structured_output
        print(f"Invoice: {data.get('invoice_number')}")
        print(f"Amount: {data.get('total_amount_due')} {data.get('currency')}")
        print(f"Due: {data.get('due_date')}")

    # Raw JSON is also available
    print(f"Raw JSON: {response.raw_json}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Define the output schema
    const invoiceSchema = {
      type: 'object',
      properties: {
        invoice_number: { type: ['string', 'null'] },
        total_amount_due: { type: ['number', 'null'] },
        currency: { type: ['string', 'null'] },
        due_date: { type: ['string', 'null'] },
      },
    };

    // Ask with structured output
    const response = await client.sources.ask({
      question: 'Extract the invoice number, total amount, currency, and due date.',
      file_names: ['invoice-2024.pdf'],
      output_schema: invoiceSchema,
    });

    // Access the structured data
    if (response.structured_output) {
      const data = response.structured_output as Record<string, unknown>;
      console.log(`Invoice: ${data.invoice_number}`);
      console.log(`Amount: ${data.total_amount_due} ${data.currency}`);
      console.log(`Due: ${data.due_date}`);
    }

    // Raw JSON is also available
    console.log(`Raw JSON: ${response.raw_json}`);
    ```
  </Tab>
</Tabs>

### Extract Array of Items

Extract multiple items with a schema:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    # Schema for extracting a list of products
    products_schema = {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "price": {"type": ["number", "null"]},
                "quantity": {"type": ["integer", "null"]}
            }
        }
    }

    response = client.sources.ask(
        question="Extract all products with their prices and quantities from the order.",
        file_names=["order.pdf"],
        output_schema=products_schema
    )

    if response.structured_output:
        products = response.structured_output
        for product in products:
            print(f"- {product['name']}: ${product.get('price', 'N/A')} x {product.get('quantity', 'N/A')}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    // Schema for extracting a list of products
    const productsSchema = {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          price: { type: ['number', 'null'] },
          quantity: { type: ['integer', 'null'] },
        },
      },
    };

    const response = await client.sources.ask({
      question: 'Extract all products with their prices and quantities from the order.',
      file_names: ['order.pdf'],
      output_schema: productsSchema,
    });

    if (response.structured_output) {
      const products = response.structured_output as Array<Record<string, unknown>>;
      for (const product of products) {
        console.log(`- ${product.name}: $${product.price ?? 'N/A'} x ${product.quantity ?? 'N/A'}`);
      }
    }
    ```
  </Tab>
</Tabs>

### Async Usage

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import asyncio
    from graphor import AsyncGraphor

    async def ask_questions():
        client = AsyncGraphor()
        
        # Ask a question
        response = await client.sources.ask(
            question="What are the key terms in this contract?"
        )
        
        print(f"Answer: {response.answer}")
        
        # Follow-up
        follow_up = await client.sources.ask(
            question="When does it expire?",
            conversation_id=response.conversation_id
        )
        
        print(f"Follow-up: {follow_up.answer}")

    asyncio.run(ask_questions())
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    async function askQuestions() {
      // Ask a question
      const response = await client.sources.ask({
        question: 'What are the key terms in this contract?',
      });

      console.log(`Answer: ${response.answer}`);

      // Follow-up
      const followUp = await client.sources.ask({
        question: 'When does it expire?',
        conversation_id: response.conversation_id,
      });

      console.log(`Follow-up: ${followUp.answer}`);
    }

    await askQuestions();
    ```
  </Tab>
</Tabs>

### Error Handling

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import graphor
    from graphor import Graphor

    client = Graphor()

    try:
        response = client.sources.ask(
            question="What is the summary of this document?"
        )
        print(f"Answer: {response.answer}")
        
    except graphor.BadRequestError as e:
        print(f"Invalid request: {e}")
        
    except graphor.AuthenticationError as e:
        print(f"Invalid API key: {e}")
        
    except graphor.NotFoundError as e:
        print(f"Document not found: {e}")
        
    except graphor.UnprocessableEntityError as e:
        print(f"Invalid output_schema or structured output validation failed: {e}")
        
    except graphor.RateLimitError as e:
        print(f"Rate limit exceeded: {e}")
        
    except graphor.APIConnectionError as e:
        print(f"Connection error: {e}")
        
    except graphor.APIStatusError as e:
        print(f"API error (status {e.status_code}): {e}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    try {
      const response = await client.sources.ask({
        question: 'What is the summary of this document?',
      });
      console.log(`Answer: ${response.answer}`);
    } catch (err) {
      if (err instanceof Graphor.BadRequestError) {
        console.log(`Invalid request: ${err.message}`);
      } else if (err instanceof Graphor.AuthenticationError) {
        console.log(`Invalid API key: ${err.message}`);
      } else if (err instanceof Graphor.NotFoundError) {
        console.log(`Document not found: ${err.message}`);
      } else if (err instanceof Graphor.UnprocessableEntityError) {
        console.log(`Invalid output_schema or validation failed: ${err.message}`);
      } else if (err instanceof Graphor.RateLimitError) {
        console.log(`Rate limit exceeded: ${err.message}`);
      } else if (err instanceof Graphor.APIConnectionError) {
        console.log(`Connection error: ${err.message}`);
      } else if (err instanceof Graphor.APIError) {
        console.log(`API error (status ${err.status}): ${err.message}`);
      } else {
        throw err;
      }
    }
    ```
  </Tab>
</Tabs>

## Advanced Examples

### Chatbot Class

Build a conversational chatbot:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor
    import graphor

    class DocumentChatbot:
        def __init__(self, api_key: str | None = None):
            self.client = Graphor(api_key=api_key) if api_key else Graphor()
            self.conversation_id = None
            self.history = []
        
        def ask(self, question: str, file_names: list[str] | None = None) -> str:
            """Ask a question and maintain conversation history."""
            try:
                response = self.client.sources.ask(
                    question=question,
                    conversation_id=self.conversation_id,
                    file_names=file_names
                )
                
                # Update conversation ID
                self.conversation_id = response.conversation_id
                
                # Store in history
                self.history.append({
                    "question": question,
                    "answer": response.answer
                })
                
                return response.answer
                
            except graphor.APIStatusError as e:
                return f"Error: {e}"
        
        def reset(self):
            """Start a new conversation."""
            self.conversation_id = None
            self.history = []
        
        def get_history(self) -> list[dict]:
            """Get conversation history."""
            return self.history.copy()

    # Usage
    chatbot = DocumentChatbot()

    # Have a conversation
    print(chatbot.ask("What products are available?"))
    print(chatbot.ask("Tell me more about the first one"))
    print(chatbot.ask("What's its price?"))

    # View history
    for entry in chatbot.get_history():
        print(f"Q: {entry['question']}")
        print(f"A: {entry['answer'][:100]}...")
        print()

    # Reset for a new topic
    chatbot.reset()
    print(chatbot.ask("What are the shipping options?"))
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    interface ChatEntry {
      question: string;
      answer: string;
    }

    class DocumentChatbot {
      private client: Graphor;
      private conversationId: string | null = null;
      private history: ChatEntry[] = [];

      constructor(apiKey?: string) {
        this.client = apiKey ? new Graphor({ apiKey }) : new Graphor();
      }

      async ask(question: string, fileNames?: string[]): Promise<string> {
        try {
          const response = await this.client.sources.ask({
            question,
            conversation_id: this.conversationId ?? undefined,
            file_names: fileNames,
          });

          // Update conversation ID
          this.conversationId = response.conversation_id ?? null;

          // Store in history
          this.history.push({ question, answer: response.answer });

          return response.answer;
        } catch (err) {
          if (err instanceof Graphor.APIError) {
            return `Error: ${err.message}`;
          }
          throw err;
        }
      }

      reset(): void {
        this.conversationId = null;
        this.history = [];
      }

      getHistory(): ChatEntry[] {
        return [...this.history];
      }
    }

    // Usage
    const chatbot = new DocumentChatbot();

    // Have a conversation
    console.log(await chatbot.ask('What products are available?'));
    console.log(await chatbot.ask('Tell me more about the first one'));
    console.log(await chatbot.ask("What's its price?"));

    // View history
    for (const entry of chatbot.getHistory()) {
      console.log(`Q: ${entry.question}`);
      console.log(`A: ${entry.answer.slice(0, 100)}...`);
      console.log();
    }

    // Reset for a new topic
    chatbot.reset();
    console.log(await chatbot.ask('What are the shipping options?'));
    ```
  </Tab>
</Tabs>

### Multi-Document Q\&A

Ask questions across multiple documents:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor

    client = Graphor()

    def compare_documents(file_names: list[str], question: str) -> str:
        """Ask a comparative question across multiple documents."""
        response = client.sources.ask(
            question=question,
            file_names=file_names
        )
        return response.answer

    # Compare financial reports
    answer = compare_documents(
        file_names=["report-2023.pdf", "report-2024.pdf"],
        question="How did revenue change between 2023 and 2024?"
    )
    print(answer)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    async function compareDocuments(fileNames: string[], question: string): Promise<string> {
      const response = await client.sources.ask({
        question,
        file_names: fileNames,
      });
      return response.answer;
    }

    // Compare financial reports
    const answer = await compareDocuments(
      ['report-2023.pdf', 'report-2024.pdf'],
      'How did revenue change between 2023 and 2024?',
    );
    console.log(answer);
    ```
  </Tab>
</Tabs>

### Structured Data Extraction Pipeline

Extract structured data from multiple documents:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor
    import graphor
    from typing import Any

    client = Graphor()

    def extract_structured_data(
        file_names: list[str],
        question: str,
        schema: dict
    ) -> list[dict[str, Any]]:
        """Extract structured data from multiple documents."""
        results = []
        
        for file_name in file_names:
            try:
                response = client.sources.ask(
                    question=question,
                    file_names=[file_name],
                    output_schema=schema
                )
                
                if response.structured_output:
                    results.append({
                        "file": file_name,
                        "data": response.structured_output,
                        "success": True
                    })
                else:
                    results.append({
                        "file": file_name,
                        "data": None,
                        "success": False,
                        "error": "No structured output returned"
                    })
                    
            except graphor.APIStatusError as e:
                results.append({
                    "file": file_name,
                    "data": None,
                    "success": False,
                    "error": str(e)
                })
        
        return results

    # Extract invoice data from multiple invoices
    invoice_schema = {
        "type": "object",
        "properties": {
            "invoice_number": {"type": ["string", "null"]},
            "vendor": {"type": ["string", "null"]},
            "total": {"type": ["number", "null"]},
            "date": {"type": ["string", "null"]}
        }
    }

    invoices = ["invoice1.pdf", "invoice2.pdf", "invoice3.pdf"]
    results = extract_structured_data(
        file_names=invoices,
        question="Extract the invoice number, vendor name, total amount, and date.",
        schema=invoice_schema
    )

    for result in results:
        if result["success"]:
            data = result["data"]
            print(f"OK - {result['file']}: #{data.get('invoice_number')} - ${data.get('total')}")
        else:
            print(f"FAIL - {result['file']}: {result['error']}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    interface ExtractionResult {
      file: string;
      data: Record<string, unknown> | null;
      success: boolean;
      error?: string;
    }

    async function extractStructuredData(
      fileNames: string[],
      question: string,
      schema: Record<string, unknown>,
    ): Promise<ExtractionResult[]> {
      const results: ExtractionResult[] = [];

      for (const fileName of fileNames) {
        try {
          const response = await client.sources.ask({
            question,
            file_names: [fileName],
            output_schema: schema,
          });

          if (response.structured_output) {
            results.push({
              file: fileName,
              data: response.structured_output as Record<string, unknown>,
              success: true,
            });
          } else {
            results.push({
              file: fileName,
              data: null,
              success: false,
              error: 'No structured output returned',
            });
          }
        } catch (err) {
          results.push({
            file: fileName,
            data: null,
            success: false,
            error: err instanceof Graphor.APIError ? err.message : String(err),
          });
        }
      }

      return results;
    }

    // Extract invoice data from multiple invoices
    const invoiceSchema = {
      type: 'object',
      properties: {
        invoice_number: { type: ['string', 'null'] },
        vendor: { type: ['string', 'null'] },
        total: { type: ['number', 'null'] },
        date: { type: ['string', 'null'] },
      },
    };

    const invoices = ['invoice1.pdf', 'invoice2.pdf', 'invoice3.pdf'];
    const results = await extractStructuredData(
      invoices,
      'Extract the invoice number, vendor name, total amount, and date.',
      invoiceSchema,
    );

    for (const result of results) {
      if (result.success && result.data) {
        console.log(`OK - ${result.file}: #${result.data.invoice_number} - $${result.data.total}`);
      } else {
        console.log(`FAIL - ${result.file}: ${result.error}`);
      }
    }
    ```
  </Tab>
</Tabs>

### Parallel Questions

Ask multiple questions in parallel:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import asyncio
    from graphor import AsyncGraphor

    async def ask_parallel_questions(questions: list[str]):
        """Ask multiple questions in parallel."""
        client = AsyncGraphor()
        
        tasks = [
            client.sources.ask(question=q)
            for q in questions
        ]
        
        responses = await asyncio.gather(*tasks, return_exceptions=True)
        
        results = []
        for question, response in zip(questions, responses):
            if isinstance(response, Exception):
                results.append({"question": question, "error": str(response)})
            else:
                results.append({"question": question, "answer": response.answer})
        
        return results

    # Usage
    questions = [
        "What is the total revenue?",
        "Who are the main competitors?",
        "What are the key risks?"
    ]

    results = asyncio.run(ask_parallel_questions(questions))

    for result in results:
        print(f"Q: {result['question']}")
        if "answer" in result:
            print(f"A: {result['answer'][:200]}...")
        else:
            print(f"Error: {result['error']}")
        print()
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';

    const client = new Graphor();

    async function askParallelQuestions(questions: string[]) {
      const promises = questions.map((question) =>
        client.sources
          .ask({ question })
          .then((response) => ({ question, answer: response.answer }))
          .catch((err) => ({
            question,
            error: err instanceof Graphor.APIError ? err.message : String(err),
          })),
      );

      return Promise.all(promises);
    }

    // Usage
    const questions = [
      'What is the total revenue?',
      'Who are the main competitors?',
      'What are the key risks?',
    ];

    const results = await askParallelQuestions(questions);

    for (const result of results) {
      console.log(`Q: ${result.question}`);
      if ('answer' in result) {
        console.log(`A: ${result.answer.slice(0, 200)}...`);
      } else {
        console.log(`Error: ${result.error}`);
      }
      console.log();
    }
    ```
  </Tab>
</Tabs>

### Interactive Q\&A Session

Build an interactive command-line Q\&A:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from graphor import Graphor
    import graphor

    def interactive_qa():
        """Interactive Q&A session with documents."""
        client = Graphor()
        conversation_id = None
        
        print("Document Q&A Session")
        print("Type 'quit' to exit, 'reset' to start a new conversation")
        print("-" * 50)
        
        while True:
            question = input("\nYou: ").strip()
            
            if question.lower() == "quit":
                print("Goodbye!")
                break
            
            if question.lower() == "reset":
                conversation_id = None
                print("Conversation reset.")
                continue
            
            if not question:
                continue
            
            try:
                response = client.sources.ask(
                    question=question,
                    conversation_id=conversation_id
                )
                
                conversation_id = response.conversation_id
                print(f"\nAssistant: {response.answer}")
                
            except graphor.APIStatusError as e:
                print(f"\nError: {e}")

    # Run interactive session
    # interactive_qa()
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import Graphor from 'graphor';
    import readline from 'readline';

    async function interactiveQA() {
      const client = new Graphor();
      let conversationId: string | null = null;

      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
      });

      const prompt = (query: string): Promise<string> =>
        new Promise((resolve) => rl.question(query, resolve));

      console.log('Document Q&A Session');
      console.log("Type 'quit' to exit, 'reset' to start a new conversation");
      console.log('-'.repeat(50));

      while (true) {
        const question = (await prompt('\nYou: ')).trim();

        if (question.toLowerCase() === 'quit') {
          console.log('Goodbye!');
          rl.close();
          break;
        }

        if (question.toLowerCase() === 'reset') {
          conversationId = null;
          console.log('Conversation reset.');
          continue;
        }

        if (!question) continue;

        try {
          const response = await client.sources.ask({
            question,
            conversation_id: conversationId ?? undefined,
          });

          conversationId = response.conversation_id ?? null;
          console.log(`\nAssistant: ${response.answer}`);
        } catch (err) {
          if (err instanceof Graphor.APIError) {
            console.log(`\nError: ${err.message}`);
          } else {
            throw err;
          }
        }
      }
    }

    // Run interactive session
    // await interactiveQA();
    ```
  </Tab>
</Tabs>

## Output Schema Guidelines

When using `output_schema`, follow these guidelines:

### Supported Schema Features

* Basic types: `string`, `number`, `integer`, `boolean`, `null`
* Objects with `properties`
* Arrays with `items`
* Union with `null` only: `["string", "null"]`

### Unsupported Features

* `oneOf`, `anyOf`, `allOf`
* `$ref` references
* Complex unions beyond `null`

### Schema Examples

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # Simple object
    person_schema = {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": ["integer", "null"]},
            "email": {"type": ["string", "null"]}
        }
    }

    # Array of objects
    items_schema = {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "item": {"type": "string"},
                "quantity": {"type": "integer"},
                "price": {"type": "number"}
            }
        }
    }

    # Nested objects
    order_schema = {
        "type": "object",
        "properties": {
            "order_id": {"type": "string"},
            "customer": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "address": {"type": ["string", "null"]}
                }
            },
            "total": {"type": "number"}
        }
    }
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Simple object
    const personSchema = {
      type: 'object',
      properties: {
        name: { type: 'string' },
        age: { type: ['integer', 'null'] },
        email: { type: ['string', 'null'] },
      },
    };

    // Array of objects
    const itemsSchema = {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          item: { type: 'string' },
          quantity: { type: 'integer' },
          price: { type: 'number' },
        },
      },
    };

    // Nested objects
    const orderSchema = {
      type: 'object',
      properties: {
        order_id: { type: 'string' },
        customer: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            address: { type: ['string', 'null'] },
          },
        },
        total: { type: 'number' },
      },
    };
    ```
  </Tab>
</Tabs>

## Error Reference

| Error Type                 | Status Code | Description                                                    |
| -------------------------- | ----------- | -------------------------------------------------------------- |
| `BadRequestError`          | 400         | Invalid parameters or request format                           |
| `AuthenticationError`      | 401         | Invalid or missing API key                                     |
| `NotFoundError`            | 404         | Specified file not found                                       |
| `UnprocessableEntityError` | 422         | Invalid `output_schema` or structured output validation failed |
| `RateLimitError`           | 429         | Too many requests, please retry after waiting                  |
| `InternalServerError`      | ≥500        | Server-side error                                              |
| `APIConnectionError`       | N/A         | Network connectivity issues                                    |
| `APITimeoutError`          | N/A         | Request timed out                                              |

## Best Practices

1. **Use conversation memory** — Pass `conversation_id` for follow-up questions to maintain context

2. **Be specific** — Clear, specific questions get better answers

3. **Scope when needed** — Use `file_ids` or `file_names` to focus on specific documents for faster, more accurate responses

4. **Use structured output for integration** — Provide `output_schema` to get JSON you can reliably parse in code

5. **Reset when changing topics** — Set `reset=True` when switching to unrelated questions

6. **Lazy-load citation images** — Keep `include_citation_images=False` (the default) and call [`get_page_screenshot`](/sdk/sources/page-screenshot) on demand. Inlining base64 only makes sense for low-citation, low-frequency requests — for typical chat UIs it bloats the payload by hundreds of KB per cited page.

7. **Parse `citations`, not the markup** — Use the structured `citations` array. The inline `[N](file_id|pX|...)` markup is hidden by default and is an implementation detail that may change.

8. **Handle errors gracefully** — Implement proper error handling for production applications

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # Good: Specific question with context
    response = client.sources.ask(
        question="What was the total revenue for Q4 2024 compared to Q4 2023?",
        file_names=["annual-report-2024.pdf"]
    )

    # Good: Follow-up with conversation memory
    follow_up = client.sources.ask(
        question="What were the main drivers of this change?",
        conversation_id=response.conversation_id
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Good: Specific question with context
    const response = await client.sources.ask({
      question: 'What was the total revenue for Q4 2024 compared to Q4 2023?',
      file_names: ['annual-report-2024.pdf'],
    });

    // Good: Follow-up with conversation memory
    const followUp = await client.sources.ask({
      question: 'What were the main drivers of this change?',
      conversation_id: response.conversation_id,
    });
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Get Page Screenshot" icon="image" href="/sdk/sources/page-screenshot">
    Lazy-load citation page previews on demand
  </Card>

  <Card title="Document Chat Guide" icon="comments" href="/guides/document-chat">
    Learn best practices for chatting with your documents
  </Card>

  <Card title="Extract API" icon="file-export" href="/sdk/extract">
    Extract structured data from documents
  </Card>

  <Card title="Upload Sources" icon="arrow-up-from-bracket" href="/sdk/sources/upload">
    Upload documents to chat with
  </Card>

  <Card title="Prebuilt RAG" icon="magnifying-glass" href="/sdk/prebuilt-rag">
    Retrieve relevant chunks from your documents
  </Card>
</CardGroup>
