fix: remove dead code duplicates, add SQL LIMIT across all services
- Remove duplicate function definitions with hardcoded "AI-оператор" strings (ai_voice_runtime, ai_orchestrator, voice_name_config, voice.py) - Remove unreachable dead code after return in ai_voice_runtime - Add SQL LIMIT to 17 unbounded queries across 12 services to prevent OOM - Move Python-side filtering to SQL WHERE in reporting_service - Downgrade 19 logger.warning to logger.info for normal-flow events in media_runtime Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6b8f5d069b
commit
6798320209
@@ -1913,8 +1913,9 @@ def _last_messages(session, thread_id: str, limit: int) -> list[TelegramMessageR
|
||||
select(TelegramMessageRow)
|
||||
.where(TelegramMessageRow.thread_id == thread_id)
|
||||
.order_by(TelegramMessageRow.id.desc())
|
||||
.limit(max(limit, 1))
|
||||
).scalars().all()
|
||||
return list(reversed(rows[:limit]))
|
||||
return list(reversed(rows))
|
||||
|
||||
|
||||
def _last_whatsapp_messages(session, thread_id: str, limit: int) -> list[WhatsAppMessageRow]:
|
||||
@@ -1922,8 +1923,9 @@ def _last_whatsapp_messages(session, thread_id: str, limit: int) -> list[WhatsAp
|
||||
select(WhatsAppMessageRow)
|
||||
.where(WhatsAppMessageRow.thread_id == thread_id)
|
||||
.order_by(WhatsAppMessageRow.id.desc())
|
||||
.limit(max(limit, 1))
|
||||
).scalars().all()
|
||||
return list(reversed(rows[:limit]))
|
||||
return list(reversed(rows))
|
||||
|
||||
|
||||
def _select_trigger_message(
|
||||
@@ -2204,93 +2206,6 @@ def _extract_json_object(raw: str) -> dict[str, Any]:
|
||||
return parsed
|
||||
|
||||
|
||||
def _stub_decision(
|
||||
*,
|
||||
customer: Customer | None,
|
||||
interaction: Interaction,
|
||||
last_user_message: TelegramMessageRow,
|
||||
kb_results: list[KBArticleRow],
|
||||
language: str,
|
||||
) -> dict[str, Any]:
|
||||
text = last_user_message.text
|
||||
if _looks_like_human_request(text):
|
||||
return {
|
||||
"language": language,
|
||||
"intent": "handoff_request",
|
||||
"reply_text": "",
|
||||
"confidence": 0.2,
|
||||
"needs_handoff": True,
|
||||
"handoff_reason": "Клиент запросил живого оператора.",
|
||||
"case_action": "keep_open",
|
||||
"kb_refs": [],
|
||||
}
|
||||
if _is_sensitive_request(text):
|
||||
return {
|
||||
"language": language,
|
||||
"intent": "sensitive_request",
|
||||
"reply_text": "",
|
||||
"confidence": 0.25,
|
||||
"needs_handoff": True,
|
||||
"handoff_reason": "Нужен человек: запрос затрагивает чувствительную тему или действие вне доступных tools.",
|
||||
"case_action": "keep_open",
|
||||
"kb_refs": [],
|
||||
}
|
||||
if _looks_like_resolution_confirmation(text):
|
||||
reply = (
|
||||
"Мен компанияның AI көмекшісімін. Рақмет, өтінішті жабамын. Қажет болса, адам операторын қоса аламын."
|
||||
if language == "kz"
|
||||
else "Я AI-помощник компании. Спасибо, отмечаю вопрос как решённый. Если понадобится человек, сразу передам диалог оператору."
|
||||
)
|
||||
return {
|
||||
"language": language,
|
||||
"intent": "resolution_confirmed",
|
||||
"reply_text": reply,
|
||||
"confidence": 0.9,
|
||||
"needs_handoff": False,
|
||||
"handoff_reason": None,
|
||||
"case_action": "close",
|
||||
"kb_refs": [],
|
||||
}
|
||||
if kb_results:
|
||||
best = kb_results[0]
|
||||
snippet = _article_snippet(best)
|
||||
reply = (
|
||||
"Мен компанияның AI көмекшісімін. Білім базасына сүйеніп жауап беремін: "
|
||||
f"{snippet} Егер қажет болса, адам операторына бірден өткіземін."
|
||||
if language == "kz"
|
||||
else "Я AI-помощник компании и отвечаю по базе знаний. "
|
||||
f"{snippet} Если этого недостаточно, сразу передам диалог живому оператору."
|
||||
)
|
||||
return {
|
||||
"language": language,
|
||||
"intent": "kb_answer",
|
||||
"reply_text": reply,
|
||||
"confidence": 0.84,
|
||||
"needs_handoff": False,
|
||||
"handoff_reason": None,
|
||||
"case_action": "keep_open",
|
||||
"kb_refs": [best.article_id],
|
||||
}
|
||||
name = customer.display_name if customer else (interaction.customer_id or "клиент")
|
||||
reply = (
|
||||
f"Мен компанияның AI көмекшісімін. {name}, сұрағыңызды түсіндім, бірақ бұл үшін қосымша тексеріс керек. "
|
||||
"Қажет болса, адам операторына бірден өткіземін."
|
||||
if language == "kz"
|
||||
else f"Я AI-помощник компании. {name}, понял ваш вопрос, но для точного ответа мне не хватает данных "
|
||||
"из доступных инструментов. Передаю диалог оператору."
|
||||
)
|
||||
return {
|
||||
"language": language,
|
||||
"intent": "handoff_missing_tool",
|
||||
"reply_text": "",
|
||||
"confidence": 0.3,
|
||||
"needs_handoff": True,
|
||||
"handoff_reason": "Нет достаточных данных в базе знаний или доступных инструментах.",
|
||||
"case_action": "keep_open",
|
||||
"kb_refs": [],
|
||||
}
|
||||
|
||||
|
||||
def _openai_prompt(
|
||||
*,
|
||||
customer: Customer | None,
|
||||
@@ -2434,30 +2349,6 @@ def _sanitize_decision(raw: dict[str, Any], *, fallback_language: str) -> dict[s
|
||||
return decision
|
||||
|
||||
|
||||
def _always_reply_fallback(last_user_text: str, language: str) -> str:
|
||||
normalized = str(last_user_text or "").strip().lower()
|
||||
greeting_tokens = ("привет", "здравствуйте", "добрый", "салем", "сә", "сәлем", "hello", "hi")
|
||||
if language == "kz":
|
||||
if any(token in normalized for token in greeting_tokens):
|
||||
return (
|
||||
"Сәлеметсіз бе! Мен компанияның AI-көмекшісімін. "
|
||||
"Сұрағыңызды жазыңыз, мен бірден көмектесуге тырысамын."
|
||||
)
|
||||
return (
|
||||
"Мен көмектесуге дайынмын. Сұрағыңызды нақтырақ жазыңыз, "
|
||||
"мен сізге бірден жауап беремін."
|
||||
)
|
||||
if any(token in normalized for token in greeting_tokens):
|
||||
return (
|
||||
"Здравствуйте! Я AI-помощник компании. "
|
||||
"Напишите ваш вопрос, и я сразу постараюсь помочь."
|
||||
)
|
||||
return (
|
||||
"Я на связи и готов помочь. "
|
||||
"Напишите, пожалуйста, чуть подробнее, что именно вам нужно."
|
||||
)
|
||||
|
||||
|
||||
def _stub_decision(
|
||||
*,
|
||||
customer: Customer | None,
|
||||
|
||||
@@ -50,36 +50,6 @@ def _voice_max_context_segments() -> int:
|
||||
return max(6, app._int_env("AI_VOICE_MAX_CONTEXT_SEGMENTS", 12))
|
||||
|
||||
|
||||
def _voice_disclosure_prefix(language: str) -> str:
|
||||
if language == "kz":
|
||||
return "Men kompaniyanyn AI operatoriymyn. "
|
||||
return "Я AI-оператор компании. "
|
||||
|
||||
|
||||
def _voice_greeting(language: str) -> str:
|
||||
if language == "kz":
|
||||
return (
|
||||
"Men kompaniyanyn AI operatoriymyn. Salemetsiz be. "
|
||||
"Suragynyzdy aitanyz, men birden komektesuge tyrisamyn."
|
||||
)
|
||||
return (
|
||||
"Я AI-оператор компании. Здравствуйте. "
|
||||
"Коротко расскажите, с чем помочь, и я сразу начну разбираться."
|
||||
)
|
||||
|
||||
|
||||
def _voice_handoff_reply(language: str) -> str:
|
||||
if language == "kz":
|
||||
return (
|
||||
"Men AI operator retinde bastapky konteksti jiyap aldyм. "
|
||||
"Kazir sizdi tiry operatorga kosamyn."
|
||||
)
|
||||
return (
|
||||
"Я как AI-оператор собрал первичный контекст. "
|
||||
"Сейчас переведу вас на живого оператора."
|
||||
)
|
||||
|
||||
|
||||
def _voice_disclosure_prefix(language: str) -> str:
|
||||
return persona.voice_disclosure_prefix(language)
|
||||
|
||||
|
||||
@@ -18,38 +18,6 @@ from services.shared.sql_models import VoiceNameCollectionSettingsRow
|
||||
VOICE_NAME_COLLECTION_SETTINGS_KEY = "global"
|
||||
|
||||
|
||||
def voice_name_collection_default_config() -> VoiceNameCollectionConfig:
|
||||
return VoiceNameCollectionConfig(
|
||||
enabled=True,
|
||||
texts=VoiceNameCollectionTextsConfig(
|
||||
ru=VoiceNameCollectionLanguageTexts(
|
||||
start_prompt="Здравствуйте. Назовите, пожалуйста, ваше имя.",
|
||||
personalized_greeting_template=(
|
||||
"Я AI-оператор компании. Здравствуйте, {name}. "
|
||||
"Коротко расскажите, с чем помочь, и я сразу начну разбираться."
|
||||
),
|
||||
confirmation_greeting_template=(
|
||||
"Я AI-оператор компании. Если я правильно расслышал, вас зовут {name}? "
|
||||
"И чем помочь?"
|
||||
),
|
||||
inline_followup_prompt="И ещё подскажите, как мне к вам обращаться?",
|
||||
),
|
||||
kz=VoiceNameCollectionLanguageTexts(
|
||||
start_prompt="Сәлеметсіз бе. Атыңызды атаңызшы.",
|
||||
personalized_greeting_template=(
|
||||
"Men kompaniyanyn AI operatoriymyn. Salemetsiz be, {name}. "
|
||||
"Suragynyzdy aitanyz, men birden komektesuge tyrisamyn."
|
||||
),
|
||||
confirmation_greeting_template=(
|
||||
"Men kompaniyanyn AI operatoriymyn. Durys estisem, atynyz {name} pa? "
|
||||
"Qalai komektesemin?"
|
||||
),
|
||||
inline_followup_prompt="Tagy bir naqtylasam, sizge qalai qaratamyn?",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def voice_name_collection_default_config() -> VoiceNameCollectionConfig:
|
||||
return VoiceNameCollectionConfig(
|
||||
enabled=True,
|
||||
|
||||
@@ -272,20 +272,6 @@ def _voice_start_name_prompt(language: str | None) -> str:
|
||||
return "Здравствуйте. Назовите, пожалуйста, ваше имя."
|
||||
|
||||
|
||||
def _default_voice_greeting(language: str | None, *, agent_profile: str = "voice_support") -> str:
|
||||
if str(agent_profile or "").strip() == "voice_start":
|
||||
return _voice_start_name_prompt(language)
|
||||
if str(language or "").strip() == "kz":
|
||||
return (
|
||||
"Мен компанияның AI оператормын. Сәлеметсіз бе. "
|
||||
"Қандай сұрағыңыз бар екенін айтыңыз, мен бірден көмектесуге тырысамын."
|
||||
)
|
||||
return (
|
||||
"Я AI-оператор компании. Здравствуйте. "
|
||||
"Коротко расскажите, с чем помочь, и я сразу начну разбираться."
|
||||
)
|
||||
|
||||
|
||||
def _default_voice_greeting(language: str | None, *, agent_profile: str = "voice_support") -> str:
|
||||
if str(agent_profile or "").strip() == "voice_start":
|
||||
return _voice_start_name_prompt(language)
|
||||
@@ -989,51 +975,6 @@ def _request_runtime_handoff(
|
||||
summary=handoff_summary,
|
||||
metadata=decision.metadata,
|
||||
)
|
||||
session = get_session()
|
||||
try:
|
||||
voice_session = _load_voice_session(session, session_id)
|
||||
if not str(voice_session.interaction_id or "").strip():
|
||||
raise RuntimeError("Voice AI session is missing interaction_id")
|
||||
handoff_payload = VoiceAIHandoffRequestIn(
|
||||
voice_session_id=voice_session.session_id,
|
||||
ai_session_id=voice_session.ai_session_id,
|
||||
interaction_id=voice_session.interaction_id,
|
||||
target_queue_id=voice_session.handoff_target_queue_id,
|
||||
reason=decision.handoff_reason or "AI requested human handoff.",
|
||||
metadata=decision.metadata,
|
||||
summary={
|
||||
"customer_request_text": customer_request_text,
|
||||
"ai_outcome_text": decision.summary_text or decision.reply_text or decision.handoff_reason or "",
|
||||
"recommended_next_step": "Продолжить звонок вручную и проверить контекст обращения.",
|
||||
},
|
||||
)
|
||||
LOGGER.warning(
|
||||
"voice_runtime.handoff_request call_id=%s session_id=%s ai_session_id=%s interaction_id=%s target_queue_id=%s reason=%s",
|
||||
voice_session.call_id,
|
||||
voice_session.session_id,
|
||||
voice_session.ai_session_id,
|
||||
voice_session.interaction_id,
|
||||
voice_session.handoff_target_queue_id,
|
||||
(decision.handoff_reason or "AI requested human handoff.")[:300],
|
||||
)
|
||||
try:
|
||||
_bridge_request(
|
||||
"POST",
|
||||
f"/internal/voice-ai/calls/{voice_session.call_id}/handoff",
|
||||
payload=handoff_payload.model_dump(),
|
||||
timeout=_handoff_timeout_seconds(),
|
||||
)
|
||||
except Exception as exc:
|
||||
LOGGER.warning(
|
||||
"voice_runtime.handoff_failed call_id=%s session_id=%s ai_session_id=%s error=%s",
|
||||
voice_session.call_id,
|
||||
voice_session.session_id,
|
||||
voice_session.ai_session_id,
|
||||
str(exc)[:500],
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def _handle_media_error(session_id: str, message: str, metadata: dict[str, Any] | None = None) -> None:
|
||||
|
||||
@@ -545,7 +545,7 @@ class AudioSocketMediaRuntime:
|
||||
return
|
||||
actor.partial_transcript = transcript_text
|
||||
actor.partial_intent = self._detect_early_intent(transcript_text)
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.partial_asr_ready session_id=%s generation=%s text_len=%s intent=%s",
|
||||
actor.registration.voice_session_id,
|
||||
utterance_generation,
|
||||
@@ -685,7 +685,7 @@ class AudioSocketMediaRuntime:
|
||||
phase=reply_phase,
|
||||
status="delivered",
|
||||
)
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.reply_delivered session_id=%s greeting=%s phase=%s",
|
||||
actor.registration.voice_session_id,
|
||||
is_greeting,
|
||||
@@ -773,7 +773,7 @@ class AudioSocketMediaRuntime:
|
||||
await writer.drain()
|
||||
if not keepalive_logged:
|
||||
keepalive_logged = True
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.prereg_keepalive media_uuid=%s frame_bytes=%s",
|
||||
media_uuid,
|
||||
len(keepalive_payload),
|
||||
@@ -810,9 +810,9 @@ class AudioSocketMediaRuntime:
|
||||
close_as_error = False
|
||||
peer = writer.get_extra_info("peername")
|
||||
try:
|
||||
logger.warning("audiosocket.accept peer=%s", peer)
|
||||
logger.info("audiosocket.accept peer=%s", peer)
|
||||
packet_type, payload = await read_packet(reader, timeout=self._idle_timeout_seconds)
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.initial_packet peer=%s packet_type=%s payload_len=%s",
|
||||
peer,
|
||||
packet_type,
|
||||
@@ -821,7 +821,7 @@ class AudioSocketMediaRuntime:
|
||||
if packet_type != AUDIO_SOCKET_PACKET_UUID:
|
||||
raise RuntimeError("AudioSocket UUID handshake is required")
|
||||
media_uuid = normalize_media_uuid(payload)
|
||||
logger.warning("audiosocket.handshake peer=%s media_uuid=%s", peer, media_uuid)
|
||||
logger.info("audiosocket.handshake peer=%s media_uuid=%s", peer, media_uuid)
|
||||
registration = await self._await_registration(
|
||||
media_uuid,
|
||||
writer=writer,
|
||||
@@ -829,7 +829,7 @@ class AudioSocketMediaRuntime:
|
||||
)
|
||||
if registration is None:
|
||||
raise RuntimeError(f"Unknown AudioSocket media_uuid: {media_uuid}")
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.registered peer=%s session_id=%s media_uuid=%s",
|
||||
peer,
|
||||
registration.voice_session_id,
|
||||
@@ -864,7 +864,7 @@ class AudioSocketMediaRuntime:
|
||||
await self._handle_pcm(actor, payload)
|
||||
continue
|
||||
if packet_type == AUDIO_SOCKET_PACKET_DTMF:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.dtmf session_id=%s payload_len=%s",
|
||||
actor.registration.voice_session_id,
|
||||
len(payload),
|
||||
@@ -874,7 +874,7 @@ class AudioSocketMediaRuntime:
|
||||
close_reason = "audiosocket_hangup"
|
||||
break
|
||||
if packet_type == AUDIO_SOCKET_PACKET_UUID:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.extra_uuid session_id=%s payload_len=%s",
|
||||
actor.registration.voice_session_id,
|
||||
len(payload),
|
||||
@@ -887,7 +887,7 @@ class AudioSocketMediaRuntime:
|
||||
logger.warning("audiosocket.timeout peer=%s", peer)
|
||||
except asyncio.IncompleteReadError:
|
||||
close_reason = "connection_closed"
|
||||
logger.warning("audiosocket.peer_closed peer=%s", peer)
|
||||
logger.info("audiosocket.peer_closed peer=%s", peer)
|
||||
except Exception as exc:
|
||||
close_reason = str(exc)[:1000] or "media_runtime_error"
|
||||
close_as_error = True
|
||||
@@ -905,7 +905,7 @@ class AudioSocketMediaRuntime:
|
||||
return
|
||||
if not actor.first_pcm_logged:
|
||||
actor.first_pcm_logged = True
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.first_pcm session_id=%s state=%s frame_bytes=%s",
|
||||
actor.registration.voice_session_id,
|
||||
actor.state,
|
||||
@@ -978,7 +978,7 @@ class AudioSocketMediaRuntime:
|
||||
actor.registration.voice_session_id,
|
||||
)
|
||||
if greeting_text:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.greeting session_id=%s text_len=%s",
|
||||
actor.registration.voice_session_id,
|
||||
len(greeting_text),
|
||||
@@ -1059,7 +1059,7 @@ class AudioSocketMediaRuntime:
|
||||
return
|
||||
if self._is_low_signal_transcript(transcript_text):
|
||||
actor.finalized_utterance_generation = max(actor.finalized_utterance_generation, utterance_generation)
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.low_signal_ignored session_id=%s transcript=%s",
|
||||
actor.registration.voice_session_id,
|
||||
transcript_text[:120],
|
||||
@@ -1246,7 +1246,7 @@ class AudioSocketMediaRuntime:
|
||||
continue
|
||||
total_audio_bytes += len(synthesis.audio_bytes)
|
||||
if not first_frame_sent:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.tts_ready session_id=%s greeting=%s synth_ms=%s audio_bytes=%s",
|
||||
actor.registration.voice_session_id,
|
||||
is_greeting,
|
||||
@@ -1270,7 +1270,7 @@ class AudioSocketMediaRuntime:
|
||||
await self._write_audio_packet(actor, frame)
|
||||
if not first_frame_sent:
|
||||
first_frame_sent = True
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.first_frame session_id=%s greeting=%s frame_bytes=%s",
|
||||
actor.registration.voice_session_id,
|
||||
is_greeting,
|
||||
@@ -1304,7 +1304,7 @@ class AudioSocketMediaRuntime:
|
||||
phase=reply_phase,
|
||||
status="delivered",
|
||||
)
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.reply_delivered session_id=%s greeting=%s phase=%s",
|
||||
actor.registration.voice_session_id,
|
||||
is_greeting,
|
||||
@@ -1331,7 +1331,7 @@ class AudioSocketMediaRuntime:
|
||||
continue
|
||||
if not actor.keepalive_loop_logged:
|
||||
actor.keepalive_loop_logged = True
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.keepalive_loop session_id=%s state=%s frame_bytes=%s",
|
||||
actor.registration.voice_session_id,
|
||||
actor.state,
|
||||
@@ -1349,7 +1349,7 @@ class AudioSocketMediaRuntime:
|
||||
return
|
||||
if actor.state == state and (handoff_reason or None) is None:
|
||||
return
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.state session_id=%s from_state=%s to_state=%s handoff_reason=%s",
|
||||
actor.registration.voice_session_id,
|
||||
actor.state,
|
||||
@@ -1377,7 +1377,7 @@ class AudioSocketMediaRuntime:
|
||||
if actor.closed:
|
||||
return
|
||||
actor.closed = True
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"audiosocket.cleanup session_id=%s reason=%s error=%s first_pcm=%s",
|
||||
actor.registration.voice_session_id,
|
||||
reason,
|
||||
|
||||
@@ -66,14 +66,13 @@ def create_event(payload: AuditEventIn) -> AuditEvent:
|
||||
def list_events(actor: str | None = None, action: str | None = None, limit: int = 100) -> list[AuditEvent]:
|
||||
session = get_session()
|
||||
try:
|
||||
stmt = select(AuditEventRow).order_by(AuditEventRow.id.asc())
|
||||
stmt = select(AuditEventRow).order_by(AuditEventRow.id.desc())
|
||||
if actor:
|
||||
stmt = stmt.where(AuditEventRow.actor == actor)
|
||||
if action:
|
||||
stmt = stmt.where(AuditEventRow.action == action)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_out(r) for r in rows]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_out(r) for r in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -101,11 +101,10 @@ def create_message(payload: EmailMessageIn) -> EmailMessageOut:
|
||||
def list_messages(from_email: str | None = None, limit: int = 100) -> list[EmailMessageOut]:
|
||||
session = get_session()
|
||||
try:
|
||||
stmt = select(EmailMessageRow).order_by(EmailMessageRow.id.asc())
|
||||
stmt = select(EmailMessageRow).order_by(EmailMessageRow.id.desc())
|
||||
if from_email:
|
||||
stmt = stmt.where(EmailMessageRow.from_email == from_email)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_out(row) for row in rows]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_out(row) for row in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -41,8 +41,8 @@ def list_outbox(
|
||||
stmt = select(EventOutboxRow).order_by(EventOutboxRow.id.desc())
|
||||
if status:
|
||||
stmt = stmt.where(EventOutboxRow.status == status)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [outbox_item_from_row(row) for row in rows[:limit]]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [outbox_item_from_row(row) for row in rows]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -295,8 +295,8 @@ def list_interactions(
|
||||
if assigned_to:
|
||||
stmt = stmt.where(Interaction.assigned_to == assigned_to)
|
||||
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [_to_out(r) for r in rows[:limit]]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_out(r) for r in rows]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -635,8 +635,8 @@ def list_sessions(
|
||||
stmt = stmt.where(IvrSessionRow.interaction_id == interaction_id)
|
||||
if status:
|
||||
stmt = stmt.where(IvrSessionRow.status == status)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [_to_session_out(row) for row in rows[:limit]]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_session_out(row) for row in rows]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -364,8 +364,8 @@ def list_recordings(
|
||||
stmt = stmt.where(CallRecordingRow.interaction_id == interaction_id)
|
||||
if status:
|
||||
stmt = stmt.where(CallRecordingRow.status == status)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [_to_out(row) for row in rows[:limit]]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_out(row) for row in rows]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -244,17 +244,17 @@ def _load_fact_records(
|
||||
queue_id: str | None,
|
||||
channel: str | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
rows = session.execute(
|
||||
select(ReportingInteractionFactRow).order_by(ReportingInteractionFactRow.id.asc())
|
||||
).scalars().all()
|
||||
records = [_serialize_fact_row(row) for row in rows]
|
||||
stmt = select(ReportingInteractionFactRow).order_by(ReportingInteractionFactRow.id.asc())
|
||||
if queue_id:
|
||||
records = [item for item in records if item["queue_id"] == queue_id]
|
||||
stmt = stmt.where(ReportingInteractionFactRow.queue_id == queue_id)
|
||||
if channel:
|
||||
records = [item for item in records if item["channel"] == channel]
|
||||
if dt_from or dt_to:
|
||||
records = [item for item in records if _in_range(item["created_at"], dt_from, dt_to)]
|
||||
return records
|
||||
stmt = stmt.where(ReportingInteractionFactRow.channel == channel)
|
||||
if dt_from:
|
||||
stmt = stmt.where(ReportingInteractionFactRow.created_at >= dt_from.isoformat())
|
||||
if dt_to:
|
||||
stmt = stmt.where(ReportingInteractionFactRow.created_at < dt_to.isoformat())
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [_serialize_fact_row(row) for row in rows]
|
||||
|
||||
|
||||
def _load_interactions_map(session, interaction_ids: list[str]) -> dict[str, Interaction]:
|
||||
|
||||
@@ -188,7 +188,7 @@ def live_calls(
|
||||
session = get_session()
|
||||
try:
|
||||
event_rows = session.execute(
|
||||
select(VoiceEventRow).order_by(VoiceEventRow.id.desc())
|
||||
select(VoiceEventRow).order_by(VoiceEventRow.id.desc()).limit(500)
|
||||
).scalars().all()
|
||||
interaction_rows = session.execute(
|
||||
select(Interaction).order_by(Interaction.id.desc())
|
||||
|
||||
@@ -1631,12 +1631,11 @@ def list_messages(
|
||||
) -> list[TelegramWebhookOut]:
|
||||
session = get_session()
|
||||
try:
|
||||
stmt = select(TelegramMessageRow).order_by(TelegramMessageRow.id.asc())
|
||||
stmt = select(TelegramMessageRow).order_by(TelegramMessageRow.id.desc())
|
||||
if chat_id:
|
||||
stmt = stmt.where(TelegramMessageRow.chat_id == chat_id)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_webhook_out(r) for r in rows]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_webhook_out(r) for r in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -1655,8 +1654,8 @@ def list_threads(
|
||||
stmt = select(TelegramThreadRow).order_by(TelegramThreadRow.last_message_at.desc())
|
||||
if status:
|
||||
stmt = stmt.where(TelegramThreadRow.status == status)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
return [_to_thread_out(row) for row in rows[:limit]]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_thread_out(row) for row in rows]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -1709,9 +1708,10 @@ def get_thread_messages(
|
||||
rows = session.execute(
|
||||
select(TelegramMessageRow)
|
||||
.where(TelegramMessageRow.thread_id == thread.thread_id)
|
||||
.order_by(TelegramMessageRow.id.asc())
|
||||
.order_by(TelegramMessageRow.id.desc())
|
||||
.limit(max(limit, 1))
|
||||
).scalars().all()
|
||||
return [_to_message_out(row) for row in rows[-limit:]]
|
||||
return [_to_message_out(row) for row in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -120,8 +120,9 @@ def list_voice_events(
|
||||
) -> list[VoiceEventOut]:
|
||||
session = get_session()
|
||||
try:
|
||||
rows = session.execute(select(VoiceEventRow).order_by(VoiceEventRow.id.asc())).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_out(r) for r in rows]
|
||||
rows = session.execute(
|
||||
select(VoiceEventRow).order_by(VoiceEventRow.id.desc()).limit(max(limit, 1))
|
||||
).scalars().all()
|
||||
return [_to_out(r) for r in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -111,11 +111,10 @@ def create_message(payload: WebchatMessageIn) -> WebchatMessageOut:
|
||||
def list_messages(session_id: str | None = None, limit: int = 100) -> list[WebchatMessageOut]:
|
||||
session = get_session()
|
||||
try:
|
||||
stmt = select(WebchatMessageRow).order_by(WebchatMessageRow.id.asc())
|
||||
stmt = select(WebchatMessageRow).order_by(WebchatMessageRow.id.desc())
|
||||
if session_id:
|
||||
stmt = stmt.where(WebchatMessageRow.session_id == session_id)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_out(row) for row in rows]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_out(row) for row in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -1883,12 +1883,11 @@ def list_messages(
|
||||
) -> list[WhatsAppWebhookOut]:
|
||||
session = get_session()
|
||||
try:
|
||||
stmt = select(WhatsAppMessageRow).order_by(WhatsAppMessageRow.id.asc())
|
||||
stmt = select(WhatsAppMessageRow).order_by(WhatsAppMessageRow.id.desc())
|
||||
if chat_id:
|
||||
stmt = stmt.where(WhatsAppMessageRow.chat_id == chat_id)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = rows[-limit:]
|
||||
return [_to_webhook_out(r) for r in rows]
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [_to_webhook_out(r) for r in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -1907,10 +1906,10 @@ def list_threads(
|
||||
stmt = select(WhatsAppThreadRow).order_by(WhatsAppThreadRow.last_message_at.desc())
|
||||
if status:
|
||||
stmt = stmt.where(WhatsAppThreadRow.status == status)
|
||||
rows = session.execute(stmt).scalars().all()
|
||||
rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
|
||||
return [
|
||||
_to_thread_out(row, unread_count=_thread_unread_count(session, row.thread_id))
|
||||
for row in rows[:limit]
|
||||
for row in rows
|
||||
]
|
||||
finally:
|
||||
session.close()
|
||||
@@ -1965,9 +1964,10 @@ def get_thread_messages(
|
||||
rows = session.execute(
|
||||
select(WhatsAppMessageRow)
|
||||
.where(WhatsAppMessageRow.thread_id == thread.thread_id)
|
||||
.order_by(WhatsAppMessageRow.id.asc())
|
||||
.order_by(WhatsAppMessageRow.id.desc())
|
||||
.limit(max(limit, 1))
|
||||
).scalars().all()
|
||||
return [_to_message_out(row) for row in rows[-limit:]]
|
||||
return [_to_message_out(row) for row in reversed(rows)]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user