{ "cells": [ { "cell_type": "code", "execution_count": 2, "id": "6437100e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import os\n", "import json\n", "import re\n", "import hashlib\n", "import numpy as np\n", "from collections import defaultdict\n", "from pathlib import Path\n", "from threading import Lock\n", "from typing import TypedDict, Annotated, Sequence, Dict, Optional, List, Literal, Type\n", "from uuid import uuid4\n", "from dotenv import load_dotenv\n", "load_dotenv()" ] }, { "cell_type": "code", "execution_count": 3, "id": "309266a4", "metadata": {}, "outputs": [], "source": [ "from langchain_core.messages import BaseMessage, ToolMessage, SystemMessage\n", "from langchain_core.tools import tool\n", "from langchain_openai import ChatOpenAI\n", "from langchain_core.output_parsers import StrOutputParser\n", "# from langchain_core.prompts import ChatPromptTemplate\n", "from langchain_huggingface import HuggingFaceEmbeddings\n", "from langchain_community.vectorstores import FAISS\n", "from langchain_community.retrievers import BM25Retriever\n", "from langchain_nvidia_ai_endpoints import ChatNVIDIA\n", "from langgraph.graph.message import add_messages\n", "from langgraph.graph import StateGraph, START, END\n", "from langgraph.prebuilt import ToolNode\n", "from langgraph.checkpoint.memory import MemorySaver" ] }, { "cell_type": "code", "execution_count": 4, "id": "c3bfc566", "metadata": {}, "outputs": [], "source": [ "api_key = os.environ.get(\"NVIDIA_API_KEY\")\n", "if not api_key:\n", " raise RuntimeError(\"🚨 NVIDIA_API_KEY not found in environment!\")" ] }, { "cell_type": "code", "execution_count": 5, "id": "c2b25c54", "metadata": {}, "outputs": [], "source": [ "# Constants\n", "FAISS_PATH = \"data/faiss_store/v64_600-150\"\n", "CHUNKS_PATH = \"data/all_chunks.json\"" ] }, { "cell_type": "code", "execution_count": 6, "id": "9e567842", "metadata": {}, "outputs": [], "source": [ "# Validate files\n", "if not Path(FAISS_PATH).exists():\n", " raise FileNotFoundError(f\"FAISS index not found at {FAISS_PATH}\")\n", "if not Path(CHUNKS_PATH).exists():\n", " raise FileNotFoundError(f\"Chunks file not found at {CHUNKS_PATH}\")" ] }, { "cell_type": "code", "execution_count": 7, "id": "1675322b", "metadata": {}, "outputs": [], "source": [ "KRISHNA_BIO = \"\"\"Krishna Vamsi Dhulipalla completed masters in Computer Science at Virginia Tech, awarded degree in december 2024, with over 3 years of experience across data engineering, machine learning research, and real-time analytics. He specializes in building scalable data systems and intelligent LLM-powered applications, with strong expertise in Python, PyTorch, Hugging Face Transformers, and end-to-end ML pipelines.\n", "He has led projects involving retrieval-augmented generation (RAG), feature selection for genomic classification, fine-tuning domain-specific LLMs (e.g., DNABERT, HyenaDNA), and real-time forecasting systems using Kafka, Spark, and Airflow. His cloud proficiency spans AWS (S3, SageMaker, ECS, CloudWatch), GCP (BigQuery, Cloud Composer), and DevOps tools like Docker, Kubernetes, and MLflow.\n", "Krishna’s research has focused on genomic sequence modeling, transformer optimization, MLOps automation, and cross-domain generalization. He has published work in bioinformatics and machine learning applications for circadian transcription prediction and transcription factor binding.\n", "He holds certifications in NVIDIA’s RAG Agents with LLMs, Google Cloud Data Engineering, and AWS ML Specialization. Krishna is passionate about scalable LLM infrastructure, data-centric AI, and domain-adaptive ML solutions — combining deep technical expertise with real-world engineering impact.\n", "\\n\\n\n", "Beside carrer, Krishna loves hiking, cricket, and exploring new technologies. He is big fan of Marvel Movies and Space exploration.\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": 8, "id": "62e79289", "metadata": {}, "outputs": [], "source": [ "# Load resources\n", "def load_chunks(path=CHUNKS_PATH) -> List[Dict]:\n", " with open(path, \"r\", encoding=\"utf-8\") as f:\n", " return json.load(f)\n", "\n", "def load_faiss(path=FAISS_PATH, model_name=\"sentence-transformers/all-MiniLM-L6-v2\") -> FAISS:\n", " embeddings = HuggingFaceEmbeddings(model_name=model_name)\n", " return FAISS.load_local(path, embeddings, allow_dangerous_deserialization=True)" ] }, { "cell_type": "code", "execution_count": 9, "id": "294e0e9b", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\vamsi\\OneDrive\\Desktop\\LangGraph\\env\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", " from .autonotebook import tqdm as notebook_tqdm\n" ] } ], "source": [ "vectorstore = load_faiss()\n", "all_chunks = load_chunks()\n", "all_texts = [chunk[\"text\"] for chunk in all_chunks]\n", "metadatas = [chunk[\"metadata\"] for chunk in all_chunks]\n", "bm25_retriever = BM25Retriever.from_texts(texts=all_texts, metadatas=metadatas)" ] }, { "cell_type": "code", "execution_count": 10, "id": "f5e5c86b", "metadata": {}, "outputs": [], "source": [ "K_PER_QUERY = 8 # how many from each retriever\n", "TOP_K = 8 # final results to return\n", "RRF_K = 60 # reciprocal-rank-fusion constant\n", "RERANK_TOP_N = 50 # rerank this many fused hits\n", "MMR_LAMBDA = 0.7 # 0..1 (higher favors query relevance; lower favors diversity)\n", "CE_MODEL = \"cross-encoder/ms-marco-MiniLM-L-6-v2\"\n", "ALPHA = 0.7" ] }, { "cell_type": "code", "execution_count": 11, "id": "c92d2f3e", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\vamsi\\OneDrive\\Desktop\\LangGraph\\env\\Lib\\site-packages\\huggingface_hub\\file_download.py:143: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\\Users\\vamsi\\.cache\\huggingface\\hub\\models--cross-encoder--ms-marco-MiniLM-L-6-v2. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.\n", "To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development\n", " warnings.warn(message)\n", "Xet Storage is enabled for this repo, but the 'hf_xet' package is not installed. Falling back to regular HTTP download. For better performance, install the package with: `pip install huggingface_hub[hf_xet]` or `pip install hf_xet`\n" ] } ], "source": [ "from sentence_transformers import CrossEncoder\n", "_cross_encoder = CrossEncoder(CE_MODEL)" ] }, { "cell_type": "code", "execution_count": 12, "id": "aae2d763", "metadata": {}, "outputs": [], "source": [ "embeddings = HuggingFaceEmbeddings(\n", " model_name=\"sentence-transformers/all-MiniLM-L6-v2\",\n", " model_kwargs={\"device\": \"cpu\"},\n", " encode_kwargs={\"normalize_embeddings\": True},\n", " )" ] }, { "cell_type": "code", "execution_count": 13, "id": "e1bcf244", "metadata": {}, "outputs": [], "source": [ "def _cosine_sim_matrix(A: np.ndarray, B: np.ndarray) -> np.ndarray:\n", " # A: mxd, B: nxd, both should be L2-normalized\n", " return A @ B.T\n", "\n", "def _l2_normalize(v: np.ndarray) -> np.ndarray:\n", " n = np.linalg.norm(v, axis=1, keepdims=True) + 1e-12\n", " return v / n\n", "\n", "def _mmr_select(query_vec: np.ndarray, cand_vecs: np.ndarray, k: int, mmr_lambda: float):\n", " # returns list of selected indices using MMR\n", " selected = []\n", " remaining = list(range(cand_vecs.shape[0]))\n", "\n", " # precompute similarities\n", " q_sim = (cand_vecs @ query_vec.reshape(-1, 1)).ravel() # cosine since normalized\n", " doc_sims = _cosine_sim_matrix(cand_vecs, cand_vecs)\n", "\n", " # pick first by highest query similarity\n", " first = int(np.argmax(q_sim))\n", " selected.append(first)\n", " remaining.remove(first)\n", "\n", " while remaining and len(selected) < k:\n", " # for each remaining, compute MMR score = λ * Sim(q, d) - (1-λ) * max Sim(d, s in selected)\n", " sub = np.array(remaining)\n", " sim_to_selected = doc_sims[np.ix_(sub, selected)].max(axis=1)\n", " mmr_scores = mmr_lambda * q_sim[sub] - (1.0 - mmr_lambda) * sim_to_selected\n", " nxt = int(sub[np.argmax(mmr_scores)])\n", " selected.append(nxt)\n", " remaining.remove(nxt)\n", "\n", " return selected" ] }, { "cell_type": "code", "execution_count": null, "id": "d3cad454", "metadata": {}, "outputs": [], "source": [ "@tool(\"retriever\")\n", "def retriever(query: str) -> list[str]:\n", " \"\"\"Retrieve relevant chunks from the profile using FAISS + BM25, fused with RRF.\"\"\"\n", " # ensure both retrievers return K_PER_QUERY\n", " # For BM25Retriever in LangChain this is usually `.k`\n", " try:\n", " bm25_retriever.k = K_PER_QUERY\n", " except Exception:\n", " pass\n", "\n", " vec_hits = vectorstore.similarity_search_with_score(query, k=K_PER_QUERY) # [(Document, score)]\n", " bm_hits = bm25_retriever.invoke(query) # [Document]\n", "\n", " # --- fuse via RRF (rank-only) ---\n", " fused = defaultdict(lambda: {\n", " \"rrf\": 0.0,\n", " \"vec_rank\": None, \"bm_rank\": None,\n", " \"content\": None, \"metadata\": None,\n", " })\n", "\n", " for rank, (doc, _score) in enumerate(vec_hits):\n", " key = hashlib.md5(doc.page_content.encode(\"utf-8\")).hexdigest()\n", " fused[key][\"rrf\"] += 1.0 / (rank + 1 + RRF_K)\n", " fused[key][\"vec_rank\"] = rank\n", " fused[key][\"content\"] = doc.page_content # keep FULL text\n", " fused[key][\"metadata\"] = getattr(doc, \"metadata\", {}) or {}\n", "\n", " for rank, doc in enumerate(bm_hits):\n", " key = hashlib.md5(doc.page_content.encode(\"utf-8\")).hexdigest()\n", " fused[key][\"rrf\"] += 1.0 / (rank + 1 + RRF_K)\n", " fused[key][\"bm_rank\"] = rank\n", " fused[key][\"content\"] = doc.page_content # keep FULL text\n", " fused[key][\"metadata\"] = getattr(doc, \"metadata\", {}) or {}\n", "\n", " items = list(fused.values())\n", " items.sort(key=lambda x: x[\"rrf\"], reverse=True)\n", "\n", " # --- cross-encoder rerank on top-N (keeps exact text; just reorders) ---\n", " topN = items[:RERANK_TOP_N] if RERANK_TOP_N > 0 else items\n", " try:\n", " pairs = [(query, it[\"content\"] or \"\") for it in topN]\n", " ce_scores = _cross_encoder.predict(pairs) # higher is better\n", " for it, s in zip(topN, ce_scores):\n", " it[\"rerank\"] = float(s)\n", " topN.sort(key=lambda x: x.get(\"rerank\", 0.0), reverse=True)\n", " except Exception as e:\n", " # if CE fails, fall back to RRF order\n", " for it in topN:\n", " it[\"rerank\"] = it[\"rrf\"]\n", "\n", " # --- MMR diversity on the reranked list (uses your HF embeddings) ---\n", " try:\n", " # embed the query + candidates; normalize to cosine space\n", " emb_fn = getattr(vectorstore, \"embedding_function\", embeddings)\n", " q_vec = np.array(emb_fn.embed_query(query), dtype=np.float32).reshape(1, -1)\n", " d_vecs = np.array(emb_fn.embed_documents([it[\"content\"] or \"\" for it in topN]), dtype=np.float32)\n", "\n", " q_vec = _l2_normalize(q_vec)[0] # (d,)\n", " d_vecs = _l2_normalize(d_vecs) # (N, d)\n", "\n", " sel_idx = _mmr_select(q_vec, d_vecs, k=TOP_K, mmr_lambda=MMR_LAMBDA)\n", " final_items = [topN[i] for i in sel_idx]\n", " except Exception as e:\n", " # fallback: take first TOP_K after rerank\n", " final_items = topN[:TOP_K]\n", "\n", " # --- return verbatim content, with soft dedupe by (source, first 300 normalized chars) ---\n", " results = []\n", " seen = set()\n", " for it in final_items:\n", " content = it[\"content\"] or \"\"\n", " meta = it[\"metadata\"] or {}\n", " source = meta.get(\"source\", \"\")\n", "\n", " # fingerprint for dedupe (does NOT modify returned text)\n", " clean = re.sub(r\"\\W+\", \"\", content.lower())[:300]\n", " fp = (source, clean)\n", " if fp in seen:\n", " continue\n", " seen.add(fp)\n", " results.append(content)\n", "\n", " if len(results) >= TOP_K:\n", " break\n", " \n", " # optional: quick debug\n", " from pprint import pprint\n", " pprint([{\n", " \"content\": i[\"content\"],\n", " \"src\": (i[\"metadata\"] or {}).get(\"source\", \"\"),\n", " \"rrf\": round(i[\"rrf\"], 6),\n", " \"vec_rank\": i[\"vec_rank\"],\n", " \"bm_rank\": i[\"bm_rank\"],\n", " } for i in final_items], width=120)\n", "\n", " return results" ] }, { "cell_type": "code", "execution_count": 15, "id": "aa868d21", "metadata": {}, "outputs": [], "source": [ "tools = [ retriever ]" ] }, { "cell_type": "code", "execution_count": 16, "id": "dde6e8a6", "metadata": {}, "outputs": [], "source": [ "model = ChatOpenAI(\n", " model=\"gpt-4o\", \n", " temperature=0.3, \n", " openai_api_key=os.getenv(\"OPENAI_API_KEY\"),\n", " streaming=True\n", ").bind_tools(tools)" ] }, { "cell_type": "code", "execution_count": 17, "id": "1603d2f8", "metadata": {}, "outputs": [], "source": [ "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], add_messages]" ] }, { "cell_type": "code", "execution_count": 18, "id": "4ca0ca9b", "metadata": {}, "outputs": [], "source": [ "system_prompt = SystemMessage(\n", " content=(\n", " \"You are Krishna's personal AI assistant. \"\n", " \"Use the retriever tool to fetch context about Krishna when the user asks about him. \"\n", " \"Cite concrete facts from retrieved chunks (no fabrication).\"\n", " )\n", ")" ] }, { "cell_type": "code", "execution_count": 19, "id": "e11c5fc5", "metadata": {}, "outputs": [], "source": [ "def model_call(state: AgentState) -> AgentState:\n", " \"\"\"Call the model with the current state messages.\"\"\"\n", " msgs = [system_prompt, *state[\"messages\"]]\n", " ai_msg = model.invoke(msgs) # returns an AIMessage with .tool_calls if any\n", " return {\"messages\": [ai_msg]}" ] }, { "cell_type": "code", "execution_count": 20, "id": "1347ca59", "metadata": {}, "outputs": [], "source": [ "def should_continue(state: AgentState) -> Literal[\"continue\", \"end\"]:\n", " \"\"\"Determine if the agent should continue.\"\"\"\n", " last_message = state[\"messages\"][-1]\n", " if getattr(last_message, \"tool_calls\", None):\n", " return \"continue\"\n", " return \"end\"" ] }, { "cell_type": "code", "execution_count": 21, "id": "d7085258", "metadata": {}, "outputs": [], "source": [ "graph = StateGraph(AgentState)\n", "\n", "graph.add_node('agent', model_call)\n", "tools_node = ToolNode(tools=tools)\n", "graph.add_node('tools', tools_node)\n", "\n", "graph.add_edge(START, 'agent')\n", "graph.add_conditional_edges('agent', should_continue, {\n", " \"continue\": 'tools',\n", " \"end\": END})\n", "graph.add_edge('tools', 'agent')\n", "\n", "checkpointer = MemorySaver() # dev-only; for prod use SQLite/Postgres\n", "app = graph.compile(checkpointer=checkpointer)" ] }, { "cell_type": "code", "execution_count": 22, "id": "5324b57e", "metadata": {}, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "app" ] }, { "cell_type": "code", "execution_count": 23, "id": "741d3a49", "metadata": {}, "outputs": [], "source": [ "def print_stream(stream):\n", " for s in stream:\n", " message = s[\"messages\"][-1]\n", " if isinstance(message, tuple):\n", " print(message)\n", " else:\n", " message.pretty_print()" ] }, { "cell_type": "code", "execution_count": 24, "id": "8ff97f3f", "metadata": {}, "outputs": [], "source": [ "def run_turn(thread_id: str, user_text: str):\n", " config = {\"configurable\": {\"thread_id\": thread_id}}\n", " final_text = \"\"\n", " for s in app.stream({\"messages\": [(\"user\", user_text)]}, config=config, stream_mode=\"values\"):\n", " msg = s[\"messages\"][-1]\n", " # stream-friendly: accumulate only the latest AI message content if present\n", " if getattr(msg, \"content\", None) and msg.type == \"ai\":\n", " final_text = msg.content\n", " return final_text" ] }, { "cell_type": "code", "execution_count": 25, "id": "22a806f5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'bm_rank': None, 'rrf': 0.016393, 'src': 'xPersonal_Interests_Cleaned.md', 'vec_rank': 0},\n", " {'bm_rank': None, 'rrf': 0.016129, 'src': 'aprofile.md', 'vec_rank': 1},\n", " {'bm_rank': None, 'rrf': 0.015152, 'src': 'xPersonal_Interests_Cleaned.md', 'vec_rank': 5},\n", " {'bm_rank': None, 'rrf': 0.014706, 'src': 'aprofile.md', 'vec_rank': 7},\n", " {'bm_rank': 2, 'rrf': 0.015873, 'src': 'aprofile.md', 'vec_rank': None},\n", " {'bm_rank': None, 'rrf': 0.014925, 'src': 'xPersonal_Interests_Cleaned.md', 'vec_rank': 6},\n", " {'bm_rank': None, 'rrf': 0.015873, 'src': 'aprofile.md', 'vec_rank': 2},\n", " {'bm_rank': 5, 'rrf': 0.015152, 'src': 'aprofile.md', 'vec_rank': None}]\n", "Krishna Vamsi Dhulipalla is a Computer Science graduate student at Virginia Tech, expected to complete his Master of Engineering (M.Eng) in December 2024. He has over three years of experience in data engineering, machine learning research, and real-time analytics. His professional interests are focused on LLM-driven systems, genomic computing, and scalable AI infrastructure.\n", "\n", "Currently, Krishna works as a Data Scientist at Virginia Tech, where he leads the development of machine learning systems for biological data. He is also culturally open, celebrating a variety of Indian and global festivals, and enjoys exploring different cultural backgrounds.\n", "\n", "Krishna has a passion for food, with favorites including Mutton Biryani from Hyderabad, Indian milk sweets like Rasgulla and Kaju Katli, and classic burgers. His hobbies and passions outside of work keep him energized and curious.\n", "\n", "For more information, you can visit his [personal website](http://krishna-dhulipalla.github.io), [GitHub](https://github.com/Krishna-dhulipalla), or [LinkedIn](https://www.linkedin.com/in/krishnavamsidhulipalla). You can also contact him via email at dhulipallakrishnavamsi@gmail.com or phone at +1 (540) 558-3528.\n", "Krishna Vamsi Dhulipalla's favorite foods include a mix of traditional Indian dishes and international comfort foods. His favorites are:\n", "\n", "- **Mutton Biryani from Hyderabad** — Considered his gold standard of comfort food.\n", "- **Indian Milk Sweets** — Especially Rasgulla and Kaju Katli.\n", "- **Classic Burger** — The messier, the better.\n", "- **Puri with Aloo Sabzi** — A perfect nostalgic breakfast.\n", "- **Gulab Jamun** — Always room for dessert.\n" ] } ], "source": [ "thread_id = str(uuid4()) # persist per user/session\n", "print(run_turn(thread_id, \"tell me about Krishna Vamsi Dhulipalla\"))\n", "print(run_turn(thread_id, \"what are his favorite foods?\"))" ] }, { "cell_type": "code", "execution_count": null, "id": "fc535d37", "metadata": {}, "outputs": [], "source": [ "from fastapi import FastAPI, Request, Query\n", "from pydantic import BaseModel\n", "from sse_starlette.sse import EventSourceResponse" ] }, { "cell_type": "code", "execution_count": null, "id": "4d817212", "metadata": {}, "outputs": [], "source": [ "api = FastAPI()\n", "\n", "# Optional CORS for local dev (if your UI runs on another port)\n", "from fastapi.middleware.cors import CORSMiddleware\n", "api.add_middleware(\n", " CORSMiddleware,\n", " allow_origins=[\"*\"], # tighten for prod\n", " allow_credentials=True,\n", " allow_methods=[\"*\"],\n", " allow_headers=[\"*\"],\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "fba095c4", "metadata": {}, "outputs": [], "source": [ "async def _event_stream(thread_id: str, message: str, request: Request):\n", " \"\"\"Common streamer for GET/POST routes.\"\"\"\n", " config = {\"configurable\": {\"thread_id\": thread_id}}\n", "\n", " # You can emit the thread id up front; UI also accepts it at the end.\n", " yield {\"event\": \"thread\", \"data\": thread_id}\n", "\n", " try:\n", " async for event in app.astream_events(\n", " {\"messages\": [(\"user\", message)]},\n", " config=config,\n", " version=\"v2\",\n", " ):\n", " # Stream model tokens\n", " if event[\"event\"] == \"on_chat_model_stream\":\n", " chunk = event[\"data\"][\"chunk\"].content\n", " # Guard: sometimes chunk is empty or a list of parts\n", " if isinstance(chunk, list):\n", " # join any text parts if needed\n", " text = \"\".join(getattr(p, \"text\", \"\") or str(p) for p in chunk)\n", " else:\n", " text = chunk or \"\"\n", " if text:\n", " yield {\"event\": \"token\", \"data\": text}\n", "\n", " # Optional: forward tool traces or other events if you want\n", " # if event[\"event\"] == \"on_tool_end\": ...\n", "\n", " # Client disconnected?\n", " if await request.is_disconnected():\n", " break\n", " finally:\n", " # Signal completion so UI can close cleanly\n", " yield {\"event\": \"done\", \"data\": \"1\"}" ] }, { "cell_type": "code", "execution_count": null, "id": "8107d680", "metadata": {}, "outputs": [], "source": [ "@api.get(\"/chat\")\n", "async def chat_get(\n", " request: Request,\n", " message: str = Query(...),\n", " thread_id: Optional[str] = Query(None),\n", "):\n", " tid = thread_id or str(uuid4())\n", " return EventSourceResponse(_event_stream(tid, message, request))\n", "\n", "\n", "# ========== POST endpoint (for fetch/ReadableStream clients) ==========\n", "class ChatIn(BaseModel):\n", " thread_id: Optional[str] = None\n", " message: str" ] }, { "cell_type": "code", "execution_count": null, "id": "bed93e87", "metadata": {}, "outputs": [], "source": [ "@api.post(\"/chat\")\n", "async def chat_post(body: ChatIn, request: Request):\n", " tid = body.thread_id or str(uuid4())\n", " return EventSourceResponse(_event_stream(tid, body.message, request))" ] } ], "metadata": { "kernelspec": { "display_name": "env", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.1" } }, "nbformat": 4, "nbformat_minor": 5 }