diff --git a/api/app.py b/api/app.py index 735de66..3538a60 100644 --- a/api/app.py +++ b/api/app.py @@ -27,6 +27,7 @@ from fastapi.middleware.cors import CORSMiddleware from loguru import logger from api.routes.main import router as main_router +from api.services.pipecat.tracing_config import load_all_org_langfuse_credentials from api.tasks.arq import get_arq_redis API_PREFIX = "/api/v1" @@ -37,6 +38,10 @@ async def lifespan(app: FastAPI): # warmup arq pool await get_arq_redis() + # Pre-register all org-specific Langfuse exporters so they're ready + # before any pipeline runs, without per-call DB lookups. + await load_all_org_langfuse_credentials() + yield # Run app # Shutdown sequence - this runs when FastAPI is shutting down diff --git a/api/db/campaign_client.py b/api/db/campaign_client.py index e8b41af..d7cb978 100644 --- a/api/db/campaign_client.py +++ b/api/db/campaign_client.py @@ -365,13 +365,22 @@ class CampaignClient(BaseDBClient): result = await session.execute(query) return list(result.scalars().all()) - async def get_completed_runs_for_report( - self, campaign_id: int - ) -> list[WorkflowRunModel]: - """Get completed workflow runs with call duration for campaign report CSV.""" + async def get_completed_runs_for_report(self, campaign_id: int) -> list: + """Get completed workflow runs for campaign report CSV. + + Returns rows with only the columns needed for report generation. + """ async with self.async_session() as session: query = ( - select(WorkflowRunModel) + select( + WorkflowRunModel.id, + WorkflowRunModel.created_at, + WorkflowRunModel.initial_context, + WorkflowRunModel.gathered_context, + WorkflowRunModel.cost_info, + WorkflowRunModel.logs, + WorkflowRunModel.public_access_token, + ) .where( WorkflowRunModel.campaign_id == campaign_id, WorkflowRunModel.is_completed.is_(True), @@ -379,14 +388,10 @@ class CampaignClient(BaseDBClient): .as_string() .isnot(None), ) - .order_by( - WorkflowRunModel.cost_info["call_duration_seconds"] - .as_float() - .desc() - ) + .order_by(WorkflowRunModel.created_at.desc()) ) result = await session.execute(query) - return list(result.scalars().all()) + return list(result.all()) async def create_queued_run( self, diff --git a/api/db/organization_configuration_client.py b/api/db/organization_configuration_client.py index def483a..94cb757 100644 --- a/api/db/organization_configuration_client.py +++ b/api/db/organization_configuration_client.py @@ -95,6 +95,26 @@ class OrganizationConfigurationClient(BaseDBClient): config = await self.get_configuration(organization_id, key) return config.value if config else default + async def get_all_configurations_by_key(self, key: str) -> list[dict[str, Any]]: + """Get all organization configurations for a given key. + + Returns a list of dicts with organization_id and the config value. + """ + async with self.async_session() as session: + result = await session.execute( + select(OrganizationConfigurationModel).where( + OrganizationConfigurationModel.key == key, + ) + ) + return [ + { + "organization_id": config.organization_id, + "value": config.value, + } + for config in result.scalars().all() + if config.value + ] + async def get_configurations_by_provider( self, key: str, provider: str ) -> List[Dict[str, Any]]: diff --git a/api/db/workflow_client.py b/api/db/workflow_client.py index 60269de..17b67e9 100644 --- a/api/db/workflow_client.py +++ b/api/db/workflow_client.py @@ -2,6 +2,7 @@ import hashlib import json from typing import Optional +from loguru import logger from sqlalchemy import func, update from sqlalchemy.future import select from sqlalchemy.orm import load_only, selectinload @@ -174,6 +175,16 @@ class WorkflowClient(BaseDBClient): return counts + async def get_workflow_organization_id(self, workflow_id: int) -> int | None: + """Fetch only the organization_id for a workflow. Lightweight query.""" + async with self.async_session() as session: + result = await session.execute( + select(WorkflowModel.organization_id).where( + WorkflowModel.id == workflow_id + ) + ) + return result.scalar_one_or_none() + async def get_workflow( self, workflow_id: int, user_id: int = None, organization_id: int = None ) -> WorkflowModel | None: @@ -434,3 +445,38 @@ class WorkflowClient(BaseDBClient): counts[workflow_id] = run_count return counts + + async def add_call_disposition_code( + self, workflow_id: int, disposition_code: str + ) -> None: + """Add a disposition code to the workflow's call_disposition_codes if not already present. + + The codes are stored as {"disposition_codes": ["code1", "code2", ...]}. + """ + if not disposition_code: + return + + async with self.async_session() as session: + result = await session.execute( + select(WorkflowModel).where(WorkflowModel.id == workflow_id) + ) + workflow = result.scalars().first() + if not workflow: + return + + existing = workflow.call_disposition_codes or {} + codes = existing.get("disposition_codes", []) + if disposition_code in codes: + return + + codes.append(disposition_code) + workflow.call_disposition_codes = {"disposition_codes": codes} + + try: + await session.commit() + except Exception as e: + await session.rollback() + logger.error( + f"Failed to add disposition code '{disposition_code}' " + f"to workflow {workflow_id}: {e}" + ) diff --git a/api/enums.py b/api/enums.py index 03848e2..e6ea04b 100644 --- a/api/enums.py +++ b/api/enums.py @@ -83,6 +83,9 @@ class OrganizationConfigurationKey(Enum): TWILIO_CONFIGURATION = ( "TWILIO_CONFIGURATION" # Deprecated - for backward compatibility ) + LANGFUSE_CREDENTIALS = ( + "LANGFUSE_CREDENTIALS" # Org-level Langfuse tracing credentials + ) class WorkflowStatus(Enum): diff --git a/api/routes/campaign.py b/api/routes/campaign.py index 8f8abe2..722223b 100644 --- a/api/routes/campaign.py +++ b/api/routes/campaign.py @@ -1,5 +1,3 @@ -import csv -import io import json from datetime import datetime from typing import List, Optional @@ -10,7 +8,6 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field, field_validator, model_validator from api.constants import ( - BACKEND_API_ENDPOINT, DEFAULT_CAMPAIGN_RETRY_CONFIG, DEFAULT_ORG_CONCURRENCY_LIMIT, ) @@ -18,12 +15,12 @@ from api.db import db_client from api.db.models import UserModel from api.enums import OrganizationConfigurationKey from api.services.auth.depends import get_user +from api.services.campaign.report import generate_campaign_report_csv from api.services.campaign.runner import campaign_runner_service from api.services.campaign.source_sync import CampaignSourceSyncService from api.services.campaign.source_sync_factory import get_sync_service from api.services.quota_service import check_dograh_quota from api.services.storage import storage_fs -from api.utils.transcript import generate_transcript_text router = APIRouter(prefix="/campaign") @@ -705,14 +702,6 @@ async def get_campaign_source_download_url( ) -def _transcript_from_logs(logs: dict | None) -> str: - """Extract transcript text from workflow run logs JSON.""" - if not logs: - return "" - events = logs.get("realtime_feedback_events", []) - return generate_transcript_text(events).strip() - - @router.get("/{campaign_id}/report") async def download_campaign_report( campaign_id: int, @@ -723,56 +712,8 @@ async def download_campaign_report( if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") - runs = await db_client.get_completed_runs_for_report(campaign_id) + output, filename = await generate_campaign_report_csv(campaign_id) - output = io.StringIO() - writer = csv.writer(output) - writer.writerow( - [ - "Run ID", - "Created At", - "Customer Name", - "Phone Number", - "Call Disposition", - "Call Tags", - "Call Duration (s)", - "Transcript", - "Recording URL", - ] - ) - - for run in runs: - initial = run.initial_context or {} - gathered = run.gathered_context or {} - cost = run.cost_info or {} - - recording_url = "" - if run.public_access_token: - recording_url = ( - f"{BACKEND_API_ENDPOINT}/api/v1/public/download/workflow" - f"/{run.public_access_token}/recording" - ) - - call_tags = gathered.get("call_tags", []) - if isinstance(call_tags, list): - call_tags = ", ".join(str(t) for t in call_tags) - - writer.writerow( - [ - run.id, - run.created_at.isoformat() if run.created_at else "", - initial.get("first_name", ""), - initial.get("phone_number", ""), - gathered.get("mapped_call_disposition", ""), - call_tags, - cost.get("call_duration_seconds", ""), - _transcript_from_logs(run.logs), - recording_url, - ] - ) - - output.seek(0) - filename = f"campaign_{campaign_id}_report.csv" return StreamingResponse( output, media_type="text/csv", diff --git a/api/routes/organization.py b/api/routes/organization.py index f96d7c1..b212821 100644 --- a/api/routes/organization.py +++ b/api/routes/organization.py @@ -22,6 +22,7 @@ from api.schemas.telephony_config import ( ) from api.services.auth.depends import get_user from api.services.configuration.masking import is_mask_of, mask_key +from api.services.pipecat.tracing_config import unregister_org_langfuse_credentials router = APIRouter(prefix="/organizations", tags=["organizations"]) @@ -248,6 +249,107 @@ def preserve_masked_fields(request, existing_config, config_value): config_value[field_name] = existing_config.value[field_name] +class LangfuseCredentialsRequest(BaseModel): + host: str + public_key: str + secret_key: str + + +class LangfuseCredentialsResponse(BaseModel): + host: str = "" + public_key: str = "" + secret_key: str = "" + configured: bool = False + + +@router.get("/langfuse-credentials", response_model=LangfuseCredentialsResponse) +async def get_langfuse_credentials(user: UserModel = Depends(get_user)): + """Get Langfuse credentials for the user's organization with masked sensitive fields.""" + if not user.selected_organization_id: + raise HTTPException(status_code=400, detail="No organization selected") + + config = await db_client.get_configuration( + user.selected_organization_id, + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + ) + + if not config or not config.value: + return LangfuseCredentialsResponse() + + return LangfuseCredentialsResponse( + host=config.value.get("host", ""), + public_key=mask_key(config.value.get("public_key", "")), + secret_key=mask_key(config.value.get("secret_key", "")), + configured=True, + ) + + +@router.post("/langfuse-credentials") +async def save_langfuse_credentials( + request: LangfuseCredentialsRequest, + user: UserModel = Depends(get_user), +): + """Save Langfuse credentials for the user's organization.""" + if not user.selected_organization_id: + raise HTTPException(status_code=400, detail="No organization selected") + + existing_config = await db_client.get_configuration( + user.selected_organization_id, + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + ) + + config_value = { + "host": request.host, + "public_key": request.public_key, + "secret_key": request.secret_key, + } + + # Preserve masked fields + if existing_config and existing_config.value: + if is_mask_of(request.public_key, existing_config.value.get("public_key", "")): + config_value["public_key"] = existing_config.value["public_key"] + if is_mask_of(request.secret_key, existing_config.value.get("secret_key", "")): + config_value["secret_key"] = existing_config.value["secret_key"] + + await db_client.upsert_configuration( + user.selected_organization_id, + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + config_value, + ) + + # Update the in-memory OTEL exporter so new traces route immediately + from api.services.pipecat.tracing_config import register_org_langfuse_credentials + + register_org_langfuse_credentials( + org_id=user.selected_organization_id, + host=config_value["host"], + public_key=config_value["public_key"], + secret_key=config_value["secret_key"], + ) + + return {"message": "Langfuse credentials saved successfully"} + + +@router.delete("/langfuse-credentials") +async def delete_langfuse_credentials(user: UserModel = Depends(get_user)): + """Delete Langfuse credentials for the user's organization.""" + if not user.selected_organization_id: + raise HTTPException(status_code=400, detail="No organization selected") + + deleted = await db_client.delete_configuration( + user.selected_organization_id, + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + ) + + if not deleted: + raise HTTPException(status_code=404, detail="No Langfuse credentials found") + + # Remove the in-memory OTEL exporter so traces fall back to default + unregister_org_langfuse_credentials(user.selected_organization_id) + + return {"message": "Langfuse credentials deleted successfully"} + + class RetryConfigResponse(BaseModel): enabled: bool max_retries: int diff --git a/api/routes/webrtc_signaling.py b/api/routes/webrtc_signaling.py index 2dadd83..bb5372e 100644 --- a/api/routes/webrtc_signaling.py +++ b/api/routes/webrtc_signaling.py @@ -44,7 +44,7 @@ from api.services.pipecat.ws_sender_registry import ( ) from api.services.quota_service import check_dograh_quota from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection -from pipecat.utils.run_context import set_current_run_id +from pipecat.utils.run_context import set_current_org_id, set_current_run_id router = APIRouter(prefix="/ws") @@ -213,8 +213,12 @@ class SignalingManager: type_ = payload.get("type") call_context_vars = payload.get("call_context_vars", {}) - # Set run context for logging + # Set run context for logging and tracing. org_id must be set before + # pc.initialize() so that aiortc's internal tasks inherit it. set_current_run_id(workflow_run_id) + org_id = await db_client.get_workflow_organization_id(workflow_id) + if org_id: + set_current_org_id(org_id) # Check Dograh quota before initiating the call quota_result = await check_dograh_quota(user) diff --git a/api/services/campaign/report.py b/api/services/campaign/report.py new file mode 100644 index 0000000..e9e9a0c --- /dev/null +++ b/api/services/campaign/report.py @@ -0,0 +1,96 @@ +import csv +import io +from typing import Any, List + +from api.constants import BACKEND_API_ENDPOINT +from api.db import db_client +from api.utils.transcript import generate_transcript_text + + +def _transcript_from_logs(logs: dict | None) -> str: + """Extract transcript text from workflow run logs JSON.""" + if not logs: + return "" + events = logs.get("realtime_feedback_events", []) + return generate_transcript_text(events).strip() + + +def _collect_extracted_variable_keys(runs: List[Any]) -> list[str]: + """Collect all unique extracted variable keys across runs, preserving insertion order.""" + keys: dict[str, None] = {} + for run in runs: + gathered = run.gathered_context or {} + extracted = gathered.get("extracted_variables", {}) + if isinstance(extracted, dict): + for key in extracted: + keys.setdefault(key, None) + return list(keys) + + +async def generate_campaign_report_csv(campaign_id: int) -> tuple[io.StringIO, str]: + """Generate a CSV report for a campaign. + + Returns a tuple of (csv_output, filename). + """ + runs = await db_client.get_completed_runs_for_report(campaign_id) + + # Collect dynamic extracted variable columns + extracted_var_keys = _collect_extracted_variable_keys(runs) + + output = io.StringIO() + writer = csv.writer(output) + + pre_headers = [ + "Run ID", + "Created At", + "Phone Number", + "Call Disposition", + "Call Duration (s)", + ] + post_headers = [ + "Call Tags", + "Transcript", + "Recording URL", + ] + writer.writerow(pre_headers + extracted_var_keys + post_headers) + + for run in runs: + initial = run.initial_context or {} + gathered = run.gathered_context or {} + cost = run.cost_info or {} + + recording_url = "" + if run.public_access_token: + recording_url = ( + f"{BACKEND_API_ENDPOINT}/api/v1/public/download/workflow" + f"/{run.public_access_token}/recording" + ) + + call_tags = gathered.get("call_tags", []) + if isinstance(call_tags, list): + call_tags = ", ".join(str(t) for t in call_tags) + + pre_values = [ + run.id, + run.created_at.isoformat() if run.created_at else "", + initial.get("phone_number", ""), + gathered.get("mapped_call_disposition", ""), + cost.get("call_duration_seconds", ""), + ] + + extracted = gathered.get("extracted_variables", {}) + if not isinstance(extracted, dict): + extracted = {} + extracted_values = [extracted.get(key, "") for key in extracted_var_keys] + + post_values = [ + call_tags, + _transcript_from_logs(run.logs), + recording_url, + ] + + writer.writerow(pre_values + extracted_values + post_values) + + output.seek(0) + filename = f"campaign_{campaign_id}_report.csv" + return output, filename diff --git a/api/services/pipecat/event_handlers.py b/api/services/pipecat/event_handlers.py index 5571421..c8cf483 100644 --- a/api/services/pipecat/event_handlers.py +++ b/api/services/pipecat/event_handlers.py @@ -9,6 +9,7 @@ from api.services.pipecat.in_memory_buffers import ( InMemoryLogsBuffer, ) from api.services.pipecat.pipeline_metrics_aggregator import PipelineMetricsAggregator +from api.services.pipecat.tracing_config import get_trace_url from api.services.workflow.pipecat_engine import PipecatEngine from api.tasks.arq import enqueue_job from api.tasks.function_names import FunctionNames @@ -139,10 +140,12 @@ def register_event_handlers( # Add trace URL if available (must be done before conversation tracing ends) if task.turn_trace_observer: - trace_url = task.turn_trace_observer.get_trace_url() - if trace_url: - gathered_context["trace_url"] = trace_url - logger.debug(f"Added trace URL to gathered_context: {trace_url}") + trace_id = task.turn_trace_observer.get_trace_id() + if trace_id: + trace_url = get_trace_url(trace_id) + if trace_url: + gathered_context["trace_url"] = trace_url + logger.debug(f"Added trace URL to gathered_context: {trace_url}") # also consider existing gathered context in workflow_run gathered_context = {**gathered_context, **workflow_run.gathered_context} @@ -165,6 +168,19 @@ def register_event_handlers( gathered_context["call_tags"] = call_tags + # Store disposition code in workflow for dynamic filtering + disposition_code = gathered_context.get("mapped_call_disposition") + if disposition_code and workflow_run: + try: + await db_client.add_call_disposition_code( + workflow_run.workflow_id, disposition_code + ) + except Exception as e: + logger.error( + f"Error storing disposition code in workflow: {e}", + exc_info=True, + ) + # Clean up engine resources (including voicemail detector) await engine.cleanup() diff --git a/api/services/pipecat/run_pipeline.py b/api/services/pipecat/run_pipeline.py index c98a46b..d5bcb2c 100644 --- a/api/services/pipecat/run_pipeline.py +++ b/api/services/pipecat/run_pipeline.py @@ -38,7 +38,9 @@ from api.services.pipecat.service_factory import ( create_stt_service, create_tts_service, ) -from api.services.pipecat.tracing_config import setup_tracing_exporter +from api.services.pipecat.tracing_config import ( + ensure_tracing, +) from api.services.pipecat.transport_setup import ( create_ari_transport, create_cloudonix_transport, @@ -82,10 +84,10 @@ from pipecat.turns.user_stop import ( ) from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.enums import EndTaskReason, RealtimeFeedbackType -from pipecat.utils.run_context import set_current_run_id +from pipecat.utils.run_context import set_current_org_id, set_current_run_id # Setup tracing if enabled -setup_tracing_exporter() +ensure_tracing() async def run_pipeline_twilio( @@ -108,6 +110,11 @@ async def run_pipeline_twilio( # Get workflow to extract all pipeline configurations workflow = await db_client.get_workflow(workflow_id, user_id) + + # Set org context early so tasks created by the transport inherit it + if workflow: + set_current_org_id(workflow.organization_id) + vad_config = None ambient_noise_config = None if workflow and workflow.workflow_configurations: @@ -156,6 +163,7 @@ async def run_pipeline_vonage( """ logger.info(f"Starting Vonage pipeline for workflow run {workflow_run_id}") set_current_run_id(workflow_run_id) + set_current_org_id(organization_id) # Store call ID in cost_info for later cost calculation (provider-agnostic) cost_info = {"call_id": call_uuid} @@ -226,6 +234,11 @@ async def run_pipeline_ari( # Get workflow to extract configurations workflow = await db_client.get_workflow(workflow_id, user_id) + + # Set org context early so tasks created by the transport inherit it + if workflow: + set_current_org_id(workflow.organization_id) + vad_config = None ambient_noise_config = None if workflow and workflow.workflow_configurations: @@ -281,6 +294,11 @@ async def run_pipeline_vobiz( await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info) workflow = await db_client.get_workflow(workflow_id, user_id) + + # Set org context early so tasks created by the transport inherit it + if workflow: + set_current_org_id(workflow.organization_id) + vad_config = None ambient_noise_config = None if workflow and workflow.workflow_configurations: @@ -350,6 +368,11 @@ async def run_pipeline_cloudonix( # Get workflow to extract all pipeline configurations workflow = await db_client.get_workflow(workflow_id, user_id) + + # Set org context early so tasks created by the transport inherit it + if workflow: + set_current_org_id(workflow.organization_id) + vad_config = None ambient_noise_config = None if workflow and workflow.workflow_configurations: @@ -397,6 +420,11 @@ async def run_pipeline_smallwebrtc( # Get workflow to extract all pipeline configurations workflow = await db_client.get_workflow(workflow_id, user_id) + + # Set org context early so tasks created by the transport inherit it + if workflow: + set_current_org_id(workflow.organization_id) + vad_config = None ambient_noise_config = None if workflow and workflow.workflow_configurations: diff --git a/api/services/pipecat/tracing_config.py b/api/services/pipecat/tracing_config.py index 20d95bb..f71ad27 100644 --- a/api/services/pipecat/tracing_config.py +++ b/api/services/pipecat/tracing_config.py @@ -1,8 +1,9 @@ import base64 -import os from loguru import logger from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import SpanProcessor +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from api.constants import ( ENABLE_TRACING, @@ -10,43 +11,229 @@ from api.constants import ( LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, ) +from pipecat.utils.run_context import get_current_org_id from pipecat.utils.tracing.setup import setup_tracing _tracing_initialized = False +_org_routing_exporter = None -def is_tracing_enabled(): - """Check if tracing should be enabled based on ENABLE_TRACING flag.""" - # Tracing is only enabled when ENABLE_TRACING is explicitly set to true - # This makes the system OSS-friendly by default (no external dependencies required) - return ENABLE_TRACING +class _OrgAttributeSpanProcessor(SpanProcessor): + """Stamps each span with the current org_id from the async context var.""" + + def on_start(self, span, parent_context=None): + from pipecat.utils.run_context import get_current_org_id + + org_id = get_current_org_id() + if org_id: + span.set_attribute("dograh.org_id", str(org_id)) + + def on_end(self, span): + pass + + def shutdown(self): + pass + + def force_flush(self, timeout_millis=30000): + return True -def setup_tracing_exporter(): - """Setup the OTEL tracing exporter for Langfuse if enabled. +class _OrgRoutingExporter(SpanExporter): + """Routes spans to org-specific or default Langfuse exporter. + + Spans with a ``dograh.org_id`` attribute whose org has registered + credentials are forwarded to that org's exporter. All other spans + go to the default exporter (env-var credentials). + """ + + def __init__(self, default_exporter): + self._default_exporter = default_exporter + self._org_exporters = {} + self._org_hosts = {} + + def get_org_host(self, org_id): + return self._org_hosts.get(str(org_id)) + + def register_org(self, org_id, host, public_key, secret_key): + key = str(org_id) + normalized_host = host.rstrip("/") + auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode() + endpoint = f"{normalized_host}/api/public/otel/v1/traces" + + # Skip if already registered with identical settings + if key in self._org_exporters: + existing = self._org_exporters[key] + if ( + self._org_hosts.get(key) == normalized_host + and getattr(existing, "_endpoint", None) == endpoint + and existing._headers.get("Authorization") == f"Basic {auth}" + ): + return + # Credentials changed — shut down the old exporter + logger.info(f"Updating OTEL exporter for org {org_id}") + existing.shutdown() + + self._org_hosts[key] = normalized_host + exporter = OTLPSpanExporter( + endpoint=endpoint, + headers={"Authorization": f"Basic {auth}"}, + ) + self._org_exporters[key] = exporter + logger.info(f"Registered OTEL exporter for org {org_id}") + + def unregister_org(self, org_id): + key = str(org_id) + exporter = self._org_exporters.pop(key, None) + self._org_hosts.pop(key, None) + if exporter: + exporter.shutdown() + logger.info(f"Unregistered OTEL exporter for org {org_id}") + + def export(self, spans): + default_spans = [] + org_buckets = {} + + for span in spans: + org_id = span.attributes.get("dograh.org_id") if span.attributes else None + if org_id and str(org_id) in self._org_exporters: + org_buckets.setdefault(str(org_id), []).append(span) + else: + default_spans.append(span) + + result = SpanExportResult.SUCCESS + + if default_spans and self._default_exporter: + r = self._default_exporter.export(default_spans) + if r != SpanExportResult.SUCCESS: + result = r + + for oid, batch in org_buckets.items(): + r = self._org_exporters[oid].export(batch) + if r != SpanExportResult.SUCCESS: + result = r + + return result + + def shutdown(self): + if self._default_exporter: + self._default_exporter.shutdown() + for exp in self._org_exporters.values(): + exp.shutdown() + + def force_flush(self, timeout_millis=30000): + ok = True + if self._default_exporter: + ok = self._default_exporter.force_flush(timeout_millis) and ok + for exp in self._org_exporters.values(): + ok = exp.force_flush(timeout_millis) and ok + return ok + + +def ensure_tracing() -> bool: + """Initialize OTEL tracing if enabled. Returns True if tracing is available. + + Installs an ``_OrgRoutingExporter`` so that spans can be routed to + org-specific Langfuse projects at export time. Idempotent — safe to call from both the pipeline process and the ARQ worker. """ - global _tracing_initialized + global _tracing_initialized, _org_routing_exporter if _tracing_initialized: - return + return True - if is_tracing_enabled(): - if not all([LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY]): - logger.warning( - "Warning: ENABLE_TRACING is true but Langfuse credentials are not configured. Tracing disabled." - ) - return + if not ENABLE_TRACING: + return False + # Build the default exporter from env-var credentials (may be None) + default_exporter = None + if all([LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY]): langfuse_auth = base64.b64encode( f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode() ).decode() - - os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = f"{LANGFUSE_HOST}/api/public/otel" - os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = ( - f"Authorization=Basic {langfuse_auth}" + default_exporter = OTLPSpanExporter( + endpoint=f"{LANGFUSE_HOST}/api/public/otel/v1/traces", + headers={"Authorization": f"Basic {langfuse_auth}"}, + ) + else: + logger.warning( + "ENABLE_TRACING is true but default Langfuse credentials are not configured. " + "Only org-level credentials will be used." ) - otlp_exporter = OTLPSpanExporter() - setup_tracing(service_name="dograh-pipeline", exporter=otlp_exporter) - _tracing_initialized = True + _org_routing_exporter = _OrgRoutingExporter(default_exporter) + setup_tracing(service_name="dograh-pipeline", exporter=_org_routing_exporter) + + # Add processor that stamps every span with the current org_id context var + from opentelemetry import trace as otel_trace + + provider = otel_trace.get_tracer_provider() + if hasattr(provider, "add_span_processor"): + provider.add_span_processor(_OrgAttributeSpanProcessor()) + + _tracing_initialized = True + return True + + +def register_org_langfuse_credentials(org_id, host, public_key, secret_key): + """Register or update org-specific Langfuse credentials for span routing. + + Safe to call multiple times — updates credentials if they changed. + """ + if not ensure_tracing(): + return + if not all([host, public_key, secret_key]): + logger.warning( + f"Incomplete Langfuse credentials for org {org_id}, skipping registration" + ) + return + _org_routing_exporter.register_org(org_id, host, public_key, secret_key) + + +def unregister_org_langfuse_credentials(org_id): + """Remove org-specific Langfuse credentials. Spans will fall back to the default exporter.""" + if not ensure_tracing(): + return + _org_routing_exporter.unregister_org(org_id) + + +async def load_all_org_langfuse_credentials(): + """Load Langfuse credentials for all orgs at startup. + + Called once during app lifespan so that org-specific exporters are ready + before any pipeline runs, without per-call DB lookups. + """ + if not ensure_tracing(): + return + + from api.db import db_client + from api.enums import OrganizationConfigurationKey + + configs = await db_client.get_all_configurations_by_key( + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + ) + for config in configs: + org_id = config["organization_id"] + value = config["value"] + register_org_langfuse_credentials( + org_id=org_id, + host=value.get("host"), + public_key=value.get("public_key"), + secret_key=value.get("secret_key"), + ) + logger.info(f"Loaded Langfuse credentials for {len(configs)} org(s)") + + +def get_trace_url(trace_id: str, org_id=None) -> str | None: + """Build a Langfuse trace URL, using org-specific host when available.""" + if org_id is None: + org_id = get_current_org_id() + + host = None + if org_id and _org_routing_exporter: + host = _org_routing_exporter.get_org_host(str(org_id)) + if not host: + host = LANGFUSE_HOST + if not host: + return None + + return f"{host.rstrip('/')}/trace/{trace_id}" diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 4649ec0..1905b46 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -25,6 +25,7 @@ if TYPE_CHECKING: from pipecat.services.anthropic.llm import AnthropicLLMService from pipecat.services.google.llm import GoogleLLMService from pipecat.services.openai.llm import OpenAILLMService + from pipecat.utils.tracing.tracing_context import TracingContext LLMService = Union[OpenAILLMService, AnthropicLLMService, GoogleLLMService] @@ -135,7 +136,9 @@ class PipecatEngine: Returns the turn-level context if available, otherwise the conversation-level context, or None. """ - tracing_ctx = getattr(self.task, "_tracing_context", None) + tracing_ctx: TracingContext | None = getattr( + self.task, "_tracing_context", None + ) if not tracing_ctx: return None return tracing_ctx.get_turn_context() or tracing_ctx.get_conversation_context() @@ -439,6 +442,10 @@ class PipecatEngine: ) ) self._gathered_context.update(extracted_data) + extracted_variables = self._gathered_context.setdefault( + "extracted_variables", {} + ) + extracted_variables.update(extracted_data) logger.debug( f"Variable extraction completed. Extracted: {extracted_data}" ) diff --git a/api/services/workflow/pipecat_engine_variable_extractor.py b/api/services/workflow/pipecat_engine_variable_extractor.py index 974c247..05e198b 100644 --- a/api/services/workflow/pipecat_engine_variable_extractor.py +++ b/api/services/workflow/pipecat_engine_variable_extractor.py @@ -7,7 +7,7 @@ from loguru import logger from opentelemetry import trace from api.services.gen_ai.json_parser import parse_llm_json -from api.services.pipecat.tracing_config import is_tracing_enabled +from api.services.pipecat.tracing_config import ensure_tracing from api.services.workflow.dto import ExtractionVariableDTO from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.utils.tracing.service_attributes import add_llm_span_attributes @@ -206,7 +206,7 @@ class VariableExtractionManager: # Get model name for tracing model_name = getattr(self._engine.llm, "model_name", "unknown") - if is_tracing_enabled(): + if ensure_tracing(): tracer = trace.get_tracer("pipecat") with tracer.start_as_current_span( "llm-variable-extraction", context=parent_ctx diff --git a/api/services/workflow/qa/tracing.py b/api/services/workflow/qa/tracing.py index ab2bc33..de92afc 100644 --- a/api/services/workflow/qa/tracing.py +++ b/api/services/workflow/qa/tracing.py @@ -5,18 +5,21 @@ import re from loguru import logger from api.db.models import WorkflowRunModel +from api.services.pipecat.tracing_config import get_trace_url def extract_trace_id(gathered_context: dict) -> str | None: """Extract Langfuse trace_id from gathered_context trace_url. - URL format: https://langfuse.dograh.com/project//traces/ + Supports both URL formats: + - New: https://langfuse.dograh.com/trace/ + - Legacy: https://langfuse.dograh.com/project//traces/ """ trace_url = gathered_context.get("trace_url") if not trace_url: return None try: - match = re.search(r"/traces/([a-fA-F0-9]+)$", trace_url) + match = re.search(r"/traces?/([a-fA-F0-9]+)$", trace_url) if match: return match.group(1) except Exception: @@ -37,16 +40,11 @@ def setup_langfuse_parent_context(workflow_run: WorkflowRunModel): set_span_in_context, ) - from api.services.pipecat.tracing_config import ( - is_tracing_enabled, - setup_tracing_exporter, - ) + from api.services.pipecat.tracing_config import ensure_tracing - if not is_tracing_enabled(): + if not ensure_tracing(): return None - setup_tracing_exporter() - gathered_context = workflow_run.gathered_context or {} trace_id = extract_trace_id(gathered_context) if not trace_id: @@ -114,17 +112,12 @@ def create_node_summary_trace( from opentelemetry import trace as otel_trace from opentelemetry.context import Context - from api.services.pipecat.tracing_config import ( - is_tracing_enabled, - setup_tracing_exporter, - ) + from api.services.pipecat.tracing_config import ensure_tracing from pipecat.utils.tracing.service_attributes import add_llm_span_attributes - if not is_tracing_enabled(): + if not ensure_tracing(): return None - setup_tracing_exporter() - tracer = otel_trace.get_tracer("pipecat") # Create a root span (new trace) for this node summary generation @@ -144,10 +137,7 @@ def create_node_summary_trace( ) trace_id = format(span.get_span_context().trace_id, "032x") - from langfuse import get_client - - langfuse = get_client() - return langfuse.get_trace_url(trace_id=trace_id) + return get_trace_url(trace_id) except Exception as e: logger.warning(f"Failed to create node summary trace for '{node_name}': {e}") diff --git a/api/services/workflow/tools/knowledge_base.py b/api/services/workflow/tools/knowledge_base.py index cd4c5b2..71a94e1 100644 --- a/api/services/workflow/tools/knowledge_base.py +++ b/api/services/workflow/tools/knowledge_base.py @@ -14,7 +14,7 @@ from opentelemetry import trace from api.db import db_client from api.services.gen_ai import OpenAIEmbeddingService -from api.services.pipecat.tracing_config import is_tracing_enabled +from api.services.pipecat.tracing_config import ensure_tracing async def retrieve_from_knowledge_base( @@ -51,7 +51,7 @@ async def retrieve_from_knowledge_base( - total_results: Number of results returned """ # Create span for retrieval operation if tracing is enabled - if is_tracing_enabled(): + if ensure_tracing(): try: parent_context = tracing_context diff --git a/api/tasks/run_integrations.py b/api/tasks/run_integrations.py index 5b61386..fab3f83 100644 --- a/api/tasks/run_integrations.py +++ b/api/tasks/run_integrations.py @@ -9,11 +9,13 @@ from loguru import logger from api.constants import BACKEND_API_ENDPOINT from api.db import db_client from api.db.models import WorkflowRunModel +from api.enums import OrganizationConfigurationKey +from api.services.pipecat.tracing_config import register_org_langfuse_credentials from api.services.workflow.qa import run_per_node_qa_analysis from api.utils.credential_auth import build_auth_header from api.utils.template_renderer import render_template from pipecat.utils.enums import EndTaskReason -from pipecat.utils.run_context import set_current_run_id +from pipecat.utils.run_context import set_current_org_id, set_current_run_id def _should_skip_qa( @@ -169,6 +171,22 @@ async def run_integrations_post_workflow_run(_ctx, workflow_run_id: int): logger.warning("No organization found, skipping integrations") return + # Set org context for tracing and register org-specific Langfuse credentials + # FIXME: If an org removes langfuse credentials during an exisitng deployment + # we should unregister an existing langfuse credentials for that org. + set_current_org_id(organization_id) + langfuse_config = await db_client.get_configuration_value( + organization_id, + OrganizationConfigurationKey.LANGFUSE_CREDENTIALS.value, + ) + if langfuse_config: + register_org_langfuse_credentials( + org_id=organization_id, + host=langfuse_config.get("host"), + public_key=langfuse_config.get("public_key"), + secret_key=langfuse_config.get("secret_key"), + ) + # Step 2: Get workflow definition (prefer the run-specific definition) if workflow_run.definition: workflow_definition = workflow_run.definition.workflow_json diff --git a/ui/src/app/handler/[...stack]/page.tsx b/ui/src/app/handler/[...stack]/page.tsx index 2ae9448..acb76b3 100644 --- a/ui/src/app/handler/[...stack]/page.tsx +++ b/ui/src/app/handler/[...stack]/page.tsx @@ -1,5 +1,6 @@ import { StackHandler } from "@stackframe/stack"; +import { TelemetrySection } from "@/components/TelemetrySection"; import { getAuthProvider } from "@/lib/auth/config"; import { BackButton } from "./BackButton"; @@ -28,6 +29,18 @@ export default async function Handler(props: unknown) { fullPage app={app!} routeProps={props} + componentProps={{ + AccountSettings: { + extraItems: [ + { + id: "telemetry", + title: "Telemetry", + iconName: "Key", + content: , + }, + ], + }, + }} /> diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx index acbe6cc..d71fcd9 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx @@ -6,7 +6,6 @@ import { useCallback, useEffect, useState } from "react"; import { getWorkflowApiV1WorkflowFetchWorkflowIdGet, getWorkflowRunsApiV1WorkflowWorkflowIdRunsGet } from "@/client/sdk.gen"; import { WorkflowRunResponseSchema } from "@/client/types.gen"; import { WorkflowRunsTable } from "@/components/workflow-runs"; -import { DISPOSITION_CODES } from "@/constants/dispositionCodes"; import { useAuth } from '@/lib/auth'; import { decodeFiltersFromURL, encodeFiltersToURL } from "@/lib/filters"; import { ActiveFilter, availableAttributes, FilterAttribute } from "@/types/filters"; @@ -60,17 +59,15 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti }); const workflow = response.data; - if (workflow?.call_disposition_codes) { - // Update the disposition code attribute with actual options + const codes = workflow?.call_disposition_codes?.disposition_codes; + if (codes && codes.length > 0) { setConfiguredAttributes(prev => prev.map(attr => { if (attr.id === 'dispositionCode') { return { ...attr, config: { ...attr.config, - options: Object.keys(workflow.call_disposition_codes || {}).length > 0 - ? Object.keys(workflow.call_disposition_codes || {}) - : [...DISPOSITION_CODES] + options: codes, } }; } diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index 650d68f..20aea4e 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; import { client as _heyApiClient } from './client.gen'; -import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateRecordingApiV1WorkflowRecordingsPostData, CreateRecordingApiV1WorkflowRecordingsPostError, CreateRecordingApiV1WorkflowRecordingsPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteRecordingApiV1WorkflowRecordingsRecordingIdDeleteData, DeleteRecordingApiV1WorkflowRecordingsRecordingIdDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadCampaignReportApiV1CampaignCampaignIdReportGetData, DownloadCampaignReportApiV1CampaignCampaignIdReportGetError, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostData, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostError, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetData, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetError, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetCurrentUserApiV1AuthMeGetData, GetCurrentUserApiV1AuthMeGetError, GetCurrentUserApiV1AuthMeGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetData, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetError, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetError, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostData, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostError, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListRecordingsApiV1WorkflowRecordingsGetData, ListRecordingsApiV1WorkflowRecordingsGetError, ListRecordingsApiV1WorkflowRecordingsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SignupApiV1AuthSignupPostData, SignupApiV1AuthSignupPostError, SignupApiV1AuthSignupPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCampaignApiV1CampaignCampaignIdPatchData, UpdateCampaignApiV1CampaignCampaignIdPatchError, UpdateCampaignApiV1CampaignCampaignIdPatchResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateRecordingApiV1WorkflowRecordingsPostData, CreateRecordingApiV1WorkflowRecordingsPostError, CreateRecordingApiV1WorkflowRecordingsPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteData, DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteError, DeleteRecordingApiV1WorkflowRecordingsRecordingIdDeleteData, DeleteRecordingApiV1WorkflowRecordingsRecordingIdDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadCampaignReportApiV1CampaignCampaignIdReportGetData, DownloadCampaignReportApiV1CampaignCampaignIdReportGetError, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostData, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostError, DuplicateWorkflowEndpointApiV1WorkflowWorkflowIdDuplicatePostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetData, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetError, GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetCurrentUserApiV1AuthMeGetData, GetCurrentUserApiV1AuthMeGetError, GetCurrentUserApiV1AuthMeGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetData, GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetError, GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetData, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetError, GetMpsCreditsApiV1OrganizationsUsageMpsCreditsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetError, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostData, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostError, GetUploadUrlApiV1WorkflowRecordingsUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListRecordingsApiV1WorkflowRecordingsGetData, ListRecordingsApiV1WorkflowRecordingsGetError, ListRecordingsApiV1WorkflowRecordingsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostData, SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostError, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SignupApiV1AuthSignupPostData, SignupApiV1AuthSignupPostError, SignupApiV1AuthSignupPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCampaignApiV1CampaignCampaignIdPatchData, UpdateCampaignApiV1CampaignCampaignIdPatchError, UpdateCampaignApiV1CampaignCampaignIdPatchResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -1012,6 +1012,43 @@ export const saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost = < }); }; +/** + * Delete Langfuse Credentials + * Delete Langfuse credentials for the user's organization. + */ +export const deleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDelete = (options?: Options) => { + return (options?.client ?? _heyApiClient).delete({ + url: '/api/v1/organizations/langfuse-credentials', + ...options + }); +}; + +/** + * Get Langfuse Credentials + * Get Langfuse credentials for the user's organization with masked sensitive fields. + */ +export const getLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGet = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/api/v1/organizations/langfuse-credentials', + ...options + }); +}; + +/** + * Save Langfuse Credentials + * Save Langfuse credentials for the user's organization. + */ +export const saveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPost = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/api/v1/organizations/langfuse-credentials', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); +}; + /** * Get Campaign Defaults * Get campaign limits for the user's organization. diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index ef287f0..b8cad0e 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -732,6 +732,19 @@ export type IntegrationResponse = { export type ItemKind = 'node' | 'edge' | 'workflow'; +export type LangfuseCredentialsRequest = { + host: string; + public_key: string; + secret_key: string; +}; + +export type LangfuseCredentialsResponse = { + host?: string; + public_key?: string; + secret_key?: string; + configured?: boolean; +}; + export type LastCampaignSettingsResponse = { retry_config?: RetryConfigResponse | null; max_concurrency?: number | null; @@ -3802,6 +3815,101 @@ export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostRespo 200: unknown; }; +export type DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteData = { + body?: never; + headers?: { + authorization?: string | null; + 'X-API-Key'?: string | null; + }; + path?: never; + query?: never; + url: '/api/v1/organizations/langfuse-credentials'; +}; + +export type DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteError = DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteErrors[keyof DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteErrors]; + +export type DeleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDeleteResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetData = { + body?: never; + headers?: { + authorization?: string | null; + 'X-API-Key'?: string | null; + }; + path?: never; + query?: never; + url: '/api/v1/organizations/langfuse-credentials'; +}; + +export type GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetError = GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetErrors[keyof GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetErrors]; + +export type GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetResponses = { + /** + * Successful Response + */ + 200: LangfuseCredentialsResponse; +}; + +export type GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetResponse = GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetResponses[keyof GetLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGetResponses]; + +export type SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostData = { + body: LangfuseCredentialsRequest; + headers?: { + authorization?: string | null; + 'X-API-Key'?: string | null; + }; + path?: never; + query?: never; + url: '/api/v1/organizations/langfuse-credentials'; +}; + +export type SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostError = SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostErrors[keyof SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostErrors]; + +export type SaveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPostResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type GetCampaignDefaultsApiV1OrganizationsCampaignDefaultsGetData = { body?: never; headers?: { diff --git a/ui/src/components/TelemetrySection.tsx b/ui/src/components/TelemetrySection.tsx new file mode 100644 index 0000000..c7e94a3 --- /dev/null +++ b/ui/src/components/TelemetrySection.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +import { + deleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDelete, + getLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGet, + saveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPost, +} from "@/client/sdk.gen"; +import type { LangfuseCredentialsResponse } from "@/client/types.gen"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function TelemetrySection() { + const [credentials, setCredentials] = useState({ + host: "", + public_key: "", + secret_key: "", + configured: false, + }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + fetchCredentials(); + }, []); + + async function fetchCredentials() { + try { + const { data } = await getLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsGet(); + if (data) { + setCredentials(data); + } + } catch { + // No credentials configured yet — that's fine + } finally { + setLoading(false); + } + } + + async function handleSave(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + const { error } = await saveLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsPost({ + body: { + host: credentials.host ?? "", + public_key: credentials.public_key ?? "", + secret_key: credentials.secret_key ?? "", + }, + }); + if (error) { + throw new Error("Failed to save"); + } + toast.success("Telemetry credentials saved"); + await fetchCredentials(); + } catch { + toast.error("Failed to save telemetry credentials"); + } finally { + setSaving(false); + } + } + + async function handleDelete() { + setSaving(true); + try { + await deleteLangfuseCredentialsApiV1OrganizationsLangfuseCredentialsDelete(); + setCredentials({ host: "", public_key: "", secret_key: "", configured: false }); + toast.success("Telemetry credentials removed"); + } catch { + toast.error("Failed to remove telemetry credentials"); + } finally { + setSaving(false); + } + } + + if (loading) { + return

Loading...

; + } + + return ( +
+

+ Connect your Langfuse project to receive call tracing data. +

+
+ + setCredentials({ ...credentials, host: e.target.value })} + required + /> +
+
+ + setCredentials({ ...credentials, public_key: e.target.value })} + required + /> +
+
+ + setCredentials({ ...credentials, secret_key: e.target.value })} + required + /> +
+
+ + {credentials.configured && ( + + )} +
+
+ ); +} diff --git a/ui/src/components/workflow-runs/CampaignRuns.tsx b/ui/src/components/workflow-runs/CampaignRuns.tsx index 459015d..986c350 100644 --- a/ui/src/components/workflow-runs/CampaignRuns.tsx +++ b/ui/src/components/workflow-runs/CampaignRuns.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; -import { getCampaignRunsApiV1CampaignCampaignIdRunsGet } from "@/client/sdk.gen"; +import { getCampaignRunsApiV1CampaignCampaignIdRunsGet, getWorkflowApiV1WorkflowFetchWorkflowIdGet } from "@/client/sdk.gen"; import { WorkflowRunResponseSchema } from "@/client/types.gen"; import { WorkflowRunsTable } from "@/components/workflow-runs"; import { useAuth } from "@/lib/auth"; @@ -49,6 +49,40 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : []; }); + const [configuredAttributes, setConfiguredAttributes] = useState(availableAttributes); + + // Load disposition codes from workflow configuration + const loadDispositionCodes = useCallback(async () => { + if (!isAuthenticated) return; + try { + const response = await getWorkflowApiV1WorkflowFetchWorkflowIdGet({ + path: { workflow_id: workflowId }, + }); + + const workflow = response.data; + const codes = workflow?.call_disposition_codes?.disposition_codes; + setConfiguredAttributes(prev => prev.map(attr => { + if (attr.id === 'dispositionCode') { + return { + ...attr, + config: { + ...attr.config, + options: codes, + } + }; + } + return attr; + })); + } + } catch (err) { + console.error("Failed to load disposition codes:", err); + } + }, [workflowId, isAuthenticated]); + + useEffect(() => { + loadDispositionCodes(); + }, [loadDispositionCodes]); + const fetchCampaignRuns = useCallback(async ( page: number, filters?: ActiveFilter[], @@ -176,7 +210,7 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR }, [fetchCampaignRuns, currentPage, appliedFilters, sortBy, sortOrder]); // Use a subset of filter attributes relevant for campaigns - const campaignFilterAttributes: FilterAttribute[] = availableAttributes.filter( + const campaignFilterAttributes: FilterAttribute[] = configuredAttributes.filter( attr => ['dateRange', 'dispositionCode', 'duration', 'status', 'tokenUsage'].includes(attr.id) );