""" Core agent orchestration — entry point dùng chung cho API và UI. """ import base64 import json import logging import mimetypes import time from datetime import datetime from typing import Optional logger = logging.getLogger(__name__) from langchain_core.messages import HumanMessage, ToolMessage from src.conversation_memory import add_turn from src.graph import run from src.nodes import final_response_node, image_response_node from src.pdf_processing import format_chat_history, pdf_to_markdown from src.qdrant_store import get_custom_prompt from src.quiz import generate_quiz from src.redis_client import redis_client from src.state import MAX_ITERS, AgentState def final_answer( conversation_id: str, sender_id: str, query: str, pdf_path: Optional[str] = None, image_path: Optional[str] = None, pdf_name: Optional[str] = None, gen_quiz: bool = False, k_question: Optional[int] = None, skip_pdf_indexing: bool = False, ) -> tuple[str, str, str | None, str | None]: """ Khởi tạo AgentState, chạy graph, trả về 4 giá trị. Returns: (answer, elapsed, chart_type, chart_data) - chart_type : "column" | "pie" | None - chart_data : JSON string | None (chỉ có khi tool summarize_chart được gọi) Raises: ValueError: nếu bất kỳ tham số bắt buộc nào rỗng. """ conversation_id = conversation_id.strip() sender_id = sender_id.strip() query = query.strip() if not conversation_id: raise ValueError("conversation_id không được để trống.") if not sender_id: raise ValueError("sender_id không được để trống.") if not query: raise ValueError("query không được để trống.") if gen_quiz: t0 = time.perf_counter() answer = generate_quiz(query, k_question or 10) return answer, f"{time.perf_counter() - t0:.2f}s", None, None custom_prompt = get_custom_prompt(sender_id) if pdf_path is not None and not skip_pdf_indexing: # Auto-index vào Qdrant để dùng với rag_search sau này (idempotent) try: from src.pdf_rag import index_pdf index_pdf(pdf_path, pdf_name or "document.pdf", conversation_id) except Exception: logger.exception("Auto-index PDF thất bại.") pdf_content = pdf_to_markdown(pdf_path) chat_history = redis_client.get_chat_history(conversation_id) chat_text = format_chat_history(chat_history) tool_content = ( f"[Nội dung PDF]\n{pdf_content}" f"\n\n[Lịch sử trò chuyện]\n{chat_text}" ) state: AgentState = { "conversation_id": conversation_id, "sender_id": sender_id, "time": datetime.now().isoformat(), "raw_query": query, "query_type": None, "messages": [ HumanMessage(content=query), ToolMessage(content=tool_content, tool_call_id="pdf_reader", name="pdf_reader"), ], "iters": 0, "max_iters": MAX_ITERS, "final_answer": None, "custom_prompt": custom_prompt, } t0 = time.perf_counter() result = final_response_node(state) elapsed = f"{time.perf_counter() - t0:.2f}s" answer = result.get("final_answer") or "(Không có kết quả)" add_turn(conversation_id, sender_id, query, answer) return answer, elapsed, None, None if image_path is not None: mime_type, _ = mimetypes.guess_type(image_path) mime_type = mime_type or "image/jpeg" with open(image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode() chat_history = redis_client.get_chat_history(conversation_id) chat_text = format_chat_history(chat_history) text_content = f"{query}\n\n[Lịch sử trò chuyện]\n{chat_text}" state: AgentState = { "conversation_id": conversation_id, "sender_id": sender_id, "time": datetime.now().isoformat(), "raw_query": query, "query_type": None, "messages": [ HumanMessage(content=[ {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{image_b64}"}}, {"type": "text", "text": text_content}, ]), ], "iters": 0, "max_iters": MAX_ITERS, "final_answer": None, "custom_prompt": custom_prompt, } t0 = time.perf_counter() result = image_response_node(state) elapsed = f"{time.perf_counter() - t0:.2f}s" answer = result.get("final_answer") or "(Không có kết quả)" add_turn(conversation_id, sender_id, query, answer) return answer, elapsed, None, None initial_state: AgentState = { "conversation_id": conversation_id, "sender_id": sender_id, "time": datetime.now().isoformat(), "raw_query": query, "query_type": None, "messages": [], "iters": 0, "max_iters": MAX_ITERS, "final_answer": None, "custom_prompt": custom_prompt, } t0 = time.perf_counter() result = run(initial_state) elapsed = f"{time.perf_counter() - t0:.2f}s" answer = result.get("final_answer") or "(Không có kết quả)" chart_type: str | None = None chart_data: str | None = None for msg in result.get("messages", []): if isinstance(msg, ToolMessage) and getattr(msg, "name", None) == "summarize_chart": try: parsed = json.loads(msg.content) if parsed.get("status") == "success": chart_type = parsed.get("chart_type") chart_data = json.dumps(parsed.get("chart_data", []), ensure_ascii=False) except Exception: pass break add_turn(conversation_id, sender_id, query, answer) return answer, elapsed, chart_type, chart_data if __name__ == "__main__": answer, elapsed = final_answer( conversation_id="04ba40fe-61c7-4906-9f51-5ada0a392dac", sender_id="@slavakpa", query="tóm tắt nội dung tài liệu này", pdf_path="temp/test_doc.pdf", ) print(answer) print(f"\n({elapsed})")