feat: add Tuner Integration to Dograh (#311)

* Add tuner integration

* bump pipecat version

* chore: update pipecat submodule to match upstream and use tuner-pipecat-sdk 0.2.0

Update pipecat submodule from 0.0.109.dev23 to 13e98d0d9 (the exact commit
upstream dograh-hq/dograh uses after v1.30.1). This installs pipecat-ai as
1.1.0.post277 via setuptools_scm, satisfying tuner-pipecat-sdk 0.2.0's
pipecat-ai>=1.0.0 requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* wire tuner

* feat: refactor integrations into self contained packages

* chore: simplify ensure_public_access_token

* fix: remove NodeSpec and make DTOs the source of truth

* feat: send relevant signal to mcp using to_mcp_dict

* fix: fix tests

* cleanup: remove nango integrations

* feat: add agents.md for integrations

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
Mohamed-Mamdouh 2026-05-20 10:07:33 +01:00 committed by GitHub
parent afa78fe859
commit 5f28c1b2a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 3388 additions and 3414 deletions

View file

@ -31,7 +31,8 @@ type NodeStyleVariant =
| "global"
| "trigger"
| "webhook"
| "qa";
| "qa"
| "integration";
const STYLE_VARIANT_BY_SPEC: Record<string, NodeStyleVariant> = {
startCall: "start",
@ -100,6 +101,70 @@ function buildTriggerEndpoints(
};
}
function resolveIntegrationEnabled(
spec: NodeSpec,
data: FlowNodeData,
): boolean {
for (const prop of spec.properties) {
if (!prop.name.endsWith("enabled")) continue;
const value = data[prop.name];
if (typeof value === "boolean") return value;
}
return true;
}
function resolveIntegrationSummary(
spec: NodeSpec,
data: FlowNodeData,
): string {
for (const prop of spec.properties) {
if (
prop.name === "name" ||
prop.name.endsWith("enabled") ||
/api[_-]?key|token|secret/i.test(prop.name)
) {
continue;
}
const value = data[prop.name];
if (typeof value === "string" && value.trim().length > 0) {
return value.length > 30 ? `${value.slice(0, 30)}...` : value;
}
if (typeof value === "number") {
return String(value);
}
}
return "Not configured";
}
function getBadgeForSpec(
spec: NodeSpec | undefined,
variant: NodeStyleVariant,
): { label: string; className: string } {
if (!spec) {
return { label: "Node", className: "bg-zinc-500 text-white" };
}
switch (variant) {
case "start":
return { label: "Start Node", className: "bg-emerald-500 text-white" };
case "agent":
return { label: "Agent Node", className: "bg-blue-500 text-white" };
case "end":
return { label: "End Node", className: "bg-rose-500 text-white" };
case "global":
return { label: "Global Node", className: "bg-amber-500 text-white" };
case "trigger":
return { label: "API Trigger", className: "bg-purple-500 text-white" };
case "webhook":
return { label: "Webhook", className: "bg-indigo-500 text-white" };
case "qa":
return { label: "QA Analysis", className: "bg-teal-500 text-white" };
case "integration":
return { label: spec.display_name, className: "bg-cyan-600 text-white" };
}
}
// ─── Canvas preview dispatch ──────────────────────────────────────────────
function CanvasPreview({
@ -188,6 +253,21 @@ function CanvasPreview({
);
}
if (spec.category === "integration") {
const enabled = resolveIntegrationEnabled(spec, data);
const destination = resolveIntegrationSummary(spec, data);
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">
{destination}
</span>
</div>
<StatusDot enabled={enabled} />
</div>
);
}
// Default: prompt preview + tool/document badges (when spec declares them).
const hasToolRefs = spec.properties.some((p) => p.type === "tool_refs");
const hasDocRefs = spec.properties.some((p) => p.type === "document_refs");
@ -501,10 +581,20 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
}, [data, open]);
// ── Render ──────────────────────────────────────────────────────────
const styleVariant = STYLE_VARIANT_BY_SPEC[type];
const handles = HANDLES_BY_SPEC[type] ?? { source: true, target: true };
const styleVariant =
STYLE_VARIANT_BY_SPEC[type] ??
(spec?.category === "integration" ? "integration" : "agent");
const handles =
HANDLES_BY_SPEC[type] ??
(spec?.category === "integration"
? { source: false, target: false }
: { source: true, target: true });
const badge = getBadgeForSpec(spec, styleVariant);
const Icon = spec ? resolveIcon(spec.icon) : Circle;
const docUrl = DOC_URL_BY_SPEC[type];
const contentLabel = spec?.properties.some((p) => p.name === "prompt")
? "Prompt"
: "Details";
// Edit dialog title: "Edit {display_name}". Webhook keeps the original
// "Edit Webhook" wording — display_name is "Webhook" so it works out.
@ -520,7 +610,9 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
hovered_through_edge={data.hovered_through_edge}
title={data.name || fallbackTitle}
icon={<Icon />}
nodeType={styleVariant}
badgeLabel={badge.label}
badgeClassName={badge.className}
contentLabel={contentLabel}
hasSourceHandle={handles.source}
hasTargetHandle={handles.target}
onDoubleClick={() => setOpen(true)}