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)}

View file

@ -12,7 +12,9 @@ interface NodeContentProps {
hovered_through_edge?: boolean;
title: string;
icon: ReactNode;
nodeType?: 'start' | 'agent' | 'end' | 'global' | 'trigger' | 'webhook' | 'qa';
badgeLabel?: string;
badgeClassName?: string;
contentLabel?: string;
hasSourceHandle?: boolean;
hasTargetHandle?: boolean;
children?: ReactNode;
@ -22,26 +24,7 @@ interface NodeContentProps {
}
// Get badge styling based on node type
const getNodeTypeBadge = (nodeType?: string) => {
switch (nodeType) {
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' };
default:
return { label: 'Node', className: 'bg-zinc-500 text-white' };
}
};
const DEFAULT_BADGE = { label: 'Node', className: 'bg-zinc-500 text-white' };
export const NodeContent = ({
selected,
@ -50,7 +33,9 @@ export const NodeContent = ({
hovered_through_edge,
title,
icon,
nodeType,
badgeLabel,
badgeClassName,
contentLabel = "Prompt",
hasSourceHandle = false,
hasTargetHandle = false,
children,
@ -58,7 +43,10 @@ export const NodeContent = ({
onDoubleClick,
nodeId,
}: NodeContentProps) => {
const badge = getNodeTypeBadge(nodeType);
const badge = {
label: badgeLabel ?? DEFAULT_BADGE.label,
className: badgeClassName ?? DEFAULT_BADGE.className,
};
return (
<BaseNode
@ -98,7 +86,9 @@ export const NodeContent = ({
{/* Content area with prompt label */}
<div className="p-4">
<div className="text-xs text-muted-foreground mb-1.5 font-medium">Prompt:</div>
<div className="text-xs text-muted-foreground mb-1.5 font-medium">
{contentLabel}:
</div>
{children}
</div>

View file

@ -5,14 +5,13 @@ export enum NodeType {
GLOBAL_NODE = 'globalNode',
TRIGGER = 'trigger',
WEBHOOK = 'webhook',
QA = 'qa'
QA = 'qa',
}
export type FlowNodeData = {
prompt: string;
prompt?: string;
name: string;
is_start?: boolean;
is_static?: boolean;
is_end?: boolean;
invalid?: boolean;
validationMessage?: string | null;
@ -26,8 +25,6 @@ export type FlowNodeData = {
greeting?: string;
greeting_type?: 'text' | 'audio';
greeting_recording_id?: string;
wait_for_user_greeting?: boolean;
detect_voicemail?: boolean;
delayed_start?: boolean;
delayed_start_duration?: number;
// Pre-call data fetch (StartCall only)
@ -61,6 +58,7 @@ export type FlowNodeData = {
mcp_tool_filters?: Record<string, string[]>;
// Documents - array of knowledge base document UUIDs that can be referenced by this node
document_uuids?: string[];
[key: string]: unknown;
}
export type FlowNode = {
@ -135,4 +133,3 @@ export interface Credential {
created_at: string;
updated_at: string;
}

View file

@ -29,10 +29,7 @@ const BLANK_WORKFLOW_DEFINITION = {
allow_interrupt: false,
invalid: false,
validationMessage: null,
is_static: false,
add_global_prompt: false,
wait_for_user_response: false,
detect_voicemail: true,
delayed_start: false,
is_start: true,
selected_through_edge: false,