{"id":131,"date":"2025-08-03T13:50:35","date_gmt":"2025-08-03T13:50:35","guid":{"rendered":"https:\/\/aiinfrahub.com\/about-us\/?p=131"},"modified":"2025-08-03T13:50:35","modified_gmt":"2025-08-03T13:50:35","slug":"building-a-resume-question-answering-system-using-llamaindex-openai-and-llamaparse","status":"publish","type":"post","link":"https:\/\/aiinfrahub.com\/about-us\/building-a-resume-question-answering-system-using-llamaindex-openai-and-llamaparse\/","title":{"rendered":"Building a Resume Question-Answering System Using LlamaIndex, OpenAI, and LlamaParse"},"content":{"rendered":"\n<p class=\"has-medium-font-size\">Resumes are full of rich, structured information\u2014but extracting this data automatically can be a challenge. In this blog, we\u2019ll walk through how to build a Resume Q&amp;A system using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a>LlamaIndex<\/a><\/li>\n\n\n\n<li>OpenAI&#8217;s Embeddings + GPT models<\/li>\n\n\n\n<li><a>LlamaParse<\/a> for intelligent document parsing<\/li>\n\n\n\n<li>Workflow-based orchestration with <code>llama_index.core.workflow<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"has-medium-font-size\">Let\u2019s dive into how you can transform a static resume into an interactive queryable system.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Overview<\/h1>\n\n\n\n<p>We&#8217;ll build a pipeline that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Parses a resume PDF and extracts content as markdown.<\/li>\n\n\n\n<li>Embeds the parsed content into a vector index using OpenAI embeddings.<\/li>\n\n\n\n<li>Allows querying via GPT-4o-mini.<\/li>\n\n\n\n<li>Enables reusable workflows and tools for consistent resume analytics.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p>Before you begin, make sure you have:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>llama-index<\/code><\/li>\n\n\n\n<li><code>llama-parse<\/code><\/li>\n\n\n\n<li><code>openai<\/code><\/li>\n\n\n\n<li><code>nest_asyncio<\/code> for Jupyter support<\/li>\n\n\n\n<li>OpenAI and LlamaParse API keys<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Step 1: Document Ingestion with LlamaParse<\/h1>\n\n\n\n<p>We use <code>LlamaParse<\/code> to extract structured content (in markdown) from resumes:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>documents = LlamaParse(<br>    api_key=llama_cloud_api_key,<br>    result_type=\"markdown\",<br>    system_prompt_append=\"Extract the content from the document and return it in markdown format.\"<br>).load_data(\"data\/Fake_Resume.pdf\")<\/code><\/pre>\n\n\n\n<p>This outputs clean, semantically rich text, ready for downstream indexing.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Build a Vector Index<\/h2>\n\n\n\n<p>Next, we embed the parsed content using OpenAI\u2019s <code>text-embedding-3-small<\/code> and build a vector index:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>index = VectorStoreIndex.from_documents(<br>    documents,<br>    embedding=OpenAIEmbedding(<br>        api_key=openai_api_key,<br>        model=\"text-embedding-3-small\"<br>    )<br>)<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Query Using GPT-4o-mini<\/h2>\n\n\n\n<p>We create a query engine that uses <code>gpt-4o-mini<\/code> as the backend LLM:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>query_engine = index.as_query_engine(<br>    llm=OpenAI(model=\"gpt-4o-mini\"),<br>    similarity_top_k=3<br>)<br><\/code><\/pre>\n\n\n\n<p>Now you can ask natural language questions like:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>response = query_engine.query(\"What is the name of the person and their current job title?\")<br>print(response)<br><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>The name of the person in the resume is Homer Simpson, and their current job title is Night Auditor.<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Persist and Reload the Index<\/h2>\n\n\n\n<p>To avoid rebuilding the index every time, persist it to disk:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>index.storage_context.persist(persist_dir=\".\/storage\")<br><\/code><\/pre>\n\n\n\n<p>Later, you can reload it using:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>storage_context = StorageContext.from_defaults(persist_dir=\".\/storage\")<br>restored_index = load_index_from_storage(storage_context)<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Turn Q&amp;A Into a Tool<\/h2>\n\n\n\n<p>You can create reusable tools with <code>FunctionTool<\/code> and invoke them via a <code>FunctionCallingAgent<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>def query_resume(query: str) -&gt; str:<br>    return str(query_engine.query(query))<br><br>resume_tool = FunctionTool.from_defaults(fn=query_resume)<br>agent = FunctionCallingAgent.from_tools(<\/code><br>                         <code>tools=[resume_tool],<\/code><br>                         <code>llm=llm,<\/code><br><code>          verbose=True,)<br><\/code><\/pre>\n\n\n\n<p>Chat with your resume:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>response = agent.chat(\"How many years of experience does the applicant have?\")<br>print(response)<br><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&gt; Running step 7f4678c6-35e6-406a-a468-a202c5ae760e. Step input: How many years of experience does the applicant has? \nAdded user message to memory: How many years of experience does the applicant has? \n=== Calling Function ===\nCalling function: query_resume with args: {\"query\": \"years of experience\"}\n=== Function Output ===\nThe name of the person in the resume is Homer Simpson, and their current job title is Night Auditor.\n&gt; Running step 21ac778c-2bb1-404a-b70b-8773e97cfbef. Step input: None\n=== Calling Function ===\nCalling function: query_resume with args: {\"query\": \"Homer Simpson's years of experience\"}\n=== Function Output ===\nThe name of the person in the resume is Homer Simpson, and their current job title is Night Auditor.\n&gt; Running step 325fcb29-9753-47c1-b62d-9a18fcb17d18. Step input: None\n=== LLM Response ===\nIt seems that I wasn't able to retrieve the specific years of experience for the applicant, Homer Simpson. If you have more details or specific sections of the resume you'd like me to check, please let me know!\nIt seems that I wasn't able to retrieve the specific years of experience for the applicant, Homer Simpson. If you have more details or specific sections of the resume you'd like me to check, please let me know!<\/code><\/pre>\n\n\n\n<p>Surprisingly, the fake resume which i have taken does does not have experience mentioned.Also the prints your are seeing twice because he have mentioned verbose.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Create a Workflow<\/h2>\n\n\n\n<p>LlamaIndex provides a declarative workflow API. Here\u2019s a two-step <code>RAGWorkflow<\/code>:<\/p>\n\n\n\n<p>The rag workflow we have created using all the experiments we have done individually above. The same code has been leveraged to create the workflow. <\/p>\n\n\n\n<p>Flow Diagram:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>StartEvent(resume_file, query)\n       |\n       v\n+--------------------+\n| setup_workflow     |\n|--------------------|\n| Check file exists|\n| Load or build index |\n| Persist if new    |\n| Create query engine|\n+--------------------+\n       |\n       v\nQueryEvent(query)<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">class RAGWorkflow(Workflow):<code><br><\/code>    storage_dir = \".\/storage\"<br>    llm: OpenAI<br>    query_engine = VectorStoreIndex<br><br>    @step<br>    async def setup_workflow(self, ctx: Context, ev: StartEvent) -&gt; QueryEvent:<br><br>        <sub>#Check if resume file is provided<\/sub><br>        if not ev.resume_file:<br>            raise ValueError(\"Resume file is required to setup the workflow.\")<br><br>      <sub>  # Initialize the LLM<\/sub><br>        self.llm = OpenAI(model=\"gpt-4o-mini\")<br><br><br>        <sub>#Load Index from Persistent Storage (if available)<\/sub><br>        if os.path.exists(self.storage_dir):<br>            storage_context = StorageContext.from_defaults(persist_dir=self.storage_dir)<br>            index = load_index_from_storage(storage_context)<br>        else:<br>            <sub>#parse and load your documents<\/sub><br>            documents = LlamaParse(<br>                api_key=llama_cloud_api_key,<br>                result_type=\"markdown\",<br>                system_prompt_append=\"Extract the resume content from the document and return it in markdown format.\"<br>            ).load_data(ev.resume_file)<br><br>            <sub>#embed and Index the documents<\/sub><br>            index = VectorStoreIndex.from_documents(<br>                documents,<br>                embedding=OpenAIEmbedding(<br>                    model_name=\"text-embedding-3-small\",<br>                )<br>            )<br>            index.storage_context.persist(persist_dir=self.storage_dir)<br><br>        <sub># Create the query engine<\/sub><br>        self.query_engine = index.as_query_engine(<br>            llm=self.llm,<br>            similarity_top_k=3<br>        )<br><br>       <em><sub> #Emit the QueryEvent to be consumed by ask_question function<\/sub><\/em><br>        return QueryEvent(query=ev.query)<br><br><br>    @step<br>    async def ask_question(self, ctx: Context, ev: QueryEvent) -&gt; StopEvent:<br>        response = self.query_engine.query(ev.query)<br>        return StopEvent(result=str(response))<code><br><\/code><\/pre>\n\n\n\n<p>Run the workflow:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>w = RAGWorkflow(timeout=60, verbose=False)<br>result = await w.run(<br>    resume_file=\"data\/Fake_Resume.pdf\",<br>    query=\"What is the name of the person and what is their current job title?\"<br>)<br>print(result)<br><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Loading llama_index.core.storage.kvstore.simple_kvstore from .\/storage\/docstore.json.\nLoading llama_index.core.storage.kvstore.simple_kvstore from .\/storage\/index_store.json.\nThe name of the person in the resume is Homer Simpson, and their current job title is Night Auditor.<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Visualize the Workflow<\/h2>\n\n\n\n<p>Generate a visualization of the workflow logic:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>draw_all_possible_flows(w, filename=\"workflows\/rag.html\")<br><\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"660\" height=\"320\" src=\"https:\/\/aiinfrahub.com\/wp-content\/uploads\/2025\/08\/image-6.png\" alt=\"\" class=\"wp-image-140\" srcset=\"https:\/\/aiinfrahub.com\/wp-content\/uploads\/2025\/08\/image-6.png 660w, https:\/\/aiinfrahub.com\/wp-content\/uploads\/2025\/08\/image-6-300x145.png 300w\" sizes=\"auto, (max-width: 660px) 100vw, 660px\" \/><\/figure>\n\n\n\n<p>This is useful for documentation or debugging multi-step workflows.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>With just a few lines of code and the power of LlamaIndex, OpenAI, and LlamaParse, we\u2019ve built an intelligent resume analysis system. This setup can be extended to handle:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Multiple resumes<\/li>\n\n\n\n<li>ATS (Applicant Tracking System) integrations<\/li>\n\n\n\n<li>Advanced analytics and scoring models<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">GitHub &amp; Resources<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/github.com\/juggarnautss\/Event_Driven_Agent_Doc_Workflow\/blob\/main\/rag.ipynb\">https:\/\/github.com\/juggarnautss\/Event_Driven_Agent_Doc_Workflow\/blob\/main\/rag.ipynb<\/a><\/li>\n\n\n\n<li><a>LlamaIndex Docs<\/a><\/li>\n\n\n\n<li><a>LlamaParse<\/a><\/li>\n\n\n\n<li><a class=\"\" href=\"https:\/\/platform.openai.com\/docs\/guides\/embeddings\">OpenAI Embeddings<\/a><\/li>\n<\/ul>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Resumes are full of rich, structured information\u2014but extracting this data automatically can be a challenge. In this blog, we\u2019ll walk through how to build a Resume Q&amp;A system using: Let\u2019s dive into how you can transform a static resume into an interactive queryable system. Overview We&#8217;ll build a pipeline that: Prerequisites Before you begin, make &#8230; <a title=\"Building a Resume Question-Answering System Using LlamaIndex, OpenAI, and LlamaParse\" class=\"read-more\" href=\"https:\/\/aiinfrahub.com\/about-us\/building-a-resume-question-answering-system-using-llamaindex-openai-and-llamaparse\/\" aria-label=\"Read more about Building a Resume Question-Answering System Using LlamaIndex, OpenAI, and LlamaParse\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":142,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[12,8,10,9,11],"class_list":["post-131","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-agenticai","tag-agenticai","tag-llamaindex","tag-llamaparse","tag-rag","tag-vectorsearch"],"_links":{"self":[{"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/posts\/131","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/comments?post=131"}],"version-history":[{"count":10,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/posts\/131\/revisions"}],"predecessor-version":[{"id":143,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/posts\/131\/revisions\/143"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/media\/142"}],"wp:attachment":[{"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/media?parent=131"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/categories?post=131"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/aiinfrahub.com\/about-us\/wp-json\/wp\/v2\/tags?post=131"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}