Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions services/ai-core-services/src/api/v1/meeting_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import APIRouter, HTTPException, status
from src.meeting_bot.schemas import DispatchBotRequest, DispatchBotResponse, BotStatusResponse
from src.meeting_bot.client import bot_client

router = APIRouter(prefix="/screening/bot", tags=["Meeting Bot Service"])


@router.post("/dispatch", response_model=DispatchBotResponse, status_code=status.HTTP_200_OK)
async def dispatch_bot(request: DispatchBotRequest):
"""Schedule an Attendee meeting bot for the specified interview session."""
try:
return await bot_client.dispatch_bot(request)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to dispatch meeting bot: {str(err)}"
)


@router.get("/{bot_id}", response_model=BotStatusResponse, status_code=status.HTTP_200_OK)
async def get_bot_status(bot_id: str):
"""Fetch status of a dispatched meeting bot."""
try:
return await bot_client.get_bot_status(bot_id)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch bot status: {str(err)}"
)
2 changes: 2 additions & 0 deletions services/ai-core-services/src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Settings(BaseSettings):
# Database & Storage Connection Credentials
database_url: str

# Real-time WebSocket Audio Stream Endpoint
websocket_url: str

model_config = SettingsConfigDict(
env_file=(str(BASE_DIR / ".env"), str(BASE_DIR.parent.parent / ".env")),
Expand Down
14 changes: 14 additions & 0 deletions services/ai-core-services/src/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from src.db.base import Base
from src.db.connection import engine, AsyncSessionLocal, get_db, close_engine, test_db_connection
from src.db.models import DBInterviewSession, DBInterviewAnalysis

__all__ = [
"Base",
"engine",
"AsyncSessionLocal",
"get_db",
"close_engine",
"test_db_connection",
"DBInterviewSession",
"DBInterviewAnalysis",
]
5 changes: 5 additions & 0 deletions services/ai-core-services/src/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
pass
64 changes: 64 additions & 0 deletions services/ai-core-services/src/db/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import Any, AsyncGenerator, Optional
from sqlalchemy import text, create_engine
from sqlalchemy.orm import sessionmaker
from src.core.config import settings
from src.core.logger import logger


def _get_sqlalchemy_url() -> str:
return settings.database_url

try:
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

async_url = _get_sqlalchemy_url()
if async_url.startswith("postgresql://"):
async_url = async_url.replace("postgresql://", "postgresql+asyncpg://", 1)

engine = create_async_engine(async_url, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
HAS_ASYNC = True
except Exception:
HAS_ASYNC = False
sync_url = _get_sqlalchemy_url()
engine = create_engine(sync_url, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(bind=engine, expire_on_commit=False)


async def get_db() -> AsyncGenerator[Any, None]:
if HAS_ASYNC:
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
else:
with AsyncSessionLocal() as session:
try:
yield session
except Exception:
session.rollback()
raise


async def close_engine() -> None:
if HAS_ASYNC:
await engine.dispose()
else:
engine.dispose()
logger.info("Database engine disposed")


async def test_db_connection() -> Any:
try:
if HAS_ASYNC:
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
else:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return True
except Exception as e:
logger.error("Database connection failed: %s", e)
return False
7 changes: 7 additions & 0 deletions services/ai-core-services/src/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from src.db.models.interview_session import DBInterviewSession
from src.db.models.interview_analysis import DBInterviewAnalysis

__all__ = [
"DBInterviewSession",
"DBInterviewAnalysis",
]
96 changes: 96 additions & 0 deletions services/ai-core-services/src/db/models/interview_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import uuid
from datetime import datetime
from typing import Optional, List, Dict, Any
from sqlalchemy import String, DateTime, Text, Index, text, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID, JSONB
from src.db.base import Base


class DBInterviewAnalysis(Base):
__tablename__ = "interview_analysis"

id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
info={"description": "Unique identifier for the interview analysis report"},
)

interview_session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
unique=True,
info={"description": "Unique foreign key reference to interview session (1-to-1)"},
)

application_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
info={"description": "Foreign key reference to candidate application"},
)

analysis_result: Mapped[Optional[Dict[str, Any]]] = mapped_column(
JSONB,
nullable=True,
info={"description": "AI screening evaluation feedback, recommendations, and scores"},
)

question_answer: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(
JSONB,
nullable=True,
info={"description": "Structured Q&A transcript analysis and answer depth scores"},
)

recording_url: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
info={"description": "Storage URL for call recording audio/video"},
)

interview_type: Mapped[str] = mapped_column(
String,
nullable=False,
default="screening_ai",
info={"description": "Type of interview session (screening_ai)"},
)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("NOW()"),
info={"description": "Timestamp when the record was created"},
)

updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("NOW()"),
onupdate=func.now(),
info={"description": "Timestamp when the record was last updated"},
)

deleted_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
info={"description": "Timestamp when the record was soft-deleted"},
)

__table_args__ = (
Index("idx_interview_analysis_session_id", "interview_session_id"),
Index("idx_interview_analysis_app_id", "application_id"),
)

def to_response(self) -> Any:
from src.meeting_bot.schemas import InterviewAnalysisDetailResponse
return InterviewAnalysisDetailResponse(
id=str(self.id),
interview_session_id=str(self.interview_session_id),
application_id=str(self.application_id),
analysis_result=self.analysis_result,
question_answer=self.question_answer,
recording_url=self.recording_url,
interview_type=self.interview_type,
created_at=self.created_at.isoformat() if self.created_at else None,
updated_at=self.updated_at.isoformat() if self.updated_at else None,
)
115 changes: 115 additions & 0 deletions services/ai-core-services/src/db/models/interview_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import uuid
from datetime import datetime
from typing import Optional, List, Dict, Any
from sqlalchemy import String, DateTime, Index, text, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID, JSONB
from src.db.base import Base


class DBInterviewSession(Base):
__tablename__ = "interview_session"

id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
info={"description": "Unique identifier for the interview session"},
)

application_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
info={"description": "Foreign key reference to candidate application"},
)

scheduled_by: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
nullable=True,
info={"description": "Foreign key reference to HR user who scheduled the session"},
)

interview_type: Mapped[str] = mapped_column(
String,
nullable=False,
default="screening_ai",
info={"description": "Type of interview session (screening_ai)"},
)

status: Mapped[str] = mapped_column(
String,
nullable=False,
default="scheduled",
info={"description": "Current state of interview session (scheduled, in_progress, completed, failed)"},
)

interview_metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(
JSONB,
nullable=True,
info={"description": "Session operational metadata payload"},
)

comment: Mapped[Optional[str]] = mapped_column(
String,
nullable=True,
info={"description": "HR notes or operational comments"},
)

generated_questions: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(
JSONB,
nullable=True,
info={"description": "Pre-generated question set for the screening interview"},
)

scheduled_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
info={"description": "Scheduled time slot for the interview"},
)

completed_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
info={"description": "Timestamp when interview call concluded"},
)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("NOW()"),
info={"description": "Timestamp when the record was created"},
)

updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("NOW()"),
onupdate=func.now(),
info={"description": "Timestamp when the record was last updated"},
)

deleted_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
info={"description": "Timestamp when the record was soft-deleted"},
)

__table_args__ = (
Index("idx_interview_session_app_id", "application_id"),
Index("idx_interview_session_status", "status"),
)

def to_response(self) -> Any:
from src.meeting_bot.schemas import InterviewSessionDetailResponse
return InterviewSessionDetailResponse(
id=str(self.id),
application_id=str(self.application_id),
scheduled_by=str(self.scheduled_by) if self.scheduled_by else None,
interview_type=self.interview_type,
status=self.status,
scheduled_at=self.scheduled_at.isoformat() if self.scheduled_at else None,
generated_questions=self.generated_questions,
interview_metadata=self.interview_metadata,
created_at=self.created_at.isoformat() if self.created_at else None,
updated_at=self.updated_at.isoformat() if self.updated_at else None,
)
9 changes: 9 additions & 0 deletions services/ai-core-services/src/meeting_bot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from src.meeting_bot.attendee import attendee_client, AttendeeApiClient
from src.meeting_bot.client import bot_client, AttendeeBotClient

__all__ = [
"attendee_client",
"AttendeeApiClient",
"bot_client",
"AttendeeBotClient",
]
Loading