Revert "sales push"

This reverts commit 8e907a6a95.
This commit is contained in:
Magzhan Zhumabayev
2026-05-09 19:23:21 +05:00
parent 8e907a6a95
commit d05c6c86a5
17 changed files with 15 additions and 436 deletions
-11
View File
@@ -80,14 +80,3 @@ SEMANTIC_ENDPOINTING_HOLD_MS=600
REALTIME_VOICE_FILLER_DELAY_MS=300
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_CHARS=10
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_WORDS=2
CRM_INTERACTION_ENABLED=false
CRM_INTERACTION_SERVICE_URL=http://interaction-service:8000
CRM_APP_TOKEN_SECRET=
CRM_QUEUE_ID=
SALES_SYNC_ENABLED=true
SALES_SERVICE_URL=http://sales-service:8000
SALES_SYNC_TIMEOUT_SECONDS=8
REALTIME_VOICE_QUEUE_ID=
REALTIME_VOICE_QUEUE_CODE=
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>13.4.0</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<copyLibs>true</copyLibs>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds" />
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
</list>
</option>
</component>
</project>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-3c075d73:1954b64a9de:-7fdf" />
</MTProjectMetadataState>
</option>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/realtime_voice_service.iml" filepath="$PROJECT_DIR$/.idea/realtime_voice_service.iml" />
</modules>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
-110
View File
@@ -1,110 +0,0 @@
from __future__ import annotations
import math
import struct
from array import array
try:
import audioop as _audioop
except ModuleNotFoundError:
try:
import audioop_lts as _audioop # type: ignore[import-not-found]
except ModuleNotFoundError:
_audioop = None
def _native_is_little_endian() -> bool:
return struct.pack("=h", 1) == struct.pack("<h", 1)
def _read_int16_samples(fragment: bytes, width: int) -> array:
if width != 2:
raise NotImplementedError("audioop_compat fallback currently supports only 16-bit PCM")
samples = array("h")
samples.frombytes(fragment)
if not _native_is_little_endian():
samples.byteswap()
return samples
def _write_int16_samples(samples: array) -> bytes:
output = array("h", samples)
if not _native_is_little_endian():
output.byteswap()
return output.tobytes()
def _clip_int16(value: float) -> int:
return max(-32768, min(32767, int(round(value))))
class _AudioopFallback:
@staticmethod
def rms(fragment: bytes, width: int) -> int:
samples = _read_int16_samples(fragment, width)
if not samples:
return 0
mean_square = sum(sample * sample for sample in samples) / len(samples)
return int(math.sqrt(mean_square))
@staticmethod
def max(fragment: bytes, width: int) -> int:
samples = _read_int16_samples(fragment, width)
if not samples:
return 0
return max(abs(sample) for sample in samples)
@staticmethod
def tomono(fragment: bytes, width: int, lfactor: float, rfactor: float) -> bytes:
samples = _read_int16_samples(fragment, width)
if len(samples) % 2 != 0:
raise ValueError("Stereo PCM must contain an even number of samples")
mono = array("h")
for index in range(0, len(samples), 2):
left = samples[index]
right = samples[index + 1]
mono.append(_clip_int16((left * lfactor) + (right * rfactor)))
return _write_int16_samples(mono)
@staticmethod
def ratecv(
fragment: bytes,
width: int,
nchannels: int,
inrate: int,
outrate: int,
state,
weightA: int = 1,
weightB: int = 0,
) -> tuple[bytes, None]:
del state, weightA, weightB
if nchannels <= 0:
raise ValueError("nchannels must be positive")
if inrate <= 0 or outrate <= 0:
raise ValueError("Sample rates must be positive")
samples = _read_int16_samples(fragment, width)
if not samples or inrate == outrate:
return fragment, None
if len(samples) % nchannels != 0:
raise ValueError("PCM fragment size does not match channel count")
frame_count = len(samples) // nchannels
output_frame_count = max(1, int(round(frame_count * outrate / inrate)))
output = array("h")
for out_index in range(output_frame_count):
position = out_index * inrate / outrate
left_index = min(int(position), frame_count - 1)
right_index = min(left_index + 1, frame_count - 1)
fraction = max(0.0, min(1.0, position - left_index))
for channel_index in range(nchannels):
left_sample = samples[(left_index * nchannels) + channel_index]
right_sample = samples[(right_index * nchannels) + channel_index]
interpolated = left_sample + ((right_sample - left_sample) * fraction)
output.append(_clip_int16(interpolated))
return _write_int16_samples(output), None
audioop = _audioop or _AudioopFallback()
__all__ = ["audioop"]
+1 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import audioop
import io
import logging
import os
@@ -9,7 +10,6 @@ import wave
from collections.abc import AsyncIterable
from collections.abc import Iterable
from realtime_voice_service.audioop_compat import audioop
from realtime_voice_service.providers.base import BaseTTS
-8
View File
@@ -1214,14 +1214,6 @@ class CallSession:
def conversation(self) -> tuple[tuple[str, str], ...]:
return tuple(self._conversation)
@property
def customer_name(self) -> str | None:
return self._customer_name
@property
def session_language(self) -> str | None:
return self._session_language
async def run(self) -> None:
if self._assistant_task is not None:
raise RuntimeError("CallSession.run() can only be called once per session")
-84
View File
@@ -6,12 +6,10 @@ import logging
import os
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, WebSocket
from realtime_voice_service import crm_client
from realtime_voice_service import sales_sync_client
from realtime_voice_service.core.filler_audio import FillerAudioLibrary
from realtime_voice_service.core.session import CallSession
from realtime_voice_service.core.session import _normalize_voice_pronunciation
@@ -150,52 +148,6 @@ def _llm_model_name() -> str:
return str(os.getenv("OPENAI_LLM_MODEL", "gpt-4o-mini")).strip() or "gpt-4o-mini"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _conversation_transcript(session: CallSession) -> str | None:
lines = [f"{speaker}: {text.strip()}" for speaker, text in session.conversation if str(text).strip()]
if not lines:
return None
return "\n".join(lines)
def _conversation_summary(session: CallSession) -> str | None:
user_turns = [text.strip() for speaker, text in session.conversation if speaker == "user" and str(text).strip()]
assistant_turns = [text.strip() for speaker, text in session.conversation if speaker == "assistant" and str(text).strip()]
if not user_turns and not assistant_turns:
return None
parts: list[str] = []
if user_turns:
parts.append(f"Customer asked: {user_turns[0][:220]}")
if assistant_turns:
parts.append(f"Assistant response: {assistant_turns[-1][:220]}")
if session.customer_name:
parts.append(f"Customer name: {session.customer_name}")
return " | ".join(parts)[:700]
def _session_sync_metadata(
session: CallSession,
transport: BaseMediaTransport,
*,
failure_reason: str | None = None,
) -> dict[str, object]:
return {
"protocol": transport.protocol,
"sample_rate_hz": transport.sample_rate_hz,
"frame_duration_ms": transport.frame_duration_ms,
"frame_bytes": transport.frame_bytes,
"session_language": session.session_language,
"customer_name": session.customer_name,
"interruptions": list(session.interruptions),
"latency_ms": dict(session.last_latency_ms),
"conversation_entries": len(session.conversation),
"failure_reason": failure_reason,
}
class RealtimeVoiceService:
def __init__(self) -> None:
@@ -324,23 +276,8 @@ class RealtimeVoiceService:
async def _run_transport_session(self, transport: BaseMediaTransport) -> None:
session = self._build_session(transport)
started_at = _utc_now_iso()
interaction_id = await crm_client.create_interaction(call_id=session.session_id)
await sales_sync_client.sync_voice_session(
call_id=session.session_id,
interaction_id=interaction_id,
voice_session_id=session.session_id,
ai_state=str(session.state.value).lower(),
telephony_status="connected",
call_status="started",
started_at=started_at,
summary="Realtime voice session started",
metadata=_session_sync_metadata(session, transport),
)
failure_reason: str | None = None
call_status = "completed"
async with self._track_session(session):
try:
LOGGER.info(
"starting realtime session %s via %s sample_rate=%s frame_ms=%s frame_bytes=%s",
session.session_id,
@@ -350,25 +287,6 @@ class RealtimeVoiceService:
transport.frame_bytes,
)
await session.run()
except Exception as exc:
call_status = "failed"
failure_reason = str(exc)[:300]
raise
finally:
await sales_sync_client.sync_voice_session(
call_id=session.session_id,
interaction_id=interaction_id,
caller_name=session.customer_name,
voice_session_id=session.session_id,
ai_state=str(session.state.value).lower(),
telephony_status="ended",
call_status=call_status,
started_at=started_at,
ended_at=_utc_now_iso(),
summary=_conversation_summary(session),
transcript_text=_conversation_transcript(session),
metadata=_session_sync_metadata(session, transport, failure_reason=failure_reason),
)
if interaction_id:
await crm_client.close_interaction(interaction_id)
@@ -446,8 +364,6 @@ async def health() -> dict[str, object]:
"sample_rate_hz": service.sample_rate_hz,
"active_sessions": service.active_session_count,
"audiosocket_port": _audiosocket_port(),
"sales_sync_enabled": os.getenv("SALES_SYNC_ENABLED", "0"),
"crm_interaction_enabled": os.getenv("CRM_INTERACTION_ENABLED", "0"),
}
+1 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import audioop
import base64
import contextlib
import io
@@ -12,7 +13,6 @@ import wave
from collections.abc import Sequence
from urllib.parse import urlencode
from realtime_voice_service.audioop_compat import audioop
from realtime_voice_service.providers.base import BaseSTT
from realtime_voice_service.providers.base import BaseSTTStream
from realtime_voice_service.providers.base import PartialTranscriptCallback
+1 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import audioop
import base64
import json
import logging
@@ -11,7 +12,6 @@ from collections.abc import AsyncIterable
from urllib.parse import quote
from urllib.parse import urlencode
from realtime_voice_service.audioop_compat import audioop
from realtime_voice_service.providers.base import BaseTTS
-126
View File
@@ -1,126 +0,0 @@
from __future__ import annotations
import logging
import os
from typing import Any
import httpx
from realtime_voice_service.crm_client import _issue_service_token
LOGGER = logging.getLogger("uvicorn.error")
def _enabled() -> bool:
return os.getenv("SALES_SYNC_ENABLED", "0").strip().lower() in {"1", "true", "yes", "on"}
def _base_url() -> str:
return str(os.getenv("SALES_SERVICE_URL", "http://sales-service:8000")).rstrip("/")
def _timeout_seconds() -> float:
raw = str(os.getenv("SALES_SYNC_TIMEOUT_SECONDS", "8")).strip()
try:
return max(float(raw), 1.0)
except ValueError:
return 8.0
def _secret() -> str:
return str(os.getenv("CRM_APP_TOKEN_SECRET", "")).strip()
def _service_headers() -> dict[str, str] | None:
secret = _secret()
if not secret:
LOGGER.warning("sales sync: CRM_APP_TOKEN_SECRET not configured")
return None
token = _issue_service_token(secret)
return {"Authorization": f"Bearer {token}"}
def _queue_id() -> str | None:
raw = str(
os.getenv("REALTIME_VOICE_QUEUE_ID")
or os.getenv("CRM_QUEUE_ID")
or ""
).strip()
return raw or None
def _queue_code() -> str | None:
raw = str(os.getenv("REALTIME_VOICE_QUEUE_CODE", "")).strip()
return raw or None
async def sync_voice_session(
*,
call_id: str,
interaction_id: str | None = None,
caller_number: str | None = None,
caller_name: str | None = None,
voice_session_id: str | None = None,
ai_session_id: str | None = None,
ai_state: str | None = None,
handoff_reason: str | None = None,
telephony_status: str | None = None,
call_status: str | None = None,
started_at: str | None = None,
ended_at: str | None = None,
summary: str | None = None,
transcript_text: str | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
if not _enabled():
return None
headers = _service_headers()
if headers is None:
return None
payload: dict[str, Any] = {
"call_id": call_id,
"interaction_id": interaction_id,
"queue_id": _queue_id(),
"queue_code": _queue_code(),
"caller_number": caller_number,
"caller_name": caller_name,
"voice_session_id": voice_session_id or call_id,
"ai_session_id": ai_session_id,
"ai_state": ai_state,
"handoff_reason": handoff_reason,
"telephony_status": telephony_status,
"call_status": call_status,
"started_at": started_at,
"ended_at": ended_at,
"summary": summary,
"transcript_text": transcript_text,
"metadata": {
"source_service": "realtime_voice_service",
**(metadata or {}),
},
}
try:
async with httpx.AsyncClient(timeout=_timeout_seconds()) as client:
response = await client.post(
f"{_base_url()}/internal/sales-sync/voice",
json=payload,
headers=headers,
)
response.raise_for_status()
data = response.json()
LOGGER.info(
"sales sync: voice session synced call_id=%s voice_session_id=%s status=%s",
call_id,
voice_session_id or call_id,
call_status,
)
return data if isinstance(data, dict) else None
except Exception:
LOGGER.exception(
"sales sync: failed to sync voice session call_id=%s voice_session_id=%s",
call_id,
voice_session_id or call_id,
)
return None
+1 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import audioop
import logging
import os
import struct
@@ -8,7 +9,6 @@ import time
import uuid
from typing import Awaitable, Callable
from realtime_voice_service.audioop_compat import audioop
from realtime_voice_service.transports.base import BaseMediaTransport