Skip to content

Answer Blocks

An assistant answer is text. Everything that reads better as a table or a chart arrives next to it in a separate blocks array: in the run result, in the message metadata and as the ai.run.blocks delivery event.

Blocks are built from platform responses, not from the model text. Everything else follows from that. The model called get_open_trades, so the conversation is about positions, so a table of positions belongs under the answer — and the rows in it are the ones GetTradesByLogin returned. The model does not retype them and therefore cannot put a price into the volume column. Asking it to draw a markdown table would have been simpler, but twenty positions are four hundred numbers typed again, and one of them would eventually be wrong.

Rules you can rely on

  1. The text is self-sufficient. content answers the question completely without any block. An interface that cannot render a block shows less, not nothing.
  2. Skip an unknown type silently. Types will be added. The schema version only changes when the meaning of an existing field changes.
  3. Values are raw, the format is a hint. Money, lots, time and instrument precision are formatted by the interface — it has the viewer's locale and time zone.
  4. No HTML, no SVG. The interface draws with its own library. The answer text is model-generated, and letting it carry markup would be an XSS hole in the chat.
  5. At most three blocks per answer and 50 rows per block ([runtime] maxBlocks, maxBlockRows). Truncation is always stated as a number.

table

{
  "type": "table",
  "schema": 1,
  "source": "get_open_trades",
  "title": "Open positions",
  "titleKey": "ai.blocks.openPositions",
  "columns": [
    { "key": "symbol", "label": "Symbol", "labelKey": "ai.columns.symbol", "format": "symbol" },
    { "key": "cmd", "label": "Side", "labelKey": "ai.columns.side", "format": "tradeType" },
    { "key": "volume", "label": "Volume", "labelKey": "ai.columns.volume", "format": "lots" },
    { "key": "profit", "label": "P&L", "labelKey": "ai.columns.profit", "format": "money" }
  ],
  "rows": [
    { "symbol": "EURUSD", "cmd": 0, "volume": 1, "profit": -18.4 },
    { "symbol": "XAUUSD", "cmd": 1, "volume": 0.5, "profit": 62.3 }
  ],
  "truncated": { "shown": 50, "total": 218 }
}

columns defines both the set and the order of columns — the interface should not infer either. A column that is empty in every row never reaches the interface: an empty column reads as lost data. truncated appears only when there are more rows than shown; print "first 50 of 218" under the table, otherwise it contradicts the number in the text.

title is an English default; titleKey and labelKey are stable keys for your own translations. The assistant answers in the language of the question, so the block captions should follow the interface locale, not the answer.

metrics

{
  "type": "metrics",
  "schema": 1,
  "source": "get_account_balance",
  "title": "Account state",
  "titleKey": "ai.blocks.accountState",
  "items": [
    { "key": "balance", "label": "Balance", "labelKey": "ai.metrics.balance", "value": 5200.45, "format": "money", "currency": "USD" },
    { "key": "margin_level", "label": "Margin level", "labelKey": "ai.metrics.marginLevel", "value": 415.01, "format": "percent" }
  ]
}

currency comes from the same platform response as the amount, and only for money: accounts of different groups can be in different currencies, and a "USD" caption under a figure in another currency is a mistake the viewer cannot notice. When currency is absent, it is the account currency the interface already knows.

series

{
  "type": "series",
  "schema": 1,
  "source": "get_trades_history",
  "title": "Cumulative result",
  "titleKey": "ai.blocks.cumulativeResult",
  "x": { "key": "close_time", "label": "Close time", "labelKey": "ai.columns.closeTime", "format": "datetime" },
  "series": [
    { "key": "cumulative", "label": "Cumulative P&L", "labelKey": "ai.series.cumulativePnl", "format": "money" }
  ],
  "points": [
    { "close_time": 1788486400, "cumulative": 43.5 },
    { "close_time": 1788586400, "cumulative": -10.7 },
    { "close_time": 1788686400, "cumulative": 15.3 }
  ]
}

Points are sorted by ascending x and carry only the keys from x.key and series[].key. The running total is calculated by the module, not by the interface — otherwise the chart in the terminal and the one in the backoffice would drift apart on rounding. A series with a single point is not sent at all.

Value formats

A closed list, so it can be handled with a switch:

format Value How to render
text string as is
number number by locale
money number amount + currency (currency or the account currency), signed for P&L
percent number 415.01415.01 %
lots number volume in lots, two decimals
price number price; decimals by instrument precision
time / datetime unix seconds in the viewer's time zone
login number account login, no thousands separator
symbol string ticker, monospace
boolean 0/1 yes/no
enabled 0/1 enabled/disabled
tradeType 0/1/… 0 buy, 1 sell, higher values are pending orders
tradeState number deal state

Which tool produces which block

Tool Blocks
get_account_balance metrics — balance, equity, credit, margin, free margin, level
get_open_trades table — open positions
get_trades_history table — closed deals, series — cumulative result
get_margin_calls table — accounts in margin call
find_accounts table — trading accounts
find_customers table — customers
find_managers table — staff
list_groups table — trading groups

get_customer, get_customer_contacts, get_account_info and get_symbol produce no blocks: those are single cards, and text beats a one-row table.

Rendering

The array order is the display order, under the answer text. ai.run.blocks arrives after the last ai.run.delta and before ai.run.completed, so the interface draws the table under an answer that is already on screen instead of waiting for it to start printing.

Blocks are stored in the message metadata, so GET ai/chat/conversation returns them with the history after a page reload. A page of 50 messages with tables is noticeably heavier than a plain one — ask for a smaller limit when rendering a conversation preview.

When there are no blocks the array is empty. That is normal: "what is a margin call" has nothing to draw.

Client chat

Blocks work the same in the trading terminal, and clients need them more than managers: "show my positions" is a table, "how did I trade this month" is a chart. A client only ever receives blocks about their own account — the same session scope that applies to the tools applies to the data inside a block, because it is literally the tool response.