feat: add session orchestration service
This commit is contained in:
246
app/services/session_service.py
Normal file
246
app/services/session_service.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import uuid
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from app.data.database import get_session
|
||||
from app.data.repositories import SessionRepository, NoteRepository, LinkRepository
|
||||
from app.data.models import Session, Note, Link, RawZettel
|
||||
from app.services.vector import VectorService
|
||||
from app.services.interviewer import InterviewerAgent
|
||||
from app.services.synthesizer import SynthesizerAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionService:
|
||||
"""Service responsible for orchestrating the complete interview-to-synthesis pipeline."""
|
||||
|
||||
def __init__(self):
|
||||
self.vector_service = VectorService()
|
||||
self.interviewer = InterviewerAgent(self.vector_service)
|
||||
self.synthesizer = SynthesizerAgent(self.vector_service)
|
||||
|
||||
async def start_session(self, topic: str) -> Dict[str, any]:
|
||||
"""Start a new interview session.
|
||||
|
||||
Args:
|
||||
topic: The initial topic for the conversation
|
||||
|
||||
Returns:
|
||||
Dictionary containing session_id, status, and initial message
|
||||
"""
|
||||
async with get_session() as db:
|
||||
session_repo = SessionRepository(db)
|
||||
|
||||
# Create new session
|
||||
session = await session_repo.create(status="active")
|
||||
|
||||
# Generate first interviewer response
|
||||
transcript = [{"role": "user", "content": f"I want to explore the topic: {topic}"}]
|
||||
|
||||
response, should_end = await self.interviewer.generate_response(
|
||||
transcript=transcript,
|
||||
context_query=topic
|
||||
)
|
||||
|
||||
# Update transcript with initial exchange
|
||||
await session_repo.append_transcript(session.id, {
|
||||
"role": "user",
|
||||
"content": f"I want to explore the topic: {topic}"
|
||||
})
|
||||
await session_repo.append_transcript(session.id, {
|
||||
"role": "assistant",
|
||||
"content": response
|
||||
})
|
||||
|
||||
if should_end:
|
||||
await session_repo.update_status(session.id, "processing")
|
||||
|
||||
return {
|
||||
"session_id": session.id,
|
||||
"status": session.status,
|
||||
"message": response
|
||||
}
|
||||
|
||||
async def handle_message(self, session_id: uuid.UUID, message: str) -> Dict[str, any]:
|
||||
"""Handle a user message in an active session.
|
||||
|
||||
Args:
|
||||
session_id: UUID of the session
|
||||
message: User's message content
|
||||
|
||||
Returns:
|
||||
Dictionary containing response and session status
|
||||
"""
|
||||
async with get_session() as db:
|
||||
session_repo = SessionRepository(db)
|
||||
|
||||
# Get session
|
||||
session = await session_repo.get(session_id)
|
||||
if not session:
|
||||
raise ValueError(f"Session {session_id} not found")
|
||||
|
||||
if session.status != "active":
|
||||
raise ValueError(f"Session {session_id} is not active (status: {session.status})")
|
||||
|
||||
# Add user message to transcript
|
||||
await session_repo.append_transcript(session.id, {
|
||||
"role": "user",
|
||||
"content": message
|
||||
})
|
||||
|
||||
# Get updated transcript
|
||||
updated_session = await session_repo.get(session_id)
|
||||
transcript = updated_session.transcript
|
||||
|
||||
# Generate response using the full conversation context
|
||||
response, should_end = await self.interviewer.generate_response(
|
||||
transcript=transcript,
|
||||
context_query=message
|
||||
)
|
||||
|
||||
# Add AI response to transcript
|
||||
await session_repo.append_transcript(session.id, {
|
||||
"role": "assistant",
|
||||
"content": response
|
||||
})
|
||||
|
||||
# Check if session should end
|
||||
if should_end:
|
||||
await session_repo.update_status(session.id, "processing")
|
||||
|
||||
return {
|
||||
"session_id": session.id,
|
||||
"status": "processing" if should_end else "active",
|
||||
"message": response,
|
||||
"session_ending": should_end
|
||||
}
|
||||
|
||||
async def process_session_background_task(self, session_id: uuid.UUID) -> None:
|
||||
"""Background task to synthesize a completed session into Zettels and links.
|
||||
|
||||
Args:
|
||||
session_id: UUID of the session to process
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting synthesis for session {session_id}")
|
||||
|
||||
async with get_session() as db:
|
||||
session_repo = SessionRepository(db)
|
||||
note_repo = NoteRepository(db)
|
||||
link_repo = LinkRepository(db)
|
||||
|
||||
# Get session with transcript
|
||||
session = await session_repo.get(session_id)
|
||||
if not session:
|
||||
logger.error(f"Session {session_id} not found")
|
||||
return
|
||||
|
||||
# Update status to processing
|
||||
await session_repo.update_status(session_id, "processing")
|
||||
|
||||
# Segment transcript into Zettels
|
||||
logger.info(f"Segmenting transcript for session {session_id}")
|
||||
segmentation_result = await self.synthesizer.segment_transcript(session.transcript)
|
||||
|
||||
# Create Note objects from Zettels
|
||||
notes_to_create = []
|
||||
for zettel in segmentation_result.notes:
|
||||
note = Note(
|
||||
title=zettel.title,
|
||||
content=zettel.content,
|
||||
tags=zettel.tags,
|
||||
session_id=session_id
|
||||
)
|
||||
notes_to_create.append(note)
|
||||
|
||||
# Save notes to database
|
||||
if notes_to_create:
|
||||
logger.info(f"Creating {len(notes_to_create)} notes for session {session_id}")
|
||||
created_notes = await note_repo.create_bulk(notes_to_create)
|
||||
|
||||
# Add notes to vector store
|
||||
await self.vector_service.add_notes(created_notes)
|
||||
logger.info(f"Added {len(created_notes)} notes to vector store")
|
||||
|
||||
# Generate links for each new note
|
||||
await self._create_links_for_notes(created_notes, link_repo)
|
||||
|
||||
# Mark session as completed
|
||||
await session_repo.update_status(session_id, "completed")
|
||||
logger.info(f"Synthesis completed for session {session_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing session {session_id}: {e}")
|
||||
async with get_session() as db:
|
||||
session_repo = SessionRepository(db)
|
||||
await session_repo.update_status(session_id, "failed")
|
||||
|
||||
async def get_session_status(self, session_id: uuid.UUID) -> Dict[str, any]:
|
||||
"""Get the current status of a session.
|
||||
|
||||
Args:
|
||||
session_id: UUID of the session
|
||||
|
||||
Returns:
|
||||
Dictionary containing session status and metadata
|
||||
"""
|
||||
async with get_session() as db:
|
||||
session_repo = SessionRepository(db)
|
||||
note_repo = NoteRepository(db)
|
||||
|
||||
session = await session_repo.get(session_id)
|
||||
if not session:
|
||||
raise ValueError(f"Session {session_id} not found")
|
||||
|
||||
notes = await note_repo.get_by_session(session_id)
|
||||
|
||||
return {
|
||||
"session_id": session.id,
|
||||
"status": session.status,
|
||||
"notes_count": len(notes),
|
||||
"created_at": session.created_at
|
||||
}
|
||||
|
||||
async def _create_links_for_notes(self, notes: List[Note], link_repo: LinkRepository) -> None:
|
||||
"""Create semantic links for a list of new notes."""
|
||||
for note in notes:
|
||||
try:
|
||||
# Find semantically similar notes
|
||||
neighbors = await self.vector_service.semantic_search(
|
||||
query=f"{note.title} {note.content}",
|
||||
k=5
|
||||
)
|
||||
|
||||
# Filter out the note itself and low similarity matches
|
||||
filtered_neighbors = [
|
||||
doc for doc in neighbors
|
||||
if doc.metadata.get("similarity_score", 0) > 0.5
|
||||
and doc.metadata.get("title") != note.title
|
||||
]
|
||||
|
||||
if filtered_neighbors:
|
||||
# Generate links using the synthesizer
|
||||
linking_result = await self.synthesizer.generate_links(note, filtered_neighbors)
|
||||
|
||||
# Create Link objects
|
||||
links_to_create = []
|
||||
for raw_link in linking_result.links:
|
||||
try:
|
||||
target_id = uuid.UUID(raw_link.target_note_id)
|
||||
link = Link(
|
||||
context=raw_link.relationship_context,
|
||||
source_id=note.id,
|
||||
target_id=target_id
|
||||
)
|
||||
links_to_create.append(link)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Invalid link target ID {raw_link.target_note_id}: {e}")
|
||||
|
||||
# Save links
|
||||
if links_to_create:
|
||||
await link_repo.create_bulk(links_to_create)
|
||||
logger.info(f"Created {len(links_to_create)} links for note {note.id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating links for note {note.id}: {e}")
|
||||
Reference in New Issue
Block a user