@teaflask/assistant 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ import { TeaflaskAssistant } from "@teaflask/assistant";
63
63
 
64
64
  ## What your agent can do
65
65
 
66
- - **Take actions.** It calls your API on the user's behalf, with the user's own session. You set every action to Allow, Ask or Deny.
66
+ - **Take actions.** It calls your API on the user's behalf, with the user's own session. You set every action to Allow, Ask or Deny. On a [GraphQL](https://docs.teaflask.com/adapters#graphql) API, it runs them through your app's own client.
67
67
  - **Follow your workflows.** Write a procedure once, like how refunds work or when to escalate, and the agent follows it.
68
68
  - **Guide users around your app.** It takes people to the right page and highlights what to click.
69
69
  - **Fill in forms.** It fills the fields; the user reviews and submits.
@@ -0,0 +1,43 @@
1
+ import { prettyPrintParameters } from "../pretty-print.js";
2
+ import { CARD_BLOCK_CLASS } from "./parts-classes.js";
3
+ import { el, factRows, recordOf, scrollRegion } from "./view-dom.js";
4
+ const VALUE_PANE_CLASS = "tf:m-0 tf:whitespace-pre-wrap tf:font-mono tf:text-xs tf:text-tf-card-foreground";
5
+ export function valueRows(value) {
6
+ const record = recordOf(value);
7
+ if (record !== null) {
8
+ return scrollRegion([
9
+ factRows(Object.entries(record).map(([key, entry]) => [key, _valueOf(entry)])),
10
+ ]);
11
+ }
12
+ return valueBlock(value);
13
+ }
14
+ export function valueBlock(value) {
15
+ const wrapper = el("div", CARD_BLOCK_CLASS);
16
+ wrapper.dataset.tfActionValueBlock = "";
17
+ wrapper.append(el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value)));
18
+ return scrollRegion([wrapper]);
19
+ }
20
+ function _valueOf(value) {
21
+ if (typeof value === "string") {
22
+ return value === "" ? "—" : value;
23
+ }
24
+ if (typeof value === "number" ||
25
+ typeof value === "boolean" ||
26
+ typeof value === "bigint") {
27
+ return String(value);
28
+ }
29
+ if (value === null || value === undefined) {
30
+ return "—";
31
+ }
32
+ return el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value));
33
+ }
34
+ export function statusBadgeOf(status) {
35
+ const badge = el("span", status >= 400
36
+ ? "tf:font-medium tf:text-tf-destructive"
37
+ : "tf:text-tf-muted-foreground", `HTTP ${String(status)}`);
38
+ badge.dataset.tfActionStatus = String(status);
39
+ return badge;
40
+ }
41
+ export function isEmptyBody(body) {
42
+ return body === undefined || body === null || body === "";
43
+ }
@@ -1,12 +1,10 @@
1
- import { prettyPrintParameters } from "../pretty-print.js";
2
- import { CARD_BLOCK_CLASS } from "./parts-classes.js";
3
- import { actionEnvelopeOf, actionFailureOf, captionRow, card, el, factRows, note, recordOf, scrollRegion, settledOutcomeNotesOf, settledWithResult, statelessToolView, } from "./view-dom.js";
1
+ import { isEmptyBody, statusBadgeOf, valueRows } from "./action-values.js";
2
+ import { actionEnvelopeOf, actionFailureOf, captionRow, card, el, factRows, note, recordOf, settledOutcomeNotesOf, settledWithResult, statelessToolView, } from "./view-dom.js";
4
3
  const REQUEST_SECTIONS = [
5
4
  ["path_params", "Path parameters"],
6
5
  ["query", "Query"],
7
6
  ["body", "Body"],
8
7
  ];
9
- const VALUE_PANE_CLASS = "tf:m-0 tf:whitespace-pre-wrap tf:font-mono tf:text-xs tf:text-tf-card-foreground";
10
8
  export const actionToolView = statelessToolView(_renderAction);
11
9
  function _renderAction({ call }) {
12
10
  const root = el("div", "tf:flex tf:flex-col tf:gap-2");
@@ -21,7 +19,7 @@ function _renderAction({ call }) {
21
19
  if (record !== null && Object.keys(record).length === 0) {
22
20
  continue;
23
21
  }
24
- sections.push(captionRow(label), _valueRows(value));
22
+ sections.push(captionRow(label), valueRows(value));
25
23
  }
26
24
  if (sections.length > 0) {
27
25
  root.append(card(sections));
@@ -52,32 +50,6 @@ function _appendFieldProblems(root, call) {
52
50
  }
53
51
  root.append(card(parts));
54
52
  }
55
- function _valueRows(value) {
56
- const record = recordOf(value);
57
- if (record !== null) {
58
- return scrollRegion([
59
- factRows(Object.entries(record).map(([key, entry]) => [key, _valueOf(entry)])),
60
- ]);
61
- }
62
- const wrapper = el("div", CARD_BLOCK_CLASS);
63
- wrapper.dataset.tfActionValueBlock = "";
64
- wrapper.append(el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value)));
65
- return scrollRegion([wrapper]);
66
- }
67
- function _valueOf(value) {
68
- if (typeof value === "string") {
69
- return value === "" ? "—" : value;
70
- }
71
- if (typeof value === "number" ||
72
- typeof value === "boolean" ||
73
- typeof value === "bigint") {
74
- return String(value);
75
- }
76
- if (value === null || value === undefined) {
77
- return "—";
78
- }
79
- return el("pre", VALUE_PANE_CLASS, prettyPrintParameters(value));
80
- }
81
53
  function _appendResponse(root, call) {
82
54
  if (call.resultText === undefined) {
83
55
  return;
@@ -88,24 +60,16 @@ function _appendResponse(root, call) {
88
60
  return;
89
61
  }
90
62
  const rows = [];
91
- let status;
92
- if (envelope.status !== null) {
93
- status = el("span", envelope.status >= 400
94
- ? "tf:font-medium tf:text-tf-destructive"
95
- : "tf:text-tf-muted-foreground", `HTTP ${String(envelope.status)}`);
96
- status.dataset.tfActionStatus = String(envelope.status);
97
- }
63
+ const status = envelope.status === null ? undefined : statusBadgeOf(envelope.status);
98
64
  rows.push(captionRow(envelope.truncated ? "Response (truncated)" : "Response", status));
99
65
  if (envelope.truncated) {
100
66
  rows.push(captionRow("The response body was truncated by the client to fit the size cap."));
101
67
  }
102
- if (envelope.body === undefined ||
103
- envelope.body === null ||
104
- envelope.body === "") {
68
+ if (isEmptyBody(envelope.body)) {
105
69
  rows.push(captionRow("Empty response body."));
106
70
  }
107
71
  else {
108
- rows.push(_valueRows(envelope.body));
72
+ rows.push(valueRows(envelope.body));
109
73
  }
110
74
  root.append(card(rows));
111
75
  }
@@ -0,0 +1,95 @@
1
+ import { prettyPrintParameters } from "../pretty-print.js";
2
+ import { isEmptyBody, statusBadgeOf, valueBlock, valueRows, } from "./action-values.js";
3
+ import { actionEnvelopeOf, captionRow, card, el, factRows, note, recordOf, settledOutcomeNotesOf, scrollRegion, settledWithResult, statelessToolView, } from "./view-dom.js";
4
+ export const graphqlActionToolView = statelessToolView(_renderGraphQLAction);
5
+ function _renderGraphQLAction({ call }) {
6
+ const root = el("div", "tf:flex tf:flex-col tf:gap-2");
7
+ root.dataset.tfGraphqlActionView = "";
8
+ const variables = recordOf(call.args.variables);
9
+ if (variables !== null && Object.keys(variables).length > 0) {
10
+ root.append(card([captionRow("Variables"), _valuesOf(variables)]));
11
+ }
12
+ root.append(...settledOutcomeNotesOf(call));
13
+ if (settledWithResult(call)) {
14
+ _appendResponse(root, call);
15
+ }
16
+ return root;
17
+ }
18
+ function _appendResponse(root, call) {
19
+ const envelope = actionEnvelopeOf(call.resultText);
20
+ if (envelope === null) {
21
+ root.append(note("The response couldn't be read as an action result and isn't rendered."));
22
+ return;
23
+ }
24
+ const response = recordOf(envelope.body);
25
+ const errors = Array.isArray(response?.errors) ? response.errors : [];
26
+ const graphqlShaped = _readsAsGraphQLResponse(response, errors);
27
+ const rows = [
28
+ captionRow(envelope.truncated ? "Response (truncated)" : "Response", _statusOf(envelope.status)),
29
+ ];
30
+ if (envelope.truncated) {
31
+ rows.push(captionRow(_cutSentenceOf(graphqlShaped, errors, response)));
32
+ }
33
+ if (response === null || !graphqlShaped) {
34
+ rows.push(isEmptyBody(envelope.body)
35
+ ? captionRow("Empty response body.")
36
+ : valueRows(envelope.body));
37
+ root.append(card(rows));
38
+ return;
39
+ }
40
+ if (errors.length > 0) {
41
+ rows.push(_errorsCaption(errors.length), _errorRows(errors));
42
+ }
43
+ const hasData = response.data !== null && response.data !== undefined;
44
+ rows.push(hasData ? captionRow("Data") : captionRow("Data", "none"));
45
+ if (hasData) {
46
+ rows.push(_valuesOf(_withoutCutMarker(response.data)));
47
+ }
48
+ root.append(card(rows));
49
+ }
50
+ function _readsAsGraphQLResponse(response, errors) {
51
+ return response !== null && (errors.length > 0 || "data" in response);
52
+ }
53
+ function _statusOf(status) {
54
+ return status === null || status === 200 ? undefined : statusBadgeOf(status);
55
+ }
56
+ function _cutSentenceOf(graphqlShaped, errors, response) {
57
+ if (!graphqlShaped || errors.length === 0) {
58
+ return "Too large to show in full: the response is shortened.";
59
+ }
60
+ return typeof response?.data === "string"
61
+ ? "Too large to show in full: the data is shortened and any other response fields dropped; the errors are complete."
62
+ : "Too large to show in full: other response fields are dropped; the data and errors are complete.";
63
+ }
64
+ function _errorsCaption(count) {
65
+ const row = captionRow("");
66
+ row.replaceChildren(el("span", "tf:font-medium tf:text-tf-destructive", `${String(count)} ${count === 1 ? "error" : "errors"}`));
67
+ return row;
68
+ }
69
+ function _errorRows(errors) {
70
+ const rows = factRows(errors.map((error) => {
71
+ const record = recordOf(error);
72
+ const path = Array.isArray(record?.path)
73
+ ? record.path.map(String).join(".")
74
+ : "";
75
+ const message = typeof record?.message === "string" && record.message !== ""
76
+ ? record.message
77
+ : prettyPrintParameters(error);
78
+ return [
79
+ path === "" ? "—" : path.replaceAll(".", ".\u200b"),
80
+ message,
81
+ ];
82
+ }));
83
+ rows.dataset.tfGraphqlErrors = String(errors.length);
84
+ return scrollRegion([rows]);
85
+ }
86
+ function _valuesOf(value) {
87
+ const record = recordOf(value);
88
+ const flat = record !== null &&
89
+ Object.values(record).every((entry) => entry === null || typeof entry !== "object");
90
+ return flat ? valueRows(value) : valueBlock(value);
91
+ }
92
+ const CUT_MARKER = /… \[truncated: [^\]]*\]$/;
93
+ function _withoutCutMarker(data) {
94
+ return typeof data === "string" ? data.replace(CUT_MARKER, "…") : data;
95
+ }
@@ -39,9 +39,41 @@ export interface ActionIntent {
39
39
  query: Record<string, unknown>;
40
40
  /**
41
41
  * The JSON request body; absent when the action sends none. The cookies and
42
- * headers adapters send it with Content-Type application/json.
42
+ * headers adapters send it with Content-Type application/json. For a
43
+ * GraphQL action it is the standard GraphQL request, {query, operationName,
44
+ * variables}.
43
45
  */
44
46
  body?: unknown;
47
+ /**
48
+ * Present when the action is an operation of your app's GraphQL API, so a
49
+ * request adapter can run it through your own GraphQL client; absent for
50
+ * every other action.
51
+ */
52
+ graphql?: ActionIntentGraphQL;
53
+ }
54
+ /**
55
+ * The GraphQL operation an action runs, one your app's own code defines. Run it
56
+ * through your app's GraphQL client. Looking it up by operationName among your
57
+ * app's own operations sends exactly the request your app sends; parsing
58
+ * document lets a client print it again, which a persisted-query allowlist may
59
+ * refuse.
60
+ */
61
+ export interface ActionIntentGraphQL {
62
+ /** The operation's name in your app's code, such as "RenamePost". */
63
+ operationName: string;
64
+ /** Whether the operation is a "query" or a "mutation". */
65
+ operationType: "query" | "mutation";
66
+ /**
67
+ * The operation's GraphQL document as found in your app's code, with every
68
+ * fragment it uses; a client that sends parsed documents, such as Apollo
69
+ * Client, can parse and run it.
70
+ */
71
+ document: string;
72
+ /**
73
+ * The operation's variables, already validated against the action's
74
+ * variables schema; empty when it takes none.
75
+ */
76
+ variables: Record<string, unknown>;
45
77
  }
46
78
  /**
47
79
  * For APIs authenticated by session cookies. The package fetches baseUrl plus
@@ -88,8 +120,10 @@ export interface RequestActionsAdapter {
88
120
  kind: "request";
89
121
  /**
90
122
  * Performs the request and resolves with your API's response body, which
91
- * the agent receives with status 200. Reject (for example by throwing on a
92
- * non-2xx response) to report the action as failed.
123
+ * the agent receives with status 200. For a GraphQL action, resolve the
124
+ * whole GraphQL response, {data, errors}, even when it carries errors.
125
+ * Reject (for example by throwing on a non-2xx response) to report the
126
+ * action as failed.
93
127
  */
94
128
  execute: (intent: ActionIntent) => Promise<unknown>;
95
129
  }
@@ -99,6 +133,10 @@ export interface RequestActionsAdapter {
99
133
  * actions and tells the user to do them. The response's status and body (parsed
100
134
  * as JSON, otherwise text) go back to the agent; a non-2xx status counts as
101
135
  * your API's answer, not a failure, and bodies over about 16,000 characters are
102
- * truncated.
136
+ * truncated. A GraphQL response that carries errors keeps all of them unless
137
+ * the errors alone exceed the limit: its other top-level fields such as
138
+ * extensions are dropped first, and its data is shortened only if it still does
139
+ * not fit. A GraphQL response without errors is truncated whole, like any other
140
+ * body.
103
141
  */
104
142
  export type ActionsAdapter = CookiesActionsAdapter | HeadersActionsAdapter | RequestActionsAdapter;
@@ -1,4 +1,5 @@
1
1
  import { actionToolView } from "../components/tool-views/action-view.js";
2
+ import { graphqlActionToolView } from "../components/tool-views/graphql-action-view.js";
2
3
  import { commandToolView } from "../components/tool-views/command-view.js";
3
4
  import { docsSearchToolView } from "../components/tool-views/docs-search-view.js";
4
5
  import { fileEditToolView } from "../components/tool-views/file-edit-view.js";
@@ -10,6 +11,7 @@ export function isReservedToolViewKey(key) {
10
11
  export const PACKAGE_TOOL_VIEWS = Object.freeze({
11
12
  "teaflask.questions": { version: 1, view: questionsToolView },
12
13
  "teaflask.action": { version: 1, view: actionToolView },
14
+ "teaflask.graphql-action": { version: 1, view: graphqlActionToolView },
13
15
  "teaflask.command": { version: 1, view: commandToolView },
14
16
  "teaflask.file-edit": { version: 1, view: fileEditToolView },
15
17
  "teaflask.docs-search": { version: 1, view: docsSearchToolView },
@@ -90,6 +90,9 @@ export declare const ApiErrorCode: {
90
90
  readonly ORG_ACTION_PROPOSAL_CHANGED: "ORG_ACTION_PROPOSAL_CHANGED";
91
91
  readonly ORG_ACTION_PROPOSAL_NOT_EDITABLE: "ORG_ACTION_PROPOSAL_NOT_EDITABLE";
92
92
  readonly ORG_ACTION_SCAN_ALREADY_RUNNING: "ORG_ACTION_SCAN_ALREADY_RUNNING";
93
+ readonly ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE: "ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE";
94
+ readonly ORG_ACTION_SPEC_UPLOAD_INVALID: "ORG_ACTION_SPEC_UPLOAD_INVALID";
95
+ readonly ORG_ACTION_SPEC_UPLOAD_TOO_LARGE: "ORG_ACTION_SPEC_UPLOAD_TOO_LARGE";
93
96
  readonly ORG_SITEMAP_PAGE_INVALID: "ORG_SITEMAP_PAGE_INVALID";
94
97
  readonly ORG_SITEMAP_ROUTE_ALREADY_REGISTERED: "ORG_SITEMAP_ROUTE_ALREADY_REGISTERED";
95
98
  readonly ORG_SITEMAP_PAGE_ALREADY_ARCHIVED: "ORG_SITEMAP_PAGE_ALREADY_ARCHIVED";
@@ -141,6 +144,12 @@ export declare const ApiErrorCode: {
141
144
  readonly SSO_REQUIRED_FOR_DOMAIN: "SSO_REQUIRED_FOR_DOMAIN";
142
145
  readonly SSO_CONNECTION_NOT_ACTIVE: "SSO_CONNECTION_NOT_ACTIVE";
143
146
  readonly EDGE_ORIGIN_REQUIRED: "EDGE_ORIGIN_REQUIRED";
147
+ readonly MCP_CONNECTIONS_NOT_AVAILABLE: "MCP_CONNECTIONS_NOT_AVAILABLE";
148
+ readonly MCP_AUTHORIZATION_EXPIRED: "MCP_AUTHORIZATION_EXPIRED";
149
+ readonly MCP_CAPABILITY_ABOVE_ROLE: "MCP_CAPABILITY_ABOVE_ROLE";
150
+ readonly MCP_CAPABILITIES_INVALID: "MCP_CAPABILITIES_INVALID";
151
+ readonly MCP_AUTHORIZATION_SERVER_UNAVAILABLE: "MCP_AUTHORIZATION_SERVER_UNAVAILABLE";
152
+ readonly MCP_CONNECTION_CONFLICT: "MCP_CONNECTION_CONFLICT";
144
153
  readonly DEMO_ERROR: "DEMO_ERROR";
145
154
  readonly HTTP_ERROR: "HTTP_ERROR";
146
155
  readonly INTERNAL_ERROR: "INTERNAL_ERROR";
@@ -89,6 +89,9 @@ export const ApiErrorCode = {
89
89
  ORG_ACTION_PROPOSAL_CHANGED: "ORG_ACTION_PROPOSAL_CHANGED",
90
90
  ORG_ACTION_PROPOSAL_NOT_EDITABLE: "ORG_ACTION_PROPOSAL_NOT_EDITABLE",
91
91
  ORG_ACTION_SCAN_ALREADY_RUNNING: "ORG_ACTION_SCAN_ALREADY_RUNNING",
92
+ ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE: "ORG_ACTION_SPEC_UPLOADS_NOT_AVAILABLE",
93
+ ORG_ACTION_SPEC_UPLOAD_INVALID: "ORG_ACTION_SPEC_UPLOAD_INVALID",
94
+ ORG_ACTION_SPEC_UPLOAD_TOO_LARGE: "ORG_ACTION_SPEC_UPLOAD_TOO_LARGE",
92
95
  ORG_SITEMAP_PAGE_INVALID: "ORG_SITEMAP_PAGE_INVALID",
93
96
  ORG_SITEMAP_ROUTE_ALREADY_REGISTERED: "ORG_SITEMAP_ROUTE_ALREADY_REGISTERED",
94
97
  ORG_SITEMAP_PAGE_ALREADY_ARCHIVED: "ORG_SITEMAP_PAGE_ALREADY_ARCHIVED",
@@ -140,6 +143,12 @@ export const ApiErrorCode = {
140
143
  SSO_REQUIRED_FOR_DOMAIN: "SSO_REQUIRED_FOR_DOMAIN",
141
144
  SSO_CONNECTION_NOT_ACTIVE: "SSO_CONNECTION_NOT_ACTIVE",
142
145
  EDGE_ORIGIN_REQUIRED: "EDGE_ORIGIN_REQUIRED",
146
+ MCP_CONNECTIONS_NOT_AVAILABLE: "MCP_CONNECTIONS_NOT_AVAILABLE",
147
+ MCP_AUTHORIZATION_EXPIRED: "MCP_AUTHORIZATION_EXPIRED",
148
+ MCP_CAPABILITY_ABOVE_ROLE: "MCP_CAPABILITY_ABOVE_ROLE",
149
+ MCP_CAPABILITIES_INVALID: "MCP_CAPABILITIES_INVALID",
150
+ MCP_AUTHORIZATION_SERVER_UNAVAILABLE: "MCP_AUTHORIZATION_SERVER_UNAVAILABLE",
151
+ MCP_CONNECTION_CONFLICT: "MCP_CONNECTION_CONFLICT",
143
152
  DEMO_ERROR: "DEMO_ERROR",
144
153
  HTTP_ERROR: "HTTP_ERROR",
145
154
  INTERNAL_ERROR: "INTERNAL_ERROR",
@@ -1,6 +1,6 @@
1
1
  export { TeaflaskAssistantProvider, type TeaflaskAssistantAnonymousProps, type TeaflaskAssistantIdentityProps, type TeaflaskAssistantProviderBaseProps, type TeaflaskAssistantProviderProps, } from "./components/teaflask-assistant-provider.js";
2
2
  export type { AssistantTheme, AssistantThemeDarkTokens, AssistantThemeMode, AssistantThemeTokens, } from "./appearance/theme.js";
3
- export type { ActionsAdapter, ActionIntent, ActionIntentAction, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
3
+ export type { ActionsAdapter, ActionIntent, ActionIntentAction, ActionIntentGraphQL, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
4
4
  export type { ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, StreamFailedTelemetryEvent, } from "./contract/telemetry.js";
5
5
  export type { AssistantSuggestion, AssistantSuggestionInput, AssistantSuggestionKind, } from "./components/conversation-view.js";
6
6
  export type { IconAdapter, JsonSchema, ToolViewAdapter, ToolViewCall, ToolViewInstance, ToolViewProps, ToolViewRegistration, ToolViewRegistry, } from "./core/tool-view.js";
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { AssistantActivityResult, AssistantActivitySnapshot, } from "./core
9
9
  export type { PendingDecisionGap } from "./core/pending-decision-gap.js";
10
10
  export { resetAssistant } from "./persistence/stored-thread.js";
11
11
  export { ServingApiError } from "./transport/serving-error.js";
12
- export type { ActionIntent, ActionIntentAction, ActionsAdapter, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
12
+ export type { ActionIntent, ActionIntentAction, ActionIntentGraphQL, ActionsAdapter, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
13
13
  export type { ServingErrorCode, ServingErrorCodeOnTheWire, } from "./contract/errors.js";
14
14
  export { ASSISTANT_MESSAGE_MAX_CHARS, type ServingAssistantThread, type TurnStatus, } from "./contract/threads.js";
15
15
  export { activeFoldElapsedOf, settledFoldDurationOf, type BlockTiming, type SegmentDuration, } from "./core/segment-timing.js";
@@ -27,19 +27,96 @@ export function actionIntentOf(wire) {
27
27
  if (wire.body !== null && wire.body !== undefined) {
28
28
  intent.body = wire.body;
29
29
  }
30
+ const graphql = _graphqlOf(wire);
31
+ if (graphql !== null) {
32
+ intent.graphql = graphql;
33
+ }
30
34
  return intent;
31
35
  }
36
+ function _graphqlOf(wire) {
37
+ const descriptor = _recordOf(wire.graphql);
38
+ const body = _recordOf(wire.body);
39
+ if (descriptor === null || body === null) {
40
+ return null;
41
+ }
42
+ const halves = {
43
+ operationName: _nonEmptyStringOf(descriptor.operation_name),
44
+ operationType: descriptor.operation_type,
45
+ document: _nonEmptyStringOf(body.query),
46
+ variables: body.variables === undefined || body.variables === null
47
+ ? {}
48
+ : _recordOf(body.variables),
49
+ };
50
+ if (!_descriptorAgreesWithBody(halves, body.operationName)) {
51
+ return null;
52
+ }
53
+ const { operationName, operationType, document, variables } = halves;
54
+ return { operationName, operationType, document, variables };
55
+ }
56
+ function _descriptorAgreesWithBody(halves, bodyOperationName) {
57
+ return (halves.operationName !== null &&
58
+ halves.operationName === bodyOperationName &&
59
+ (halves.operationType === "query" || halves.operationType === "mutation") &&
60
+ halves.document !== null &&
61
+ halves.variables !== null);
62
+ }
32
63
  export async function performActionIntent(adapter, intent) {
64
+ const land = intent.graphql === undefined ? _landableBodyOf : _landableGraphQLBodyOf;
33
65
  if (adapter.kind === "request") {
34
- return _intentResultOf(200, await adapter.execute(intent));
66
+ return _intentResultOf(200, land(await adapter.execute(intent)));
35
67
  }
36
68
  const response = await fetch(_urlOf(adapter.baseUrl, intent), await _requestInitOf(adapter, intent));
37
- return _resultOf(response);
69
+ return _resultOf(response, land);
38
70
  }
39
- function _intentResultOf(status, rawBody) {
40
- const landed = _landableBodyOf(rawBody);
71
+ function _intentResultOf(status, landed) {
41
72
  return { status, body: landed.body, truncated: landed.truncated };
42
73
  }
74
+ function _landableGraphQLBodyOf(body) {
75
+ const whole = _landableBodyOf(body);
76
+ const errored = whole.truncated ? _erroredResponseOf(body) : null;
77
+ if (errored === null) {
78
+ return whole;
79
+ }
80
+ return (_errorsAndDataWhole(errored) ??
81
+ _errorsWithDataCut(errored) ??
82
+ _wholeCutErrorsFirst(errored));
83
+ }
84
+ function _erroredResponseOf(body) {
85
+ const response = _recordOf(body);
86
+ if (response === null) {
87
+ return null;
88
+ }
89
+ const errors = JSON.parse(JSON.stringify(response.errors ?? null));
90
+ if (!Array.isArray(errors) || errors.length === 0) {
91
+ return null;
92
+ }
93
+ return { errors, data: response.data ?? null, response };
94
+ }
95
+ function _errorsAndDataWhole({ errors, data, }) {
96
+ const kept = { errors, data };
97
+ return doorLengthCeilingOf(JSON.stringify(kept)) <= RESPONSE_BODY_MAX_CHARS
98
+ ? { body: kept, truncated: true }
99
+ : null;
100
+ }
101
+ function _errorsWithDataCut({ errors, data, }) {
102
+ const dataBudget = RESPONSE_BODY_MAX_CHARS -
103
+ doorLengthCeilingOf(JSON.stringify({ errors, data: "" })) -
104
+ doorCostOfText(TRUNCATION_NOTICE);
105
+ if (dataBudget < 0) {
106
+ return null;
107
+ }
108
+ const dataText = JSON.stringify(data);
109
+ const bounded = boundedTextOf(dataText, Math.min(dataBudget, doorCostOfText(dataText) - 1));
110
+ return {
111
+ body: { errors, data: bounded + TRUNCATION_NOTICE },
112
+ truncated: true,
113
+ };
114
+ }
115
+ function _wholeCutErrorsFirst({ errors, response, }) {
116
+ const rest = { ...response };
117
+ delete rest.errors;
118
+ return _landableBodyOf(JSON.stringify({ errors, ...rest }));
119
+ }
43
120
  function _landableBodyOf(body) {
44
121
  if (body === undefined) {
45
122
  return { body: null, truncated: false };
@@ -105,8 +182,8 @@ function _queryStringOf(query) {
105
182
  const encoded = parameters.toString();
106
183
  return encoded === "" ? "" : `?${encoded}`;
107
184
  }
108
- async function _resultOf(response) {
109
- return _intentResultOf(response.status, _jsonOrTextOf(await response.text()));
185
+ async function _resultOf(response, land) {
186
+ return _intentResultOf(response.status, land(_jsonOrTextOf(await response.text())));
110
187
  }
111
188
  function _jsonOrTextOf(text) {
112
189
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teaflask/assistant",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "The AI assistant for your product: drop it into your React app or any page with a script tag.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "author": "Teaflask",