mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: add redial option in campaigns
This commit is contained in:
parent
79116e6af2
commit
7fab959e26
14 changed files with 998 additions and 58 deletions
|
|
@ -5,15 +5,12 @@ from typing import Any, List, Optional
|
|||
|
||||
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:
|
||||
def _artifact_url(token: str | None, artifact: str) -> str:
|
||||
if not token:
|
||||
return ""
|
||||
events = logs.get("realtime_feedback_events", [])
|
||||
return generate_transcript_text(events).strip()
|
||||
return f"{BACKEND_API_ENDPOINT}/api/v1/public/download/workflow/{token}/{artifact}"
|
||||
|
||||
|
||||
def _collect_extracted_variable_keys(runs: List[Any]) -> list[str]:
|
||||
|
|
@ -28,20 +25,11 @@ def _collect_extracted_variable_keys(runs: List[Any]) -> list[str]:
|
|||
return list(keys)
|
||||
|
||||
|
||||
async def generate_campaign_report_csv(
|
||||
campaign_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
) -> tuple[io.StringIO, str]:
|
||||
"""Generate a CSV report for a campaign.
|
||||
def _build_run_report_csv(runs: List[Any]) -> io.StringIO:
|
||||
"""Build a CSV from completed workflow runs.
|
||||
|
||||
Returns a tuple of (csv_output, filename).
|
||||
Shared between campaign-scoped and workflow-scoped reports.
|
||||
"""
|
||||
runs = await db_client.get_completed_runs_for_report(
|
||||
campaign_id, start_date=start_date, end_date=end_date
|
||||
)
|
||||
|
||||
# Collect dynamic extracted variable columns
|
||||
extracted_var_keys = _collect_extracted_variable_keys(runs)
|
||||
|
||||
output = io.StringIO()
|
||||
|
|
@ -49,6 +37,9 @@ async def generate_campaign_report_csv(
|
|||
|
||||
pre_headers = [
|
||||
"Run ID",
|
||||
"Campaign ID",
|
||||
"Agent ID",
|
||||
"Agent Definition ID",
|
||||
"Created At",
|
||||
"Phone Number",
|
||||
"Call Disposition",
|
||||
|
|
@ -56,7 +47,7 @@ async def generate_campaign_report_csv(
|
|||
]
|
||||
post_headers = [
|
||||
"Call Tags",
|
||||
"Transcript",
|
||||
"Transcript URL",
|
||||
"Recording URL",
|
||||
]
|
||||
writer.writerow(pre_headers + extracted_var_keys + post_headers)
|
||||
|
|
@ -66,19 +57,15 @@ async def generate_campaign_report_csv(
|
|||
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.campaign_id if run.campaign_id is not None else "",
|
||||
run.workflow_id,
|
||||
run.definition_id if run.definition_id is not None else "",
|
||||
run.created_at.isoformat() if run.created_at else "",
|
||||
initial.get("phone_number", ""),
|
||||
gathered.get("mapped_call_disposition", ""),
|
||||
|
|
@ -92,12 +79,35 @@ async def generate_campaign_report_csv(
|
|||
|
||||
post_values = [
|
||||
call_tags,
|
||||
_transcript_from_logs(run.logs),
|
||||
recording_url,
|
||||
_artifact_url(run.public_access_token, "transcript"),
|
||||
_artifact_url(run.public_access_token, "recording"),
|
||||
]
|
||||
|
||||
writer.writerow(pre_values + extracted_values + post_values)
|
||||
|
||||
output.seek(0)
|
||||
filename = f"campaign_{campaign_id}_report.csv"
|
||||
return output, filename
|
||||
return output
|
||||
|
||||
|
||||
async def generate_campaign_report_csv(
|
||||
campaign_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
) -> tuple[io.StringIO, str]:
|
||||
"""Generate a CSV report for a campaign."""
|
||||
runs = await db_client.get_completed_runs_for_report(
|
||||
campaign_id=campaign_id, start_date=start_date, end_date=end_date
|
||||
)
|
||||
return _build_run_report_csv(runs), f"campaign_{campaign_id}_report.csv"
|
||||
|
||||
|
||||
async def generate_workflow_report_csv(
|
||||
workflow_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
) -> tuple[io.StringIO, str]:
|
||||
"""Generate a CSV report for all completed runs of a workflow."""
|
||||
runs = await db_client.get_completed_runs_for_report(
|
||||
workflow_id=workflow_id, start_date=start_date, end_date=end_date
|
||||
)
|
||||
return _build_run_report_csv(runs), f"workflow_{workflow_id}_report.csv"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ from typing import Any, Dict
|
|||
from loguru import logger
|
||||
|
||||
from api.db import db_client
|
||||
from api.services.campaign.campaign_event_publisher import (
|
||||
get_campaign_event_publisher,
|
||||
)
|
||||
from api.services.campaign.circuit_breaker import circuit_breaker
|
||||
from api.tasks.arq import enqueue_job
|
||||
from api.tasks.function_names import FunctionNames
|
||||
|
|
@ -24,6 +27,29 @@ class CampaignRunnerService:
|
|||
f"Campaign must be in 'created' state to start, current state: {campaign.state}"
|
||||
)
|
||||
|
||||
# Redial campaigns have queued_runs pre-seeded from the parent campaign,
|
||||
# so skip source sync and transition straight to 'running'.
|
||||
is_redial = bool(
|
||||
(campaign.orchestrator_metadata or {}).get("parent_campaign_id")
|
||||
)
|
||||
if is_redial:
|
||||
now = datetime.now(UTC)
|
||||
await db_client.update_campaign(
|
||||
campaign_id=campaign_id,
|
||||
state="running",
|
||||
started_at=now,
|
||||
source_last_synced_at=now,
|
||||
)
|
||||
publisher = await get_campaign_event_publisher()
|
||||
await publisher.publish_sync_completed(
|
||||
campaign_id=campaign_id,
|
||||
total_rows=campaign.total_rows or 0,
|
||||
source_type=campaign.source_type,
|
||||
source_id=campaign.source_id,
|
||||
)
|
||||
logger.info(f"Redial campaign {campaign_id} started, source sync skipped")
|
||||
return
|
||||
|
||||
# Update campaign state to syncing
|
||||
await db_client.update_campaign(
|
||||
campaign_id=campaign_id,
|
||||
|
|
|
|||
|
|
@ -64,14 +64,29 @@ class S3FileSystem(BaseFileSystem):
|
|||
) as s3_client:
|
||||
params = {"Bucket": self.bucket_name, "Key": file_path}
|
||||
|
||||
# Make transcripts viewable inline in the browser when requested
|
||||
if force_inline and file_path.endswith(".txt"):
|
||||
params.update(
|
||||
{
|
||||
"ResponseContentType": "text/plain",
|
||||
"ResponseContentDisposition": "inline",
|
||||
}
|
||||
)
|
||||
# Make artifacts viewable inline in the browser when requested
|
||||
if force_inline:
|
||||
if file_path.endswith(".txt"):
|
||||
params.update(
|
||||
{
|
||||
"ResponseContentType": "text/plain",
|
||||
"ResponseContentDisposition": "inline",
|
||||
}
|
||||
)
|
||||
elif file_path.endswith(".wav"):
|
||||
params.update(
|
||||
{
|
||||
"ResponseContentType": "audio/wav",
|
||||
"ResponseContentDisposition": "inline",
|
||||
}
|
||||
)
|
||||
elif file_path.endswith(".mp3"):
|
||||
params.update(
|
||||
{
|
||||
"ResponseContentType": "audio/mpeg",
|
||||
"ResponseContentDisposition": "inline",
|
||||
}
|
||||
)
|
||||
|
||||
url = await s3_client.generate_presigned_url(
|
||||
"get_object",
|
||||
|
|
|
|||
|
|
@ -672,6 +672,13 @@ class PipecatEngine:
|
|||
self._gathered_context["call_disposition"] = reason
|
||||
self._gathered_context["mapped_call_disposition"] = mapped_disposition
|
||||
|
||||
effective_disposition = self._gathered_context.get("call_disposition", "")
|
||||
if effective_disposition:
|
||||
call_tags = self._gathered_context.get("call_tags", [])
|
||||
if effective_disposition not in call_tags:
|
||||
call_tags.append(effective_disposition)
|
||||
self._gathered_context["call_tags"] = call_tags
|
||||
|
||||
logger.debug(
|
||||
f"Finishing run with reason: {reason}, disposition: {mapped_disposition} queueing frame {frame_to_push}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -370,8 +370,8 @@ class CustomToolManager:
|
|||
logger.info(f"End call reason: {reason}")
|
||||
self._engine._gathered_context["call_disposition"] = reason
|
||||
call_tags = self._engine._gathered_context.get("call_tags", [])
|
||||
if reason not in call_tags:
|
||||
call_tags.extend([reason, "end_call_tool"])
|
||||
if "end_call_tool" not in call_tags:
|
||||
call_tags.append("end_call_tool")
|
||||
self._engine._gathered_context["call_tags"] = call_tags
|
||||
|
||||
# Send result callback first
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue