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

@ -18,6 +18,25 @@ def _qa_node(node_id="qa-1", api_key="", **extra_data):
return {"id": node_id, "type": "qa", "position": {"x": 0, "y": 0}, "data": data}
def _tuner_node(node_id="tuner-1", api_key="", **extra_data):
"""Helper to build a Tuner node."""
data = {
"name": "Tuner",
"tuner_enabled": True,
"tuner_agent_id": "sales-bot",
"tuner_workspace_id": 7,
**extra_data,
}
if api_key:
data["tuner_api_key"] = api_key
return {
"id": node_id,
"type": "tuner",
"position": {"x": 0, "y": 0},
"data": data,
}
def _agent_node(node_id="agent-1"):
"""Helper to build a non-QA node."""
return {
@ -66,6 +85,19 @@ class TestMaskWorkflowDefinition:
assert "qa_api_key" not in masked["nodes"][0]["data"]
assert masked["nodes"][1]["data"]["qa_api_key"] == mask_key("sk-secret1234")
def test_masks_tuner_api_key(self):
"""Tuner node api_key is masked, showing only last 4 chars."""
real_key = "tuner_live_abcdefghijklmnop"
wf = _make_workflow_def([_tuner_node(api_key=real_key)])
masked = mask_workflow_definition(wf)
masked_key = masked["nodes"][0]["data"]["tuner_api_key"]
assert masked_key == mask_key(real_key)
assert masked_key.endswith("mnop")
assert masked_key.startswith("*")
assert real_key not in str(masked)
def test_qa_node_without_api_key(self):
"""QA node with no api_key is left as-is."""
wf = _make_workflow_def([_qa_node()])
@ -154,6 +186,16 @@ class TestMergeWorkflowApiKeys:
assert result["nodes"][0]["data"]["qa_api_key"] == new_key
def test_masked_tuner_key_is_restored(self):
"""Masked Tuner keys round-trip without losing the stored secret."""
real_key = "tuner_live_abcdefghijklmnop"
existing = _make_workflow_def([_tuner_node(api_key=real_key)])
incoming = _make_workflow_def([_tuner_node(api_key=mask_key(real_key))])
result = merge_workflow_api_keys(incoming, existing)
assert result["nodes"][0]["data"]["tuner_api_key"] == real_key
def test_no_incoming_api_key(self):
"""QA node without api_key in incoming is left alone."""
existing = _make_workflow_def([_qa_node(api_key="sk-existing-key1")])