"""MAF workflow converted from chat-with-pdf/flow.dag.yaml.multi-node Graph: InputExecutor ──fan-out──> DownloadExecutor -> BuildIndexExecutor ──┐ └──────> RewriteQuestionExecutor ─────────────────┘──fan-in──> QnaExecutor InputExecutor: sets up directories, fans out ChatInput DownloadExecutor: downloads PDF from url BuildIndexExecutor: builds FAISS index from PDF RewriteQuestionExecutor: rewrites question using chat history QnaExecutor: finds context from index, runs QnA, yields output """ import asyncio import os from dataclasses import dataclass, field from dotenv import load_dotenv from typing_extensions import Never from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler from chat_with_pdf.download import download from chat_with_pdf.build_index import create_faiss_index from chat_with_pdf.rewrite_question import rewrite_question from chat_with_pdf.find_context import find_context from chat_with_pdf.qna import qna from chat_with_pdf.constants import PDF_DIR, INDEX_DIR load_dotenv() @dataclass class ChatInput: question: str pdf_url: str = "https://arxiv.org/pdf/1810.04805.pdf" chat_history: list = field(default_factory=list) @dataclass class QnaBranchResult: index_path: str = "" rewritten_question: str = "" chat_history: list = field(default_factory=list) def _convert_chat_history(history: list) -> list[dict]: messages = [] for item in history: messages.append({"role": "user", "content": item["inputs"]["question"]}) messages.append({"role": "assistant", "content": item["outputs"]["answer"]}) return messages class InputExecutor(Executor): @handler async def receive(self, chat_input: ChatInput, ctx: WorkflowContext[ChatInput]) -> None: os.makedirs(PDF_DIR, exist_ok=True) os.makedirs(INDEX_DIR, exist_ok=True) await ctx.send_message(chat_input) class DownloadExecutor(Executor): @handler async def run(self, chat_input: ChatInput, ctx: WorkflowContext[str]) -> None: pdf_path = download(chat_input.pdf_url) await ctx.send_message(pdf_path) class BuildIndexExecutor(Executor): @handler async def run(self, pdf_path: str, ctx: WorkflowContext[QnaBranchResult]) -> None: index_path = create_faiss_index(pdf_path) await ctx.send_message(QnaBranchResult(index_path=index_path)) class RewriteQuestionExecutor(Executor): @handler async def run(self, chat_input: ChatInput, ctx: WorkflowContext[QnaBranchResult]) -> None: rewritten = rewrite_question(chat_input.question, chat_input.chat_history) await ctx.send_message(QnaBranchResult( rewritten_question=rewritten, chat_history=chat_input.chat_history, )) class QnaExecutor(Executor): @handler async def run(self, results: list[QnaBranchResult], ctx: WorkflowContext[Never, dict]) -> None: index_path = "" rewritten_question = "" chat_history = [] for r in results: if r.index_path: index_path = r.index_path if r.rewritten_question: rewritten_question = r.rewritten_question if r.chat_history: chat_history = r.chat_history prompt, context = find_context(rewritten_question, index_path) history_messages = _convert_chat_history(chat_history) stream = qna(prompt, history_messages) answer = "".join(stream) await ctx.yield_output({"answer": answer, "context": context}) def create_workflow(): """Create a fresh workflow instance. MAF workflows do not support concurrent execution, so each concurrent caller needs its own workflow instance. """ _input = InputExecutor(id="input") _download = DownloadExecutor(id="download") _build_index = BuildIndexExecutor(id="build_index") _rewrite = RewriteQuestionExecutor(id="rewrite_question") _qna = QnaExecutor(id="qna") return ( WorkflowBuilder(name="ChatWithPdfMultiNode", start_executor=_input) .add_fan_out_edges(_input, [_download, _rewrite]) .add_edge(_download, _build_index) .add_fan_in_edges([_build_index, _rewrite], _qna) .build() ) async def main(): workflow = create_workflow() result = await workflow.run( ChatInput(question="what NLP tasks does it perform well?") ) output = result.get_outputs()[0] print(f"Answer: {output['answer']}") print(f"Context: {output['context']}") if __name__ == "__main__": asyncio.run(main())