feat: custom telemetry configuration

This commit is contained in:
Abhishek Kumar 2026-03-23 11:36:39 +05:30
parent 1967a71935
commit affb39e57f
23 changed files with 927 additions and 139 deletions

View file

@ -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",

View file

@ -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

View file

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