the-brain / python-services /summary.py
voice-rag's picture
try to add neon db
e3d8805
Raw
History Blame Contribute Delete
6.01 kB
# summary.py
# Compresses old message histories asynchronously into dense systemic memory logs.
# Prevents context window inflation and keeps voice processing latencies low.
import json
import logging
import re
import asyncio
from typing import List, Dict, Any, Optional
from client import groq
logger = logging.getLogger("summary")
NAME_STOP_WORDS = {"he", "hai", "hun", "hoon", "ho", "and", "ye", "yeh", "the", "a"}
def _build_summary_system_prompt(caller_fields_spec: Optional[List[Dict[str, Any]]]) -> str:
if not caller_fields_spec:
return (
"You are a conversation summarizer. Summarize the key facts, topics discussed, "
"decisions made, and pending actions from the conversation. "
"Do NOT mention or infer caller personal details (name, phone, ID, etc.). "
"Merge with existing summary. Keep under 3 sentences."
)
return (
"You are a conversation summarizer. Summarize the key facts, topics discussed, "
"decisions made, and pending actions from the conversation. "
"Use the provided CALLER DATA block as the authoritative source for caller identity "
"fields (name, phone, custom fields). Do NOT re-extract or guess caller identity "
"from transcript wording. Roman Urdu copulas (he, hai, hun, hoon) are NOT names — "
"never record them as caller names. If a caller field is null or empty in CALLER DATA, "
"omit it from the summary — do not invent a value. "
"Merge with existing summary. Keep under 3 sentences."
)
def _sanitize_summary(result: str, caller_info: Optional[Dict[str, Any]]) -> str:
"""Strip hallucinated stop-word names when no authoritative name exists."""
if not result or not caller_info:
return result or ""
if caller_info.get("name"):
return result
cleaned = result
for stop in NAME_STOP_WORDS:
cleaned = re.sub(
rf"(caller\s+)?name[:\s]+{re.escape(stop)}\b",
"",
cleaned,
flags=re.I,
)
return cleaned.strip()
async def generate_summary(
existing_summary: str,
messages_to_process: List[Dict[str, Any]],
caller_info: Optional[Dict[str, Any]] = None,
caller_fields_spec: Optional[List[Dict[str, Any]]] = None,
) -> str:
"""
Generate a condensed structural summary of earlier conversation message arrays.
Runs asynchronously in an isolated thread executor pool to prevent voice line stutters.
"""
if not messages_to_process:
return existing_summary or ""
conversation_string = "\n".join(
f"{'User' if m.get('role') == 'user' else 'Assistant'}: {m.get('content', '')}"
for m in messages_to_process
)
caller_info = caller_info or {}
caller_fields_spec = caller_fields_spec or []
system_prompt = _build_summary_system_prompt(caller_fields_spec)
user_parts = []
if caller_fields_spec:
user_parts.append(
"CALLER DATA (authoritative — do not override):\n"
+ json.dumps(caller_info, ensure_ascii=False)
)
user_parts.append(f"Existing Summary: {existing_summary or 'None yet.'}")
user_parts.append(f"New messages:\n{conversation_string}")
user_content = "\n\n".join(user_parts)
try:
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(
None,
lambda: groq.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
temperature=0.2,
max_tokens=150,
),
)
result = response.choices[0].message.content
if result:
cleaned = _sanitize_summary(result.strip(), caller_info)
logger.info("Summary updated successfully")
return cleaned
return existing_summary or ""
except Exception as e:
logger.error(f"Summary generation failed: {e}")
return existing_summary or ""
async def extract_caller_data_from_transcript(
transcript: str,
fields_spec: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Call Groq to extract structured caller fields from the conversation transcript
based on the fields specification. Returns a dictionary of extracted values.
"""
if not fields_spec or not transcript.strip():
return {}
system_prompt = (
"You are a structured data extractor. Your task is to extract information from "
"the provided conversation transcript matching the schema specification. "
"Return ONLY a clean JSON object containing the extracted keys. "
"If a field is not mentioned or cannot be inferred, set its value to null. "
"Do not invent or assume any values. Output valid JSON, nothing else."
)
schema_desc = {f["key"]: {"label": f.get("label", f["key"]), "type": f.get("type", "text")} for f in fields_spec}
user_content = (
f"Schema Spec:\n{json.dumps(schema_desc, ensure_ascii=False)}\n\n"
f"Transcript:\n{transcript}"
)
try:
loop = asyncio.get_running_loop()
response = await loop.run_in_executor(
None,
lambda: groq.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
temperature=0.0,
response_format={"type": "json_object"},
max_tokens=250,
),
)
result = response.choices[0].message.content
if result:
return json.loads(result)
return {}
except Exception as e:
logger.error(f"LLM caller extraction failed: {e}")
return {}