refactor: composition root in lifespan, break core↔services cycle, shared data layer
The gateway was the composition root, device registry, inbound-call policy, and call-operations service in one class, with core↔services circular imports papered over by function-local imports, wiring done by assigning private attributes, and MCP tools duplicating REST query logic against their own sessions. Composition: - main.py's lifespan now builds every service and wires them by constructor/registration. gateway.from_config() is gone; core/ no longer imports services/ anywhere — the cycle is dead. - Inbound-call policy moved to ReceptionistService.on_inbound_call (routing evaluation, reject/answer, screening dispatch); wired as the engine's on_incoming_call by the lifespan. Receptionist deps (tts/transcription/recording/routing) are constructor-injected — no more gateway._tts reach-through or importing hold_slayer's private _get_llm (now services.llm_client.get_llm, shared). - Hold-slayer launch goes through a mode-handler registry (register_mode_handler); the gateway no longer knows the service's type. CallManager takes on_call_ended in its constructor. - build_sip_engine() is a pure function taking explicit callbacks. - api/routing.py uses the routing service from app.state via a proper dependency instead of gateway._routing. Shared data layer: - db.session_scope() is the one session convention (get_db wraps it). - services/call_persistence.py gains the query/write functions and the single StoredCallFlow→CallFlow mapper; api/call_flows.py, api/call_history.py, and the six DB-touching MCP tools are thin wrappers over them — the two surfaces can't drift. - legs_for_call() replaces the three private _call_legs scans (gateway transfer/hangup, REST dtmf, MCP dtmf). 7 new tests (mode-handler launch, on_call_ended hook, receptionist inbound answer/reject, call-flow CRUD round-trip and history routes against real SQLite through the shared layer). aiosqlite added to dev deps for that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
66
main.py
66
main.py
@@ -21,9 +21,19 @@ from fastapi.staticfiles import StaticFiles
|
||||
from api import call_flows, call_history, calls, devices, routing, websocket
|
||||
from api.deps import require_token
|
||||
from config import Settings, get_settings
|
||||
from core.gateway import AIPSTNGateway
|
||||
from core.gateway import AIPSTNGateway, build_sip_engine
|
||||
from db.database import close_db, init_db
|
||||
from mcp_server.server import create_mcp_server
|
||||
from models.call import CallMode
|
||||
from services.audio_classifier import AudioClassifier
|
||||
from services.call_persistence import persist_call_on_end
|
||||
from services.hold_slayer import HoldSlayerService
|
||||
from services.notification import NotificationService
|
||||
from services.receptionist import ReceptionistService
|
||||
from services.recording import RecordingService
|
||||
from services.routing import RoutingService
|
||||
from services.transcription import TranscriptionService
|
||||
from services.tts import TTSService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
@@ -126,23 +136,61 @@ async def lifespan(app: FastAPI):
|
||||
except Exception as e:
|
||||
_handle_db_error(e)
|
||||
|
||||
# Boot the telephony engine
|
||||
gateway = AIPSTNGateway.from_config()
|
||||
# === Composition root ===
|
||||
# Build the gateway and every service here, wiring them by
|
||||
# constructor/registration — nothing constructs its own deps.
|
||||
gateway = AIPSTNGateway(settings=settings, on_call_ended=persist_call_on_end)
|
||||
|
||||
classifier = AudioClassifier(settings.classifier)
|
||||
transcription = TranscriptionService(settings.speaches)
|
||||
tts = TTSService(settings.tts)
|
||||
routing_svc = RoutingService(gateway)
|
||||
recording_svc = RecordingService()
|
||||
receptionist = ReceptionistService(
|
||||
gateway,
|
||||
tts=tts,
|
||||
transcription=transcription,
|
||||
recording=recording_svc,
|
||||
routing=routing_svc,
|
||||
)
|
||||
gateway.attach_services(tts=tts)
|
||||
|
||||
def launch_hold_slayer(call, sip_leg_id, call_flow_id):
|
||||
svc = HoldSlayerService(
|
||||
gateway=gateway,
|
||||
call_manager=gateway.call_manager,
|
||||
sip_engine=gateway.sip_engine,
|
||||
classifier=classifier,
|
||||
transcription=transcription,
|
||||
settings=settings,
|
||||
tts=tts,
|
||||
)
|
||||
gateway.spawn(
|
||||
svc.run(call, sip_leg_id, call_flow_id),
|
||||
name=f"holdslayer_{call.id}",
|
||||
)
|
||||
|
||||
gateway.register_mode_handler(CallMode.HOLD_SLAYER, launch_hold_slayer)
|
||||
|
||||
gateway.sip_engine = build_sip_engine(
|
||||
settings,
|
||||
gateway.media_pipeline,
|
||||
on_leg_state_change=gateway._on_sip_leg_state,
|
||||
on_device_registered=gateway._on_sip_device_registered,
|
||||
on_incoming_call=receptionist.on_inbound_call,
|
||||
)
|
||||
|
||||
await routing_svc.start()
|
||||
await gateway.start()
|
||||
app.state.gateway = gateway
|
||||
|
||||
# Start auxiliary services
|
||||
from services.notification import NotificationService
|
||||
from services.recording import RecordingService
|
||||
app.state.routing_service = routing_svc
|
||||
|
||||
notification_svc = NotificationService(gateway.event_bus, settings)
|
||||
await notification_svc.start()
|
||||
app.state.notification_service = notification_svc
|
||||
|
||||
recording_svc = RecordingService()
|
||||
await recording_svc.start()
|
||||
app.state.recording_service = recording_svc
|
||||
gateway._recording_service = recording_svc
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("🔥 Hold Slayer Gateway is LIVE")
|
||||
|
||||
Reference in New Issue
Block a user