from __future__ import annotations from functools import lru_cache from typing import Any from langchain_core.output_parsers import PydanticOutputParser from langchain_core.runnables import RunnableLambda, RunnablePassthrough from langchain_google_genai import ChatGoogleGenerativeAI from pydantic import BaseModel from .config import settings from .prompts import prompt from .retriever import get_retriever class StructuredAnswer(BaseModel): answer: str sources: list[str] @lru_cache(maxsize=1) def get_chain() -> Any: llm_kwargs: dict[str, Any] = { "model": settings.google_genai_model, "temperature": settings.google_genai_temperature, } if settings.google_genai_api_key: llm_kwargs["api_key"] = settings.google_genai_api_key llm = ChatGoogleGenerativeAI(**llm_kwargs) context_formatter = RunnableLambda( lambda docs: "\n\n".join( f"[{i + 1}] id={doc.metadata.get('id', '')}\n{doc.page_content}" for i, doc in enumerate(docs) ) ) output_parser = PydanticOutputParser(pydantic_object=StructuredAnswer) prompt_with_format = prompt.partial( format_instructions=output_parser.get_format_instructions() ) return ( {"context": get_retriever() | context_formatter, "question": RunnablePassthrough()} | prompt_with_format | llm | output_parser ) def answer(question: str) -> StructuredAnswer: return get_chain().invoke(question) async def aanswer(question: str) -> StructuredAnswer: return await get_chain().ainvoke(question)