e768098d0e
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled
164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
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.build_index import create_faiss_index
|
|
from chat_with_pdf.constants import PDF_DIR, INDEX_DIR
|
|
from chat_with_pdf.download import download
|
|
from chat_with_pdf.find_context import find_context
|
|
from chat_with_pdf.qna import qna
|
|
from chat_with_pdf.rewrite_question import rewrite_question
|
|
from chat_with_pdf.utils.lock import acquire_lock
|
|
|
|
load_dotenv()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data classes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class PdfChatInput:
|
|
question: str
|
|
pdf_url: str = "https://arxiv.org/pdf/1810.04805.pdf"
|
|
chat_history: list = field(default_factory=list)
|
|
config: dict = field(default_factory=lambda: {
|
|
"EMBEDDING_MODEL_DEPLOYMENT_NAME": os.environ.get("EMBEDDING_MODEL_DEPLOYMENT_NAME", "text-embedding-ada-002"),
|
|
"CHAT_MODEL_DEPLOYMENT_NAME": os.environ.get("CHAT_MODEL_DEPLOYMENT_NAME", "gpt-4"),
|
|
"PROMPT_TOKEN_LIMIT": os.environ.get("PROMPT_TOKEN_LIMIT", "3000"),
|
|
"MAX_COMPLETION_TOKENS": os.environ.get("MAX_COMPLETION_TOKENS", "1024"),
|
|
"VERBOSE": os.environ.get("VERBOSE", "true"),
|
|
"CHUNK_SIZE": os.environ.get("CHUNK_SIZE", "1024"),
|
|
"CHUNK_OVERLAP": os.environ.get("CHUNK_OVERLAP", "64"),
|
|
})
|
|
|
|
|
|
@dataclass
|
|
class BranchResult:
|
|
index_path: str | None = None
|
|
rewritten_question: str | None = None
|
|
chat_history: list = field(default_factory=list)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Executors
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _to_chatml(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):
|
|
"""Sets environment variables from config and creates working directories."""
|
|
|
|
@handler
|
|
async def receive(self, inp: PdfChatInput, ctx: WorkflowContext[PdfChatInput]) -> None:
|
|
for key, value in inp.config.items():
|
|
os.environ[key] = str(value)
|
|
|
|
base_dir = os.path.join(os.path.dirname(__file__), "chat_with_pdf")
|
|
with acquire_lock(os.path.join(base_dir, "create_folder.lock")):
|
|
os.makedirs(PDF_DIR, exist_ok=True)
|
|
os.makedirs(INDEX_DIR, exist_ok=True)
|
|
|
|
await ctx.send_message(inp)
|
|
|
|
|
|
class IndexExecutor(Executor):
|
|
"""Downloads PDF and builds FAISS index (merges download_tool + build_index_tool)."""
|
|
|
|
@handler
|
|
async def process(self, inp: PdfChatInput, ctx: WorkflowContext[BranchResult]) -> None:
|
|
pdf_path = download(inp.pdf_url)
|
|
index_path = create_faiss_index(pdf_path)
|
|
await ctx.send_message(BranchResult(index_path=index_path))
|
|
|
|
|
|
class RewriteExecutor(Executor):
|
|
"""Rewrites question using chat history for better context retrieval."""
|
|
|
|
@handler
|
|
async def process(self, inp: PdfChatInput, ctx: WorkflowContext[BranchResult]) -> None:
|
|
rewritten = rewrite_question(inp.question, _to_chatml(inp.chat_history))
|
|
await ctx.send_message(BranchResult(
|
|
rewritten_question=rewritten,
|
|
chat_history=inp.chat_history,
|
|
))
|
|
|
|
|
|
class ContextAndQnAExecutor(Executor):
|
|
"""Fan-in: finds relevant context from index, then generates answer."""
|
|
|
|
@handler
|
|
async def process(self, results: list[BranchResult], ctx: WorkflowContext[Never, dict]) -> None:
|
|
index_path = None
|
|
rewritten_question = None
|
|
chat_history: list = []
|
|
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)
|
|
|
|
stream = qna(prompt, _to_chatml(chat_history))
|
|
answer = "".join(stream)
|
|
|
|
await ctx.yield_output({
|
|
"answer": answer,
|
|
"context": [c.text for c in context],
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workflow: input → fan_out[index, rewrite] → fan_in → context_qna
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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")
|
|
_index = IndexExecutor(id="index")
|
|
_rewrite = RewriteExecutor(id="rewrite")
|
|
_context_qna = ContextAndQnAExecutor(id="context_qna")
|
|
return (
|
|
WorkflowBuilder(name="ChatWithPdfWorkflow", start_executor=_input)
|
|
.add_fan_out_edges(_input, [_index, _rewrite])
|
|
.add_fan_in_edges([_index, _rewrite], _context_qna)
|
|
.build()
|
|
)
|
|
|
|
|
|
async def main():
|
|
workflow = create_workflow()
|
|
result = await workflow.run(
|
|
PdfChatInput(
|
|
question="What is BERT?",
|
|
pdf_url="https://arxiv.org/pdf/1810.04805.pdf",
|
|
)
|
|
)
|
|
output = result.get_outputs()[0]
|
|
print(f"Answer: {output['answer']}")
|
|
print(f"Context: {output['context']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|