Files
resolutionflow/backend/app/services/unified_chat_service.py
chihlasm 36ca830481 fix: wire image uploads into correct chat endpoint (unified_chat_service)
The frontend calls /ai-sessions/{id}/chat (unified_chat_service), not
/assistant/chats/{id}/messages (assistant_chat_service). The previous
commit wired images into the wrong backend. This fixes it:

- ai_session.py schema: add upload_ids to ChatMessageRequest
- ai_sessions.py endpoint: fetch images via _fetch_upload_images
- unified_chat_service: accept and forward images to _call_ai

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 05:02:34 +00:00

132 lines
4.0 KiB
Python

"""Unified chat service — chat sessions on ai_sessions table.
Replaces assistant_chat_service for new chat sessions. Messages are stored
in ai_sessions.conversation_messages JSONB. Reuses the same AI calling
infrastructure and system prompt from assistant_chat_service.
"""
import logging
from typing import Any
from uuid import UUID
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.ai_session import AISession
from app.services.assistant_chat_service import (
ASSISTANT_SYSTEM_PROMPT,
_call_ai,
_auto_title,
)
from app.services.rag_service import search as rag_search, build_rag_context, extract_suggested_flows
logger = logging.getLogger(__name__)
async def create_chat_session(
user_id: UUID,
account_id: UUID,
team_id: UUID | None,
intake_content: dict[str, Any],
db: AsyncSession,
) -> AISession:
"""Create a new chat session on ai_sessions."""
first_message = intake_content.get("text", "")
title = _auto_title(first_message) if first_message else "New Chat"
session = AISession(
user_id=user_id,
account_id=account_id,
team_id=team_id,
session_type="chat",
title=title,
intake_type="free_text",
intake_content=intake_content,
status="active",
confidence_tier="discovery",
confidence_score=0.0,
conversation_messages=[],
)
db.add(session)
await db.flush()
return session
async def send_chat_message(
session_id: UUID,
user_id: UUID,
account_id: UUID,
message: str,
db: AsyncSession,
images: list[dict[str, Any]] | None = None,
) -> tuple[str, list[dict[str, Any]], AISession]:
"""Send a message in a chat session and get AI response.
Args:
images: Optional list of {"media_type": str, "data": str (base64)}
for vision content attached to this message.
Returns (ai_content, suggested_flows, session).
"""
result = await db.execute(
select(AISession).where(
AISession.id == session_id,
AISession.user_id == user_id,
AISession.session_type == "chat",
)
)
session = result.scalar_one_or_none()
if not session:
raise ValueError("Chat session not found")
if session.status not in ("active", "paused"):
raise ValueError(f"Cannot send messages to a {session.status} session")
# Auto-title from first message if still default
if session.step_count == 0 and message.strip():
session.title = _auto_title(message)
# Auto-detect problem domain from first message
if not session.problem_summary and message.strip():
session.problem_summary = _auto_title(message)
# RAG search for relevant flows
rag_results = await rag_search(
query=message,
account_id=account_id,
db=db,
limit=8,
)
rag_context = build_rag_context(rag_results)
# Build message history for AI
ai_messages: list[dict[str, Any]] = []
for msg in (session.conversation_messages or []):
if msg.get("role") in ("user", "assistant"):
ai_messages.append({"role": msg["role"], "content": msg["content"]})
# Call AI
ai_content, input_tokens, output_tokens = await _call_ai(
system_base=ASSISTANT_SYSTEM_PROMPT,
rag_context=rag_context,
history=ai_messages,
new_message=message,
images=images,
)
# Append messages to conversation_messages
msgs = list(session.conversation_messages or [])
msgs.append({"role": "user", "content": message})
msgs.append({"role": "assistant", "content": ai_content})
session.conversation_messages = msgs
session.step_count += 2 # message count for display
session.total_input_tokens += input_tokens
session.total_output_tokens += output_tokens
# Resume if paused
if session.status == "paused":
session.status = "active"
suggested_flows = extract_suggested_flows(rag_results)
return ai_content, suggested_flows, session