Conversational retrieval needs both relevant indexed text and the turns that give an ambiguous follow-up its meaning. A LlamaIndex chat engine combines those inputs so a question such as “Where should that ticket be escalated?” can refer to a ticket identified earlier in the conversation.
The as_chat_engine() API creates a stateful interface from an existing index. Its context mode retrieves nodes for each message, supplies their text to the language model, and retains user and assistant messages in the engine's chat history.
The local run uses MockEmbedding plus a deterministic CustomLLM adapter, which keeps the transcript repeatable without an API key or model server. The adapter extracts values from the retrieved runbook and earlier user turns; isolated controls prove that neither input can produce the ticket-specific follow-up answer alone.
$ python3 -m pip install llama-index-core
import re from typing import Any from llama_index.core import Document, Settings, VectorStoreIndex from llama_index.core.embeddings import MockEmbedding from llama_index.core.llms import CompletionResponse, CustomLLM, LLMMetadata from llama_index.core.llms.callbacks import llm_completion_callback NO_MATCH = "No matching runbook entry was found." CONTEXT_MARKER = "--------------------\n" def prompt_context(prompt: str) -> str: parts = prompt.split(CONTEXT_MARKER, 2) return parts[1].strip() if len(parts) == 3 else "" def prompt_user_messages(prompt: str) -> list[str]: return re.findall(r"^user: (.+)$", prompt, flags=re.MULTILINE)
class SupportRunbookLLM(CustomLLM): @property def metadata(self) -> LLMMetadata: return LLMMetadata( context_window=2048, num_output=128, model_name="support-runbook-llm", ) def answer_from_prompt(self, prompt: str) -> str: context = prompt_context(prompt) user_messages = prompt_user_messages(prompt) if not user_messages: return NO_MATCH question = user_messages[-1] if "who owns" in question.lower(): ticket = re.search(r"\bticket (\d+)\b", question, flags=re.IGNORECASE) if ticket: owner = re.search( rf"ticket {re.escape(ticket.group(1))} belongs to ([A-Z][A-Za-z-]+)\.", context, ) if owner: return f"Ticket {ticket.group(1)} belongs to {owner.group(1)}." if "where should" in question.lower() and "escalated" in question.lower(): earlier_turns = "\n".join(user_messages[:-1]) ticket = re.search( r"\bticket (\d+)\b", earlier_turns, flags=re.IGNORECASE, ) if ticket: escalation = re.search( rf"([A-Z][^.]*) for ticket {re.escape(ticket.group(1))} " r"should be escalated to the ([a-z0-9-]+ queue)\.", context, ) if escalation: return ( f"{escalation.group(1)} for ticket {ticket.group(1)} " f"should be escalated to the {escalation.group(2)}." ) return NO_MATCH
SupportRunbookLLM is test support for a repeatable local run, not a production language model.
@llm_completion_callback() def complete( self, prompt: str, formatted: bool = False, **kwargs: Any, ) -> CompletionResponse: return CompletionResponse(text=self.answer_from_prompt(prompt)) @llm_completion_callback() def stream_complete( self, prompt: str, formatted: bool = False, **kwargs: Any, ): text = self.answer_from_prompt(prompt) running_text = "" for token in text.split(): running_text = f"{running_text}{token} " yield CompletionResponse(text=running_text.strip(), delta=f"{token} ")
Settings.llm = SupportRunbookLLM() Settings.embed_model = MockEmbedding(embed_dim=8) documents = [ Document( text=( "Support runbook: ticket 7421 belongs to Maya. " "Billing chatbot answers for ticket 7421 should be escalated " "to the docs-review queue." ) ) ] index = VectorStoreIndex.from_documents(documents)
chat_engine = index.as_chat_engine(chat_mode="context", similarity_top_k=1) first_response = chat_engine.chat("Who owns ticket 7421?") follow_up_response = chat_engine.chat( "Where should that ticket's chatbot answers be escalated?" ) missing_history_engine = index.as_chat_engine(chat_mode="context", similarity_top_k=1) missing_history_response = missing_history_engine.chat( "Where should that ticket's chatbot answers be escalated?" ) unrelated_index = VectorStoreIndex.from_documents( [Document(text="Office support hours are 09:00 to 17:00 on weekdays.")] ) missing_context_engine = unrelated_index.as_chat_engine( chat_mode="context", similarity_top_k=1, ) missing_context_engine.chat("Who owns ticket 7421?") missing_context_response = missing_context_engine.chat( "Where should that ticket's chatbot answers be escalated?" ) print("Q: Who owns ticket 7421?") print(f"A: {first_response}") print("Q: Where should that ticket's chatbot answers be escalated?") print(f"A: {follow_up_response}") print("stored messages:", len(chat_engine.chat_history)) print("missing history:", missing_history_response) print("missing context:", missing_context_response) controls_blocked = ( str(missing_history_response) == NO_MATCH and str(missing_context_response) == NO_MATCH ) print("controls blocked:", controls_blocked) assert str(first_response) == "Ticket 7421 belongs to Maya." assert str(follow_up_response) == ( "Billing chatbot answers for ticket 7421 should be escalated " "to the docs-review queue." ) assert controls_blocked
$ python3 chat_engine_run.py Q: Who owns ticket 7421? A: Ticket 7421 belongs to Maya. Q: Where should that ticket's chatbot answers be escalated? A: Billing chatbot answers for ticket 7421 should be escalated to the docs-review queue. stored messages: 4 missing history: No matching runbook entry was found. missing context: No matching runbook entry was found. controls blocked: True
The follow-up supplies no ticket number. The positive engine extracts ticket 7421 from an earlier user turn and the escalation queue from retrieved runbook text, while each control removes one of those required inputs and reaches the non-answer path.