feat: attachment context for LLM, markdown rendering, mobile layout with back arrow
This commit is contained in:
@@ -352,6 +352,57 @@ async def execute_tool_call(
|
||||
return f"Error executing tool '{tool.name}': {exc}"
|
||||
|
||||
|
||||
async def _extract_attachment_content(
|
||||
db: AsyncSession,
|
||||
session_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> str:
|
||||
"""Extract text content from session attachments for LLM context."""
|
||||
result = await db.execute(
|
||||
select(AIChatAttachment)
|
||||
.where(AIChatAttachment.session_id == session_id)
|
||||
.where(AIChatAttachment.tenant_id == tenant_id)
|
||||
.order_by(AIChatAttachment.created_at.asc())
|
||||
)
|
||||
attachments = list(result.scalars().all())
|
||||
if not attachments:
|
||||
return ""
|
||||
|
||||
parts: list[str] = []
|
||||
for att in attachments:
|
||||
try:
|
||||
with open(att.storage_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
text_content = ""
|
||||
mime = att.mime_type.lower()
|
||||
|
||||
if mime.startswith("text/") or att.filename.endswith((".txt", ".md", ".csv", ".json", ".yaml", ".yml", ".py", ".js", ".ts", ".html", ".xml")):
|
||||
text_content = content.decode("utf-8", errors="replace")
|
||||
elif mime == "application/pdf" or att.filename.endswith(".pdf"):
|
||||
try:
|
||||
import fitz
|
||||
doc = fitz.open(stream=content, filetype="pdf")
|
||||
text_content = "\n".join(page.get_text() for page in doc)
|
||||
doc.close()
|
||||
except ImportError:
|
||||
text_content = f"[PDF file: {att.filename} - extraction not available]"
|
||||
elif mime.startswith("image/"):
|
||||
text_content = f"[Image file: {att.filename} ({att.mime_type}, {att.size_bytes} bytes)]"
|
||||
else:
|
||||
text_content = f"[Binary file: {att.filename} ({att.mime_type}, {att.size_bytes} bytes)]"
|
||||
|
||||
if len(text_content) > 10000:
|
||||
text_content = text_content[:10000] + "\n... [truncated]"
|
||||
|
||||
parts.append(f"--- Attachment: {att.filename} ---\n{text_content}")
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to extract attachment %s: %s", att.filename, exc)
|
||||
parts.append(f"--- Attachment: {att.filename} (extraction failed) ---")
|
||||
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
async def stream_chat(
|
||||
db: AsyncSession,
|
||||
session: AIChatSession,
|
||||
@@ -360,18 +411,18 @@ async def stream_chat(
|
||||
user_context: dict[str, Any],
|
||||
tenant_id: uuid.UUID,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Stream chat response via SSE with tool-calling loop.
|
||||
|
||||
Yields SSE-formatted strings.
|
||||
"""
|
||||
# Load conversation history
|
||||
"""Stream chat response via SSE with tool-calling loop."""
|
||||
history = await get_session_messages(db, session.id, tenant_id)
|
||||
messages: list[dict[str, Any]] = []
|
||||
for msg in history:
|
||||
messages.append({"role": msg.role, "content": msg.content})
|
||||
|
||||
# Add user message
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
attachment_content = await _extract_attachment_content(db, session.id, tenant_id)
|
||||
full_message = user_message
|
||||
if attachment_content:
|
||||
full_message = f"{user_message}\n\n--- Attached Files ---\n{attachment_content}"
|
||||
|
||||
messages.append({"role": "user", "content": full_message})
|
||||
await save_message(db, session.id, "user", user_message, tenant_id)
|
||||
|
||||
# Get agent tools
|
||||
|
||||
Reference in New Issue
Block a user