Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 55 additions & 55 deletions servers/genesys-cloud-architect-mcp.js

Large diffs are not rendered by default.

89 changes: 76 additions & 13 deletions skills/interpret-flow-ir/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: interpret-flow-ir
description: Use when interpreting the JSON returned by the flow_ir, flow_action, search_in_flow, find_flow_execution or flow_execution_data tools, or when the user asks about a deployed Genesys Cloud Architect flow, including one named only by flow name (resolve with find_flow first) or a queue named only by queue name (resolve with find_queue first). Covers structural questions ("analyse this flow", "trace the path", "find dead logic", "does it loop"), semantic questions ("what does this decision check", "what does pressing 2 do"), search questions ("which actions reference this queue", "which use Flow.DNIS"), prompt questions ("what does the caller hear here", "what does Prompt.Welcome say"; resolve with get_prompts), and execution questions about a real conversation or test run ("what did this conversation do", "why did the customer loop", "did the flow error", "is it working"). Use it to answer control-flow questions from the IR rather than the raw configuration JSON, to find the actions worth inspecting, to fetch per-action settings the IR omits, and to read one run's execution data against the IR.
description: Use when interpreting the JSON returned by the flow_ir, flow_action, search_in_flow, find_flow_execution, flow_execution_data or flow_versions tools, or when the user asks about a deployed Genesys Cloud Architect flow, including one named only by flow name (resolve with find_flow first) or a queue named only by queue name (resolve with find_queue first). Covers structural questions ("analyse this flow", "trace the path", "find dead logic", "does it loop"), semantic questions ("what does this decision check", "what does pressing 2 do"), search questions ("which actions reference this queue", "which use Flow.DNIS"), prompt questions ("what does the caller hear here", "what does Prompt.Welcome say"; resolve with get_prompts), execution questions about a real conversation or test run ("what did this conversation do", "why did the customer loop", "did the flow error", "is it working"), and publish-history questions ("when was this flow last published", "which version was live at 09:30", "what changed on the 5th"; resolve with flow_versions). Use it to answer control-flow questions from the IR rather than the raw configuration JSON, to find the actions worth inspecting, to fetch per-action settings the IR omits, and to read one run's execution data against the IR.
---

# Interpreting Flow IRs
Expand All @@ -24,6 +24,18 @@ The static tools take a flow id, not a flow name. When only the name is known (e
"analyse Book_Payment"), resolve it first with `find_flow`, which searches flow
names and returns each match's id, name, type, and published version.

Every tool that reads a flow's configuration (`flow_ir`, `flow_action`,
`search_in_flow`, and `get_prompts` given a `flowId`) reads the **latest**
configuration unless given a `flowVersion`, which pins the read to one version:
the `version` that `flow_versions` lists, or the `flowVersion` that
`find_flow_execution` reports. Each response says which it read, as
`configuration: { latest: true }` or `{ flowVersion }`, with `deleted: true`
added when the flow has since been deleted but its configuration was still
readable. When the question is about a past publish or a particular run, pass
the same `flowVersion` to every call in the chain: a version never changes once
created, so the ids join consistently and there is no redeploy race between
calls.

Likewise, when the user names a *queue* (e.g. "which actions transfer to Sales"),
resolve it first with `find_queue`, which searches queue names and returns each
match's id, exact name, division and in-queue flows. Give `search_in_flow` the
Expand All @@ -32,9 +44,10 @@ spelled in the flow.

## Tool output shape

On success the tool returns compact JSON: `{ flowId, ir, warnings }`. Failures
(flow not found, unparseable configuration, unknown or ambiguous `task` value)
arrive as plain-text tool errors, so any JSON response is a successful parse.
On success the tool returns compact JSON: `{ flowId, configuration, ir,
warnings }`. Failures (flow or version not found, unparseable configuration,
unknown or ambiguous `task` value) arrive as plain-text tool errors, so any
JSON response is a successful parse.
`warnings` is always present; read it before making claims, because each
warning scopes what can be asserted (see "Warnings").

Expand Down Expand Up @@ -137,7 +150,7 @@ calls of up to 50. Exceeding the cap is rejected before any lookup happens, so
size the batches up front rather than discovering the limit mid-analysis.

**The envelope.**
`{ flowId, found: [{ actionId, action, taskId, taskName, menuChoice? }], notFound, notes? }`
`{ flowId, configuration, found: [{ actionId, action, taskId, taskName, menuChoice? }], notFound, notes? }`

- `actionId` echoes the id you asked for and is the grouping key. An action id
occurring more than once in a flow yields **several `found` entries**, one per
Expand Down Expand Up @@ -178,10 +191,11 @@ Its `digit` and `name` are the choice's *presentation* — the keypress and the
spoken or displayed label that select that action. This is what answers "what
does pressing 2 do".

**`notFound` and staleness.** An id in `notFound` is absent from the flow's
*latest* configuration. Because `flow_ir` and `flow_action` are separate fetches,
the flow may have been redeployed between them. Re-run `flow_ir` and re-join
before concluding that an action was deleted.
**`notFound` and staleness.** An id in `notFound` is absent from the
configuration the call read, not necessarily from the flow: the response's
note says whether to re-run `flow_ir` on the latest configuration or to
re-read with the matching `flowVersion`. Follow it before concluding that an
action was deleted.

**What it does and does not unlock.** `flow_action` does retrieve raw config for
actions the IR treats as blind spots: a `DigitalMenuAction`'s unexpanded choices
Expand Down Expand Up @@ -218,9 +232,9 @@ reusable *user prompts* by name, as `Prompt.<name>`, so the raw config returned
`flow_action` tells you *which* prompt plays but not what the caller hears or for
how long. `get_prompts` resolves that. Three ways to ask, combinable in one call:

- `flowId` — every user prompt the flow's latest configuration references, in
encounter order. Reach for this first when the question is about a flow's whole
script; one call replaces a `flow_action` sweep.
- `flowId` — every user prompt the flow's configuration references (the latest,
or the `flowVersion` given), in encounter order. Reach for this first when the
question is about a flow's whole script; one call replaces a `flow_action` sweep.
- `promptNames` — names exactly as the config shows them (the `Prompt.` prefix is
accepted and stripped). Matching is exact, not a search.
- `promptIds` — ids from the USERPROMPT entries of `flow_dependencies`.
Expand All @@ -235,6 +249,51 @@ against the IR before reading off which prompt was playing N seconds in.
Inline `ToAudioTTS("...")` text and `PromptSystem.<name>` system prompts are not
user prompts; the former is already in the raw config, the latter is out of scope.

## Publish history: the `flow_versions` tool

`flow_versions` answers *when* a flow changed rather than *what* it does. One
call replaces bucketing flow aggregates by `flowVersion` to guess at a publish
time.

**The envelope.**
`{ flowId, flowName, flowType, publishedVersion, versions: [{ version, id, kind,
dateCreated, datePublished, datePublishedEnd, isPublished, createdBy?,
createdByClient? }], total, deleted?, notes? }`

- `version` is the checked-in version number Architect shows (`"57.0"`, as in
"Check in Version 57.0"). A pending save has no number: it is one `save`
entry whose `version` is its `saved_version_<guid>` id, and it disappears
once checked in or published, so saves never accumulate.
- `datePublished` and `datePublishedEnd` bound the window in which that version
was the live one, and consecutive publishes chain exactly: one version's end
is the next one's start. **Which version handled a conversation at time T**
is the version whose window contains T, not the newest version created
before T, since a check-in creates a version without changing what runs.
`isPublished: true` is the test for "live now". A `datePublishedEnd` of
`null` is *not*: the end date is stamped when a version is superseded, and
Genesys only began recording it around mid-2023, so a version superseded
before then has `null` however old or new it is, and its window closes at
the next publish's `datePublished`.
- `kind` is the API's type field when it returns one, otherwise derived from
which dates it stamped. `publish` means the version was the live one at some
point. A version with no date stamps at all is `unknown`; these occur only
before 2023 and have in practice always been check-ins (each sits seconds
before the publish that followed).
- A publish creates its own version rather than promoting the last check-in:
checking in 1.0 and then publishing unchanged yields a `publish` 2.0 with
1.0 left as a `checkin`. A `checkin` directly before a `publish` is the
normal workflow, not evidence of a separate change.
- On a **deleted flow** (`deleted: true`) the history is as it stood at
deletion, so `isPublished` and `publishedVersion` mean "live when deleted",
not live now. `find_flow` no longer finds it, so the id must come from an
execution record, a dependency listing or the user.
- **"What changed in v58"** is two reads, v57 and v58, compared: search both
for the thing in question, or diff the two IRs' nodes by id. Without
`flowVersion` the reading tools read the latest configuration, which is not
any particular publish.
- `createdBy`, when requested, is the user's **id only**, not a name.
- `publishedVersion: null` means the flow is not currently published.

## Content search: the `search_in_flow` tool

`search_in_flow` answers "which actions reference X" in **one call**. It matches
Expand All @@ -255,7 +314,7 @@ literal (a queue name, a variable reference, a phrase of prompt wording) over a
regex when either would answer the question.

**The envelope.**
`{ flowId, pattern, totalMatchedActions, matchedActions: [{ actionId, actionType?, name?, taskId, taskName, menuChoice?, matchedPaths: [{ path, excerpt }], truncated? }], notes? }`
`{ flowId, configuration, pattern, totalMatchedActions, matchedActions: [{ actionId, actionType?, name?, taskId, taskName, menuChoice?, matchedPaths: [{ path, excerpt }], truncated? }], notes? }`

A search matching nothing is a **success**, not an error: `totalMatchedActions:
0` and an empty `matchedActions`. That is a real answer — the flow does not
Expand Down Expand Up @@ -419,6 +478,10 @@ used the exact `flowVersion` that ran, the action lives in a called common
module or bot flow, whose actions the parent flow's IR never contains. Do not
report a redeploy without checking which.

To inspect the version that ran with the static tools, pass the instance's
`flowVersion` to them, so the design being reasoned about is the one the
customer experienced.

Three naming traps:

- Event kinds and IR types differ by convention: **`action<Type>` ↔
Expand Down
8 changes: 8 additions & 0 deletions src/mcp-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { flowAction } from "./tools/flow-action.ts";
import { flowDependencies } from "./tools/flow-dependencies.ts";
import { flowExecutionData } from "./tools/flow-execution-data.ts";
import { flowIr } from "./tools/flow-ir.ts";
import { flowVersions } from "./tools/flow-versions.ts";
import { getPrompts } from "./tools/get-prompts.ts";
import { searchInFlow } from "./tools/search-in-flow.ts";
import { testBotFlow } from "./tools/test-bot-flow.ts";
Expand Down Expand Up @@ -57,6 +58,13 @@ server.registerTool(
flowDependenciesTool.handler,
);

const flowVersionsTool = flowVersions({ architectApi });
server.registerTool(
"flow_versions",
flowVersionsTool.config,
flowVersionsTool.handler,
);

const flowIrTool = flowIr({ architectApi });
server.registerTool("flow_ir", flowIrTool.config, flowIrTool.handler);

Expand Down
3 changes: 2 additions & 1 deletion src/mcp-server/tools/find-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export const findFlow: ToolFactory<ToolConfig, typeof inputSchema> = ({
"a fragment of a word or a few words from the name both work; " +
"flows whose name equals the query exactly are listed first. " +
"The returned id is accepted verbatim by flow_ir, " +
"flow_action, search_in_flow and flow_dependencies. A " +
"flow_action, search_in_flow, flow_dependencies and " +
"flow_versions. A " +
"publishedVersion of null means the flow has never been " +
"published.",
annotations: {
Expand Down
4 changes: 2 additions & 2 deletions src/mcp-server/tools/find-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export const findQueue: ToolFactory<ToolConfig, typeof inputSchema> = ({
"Use the returned exact name as " +
"the search_in_flow query to find the actions in a flow that " +
"transfer to the queue. The in-queue flow ids are accepted " +
"verbatim by flow_ir, flow_action, search_in_flow and " +
"flow_dependencies.",
"verbatim by flow_ir, flow_action, search_in_flow, " +
"flow_dependencies and flow_versions.",
annotations: {
title: "Find Queue",
readOnlyHint: true,
Expand Down
26 changes: 20 additions & 6 deletions src/mcp-server/tools/flow-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
} from "@makingchatbots/genesys-cloud-architect-diagram-lib";
import type { ArchitectApi } from "purecloud-platform-client-v2";
import { z } from "zod/v3";
import { fetchFlowConfiguration } from "../utilities/fetch-flow-configuration.ts";
import {
fetchFlowConfiguration,
flowVersionInput,
} from "../utilities/fetch-flow-configuration.ts";
import type { ToolFactory } from "../utilities/types.ts";

export interface ToolConfig {
Expand Down Expand Up @@ -53,6 +56,7 @@ function planLookups(requestedIds: readonly string[]): {

const inputSchema = {
flowId: z.string().min(1).describe("The Genesys Cloud Architect flow ID"),
flowVersion: flowVersionInput,
actionIds: z
.array(z.string().min(1))
.min(1)
Expand Down Expand Up @@ -87,8 +91,12 @@ export const flowAction: ToolFactory<ToolConfig, typeof inputSchema> = ({
},
inputSchema,
},
handler: async ({ flowId, actionIds }) => {
const fetched = await fetchFlowConfiguration(architectApi, flowId);
handler: async ({ flowId, flowVersion, actionIds }) => {
const fetched = await fetchFlowConfiguration(
architectApi,
flowId,
flowVersion,
);
if (!fetched.ok) {
return {
isError: true,
Expand Down Expand Up @@ -116,9 +124,14 @@ export const flowAction: ToolFactory<ToolConfig, typeof inputSchema> = ({
}
if (lookup.notFound.length > 0) {
notes.push(
"Ids in notFound are absent from this flow's latest configuration. The flow " +
"may have been redeployed since flow_ir was called, so re-run flow_ir and " +
"re-join rather than concluding those actions no longer exist.",
"latest" in fetched.source
? "Ids in notFound are absent from this flow's latest configuration. The flow " +
"may have been redeployed since flow_ir was called, so re-run flow_ir and " +
"re-join rather than concluding those actions no longer exist."
: `Ids in notFound are absent from version "${fetched.source.flowVersion}" of ` +
"this flow. If they came from a flow_ir call on a different version " +
"(or on the latest configuration) they exist only there; re-run " +
"flow_ir with the same flowVersion and re-join.",
);
}

Expand All @@ -128,6 +141,7 @@ export const flowAction: ToolFactory<ToolConfig, typeof inputSchema> = ({
type: "text",
text: JSON.stringify({
flowId,
configuration: fetched.source,
found: lookup.found.map(asRequested),
notFound: lookup.notFound.map(
(id) => requestedFor.get(id) ?? id,
Expand Down
59 changes: 33 additions & 26 deletions src/mcp-server/tools/flow-execution-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
formatApiError,
toApiError,
} from "../utilities/api-error.ts";
import { fetchFlowConfiguration } from "../utilities/fetch-flow-configuration.ts";
import {
type ConfigurationSource,
fetchFlowConfiguration,
} from "../utilities/fetch-flow-configuration.ts";
import type { ToolFactory } from "../utilities/types.ts";

/**
Expand Down Expand Up @@ -50,8 +53,12 @@ interface FlowExecutionDataResult {
summary: ExecutionSummary;
events: ExecutionEvent[];
ir?: {
/** Which flow configuration the join used. */
configuration: { flowVersion: string } | { latest: true };
/**
* Which flow configuration the join used; `deleted: true` when the
* flow has been deleted and the configuration was read as a deleted
* flow's.
*/
configuration: ConfigurationSource;
unmatchedActionIds: string[];
unmatchedStateIds: string[];
actionsNotExecuted: ExecutionActionNotExecuted[];
Expand Down Expand Up @@ -446,37 +453,37 @@ async function fetchConfiguration(
): Promise<
| {
configuration: unknown;
configuration_source: { flowVersion: string } | { latest: true };
configuration_source: ConfigurationSource;
}
| undefined
> {
if (flowVersion) {
try {
const configuration =
await architectApi.getFlowVersionConfiguration(
flowId,
flowVersion,
);
const exact = await fetchFlowConfiguration(
architectApi,
flowId,
flowVersion,
);
if (exact.ok) {
return {
configuration,
configuration_source: { flowVersion },
configuration: exact.configuration,
configuration_source: exact.source,
};
} catch (err) {
if (toApiError(err).status === 410) {
// Deleted flows have no latest configuration either.
notes.push(
"IR join skipped: the flow has been deleted, so no configuration " +
"can be fetched. The events are still complete; only the " +
"ir annotations and actionsNotExecuted are unavailable.",
);
return undefined;
}
}
if (exact.status === 410) {
// Even the deleted-flow retry failed, so the latest
// configuration will not be readable either.
notes.push(
`Could not fetch the configuration of flow version ${flowVersion} ` +
`(${formatApiError(err)}); the IR join used the latest ` +
"configuration instead, which may differ from what ran.",
"IR join skipped: the flow has been deleted, so no configuration " +
"can be fetched. The events are still complete; only the " +
"ir annotations and actionsNotExecuted are unavailable.",
);
return undefined;
}
notes.push(
`Could not fetch the configuration of flow version ${flowVersion} ` +
`(${exact.message}) so the IR join used the latest ` +
"configuration instead, which may differ from what ran.",
);
} else {
notes.push(
"The execution data carried no flowVersion; the IR join used the " +
Expand All @@ -490,6 +497,6 @@ async function fetchConfiguration(
}
return {
configuration: latest.configuration,
configuration_source: { latest: true },
configuration_source: latest.source,
};
}
Loading
Loading