feat: Meeting Assistant MVP - Complete implementation
Enterprise Meeting Knowledge Management System with: Backend (FastAPI): - Authentication proxy with JWT (pj-auth-api integration) - MySQL database with 4 tables (users, meetings, conclusions, actions) - Meeting CRUD with system code generation (C-YYYYMMDD-XX, A-YYYYMMDD-XX) - Dify LLM integration for AI summarization - Excel export with openpyxl - 20 unit tests (all passing) Client (Electron): - Login page with company auth - Meeting list with create/delete - Meeting detail with real-time transcription - Editable transcript textarea (single block, easy editing) - AI summarization with conclusions/action items - 5-second segment recording (efficient for long meetings) Sidecar (Python): - faster-whisper medium model with int8 quantization - ONNX Runtime VAD (lightweight, ~20MB vs PyTorch ~2GB) - Chinese punctuation processing - OpenCC for Traditional Chinese conversion - Anti-hallucination parameters - Auto-cleanup of temp audio files OpenSpec: - add-meeting-assistant-mvp (47 tasks, archived) - add-realtime-transcription (29 tasks, archived) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
102
backend/app/routers/ai.py
Normal file
102
backend/app/routers/ai.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
import httpx
|
||||
import json
|
||||
|
||||
from ..config import settings
|
||||
from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload
|
||||
from .auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/ai/summarize", response_model=SummarizeResponse)
|
||||
async def summarize_transcript(
|
||||
request: SummarizeRequest, current_user: TokenPayload = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Send transcript to Dify for AI summarization.
|
||||
Returns structured conclusions and action items.
|
||||
"""
|
||||
if not settings.DIFY_API_KEY:
|
||||
raise HTTPException(status_code=503, detail="Dify API not configured")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(
|
||||
f"{settings.DIFY_API_URL}/chat-messages",
|
||||
headers={
|
||||
"Authorization": f"Bearer {settings.DIFY_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"inputs": {},
|
||||
"query": request.transcript,
|
||||
"response_mode": "blocking",
|
||||
"user": current_user.email,
|
||||
},
|
||||
timeout=120.0, # Long timeout for LLM processing
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(
|
||||
status_code=response.status_code,
|
||||
detail=f"Dify API error: {response.text}",
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
answer = data.get("answer", "")
|
||||
|
||||
# Try to parse structured JSON from Dify response
|
||||
parsed = parse_dify_response(answer)
|
||||
|
||||
return SummarizeResponse(
|
||||
conclusions=parsed["conclusions"],
|
||||
action_items=[
|
||||
ActionItemCreate(
|
||||
content=item.get("content", ""),
|
||||
owner=item.get("owner", ""),
|
||||
due_date=item.get("due_date"),
|
||||
)
|
||||
for item in parsed["action_items"]
|
||||
],
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=504, detail="Dify API timeout - transcript may be too long"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=503, detail=f"Dify API unavailable: {str(e)}")
|
||||
|
||||
|
||||
def parse_dify_response(answer: str) -> dict:
|
||||
"""
|
||||
Parse Dify response to extract conclusions and action items.
|
||||
Attempts JSON parsing first, then falls back to text parsing.
|
||||
"""
|
||||
# Try to find JSON in the response
|
||||
try:
|
||||
# Look for JSON block
|
||||
if "```json" in answer:
|
||||
json_start = answer.index("```json") + 7
|
||||
json_end = answer.index("```", json_start)
|
||||
json_str = answer[json_start:json_end].strip()
|
||||
elif "{" in answer and "}" in answer:
|
||||
# Try to find JSON object
|
||||
json_start = answer.index("{")
|
||||
json_end = answer.rindex("}") + 1
|
||||
json_str = answer[json_start:json_end]
|
||||
else:
|
||||
raise ValueError("No JSON found")
|
||||
|
||||
data = json.loads(json_str)
|
||||
return {
|
||||
"conclusions": data.get("conclusions", []),
|
||||
"action_items": data.get("action_items", []),
|
||||
}
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
# Fallback: return raw answer as single conclusion
|
||||
return {
|
||||
"conclusions": [answer] if answer else [],
|
||||
"action_items": [],
|
||||
}
|
||||
Reference in New Issue
Block a user