File size: 6,262 Bytes
1d253ce 1bfc642 ffcda69 1bfc642 ffcda69 1bfc642 6712e22 1d253ce 6712e22 1d253ce 6712e22 1d253ce 1bfc642 594bcbb 1d253ce 594bcbb 1d253ce 1bfc642 1d253ce 594bcbb 1bfc642 1d253ce 1bfc642 594bcbb 1d253ce 1bfc642 1d253ce 594bcbb 3e27771 1bfc642 3e27771 1bfc642 3e27771 1d253ce 3e27771 594bcbb 1d253ce 1bfc642 1d253ce 594bcbb 1d253ce 1bfc642 1d253ce 1bfc642 594bcbb 1d253ce 594bcbb 1d253ce 594bcbb 1bfc642 1d253ce 1bfc642 1d253ce 594bcbb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
import os
import streamlit as st
import nest_asyncio
# βββ 1) PATCH STREAMLITβS EVENT LOOP βββββββββββββββββββββββββββββ
nest_asyncio.apply() # allow nested awaits on Tornadoβs loop :contentReference[oaicite:3]{index=3}
import asyncio
# No new_event_loop / set_event_loop here!
# Weβll grab the existing loop when we need it.
# βββ 2) LlamaIndex & Parser Imports ββββββββββββββββββββββββββββββ
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.llms.openai import OpenAI
from llama_parse import LlamaParse
from llama_index.core import VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core.workflow import Event, StartEvent, StopEvent, Workflow, step, Context
from llama_index.core.memory import ChatMemoryBuffer
# βββ 3) Constants βββββββββββββββββββββββββββββββββββββββββββββββββ
PDF_PATH = "./data/bank-of-america.pdf"
INDEX_DIR = "./index_data"
SYSTEM_PROMPT = (
"You are an expert analyst, who excels in analyzing a company's earnings call deck. "
"Answer questions ONLY from the indexed document."
)
# βββ 4) Workflow Definition ββββββββββββββββββββββββββββββββββββββββ
class ChatResponseEvent(Event):
response: str
memory: ChatMemoryBuffer
class ChatWorkflow(Workflow):
@step
async def answer(self, ev: StartEvent) -> ChatResponseEvent:
storage = StorageContext.from_defaults(persist_dir=ev.index_dir)
index = load_index_from_storage(storage)
chat_engine = index.as_chat_engine(
chat_mode="context",
memory=ev.memory,
system_prompt=ev.system_prompt,
llm=ev.llm
)
# Still using sync .chat(), but you could switch to an async method if available :contentReference[oaicite:4]{index=4}
resp = chat_engine.chat(ev.query)
return ChatResponseEvent(response=resp.response, memory=ev.memory)
@step
async def finalize(self, ev: ChatResponseEvent) -> StopEvent:
return StopEvent(result=ev.response)
# βββ 5) Streamlit UI & Session State βββββββββββββββββββββββββββββββ
st.set_page_config(page_title="PDF Chatbot", layout="wide")
st.title("π Chat with Your PDF")
# Build or load the index
if "index_ready" not in st.session_state:
os.makedirs(INDEX_DIR, exist_ok=True)
index_meta = os.path.join(INDEX_DIR, "index_store.json")
if os.path.isfile(index_meta):
st.session_state.index_ready = True
st.success("π Loaded existing index!") # reuse existing index
else:
docs = LlamaParse(
result_type="markdown",
content_guideline_instruction="You are processing a companyβs quarterly earnings-call slide deck. "
"For each slide, produce a clearly sectioned Markdown fragment that includes:\n\n"
"1. **Slide metadata**: slide number, title, and any subtitle or date\n"
"2. **Key bullet points**: preserve existing bullets, but rewrite for clarity\n"
"3. **Tables**: convert any tables into Markdown tables, capturing headers and all rows\n"
"4. **Charts & graphs**: summarize each chart/graph in prose, highlighting axes labels, trends, and top 3 data points or percentage changes\n"
"5. **Figures & images**: if thereβs a figure caption, include it verbatim; otherwise, describe the visual in one sentence\n"
"6. **Numeric callouts**: pull out any KPIs (revenue, EPS, growth rates) into a βMetricsβ subsection\n"
"7. **Overall slide summary**: a 1β2-sentence plain-English takeaway for the slideβs purpose or conclusion\n\n"
"Keep the output strictly in Markdown, using headings (`##`, `###`), lists (`-`), and tables syntax. "
"Do not include any LLM-specific commentary or markdown outside these rules."
).load_data(PDF_PATH)
idx = VectorStoreIndex.from_documents(
docs,
embed_model=OpenAIEmbedding(model_name="text-embedding-3-small")
)
idx.storage_context.persist(persist_dir=INDEX_DIR)
st.session_state.index_ready = True
st.success("π Indexed your document and created index_store.json!")
# Initialize memory & workflow
if "memory" not in st.session_state:
st.session_state.memory = ChatMemoryBuffer.from_defaults(
llm=OpenAI(model="gpt-4o"), token_limit=1500
)
if "workflow" not in st.session_state:
st.session_state.workflow = ChatWorkflow(timeout=None, verbose=False)
# User input & async scheduling
user_input = st.text_input("Ask a question about the document:")
if user_input:
# 1) Grab the running loop (patched by nest_asyncio)
loop = asyncio.get_event_loop() # returns Tornadoβs loop :contentReference[oaicite:5]{index=5}
# 2) Schedule the workflow.run coroutine on that loop
future = asyncio.run_coroutine_threadsafe(
st.session_state.workflow.run(
index_dir=INDEX_DIR,
query=user_input,
system_prompt=SYSTEM_PROMPT,
memory=st.session_state.memory,
llm=OpenAI(model="gpt-4o")
),
loop
)
# 3) Wait for the result (non-blocking at the loop level)
stop_evt: StopEvent = future.result() # avoids run_until_complete errors :contentReference[oaicite:6]{index=6}
# 4) Update state & display
st.session_state.memory = stop_evt.memory
st.markdown(f"**Bot:** {stop_evt.result}")
# End Chat button
if st.button("End Chat"):
st.write("Chat ended. Refresh to start over.")
st.stop()
|