feat(curation): fetchStartableFlows supplements catalogue listing
The /api/ea2/flow/processes endpoint caps at ~203 items and pr_to_po_def is not in that window for this tenant despite being status=published. fetchStartableFlows() does a direct GET per allowlist key and merges results with the catalogue list (allowlist hits take precedence). Wired into Hub.loadPublishedFlows + agentTools.list_processes + agentTools.start_process.
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { chromium } from "playwright";
|
||||||
|
const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"];
|
||||||
|
const b = await chromium.launch({ headless: true, args });
|
||||||
|
const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage();
|
||||||
|
p.on("response", async (r) => {
|
||||||
|
if (/\/api\/ea2\/flow\/processes/.test(r.url())) {
|
||||||
|
const t = await r.text().catch(() => "");
|
||||||
|
console.log(`[catalogue ${r.status()}] ${r.url().replace("https://canvas.flow-master.ai","")} → ${t.slice(0,500)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await p.goto("https://canvas.flow-master.ai/", { waitUntil: "networkidle" });
|
||||||
|
await p.waitForTimeout(800);
|
||||||
|
await p.locator(".persona-chip", { hasText: /Mariana/ }).click();
|
||||||
|
await p.locator(".dev-login-btn").click();
|
||||||
|
await p.waitForTimeout(2500);
|
||||||
|
await p.locator(".hub-chip", { hasText: /Procurement Hub/ }).click();
|
||||||
|
await p.waitForTimeout(3000);
|
||||||
|
const flowCards = await p.locator(".hub-queue-row, .hub-flow-title").allTextContents();
|
||||||
|
console.log(`flowCards: ${flowCards.length} | ${JSON.stringify(flowCards.slice(0,5))}`);
|
||||||
|
await b.close();
|
||||||
+19
-7
@@ -1,7 +1,7 @@
|
|||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import { wizardApi } from "./wizardApi";
|
import { wizardApi } from "./wizardApi";
|
||||||
import { chatApi } from "./chatApi";
|
import { chatApi } from "./chatApi";
|
||||||
import { curatedPublishedFlows } from "./flowCuration";
|
import { curatedPublishedFlows, fetchStartableFlows } from "./flowCuration";
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -70,10 +70,17 @@ const TOOLS: ToolDef[] = [
|
|||||||
const processName = subjectMatch ? subjectMatch[1] : phrase;
|
const processName = subjectMatch ? subjectMatch[1] : phrase;
|
||||||
const subject = subjectMatch ? subjectMatch[3] : undefined;
|
const subject = subjectMatch ? subjectMatch[3] : undefined;
|
||||||
|
|
||||||
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
|
const token = sessionStorage.getItem("fm.mc.token.v1") || "";
|
||||||
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
|
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
}).then((r) => r.json());
|
}).then((r) => r.json());
|
||||||
const items = curatedPublishedFlows((procs?.items || []) as any[]);
|
const fromList = curatedPublishedFlows((procs?.items || []) as any[]);
|
||||||
|
const directHits = await fetchStartableFlows(api.config.baseUrl, token);
|
||||||
|
const items = (() => {
|
||||||
|
const m = new Map<string, any>();
|
||||||
|
for (const it of [...directHits, ...fromList]) m.set(it._key, it);
|
||||||
|
return Array.from(m.values());
|
||||||
|
})();
|
||||||
const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
||||||
const target = norm(processName);
|
const target = norm(processName);
|
||||||
const match = items.find((it) => norm(it.display_name || "").includes(target) || norm(it.name || "").includes(target));
|
const match = items.find((it) => norm(it.display_name || "").includes(target) || norm(it.name || "").includes(target));
|
||||||
@@ -128,12 +135,17 @@ const TOOLS: ToolDef[] = [
|
|||||||
description: "List published processes for this tenant",
|
description: "List published processes for this tenant",
|
||||||
matcher: /^(list|show|what)\s+(processes|workflows|flows)/i,
|
matcher: /^(list|show|what)\s+(processes|workflows|flows)/i,
|
||||||
async run() {
|
async run() {
|
||||||
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
|
const token = sessionStorage.getItem("fm.mc.token.v1") || "";
|
||||||
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
|
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.catch(() => ({ items: [] }));
|
.catch(() => ({ items: [] }));
|
||||||
const published = curatedPublishedFlows((procs?.items || []) as any[]).slice(0, 10);
|
const fromList = curatedPublishedFlows((procs?.items || []) as any[]);
|
||||||
|
const directHits = await fetchStartableFlows(api.config.baseUrl, token);
|
||||||
|
const merged = new Map<string, any>();
|
||||||
|
for (const it of [...directHits, ...fromList]) merged.set(it._key, it);
|
||||||
|
const published = Array.from(merged.values()).slice(0, 10);
|
||||||
if (!published.length) return { ok: true, display: "No published processes yet. Open the Studio to design one." };
|
if (!published.length) return { ok: true, display: "No published processes yet. Open the Studio to design one." };
|
||||||
const lines = published.map((it) => `• ${it.display_name}`).join("\n");
|
const lines = published.map((it) => `• ${it.display_name}`).join("\n");
|
||||||
return { ok: true, display: `Here's what's published:\n${lines}` };
|
return { ok: true, display: `Here's what's published:\n${lines}` };
|
||||||
|
|||||||
+16
-1
@@ -36,7 +36,7 @@ const NON_BUSINESS_SOURCE_CONTEXTS = new Set([
|
|||||||
"EA2_DRAFT_PROCESS:process_creation",
|
"EA2_DRAFT_PROCESS:process_creation",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const STARTABLE_FLOW_KEYS = new Set<string>([
|
export const STARTABLE_FLOW_KEYS = new Set<string>([
|
||||||
"pr_to_po_def",
|
"pr_to_po_def",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -60,6 +60,21 @@ export function curatedPublishedFlows<T extends RawFlow>(items: T[]): T[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchStartableFlows(baseUrl: string, token: string): Promise<RawFlow[]> {
|
||||||
|
const results = await Promise.all(
|
||||||
|
Array.from(STARTABLE_FLOW_KEYS).map(async (key) => {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${baseUrl}/api/ea2/flow/${key}`, { headers: { Authorization: `Bearer ${token}` } });
|
||||||
|
if (!r.ok) return null;
|
||||||
|
const doc = await r.json();
|
||||||
|
if (doc.kind !== "definition" || doc.status !== "published" || !doc.display_name) return null;
|
||||||
|
return doc as RawFlow;
|
||||||
|
} catch { return null; }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return results.filter((d): d is RawFlow => !!d);
|
||||||
|
}
|
||||||
|
|
||||||
export function friendlyShortId(id: string | undefined | null): string {
|
export function friendlyShortId(id: string | undefined | null): string {
|
||||||
if (!id) return "";
|
if (!id) return "";
|
||||||
const trimmed = id.replace(HEX_SUFFIX, "");
|
const trimmed = id.replace(HEX_SUFFIX, "");
|
||||||
|
|||||||
+9
-4
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useApp } from "../state/store";
|
import { useApp } from "../state/store";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { wizardApi } from "../lib/wizardApi";
|
import { wizardApi } from "../lib/wizardApi";
|
||||||
import { curatedPublishedFlows } from "../lib/flowCuration";
|
import { curatedPublishedFlows, fetchStartableFlows } from "../lib/flowCuration";
|
||||||
import { Branch, Layers, Pulse } from "../components/icons";
|
import { Branch, Layers, Pulse } from "../components/icons";
|
||||||
|
|
||||||
export type HubKey = "procurement" | "hr" | "it";
|
export type HubKey = "procurement" | "hr" | "it";
|
||||||
@@ -71,12 +71,17 @@ interface PublishedFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadPublishedFlows(): Promise<PublishedFlow[]> {
|
async function loadPublishedFlows(): Promise<PublishedFlow[]> {
|
||||||
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
|
const token = sessionStorage.getItem("fm.mc.token.v1") || "";
|
||||||
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
|
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`processes ${res.status}`);
|
if (!res.ok) throw new Error(`processes ${res.status}`);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
return curatedPublishedFlows((body?.items || []) as any[]).map((it) => ({
|
const fromList = curatedPublishedFlows((body?.items || []) as any[]);
|
||||||
|
const directHits = await fetchStartableFlows(api.config.baseUrl, token);
|
||||||
|
const merged = new Map<string, any>();
|
||||||
|
for (const it of [...directHits, ...fromList]) merged.set(it._key, it);
|
||||||
|
return Array.from(merged.values()).map((it) => ({
|
||||||
_key: it._key,
|
_key: it._key,
|
||||||
display_name: it.display_name!,
|
display_name: it.display_name!,
|
||||||
description: it.description,
|
description: it.description,
|
||||||
|
|||||||
Reference in New Issue
Block a user