mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
feat: custom telemetry configuration
This commit is contained in:
parent
1967a71935
commit
affb39e57f
23 changed files with 927 additions and 139 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue