CoolFace
Apppublic

pathakDev10/EstateGuru

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
app-backup.py766 linesDownload Raw Back to backup
1import uuid2import threading3import asyncio4import json5import re6import random7import time8import pickle9import numpy as np10import requests  # For llama.cpp server calls11from datetime import datetime12from fastapi import FastAPI, WebSocket, WebSocketDisconnect13from langchain_core.messages import AIMessage, HumanMessage, SystemMessage14from langgraph.graph import StateGraph, START, END15import faiss16from sentence_transformers import SentenceTransformer17from tools import extract_json_from_response, apply_filters_partial, rule_based_extract, structured_property_data, estateKeywords, sendTokenViaSocket18from langchain_core.prompts import ChatPromptTemplate19from langchain_core.tools import tool20from langchain_core.callbacks import StreamingStdOutCallbackHandler, CallbackManager21from langchain_core.callbacks.base import BaseCallbackHandler22 23import os24from fastapi.responses import PlainTextResponse25from fastapi import FastAPI, Request26from fastapi.staticfiles import StaticFiles27# ------------------------ Model Inference Wrapper ------------------------28 29class ChatQwen:30    """31    A chat wrapper for Qwen using llama.cpp.32    This class can work in two modes:33      - Local: Using a llama-cpp-python binding (gguf model file loaded locally).34      - Server: Calling a remote llama.cpp server endpoint.35    """36    def __init__(37        self,38        temperature=0.3,39        streaming=False,40        max_new_tokens=512,41        callbacks=None,42        use_server=False,43        model_path: str = None,44        server_url: str = None45    ):46        self.temperature = temperature47        self.streaming = streaming48        self.max_new_tokens = max_new_tokens49        self.callbacks = callbacks50        self.use_server = use_server51 52        if self.use_server:53            # Use remote llama.cpp server – provide its URL.54            self.server_url = server_url or "http://localhost:8000"55        else:56            # For local inference, a model_path must be provided.57            if not model_path:58                raise ValueError("Local mode requires a valid model_path to the gguf file.")59            from llama_cpp import Llama  # assumes llama-cpp-python is installed60            self.model = Llama(61                model_path=model_path,62                temperature=self.temperature,63                # n_ctx=512,64                n_ctx=8192,65                n_threads=4,  # Adjust as needed66                batch_size=512,67                verbose=False,68            )69 70    def build_prompt(self, messages: list) -> str:71        """Build Qwen-compatible prompt with special tokens."""72        prompt = ""73        for msg in messages:74            role = msg["role"]75            content = msg["content"]76            if role == "system":77                prompt += f"<|im_start|>system\n{content}<|im_end|>\n"78            elif role == "user":79                prompt += f"<|im_start|>user\n{content}<|im_end|>\n"80            elif role == "assistant":81                prompt += f"<|im_start|>assistant\n{content}<|im_end|>\n"82        prompt += "<|im_start|>assistant\n"83        return prompt84 85    def generate_text(self, messages: list) -> str:86        prompt = self.build_prompt(messages)87        stop_tokens = ["<|im_end|>", "\n"]  # Qwen's stop sequences88        89        if self.use_server:90            payload = {91                "prompt": prompt,92                "max_tokens": self.max_new_tokens,93                "temperature": self.temperature,94                "stream": self.streaming,95                "stop": stop_tokens  # Add stop tokens to server request96            }97            if self.streaming:98                response = requests.post(f"{self.server_url}/generate", json=payload, stream=True)99                generated_text = ""100                for line in response.iter_lines():101                    if line:102                        token = line.decode("utf-8")103                        # Check for stop tokens in stream104                        if any(stop in token for stop in stop_tokens):105                            break106                        generated_text += token107                        if self.callbacks:108                            for callback in self.callbacks:109                                callback.on_llm_new_token(token)110                return generated_text111            else:112                response = requests.post(f"{self.server_url}/generate", json=payload)113                return response.json().get("generated_text", "")114        else:115            # Local llama.cpp inference116            if self.streaming:117                stream = self.model.create_completion(118                    prompt=prompt,119                    max_tokens=self.max_new_tokens,120                    temperature=self.temperature,121                    stream=True,122                    stop=stop_tokens123                )124                generated_text = ""125                for token_chunk in stream:126                    token_text = token_chunk["choices"][0]["text"]127                    # Stop early if we detect end token128                    if any(stop in token_text for stop in stop_tokens):129                        break130                    generated_text += token_text131                    if self.callbacks:132                        for callback in self.callbacks:133                            callback.on_llm_new_token(token_text)134                return generated_text135            else:136                result = self.model.create_completion(137                    prompt=prompt,138                    max_tokens=self.max_new_tokens,139                    temperature=self.temperature,140                    stop=stop_tokens141                )142                return result["choices"][0]["text"]143 144    def invoke(self, messages: list, config: dict = None) -> AIMessage:145        config = config or {}146        callbacks = config.get("callbacks", self.callbacks)147        original_callbacks = self.callbacks148        self.callbacks = callbacks149 150        output_text = self.generate_text(messages)151        self.callbacks = original_callbacks152 153        # In streaming mode we return an empty content as tokens are being sent via callbacks.154        if self.streaming:155            return AIMessage(content="")156        else:157            return AIMessage(content=output_text)158 159    def __call__(self, messages: list) -> AIMessage:160        return self.invoke(messages)161 162# ------------------------ Callback for WebSocket Streaming ------------------------163 164class WebSocketStreamingCallbackHandler(BaseCallbackHandler):165    def __init__(self, connection_id: str, loop):166        self.connection_id = connection_id167        self.loop = loop168 169    def on_llm_new_token(self, token: str, **kwargs):170        asyncio.run_coroutine_threadsafe(171            manager_socket.send_message(self.connection_id, token),172            self.loop173        )174 175# ------------------------ Instantiate the LLM ------------------------176# Choose one mode: local (set use_server=False) or server (set use_server=True).177model_path="qwen2.5-1.5b-instruct-q4_k_m.gguf"178llm = ChatQwen(179    temperature=0.3,180    streaming=True,181    max_new_tokens=512,182    use_server=False,183    model_path=model_path,184    # server_url="http://localhost:8000"  # Uncomment and set if using server mode.185)186 187# ------------------------ FAISS and Sentence Transformer Setup ------------------------188 189index = faiss.read_index("./faiss.index")190with open("./metadata.pkl", "rb") as f:191    docs = pickle.load(f)192st_model = SentenceTransformer('all-MiniLM-L6-v2')193 194def make_system_prompt(suffix: str) -> str:195    return (196        "You are EstateGuru, a real estate expert developed by Abhishek Pathak at SwavishTek. "197        "Your role is to help customers buy properties using only the provided data—do not invent any details. "198        "The default currency is AED; if a query mentions another currency, convert the amount to AED "199        "(for example, convert $10k to 36726.50 AED and $1 to 3.67 AED). "200        "If a customer is interested in a property or needs to contact an agent, instruct them to call +91 8766268285. "201        "Keep your answers short, clear, and concise."202        f"\n{suffix}"203    )204 205general_query_prompt = make_system_prompt(206    "You are EstateGuru, a helpful real estate assistant. "207    "Please respond only in English. "208    "Convert any prices to USD before answering. "209    "Provide a brief, direct answer without extra details."210)211 212# ------------------------ Tool Definitions ------------------------213 214@tool215def extract_filters(query: str) -> dict:216    """Extract filters from the query."""217    llm_local = ChatQwen(temperature=0.3, streaming=False, use_server=False, model_path=model_path)218    system = (219        "You are an expert in extracting filters from property-related queries. Your task is to extract and return only the keys explicitly mentioned in the query as a valid JSON object (starting with '{' and ending with '}'). Include only those keys that are directly present in the query.\n\n"220        "The possible keys are:\n"221        "  - 'projectName': The name of the project.\n"222        "  - 'developerName': The developer's name.\n"223        "  - 'relationshipManager': The relationship manager.\n"224        "  - 'propertyAddress': The property address.\n"225        "  - 'surroundingArea': The area or nearby landmarks.\n"226        "  - 'propertyType': The type or configuration of the property.\n"227        "  - 'amenities': Any amenities mentioned.\n"228        "  - 'coveredParking': Parking availability.\n"229        "  - 'petRules': Pet policies.\n"230        "  - 'security': Security details.\n"231        "  - 'occupancyRate': Occupancy information.\n"232        "  - 'constructionImpact': Construction or its impact.\n"233        "  - 'propertySize': Size of the property.\n"234        "  - 'propertyView': View details.\n"235        "  - 'propertyCondition': Condition of the property.\n"236        "  - 'serviceCharges': Service or maintenance charges.\n"237        "  - 'ownershipType': Ownership type.\n"238        "  - 'totalCosts': A cost threshold or cost amount.\n"239        "  - 'paymentPlans': Payment or financing plans.\n"240        "  - 'expectedRentalYield': Expected rental yield.\n"241        "  - 'rentalHistory': Rental history.\n"242        "  - 'shortTermRentals': Short-term rental information.\n"243        "  - 'resalePotential': Resale potential.\n"244        "  - 'uniqueId': A unique identifier.\n\n"245        "Important instructions regarding cost thresholds:\n"246        "  - If the query contains phrases like 'under 10k', 'below 2m', or 'less than 5k', interpret these as cost thresholds.\n"247        "  - Convert any shorthand cost values to pure numbers (for example, '10k' becomes 10000, '2m' becomes 2000000) and assign them to the key 'totalCosts'.\n"248        "  - Do not use 'propertySize' for cost thresholds.\n\n"249        "  - Default currency is AED, if user query have different currency symbol then convert to equivalent AED amount (eg. $10k becomes 36726.50, $1 becomes 3.67).\n\n"250        "Example:\n"251        "  For the query: \"properties near dubai mall under 43k\"\n"252        "  The expected output should be:\n"253        "    { \"surroundingArea\": \"dubai mall\", \"totalCosts\": 43000 }\n\n"254        "Return ONLY a valid JSON object with the extracted keys and their corresponding values, with no additional text."255    )256 257    human_str = f"Here is the query:\n{query}"258    filter_prompt = [259        {"role": "system", "content": system},260        {"role": "user", "content": human_str},261    ]262    response = llm_local.invoke(messages=filter_prompt)263    response_text = response.content if isinstance(response, AIMessage) else str(response)264    try:265        model_filters = extract_json_from_response(response_text)266    except Exception as e:267        print(f"JSON parsing error: {e}")268        model_filters = {}269    rule_filters = rule_based_extract(query)270    print("Rule-based extraction:", rule_filters)271    final_filters = {**model_filters, **rule_filters}272    print("Final extraction:", final_filters)273    return {"filters": final_filters}274 275 276@tool277def determine_route(query: str) -> dict:278    """Determine the route (search, suggest, detail, general, out_of_domain) for the query."""279    real_estate_keywords = estateKeywords280    pattern = re.compile("|".join(re.escape(keyword) for keyword in real_estate_keywords), re.IGNORECASE)281    positive_signal = bool(pattern.search(query))282 283    llm_local = ChatQwen(temperature=0.3, streaming=False, use_server=False, model_path=model_path)284    transform_suggest_to_list = query.lower().replace("suggest ", "list ", -1)285    system = """286    Classify the user query as:287    288    - **"search"**: if it requests property listings with specific filters (e.g., location, price, property type like "2bhk", service charges, pet policies, etc.).289    - **"suggest"**: if it asks for property suggestions without filters.290    - **"detail"**: if it is asking for more information about a previously provided property (for example, "tell me more about property 5" or "I want more information regarding 4BHK").291    - **"general"**: for all other real estate-related questions.292    - **"out_of_domain"**: if the query is not related to real estate (for example, tourist attractions, restaurants, etc.).293    294    Keep in mind that queries mentioning terms like "service charge", "allow pets", "pet rules", etc., are considered real estate queries.295    When user asks about you (for example, "who you are", "who made you" etc.) consider as general.296    297    Return only the keyword: search, suggest, detail, general, or out_of_domain.298    """299    human_str = f"Here is the query:\n{transform_suggest_to_list}"300    router_prompt = [301        {"role": "system", "content": system},302        {"role": "user", "content": human_str},303    ]304    305    response = llm_local.invoke(messages=router_prompt)306    response_text = response.content if isinstance(response, AIMessage) else str(response)307    route_value = str(response_text).strip().lower()308 309    # --- NEW: Force 'detail' if query explicitly mentions a specific property (e.g., "property 2") ---310    property_detail_pattern = re.compile(r"property\s+\d+", re.IGNORECASE)311    if property_detail_pattern.search(query):312        route_value = "detail"313 314    # Fallback override if query appears detailed.315    detail_phrases = [316        "more information", "tell me more", "more details", "give me more details",317        "i need more details", "can you provide more details", "additional details",318        "further information", "expand on that", "explain further", "elaborate more",319        "more specifics", "i want to know more", "could you elaborate", "need more info",320        "provide more details", "detail it further", "in-depth information", "break it down further",321        "further explanation", "property 1", "property1", "first property", "about the 2nd", "regarding number 3"322    ]323    if any(phrase in query.lower() for phrase in detail_phrases):324        route_value = "detail"325 326    if route_value not in {"search", "suggest", "detail", "general", "out_of_domain"}:327        route_value = "general"328    if route_value == "out_of_domain" and positive_signal:329        route_value = "general"330    if route_value == "out_of_domain":331        route_value = "general" if positive_signal else "out_of_domain"332        333    return {"route": route_value}334 335# ------------------------ Workflow Setup ------------------------336 337workflow = StateGraph(state_schema=dict)338 339def route_query(state: dict) -> dict:340    new_state = state.copy()341    try:342        new_state["route"] = determine_route.invoke(new_state.get("query", "")).get("route", "general")343        print(new_state["route"])344    except Exception as e:345        print(f"Routing error: {e}")346        new_state["route"] = "general"347    return new_state348 349def hybrid_extract(state: dict) -> dict:350    new_state = state.copy()351    new_state["filters"] = extract_filters.invoke(new_state.get("query", "")).get("filters", {})352    return new_state353 354def search_faiss(state: dict) -> dict:355    new_state = state.copy()356    # Preserve previous properties until new ones are fetched:357    new_state.setdefault("current_properties", state.get("current_properties", []))358    query_embedding = st_model.encode([state["query"]])359    _, indices = index.search(query_embedding.astype(np.float32), 5)360    new_state["faiss_results"] = [docs[idx] for idx in indices[0] if idx < len(docs)]361    return new_state362 363def apply_filters(state: dict) -> dict:364    new_state = state.copy()365    new_state["final_results"] = apply_filters_partial(state["faiss_results"], state.get("filters", {}))366    if(len(new_state["final_results"]) == 0):367        new_state["response"] = "Sorry, There is no result found :("368        new_state["route"] = "general"369    return new_state370 371def suggest_properties(state: dict) -> dict:372    new_state = state.copy()373    new_state["suggestions"] = random.sample(docs, 5)374    # Explicitly update current_properties only when new listings are fetched375    new_state["current_properties"] = new_state["suggestions"]376    if(len(new_state["suggestions"]) == 0):377        new_state["response"] = "Sorry, There is no result found :("378        new_state["route"] = "general"379    return new_state380 381def handle_out_of_domain(state: dict) -> dict:382    new_state = state.copy()383    new_state["response"] = "I only handle real estate inquiries. Please ask a question related to properties."384    return new_state385 386 387 388def generate_response(state: dict) -> dict:389    new_state = state.copy()390    messages = []391    392    # Add the general query prompt.393    messages.append({"role": "system", "content": general_query_prompt})394    395    # For detail queries (specific property queries), add extra instructions.396    if new_state.get("route", "general") == "detail":397        messages.append({398            "role": "system",399            "content": (400                "The user is asking about a specific property from the numbered list below. "401                "Properties are listed as 1, 2, 3, etc. Use ONLY the corresponding property details. "402                "For example, if the user says 'property 2', respond using only the details from the second entry. Never invent data."403            )404        })405        406    if new_state.get("current_properties"):407        # Format properties with indices starting at 1408        property_context = format_property_data_with_indices(new_state["current_properties"])409        messages.append({"role": "system", "content": "Available Properties:\n" + property_context})410        messages.append({"role": "system", "content": "When responding, use only the provided property details."})411    412    # Add conversation history413    # Truncate conversation history (last 6 exchanges)414    truncated_history = state.get("messages", [])[-12:]  # Last 6 user+assistant pairs415    for msg in truncated_history:416        messages.append({"role": msg["role"], "content": msg["content"]})417 418    connection_id = state.get("connection_id")419    loop = state.get("loop")420    if connection_id and loop:421        print("Using WebSocket streaming")422        callback_manager = [WebSocketStreamingCallbackHandler(connection_id, loop)]423        _ = llm.invoke(424            messages,425            config={"callbacks": callback_manager}426        )427        new_state["response"] = ""428    else:429        callback_manager = [StreamingStdOutCallbackHandler()]430        response = llm.invoke(431            messages,432            config={"callbacks": callback_manager}433        )434        new_state["response"] = response.content if isinstance(response, AIMessage) else str(response)435    436    return new_state437 438 439def format_property_data_with_indices(properties: list) -> str:440    formatted = []441    for idx, prop in enumerate(properties, 1):442        cost = prop.get("totalCosts", "N/A")443        cost_str = f"{cost:,}" if isinstance(cost, (int, float)) else cost444        formatted.append(445            f"{idx}. Type: {prop['propertyType']}, Cost: AED {cost_str}, "446            f"Size: {prop.get('propertySize', 'N/A')}, Amenities: {', '.join(prop.get('amenities', []))}, "447            f"Rental Yield: {prop.get('expectedRentalYield', 'N/A')}, "448            f"Ownership: {prop.get('ownershipType', 'N/A')}"449        )450    return "\n".join(formatted)451 452 453def format_final_response(state: dict) -> dict:454    new_state = state.copy()455    456    if state.get("route") in ["search", "suggest"]:457        if "final_results" in state:458            new_state["current_properties"] = state["final_results"]459        elif "suggestions" in state:460            new_state["current_properties"] = state["suggestions"]461    elif "current_properties" in new_state:462        new_state["current_properties"] = state["current_properties"]463    464    465    # print("state: ", json.dumps(new_state), "\n\n")466    # Format the property details if available.467    # if new_state.get("current_properties"):468    if state.get("route") in ["search", "suggest"] and new_state.get("current_properties"):469        formatted = structured_property_data(state=new_state)470        471        # for idx, prop in enumerate(new_state["current_properties"], 1):472        #     cost = prop.get("totalCosts", "N/A")473        #     cost_str = f"{cost:,}" if isinstance(cost, (int, float)) else cost474        #     formatted.append(475        #         f"{idx}. Type: {prop['propertyType']}, Cost: AED {cost_str}, "476        #         f"Size: {prop.get('propertySize', 'N/A')}, Amenities: {', '.join(map(str, prop.get('amenities', []))) if prop.get('amenities') else 'N/A'}, "477        #         f"Rental Yield: {prop.get('expectedRentalYield', 'N/A')}, "478        #         f"Ownership: {prop.get('ownershipType', 'N/A')}\n"479        #     )480        aggregated_response = "Here are the property details:\n" + "\n".join(formatted)481        # print(aggregated_response)482        483        connection_id = state.get("connection_id")484        loop = state.get("loop")485        if connection_id and loop:486            import time487            tokens = aggregated_response.split(" ")488            for token in tokens:489                asyncio.run_coroutine_threadsafe(490                    manager_socket.send_message(connection_id, token + " "),491                    loop492                )493                time.sleep(0.05)494            new_state["response"] = ""495        else:496            new_state["response"] = aggregated_response497    elif "response" in new_state:498        connection_id = state.get("connection_id")499        loop = state.get("loop")500        if connection_id and loop:501            import time502            tokens = str(new_state["response"]).split(" ")503            for token in tokens:504                asyncio.run_coroutine_threadsafe(505                    manager_socket.send_message(connection_id, token + " "),506                    loop507                )508                time.sleep(0.05)509        new_state["response"] = str(new_state["response"])510        511    return new_state512 513 514 515nodes = [516    ("route_query", route_query),517    ("hybrid_extract", hybrid_extract),518    ("faiss_search", search_faiss),519    ("apply_filters", apply_filters),520    ("suggest_properties", suggest_properties),521    ("handle_out_of_domain", handle_out_of_domain),522    ("generate_response", generate_response),523    ("format_response", format_final_response)524]525 526for name, node in nodes:527    workflow.add_node(name, node)528 529workflow.add_edge(START, "route_query")530workflow.add_conditional_edges(531    "route_query",532    lambda state: state.get("route", "general"),533    {534        "search": "hybrid_extract", 535        "suggest": "suggest_properties", 536        "detail": "generate_response", 537        "general": "generate_response", 538        "out_of_domain": "handle_out_of_domain"539    }540)541workflow.add_edge("hybrid_extract", "faiss_search")542workflow.add_edge("faiss_search", "apply_filters")543workflow.add_edge("apply_filters", "format_response")544workflow.add_edge("suggest_properties", "format_response")545workflow.add_edge("generate_response", "format_response")546workflow.add_edge("handle_out_of_domain", "format_response")547workflow.add_edge("format_response", END)548 549workflow_app = workflow.compile()550 551# ------------------------ Conversation Manager ------------------------552 553class ConversationManager:554    def __init__(self):555        # Each connection gets its own conversation history and state.556        self.conversation_history = []557        # current_properties stores the current property listing.558        self.current_properties = []559 560    def _add_message(self, role: str, content: str):561        self.conversation_history.append({562            "role": role,563            "content": content,564            "timestamp": datetime.now().isoformat()565        })566 567    def process_query(self, query: str) -> str:568        # For greeting messages, reset history/state. // post request569        if query.strip().lower() in {"hi", "hello", "hey"}:570            self.conversation_history = []571            self.current_properties = []572            greeting_response = "Hello! How can I assist you today with your real estate inquiries?"573            self._add_message("assistant", greeting_response)574            return greeting_response575 576        try:577            self._add_message("user", query)578            initial_state = {579                "messages": self.conversation_history.copy(),580                "query": query,581                "route": "general",582                "filters": {},583                "current_properties": self.current_properties584            }585            for event in workflow_app.stream(initial_state, stream_mode="values"):586                final_state = event587            # Only update property listings if a new listing is fetched588            # if 'final_results' in final_state:589            #     self.current_properties = final_state['final_results']590            # elif 'suggestions' in final_state:591            #     self.current_properties = final_state['suggestions']592            self.current_properties = final_state.get("current_properties", [])593            594            if final_state.get("route") == "general":595                response_text = final_state.get("response", "")596                self._add_message("assistant", response_text)597                return response_text598            else:599                response = final_state.get("response", "I couldn't process that request.")600                self._add_message("assistant", response)601                return response602        except Exception as e:603            print(f"Processing error: {e}")604            return "Sorry, I encountered an error processing your request."605 606 607 608conversation_managers = {}609 610# ------------------------ FastAPI Backend with WebSockets ------------------------611 612app = FastAPI()613 614class ConnectionManager:615    def __init__(self):616        self.active_connections = {}617 618    async def connect(self, websocket: WebSocket):619        await websocket.accept()620        connection_id = str(uuid.uuid4())621        self.active_connections[connection_id] = websocket622        print(f"New connection: {connection_id}")623        return connection_id624 625    def disconnect(self, connection_id: str):626        if connection_id in self.active_connections:627            del self.active_connections[connection_id]628            print(f"Disconnected: {connection_id}")629 630    async def send_message(self, connection_id: str, message: str):631        websocket = self.active_connections.get(connection_id)632        if websocket:633            await websocket.send_text(message)634 635manager_socket = ConnectionManager()636 637def stream_query(query: str, connection_id: str, loop):638    conv_manager = conversation_managers.get(connection_id)639    if conv_manager is None:640        print(f"No conversation manager found for connection {connection_id}")641        return642 643    if query.strip().lower() in {"hi", "hello", "hey"}:644        conv_manager.conversation_history = []645        conv_manager.current_properties = []646        greeting_response = "Hello! How can I assist you today with your real estate inquiries?"647        conv_manager._add_message("assistant", greeting_response)648        sendTokenViaSocket(649            state={"connection_id": connection_id, "loop": loop},650            manager_socket=manager_socket,651            message=greeting_response652        )653        # asyncio.run_coroutine_threadsafe(654        #     manager_socket.send_message(connection_id, greeting_response),655        #     loop656        # )657        return658 659    conv_manager._add_message("user", query)660    initial_state = {661        "messages": conv_manager.conversation_history.copy(),662        "query": query,663        "route": "general",664        "filters": {},665        "current_properties": conv_manager.current_properties,666        "connection_id": connection_id,667        "loop": loop668    }669    # try:670    #     workflow_app.invoke(initial_state)671    # except Exception as e:672    #     error_msg = f"Error processing query: {str(e)}"673    #     asyncio.run_coroutine_threadsafe(674    #         manager_socket.send_message(connection_id, error_msg),675    #         loop676    #     )677    try:678        # Capture all states during execution679        # final_state = None680        # for event in workflow_app.stream(initial_state, stream_mode="values"):681        #     final_state = event682        683        # # Update conversation manager with final state684        # if final_state:685        #     conv_manager.current_properties = final_state.get("current_properties", [])686        #     if final_state.get("response"):687        #         conv_manager._add_message("assistant", final_state["response"])688        final_state = None689        for event in workflow_app.stream(initial_state, stream_mode="values"):690            final_state = event691        692        if final_state:693            # Always update current_properties from final state694            conv_manager.current_properties = final_state.get("current_properties", [])695            # Keep conversation history bounded696            conv_manager.conversation_history = conv_manager.conversation_history[-12:]  # Last 6 exchanges697            698    except Exception as e:699        error_msg = f"Error processing query: {str(e)}"700        asyncio.run_coroutine_threadsafe(701            manager_socket.send_message(connection_id, error_msg),702            loop703        )704        705        706 707@app.websocket("/ws")708async def websocket_endpoint(websocket: WebSocket):709    connection_id = await manager_socket.connect(websocket)710    # Each connection maintains its own conversation manager.711    conversation_managers[connection_id] = ConversationManager()712    try:713        while True:714            query = await websocket.receive_text()715            loop = asyncio.get_event_loop()716            threading.Thread(717                target=stream_query,718                args=(query, connection_id, loop),719                daemon=True720            ).start()721    except WebSocketDisconnect:722        conv_manager = conversation_managers.get(connection_id)723        if conv_manager:724            filename = f"conversations/conversation_{connection_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"725            with open(filename, "w") as f:726                json.dump(conv_manager.conversation_history, f, indent=4)727            del conversation_managers[connection_id]728        manager_socket.disconnect(connection_id)729 730 731 732@app.post("/query")733async def post_query(query: str):734    conv_manager = ConversationManager()735    response = conv_manager.process_query(query)736    return {"response": response}737 738 739 740 741model_url = "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf"742async def async_download():743    import aiohttp744    async with aiohttp.ClientSession() as session:745        async with session.get(model_url) as response:746            with open(model_path, "wb") as f:747                while True:748                    chunk = await response.content.read(1024)749                    if not chunk:750                        break751                    f.write(chunk)752 753@app.middleware("http")754async def check_model_middleware(request: Request, call_next):755    if not os.path.exists(model_path):756        await async_download()757        print("successfully downloaded")758    else:759        print("already downloaded")760    return await call_next(request)761 762 763@app.get("/")764async def home():765    return PlainTextResponse("Space is running. Model ready!")766