Stage 6: learn_call_flow rebuilt on the learner, docs truth sweep
The call-flow learner finally gets fed: exploration mode records its IVR discoveries on the call (ActiveCall.exploration_steps) instead of throwing them away, persistence stores them in the call record's metadata, and the rebuilt learn_call_flow MCP tool turns a completed exploration call into a stored flow via CallFlowLearner — correct constructor (llm_client from get_llm, heuristic labels when the LLM is unavailable), build for a new number, merge/refine when a flow already exists. save_learned_flow/update_flow_from_model keep the CallFlow↔row mapping in call_persistence. Test gaps closed: tests/test_learner.py (discoveries→linked steps, exploration persistence, learn-then-refine through the in-memory MCP client, no-data and unknown-call answers) and tests/test_websocket.py (4401 without token, trunk-status-then-replay on connect, per-call stream filtering). Docs aligned to code: README (15 tools incl. learn_call_flow, HTTP not SSE, Python 3.12+, PostgreSQL+Alembic — no SQLite fallback, media pipeline marked stub-mode until pjsua2 installed, Alembic and honest /health checked off); docs/mcp-server.md rewritten against the actual tool surface (hangup not end_call, real params, 3 real resources, /mcp/ streamable HTTP + bearer auth); architecture/development/ configuration drift fixed. pyproject: pruned never-imported deps (websockets, librosa, soundfile, python-multipart). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,36 @@ async def create_flow(
|
||||
return row
|
||||
|
||||
|
||||
async def save_learned_flow(session: AsyncSession, flow: CallFlow) -> StoredCallFlow:
|
||||
"""The one CallFlow-model → row mapping (auto-learned flows)."""
|
||||
row = StoredCallFlow(
|
||||
id=flow.id,
|
||||
name=flow.name,
|
||||
phone_number=flow.phone_number,
|
||||
description=flow.description,
|
||||
steps=[s.model_dump(mode="json") for s in flow.steps],
|
||||
tags=flow.tags,
|
||||
notes=flow.notes,
|
||||
times_used=flow.times_used,
|
||||
last_used=flow.last_used,
|
||||
last_verified=datetime.now(),
|
||||
)
|
||||
session.add(row)
|
||||
await session.flush()
|
||||
return row
|
||||
|
||||
|
||||
async def update_flow_from_model(
|
||||
session: AsyncSession, row: StoredCallFlow, flow: CallFlow
|
||||
) -> None:
|
||||
"""Write a refined CallFlow back onto its existing row."""
|
||||
row.steps = [s.model_dump(mode="json") for s in flow.steps]
|
||||
row.times_used = flow.times_used
|
||||
row.last_used = flow.last_used
|
||||
row.notes = flow.notes
|
||||
await session.flush()
|
||||
|
||||
|
||||
# ================================================================
|
||||
# Devices
|
||||
# ================================================================
|
||||
@@ -320,7 +350,10 @@ async def _finalize_call_record(call: ActiveCall, final_status: CallStatus) -> N
|
||||
}
|
||||
for c in call.classification_history
|
||||
]
|
||||
record.metadata_ = {"services": list(call.services)}
|
||||
metadata = {"services": list(call.services)}
|
||||
if call.exploration_steps:
|
||||
metadata["exploration_steps"] = call.exploration_steps
|
||||
record.metadata_ = metadata
|
||||
|
||||
# Each transcript entry gets its own row with a sequence number
|
||||
# and real offset so the dashboard can render click-to-seek.
|
||||
|
||||
@@ -294,7 +294,7 @@ class HoldSlayerService:
|
||||
logger.info(f"🔍 Exploration mode: discovering IVR for {call.remote_number}")
|
||||
await self.call_manager.update_status(call.id, CallStatus.NAVIGATING_IVR)
|
||||
|
||||
discovered_steps: list[dict] = []
|
||||
discovered_steps = call.exploration_steps # persisted with the record
|
||||
max_time = self.settings.hold_slayer.max_hold_time
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user