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¶
- The text is self-sufficient.
contentanswers the question completely without any block. An interface that cannot render a block shows less, not nothing. - Skip an unknown
typesilently. Types will be added. Theschemaversion only changes when the meaning of an existing field changes. - 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.
- 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.
- 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.01 → 415.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.