CoolFace
Apppublic

NovaRocket/Conversational_AI

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
app.py611 linesDownload Raw Back to root
1# -*- coding: utf-8 -*-2"""ConvoAI.ipynb3 4Automatically generated by Colab.5 6Original file is located at7    https://colab.research.google.com/drive/1joGBryAm8BIWFs4pZrZZmmp4OXBuMQZH8"""9 10# !pip install llama-cpp-python gradio -q11 12# !pip install --upgrade transformers sentence-transformers tensorflow13# !pip install --upgrade llama-cpp-python gradio pymilvus14# !pip install elevenlabs SpeechRecognition15 16import gradio as gr17import os18import re19import uuid20import time21import random22import datetime23import tempfile24import numpy as np25 26from transformers import pipeline27from elevenlabs.client import ElevenLabs28from elevenlabs import VoiceSettings29from pydub import AudioSegment30import speech_recognition as sr31 32# Milvus33from pymilvus import (34    connections, utility, Collection, CollectionSchema,35    FieldSchema, DataType36)37 38# Sentence embeddings39from sentence_transformers import SentenceTransformer40 41# llama-cpp for local LLM inference42from llama_cpp import Llama43 44import os45ELEVENLABS_API_KEY = os.environ.get('ELEVENLABS_API_KEY')46HUGGINGFACE_TOKEN  = os.environ.get('HF_TOKEN')47MILVUS_URI         = os.environ.get('MILVUS_URI')48MILVUS_USER        = os.environ.get('MILVUS_USER')49MILVUS_PASSWORD    = os.environ.get('MILVUS_PASSWORD')50 51# VOICE_ID_FALLBACK  = "1SM7GgM6IMuvQlz2BwM3"   # Mark - ConvoAI52VOICE_ID_FALLBACK  = "iP95p4xoKVk53GoZ742B"53 54# MODEL INIT55print("Loading embedding model...")56embed_model = SentenceTransformer("all-MiniLM-L6-v2")57 58print("Loading LLM...")59llm = Llama.from_pretrained(60    repo_id="bartowski/Llama-3.2-1B-Instruct-GGUF",61    filename="Llama-3.2-1B-Instruct-Q4_K_L.gguf",62    chat_format="chatml",63    n_ctx=2048,64    n_threads=4,65    n_gpu_layers=35,66    verbose=True,67)68 69print("Loading emotion model...")70emotion_analyzer = pipeline(71    "text-classification",72    model="bhadresh-savani/distilbert-base-uncased-emotion",73    top_k=174)75 76print("Initialising ElevenLabs...")77elevenlabs_client = ElevenLabs(api_key=ELEVENLABS_API_KEY)78 79# Resolve preferred voice ID80VOICE_ID = VOICE_ID_FALLBACK81try:82    voices = elevenlabs_client.voices.get_all()83    for v in voices.voices:84        if v.name == "Mark - ConvoAI":85            VOICE_ID = v.voice_id86            break87except Exception as e:88    print(f"Voice loading warning: {e}")89 90recognizer = sr.Recognizer()91 92# MILVUS SETUP93print("Connecting to Milvus (Zilliz Cloud)...")94connections.connect(95    alias="default",96    uri=MILVUS_URI,97    user=MILVUS_USER,98    password=MILVUS_PASSWORD,99)100 101# ── chat_history collection (assumed to exist already; create if not) ──102if not utility.has_collection("chat_history"):103    chat_schema = CollectionSchema([104        FieldSchema("id",        DataType.VARCHAR, is_primary=True, max_length=64),105        FieldSchema("session_id",DataType.VARCHAR, max_length=64),106        FieldSchema("content",   DataType.VARCHAR, max_length=4096),107        FieldSchema("role",      DataType.VARCHAR, max_length=16),108        FieldSchema("timestamp", DataType.VARCHAR, max_length=64),109        FieldSchema("embedding", DataType.FLOAT_VECTOR, dim=384),110    ], description="Chat messages")111    chat_collection = Collection("chat_history", schema=chat_schema)112    chat_collection.create_index(113        "embedding",114        {"index_type": "IVF_FLAT", "metric_type": "COSINE", "params": {"nlist": 100}}115    )116else:117    chat_collection = Collection("chat_history")118chat_collection.load()119 120# ── summaries collection ──121if not utility.has_collection("summaries"):122    summary_schema = CollectionSchema([123        FieldSchema("id",               DataType.VARCHAR, is_primary=True, max_length=64),124        FieldSchema("session_id",       DataType.VARCHAR, max_length=64),125        FieldSchema("summary",          DataType.VARCHAR, max_length=4096),126        FieldSchema("timestamp",        DataType.VARCHAR, max_length=64),127        FieldSchema("summary_embedding",DataType.FLOAT_VECTOR, dim=384),128    ], description="Session-level summaries")129    summary_collection = Collection("summaries", schema=summary_schema)130    summary_collection.create_index(131        "summary_embedding",132        {"index_type": "IVF_FLAT", "metric_type": "COSINE", "params": {"nlist": 100}}133    )134else:135    summary_collection = Collection("summaries")136summary_collection.load()137 138# ── user_profiles collection ──139if not utility.has_collection("user_profiles"):140    profile_schema = CollectionSchema([141        FieldSchema("id",               DataType.VARCHAR, is_primary=True, max_length=64),142        FieldSchema("session_id",       DataType.VARCHAR, max_length=64),143        FieldSchema("name",             DataType.VARCHAR, max_length=128),144        FieldSchema("age",              DataType.INT64),145        FieldSchema("profession",       DataType.VARCHAR, max_length=128),146        FieldSchema("likes",            DataType.VARCHAR, max_length=512),147        FieldSchema("dislikes",         DataType.VARCHAR, max_length=512),148        FieldSchema("profile_embedding",DataType.FLOAT_VECTOR, dim=384),149    ], description="User profile with embeddings")150    profile_collection = Collection("user_profiles", schema=profile_schema)151    profile_collection.create_index(152        "profile_embedding",153        {"index_type": "IVF_FLAT", "metric_type": "COSINE", "params": {"nlist": 100}}154    )155else:156    profile_collection = Collection("user_profiles")157profile_collection.load()158 159TEMPLATES = {160    "initial_greeting": [161        "Hey there! How's life treating you?",162        "Oh hi! Was just thinking about you.",163        "Hey! What's new?",164        "Yo! Long time no chat!",165        "Hiiii! How've you been?",166        "Hey, what's up!",167        "Hey pal! How's life in your world?",168        "Well look who's here! How you been?",169    ],170    "sadness": [171        "Aw man, that sounds rough... wanna talk about it?",172        "Oh no — I'm here if you need to vent.",173        "Mmm... I get that feeling. It'll pass, promise.",174        "Yeah, some days just drain you huh?",175        "*pats shoulder* You're stronger than you think.",176    ],177    "fatigue": [178        "Ugh, the tiredness struggle is real today huh?",179        "Been there... maybe some tea and deep breaths?",180        "Your body's telling you to slow down maybe?",181        "Tired brains are the worst. Be kind to yourself.",182        "Mmm... wanna just sit with this feeling for a bit?",183    ],184}185 186DEFAULT_SYSTEM_PROMPT = """You're a close AI companion friend having a natural conversation. Guidelines:1871. FIRST MESSAGE ONLY: Give a warm greeting if user says hello/hi.1882. AFTER FIRST MESSAGE: Never greet again, continue conversation naturally.1893. Be human-like: Use "Yeah...", "Mmm...", "I get that".1904. Show personality: "Oh wow!", "No way!", "Seriously?"1915. Mirror the user's emotional tone.1926. Never sound robotic or like customer service."""193 194 195# MILVUS HELPER FUNCTIONS196 197def store_message(session_id: str, role: str, content: str):198    embedding = embed_model.encode(content).tolist()199    chat_collection.insert([200        [str(uuid.uuid4())],201        [session_id],202        [content[:4096]],203        [role],204        [datetime.datetime.utcnow().isoformat()],205        [embedding],206    ])207    chat_collection.flush()208 209 210def recall_relevant_summary(user_query: str):211    embedding = embed_model.encode(user_query).tolist()212    results = summary_collection.search(213        data=[embedding],214        anns_field="summary_embedding",215        param={"metric_type": "COSINE", "params": {"nprobe": 10}},216        limit=1,217        output_fields=["summary", "session_id"],218    )219    if results and results[0]:220        return results[0][0].entity.get("summary")221    return None222 223 224def generate_and_store_summary(session_id: str, history: list):225    history_text = "\n".join([f"{r}: {m}" for r, m in history])226    prompt = f"Summarize this conversation briefly:\n{history_text}\nSummary:"227    response = llm.create_chat_completion([{"role": "user", "content": prompt}])228    summary = response["choices"][0]["message"]["content"]229    embedding = embed_model.encode(summary).tolist()230    summary_collection.insert([231        [str(uuid.uuid4())],232        [session_id],233        [summary[:4096]],234        [datetime.datetime.utcnow().isoformat()],235        [embedding],236    ])237    summary_collection.flush()238    return summary239 240 241def extract_and_store_profile(message: str, session_id: str):242    profile_info = {}243 244    name_match = re.search(r"\bmy name is ([A-Z][a-z]+)", message, re.IGNORECASE)245    if name_match:246        profile_info["name"] = name_match.group(1).title()247 248    age_match = re.search(249        r"\b(i am|i'm|my age is) (\d{1,3})\b", message, re.IGNORECASE250    )251    if age_match:252        profile_info["age"] = int(age_match.group(2))253 254    profession_match = re.search(255        r"\b(i am|i'm|i'm a|i work as|my profession is) (a |an )?([\w\s]+)",256        message, re.IGNORECASE257    )258    if profession_match:259        profile_info["profession"] = profession_match.group(3).strip().capitalize()260 261    likes_match = re.search(262        r"\b(i like|i love|i enjoy) ([\w\s,]+)", message, re.IGNORECASE263    )264    if likes_match:265        profile_info["likes"] = likes_match.group(2).strip()266 267    dislikes_match = re.search(268        r"\b(i hate|i dislike|i don't like|i do not like) ([\w\s,]+)",269        message, re.IGNORECASE270    )271    if dislikes_match:272        profile_info["dislikes"] = dislikes_match.group(2).strip()273 274    if not profile_info:275        return  # nothing to update276 277    existing = profile_collection.query(278        expr=f"session_id == '{session_id}'",279        output_fields=["id", "name", "age", "profession", "likes", "dislikes"],280    )281 282    if existing:283        doc = existing[0]284        updated = {285            "id":         doc["id"],286            "session_id": session_id,287            "name":       profile_info.get("name",       doc.get("name", "")),288            "age":        profile_info.get("age",        doc.get("age", 0)),289            "profession": profile_info.get("profession", doc.get("profession", "")),290            "likes":      profile_info.get("likes",      doc.get("likes", "")),291            "dislikes":   profile_info.get("dislikes",   doc.get("dislikes", "")),292        }293        profile_collection.delete(f"id in ['{doc['id']}']")294        profile_collection.flush()295    else:296        updated = {297            "id":         str(uuid.uuid4()),298            "session_id": session_id,299            "name":       profile_info.get("name", ""),300            "age":        profile_info.get("age", 0),301            "profession": profile_info.get("profession", ""),302            "likes":      profile_info.get("likes", ""),303            "dislikes":   profile_info.get("dislikes", ""),304        }305 306    profile_text = " ".join([307        updated["name"], updated["profession"],308        updated["likes"], updated["dislikes"]309    ])310    embedding = embed_model.encode(profile_text).tolist()311 312    profile_collection.insert([313        [updated["id"]],314        [updated["session_id"]],315        [updated["name"]],316        [updated["age"]],317        [updated["profession"]],318        [updated["likes"]],319        [updated["dislikes"]],320        [embedding],321    ])322    profile_collection.flush()323    print(f"[Profile] updated for session {session_id}: {profile_info}")324 325 326def get_user_profile_memory(session_id: str) -> str:327    results = profile_collection.query(328        expr=f"session_id == '{session_id}'",329        output_fields=["name", "age", "profession", "likes", "dislikes"],330    )331    if not results:332        return ""333    p = results[0]334    parts = []335    if p.get("name"):       parts.append(f"Name: {p['name']}")336    if p.get("age"):        parts.append(f"Age: {p['age']}")337    if p.get("profession"): parts.append(f"Profession: {p['profession']}")338    if p.get("likes"):      parts.append(f"Likes: {p['likes']}")339    if p.get("dislikes"):   parts.append(f"Dislikes: {p['dislikes']}")340    return "\n".join(parts)341 342 343 344# AUDIO HELPERS345def process_audio_input(audio) -> str:346    """Convert microphone audio to text via Google STT."""347    if audio is None:348        return ""349    try:350        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:351            if isinstance(audio, tuple):352                sample_rate, audio_data = audio353                seg = AudioSegment(354                    audio_data.tobytes(),355                    frame_rate=sample_rate,356                    sample_width=audio_data.dtype.itemsize,357                    channels=1,358                )359            else:360                seg = AudioSegment.from_file(audio)361            seg.export(tmp.name, format="wav")362            tmp_path = tmp.name363 364        with sr.AudioFile(tmp_path) as source:365            recognizer.adjust_for_ambient_noise(source, duration=0.5)366            audio_data = recognizer.record(source)367            try:368                text = recognizer.recognize_google(audio_data)369            except sr.UnknownValueError:370                text = ""371            except sr.RequestError:372                text = ""373        os.unlink(tmp_path)374        return text375    except Exception as e:376        print(f"[STT error] {e}")377        return ""378 379 380def generate_tts(text: str):381    """Generate TTS via ElevenLabs and return a temp file path."""382    try:383        audio_response = elevenlabs_client.text_to_speech.convert(384            voice_id=VOICE_ID,385            model_id="eleven_multilingual_v2",386            text=text,387            voice_settings=VoiceSettings(388                stability=0.5,389                similarity_boost=0.8,390                style=0.2,391                speaker_boost=True,392            ),393        )394        audio_bytes = b"".join(audio_response)395        with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:396            f.write(audio_bytes)397            return f.name398    except Exception as e:399        print(f"[TTS error] {e}")400        return None401 402 403 404# EMOTION & TEMPLATE ROUTING405 406_greeting_done: dict[str, bool] = {}   # keyed by session_id407 408 409def get_template_response(user_input: str, session_id: str):410    """Return a hardcoded template reply when appropriate, else None."""411    lower = user_input.lower()412 413    if not _greeting_done.get(session_id) and any(414        w in lower for w in ["hello", "hi", "hey"]415    ):416        _greeting_done[session_id] = True417        return random.choice(TEMPLATES["initial_greeting"])418 419    if "tired" in lower or "exhaust" in lower:420        return random.choice(TEMPLATES["fatigue"])421 422    try:423        emotion_result = emotion_analyzer(user_input)[0][0]424        if emotion_result["label"].lower() == "sadness" and emotion_result["score"] > 0.6:425            return random.choice(TEMPLATES["sadness"])426    except Exception:427        pass428 429    return None430 431 432 433# CORE CHAT FUNCTION434 435def truncate_history(messages: list, max_turns: int = 10) -> list:436    return messages[-max_turns * 2:]437 438 439def chat_pipeline(audio, text_input: str, history: list, session_id: str):440    """441    Main Gradio handler.442    Returns: (updated_history, cleared_text, audio_file_path, session_id)443    """444    if not session_id:445        session_id = str(int(time.time()))446 447    # ── Resolve user input ──448    user_input = ""449    if audio:450        user_input = process_audio_input(audio)451    if not user_input and text_input:452        user_input = text_input.strip()453    if not user_input:454        return history, "", None, session_id455 456    # ── Profile extraction ──457    extract_and_store_profile(user_input, session_id)458 459    # ── Memory context ──460    memory_context = ""461    recalled = recall_relevant_summary(user_input)462    if recalled:463        memory_context += f"[Memory from a past session]:\n{recalled}\n\n"464 465    user_profile = get_user_profile_memory(session_id)466    if user_profile:467        memory_context += f"[What I know about the user]:\n{user_profile}\n\n"468 469    # ── Check for template response ──470    template_reply = get_template_response(user_input, session_id)471    if template_reply:472        history.append({"role": "user",      "content": user_input})473        history.append({"role": "assistant", "content": template_reply})474        store_message(session_id, "user",      user_input)475        store_message(session_id, "assistant", template_reply)476        audio_out = generate_tts(template_reply)477 478        if user_input.lower().strip() in ["end session", "exit", "bye"]:479            pairs = []480            for i in range(0, len(history) - 1, 2):481                u = history[i].get("content", "")482                b = history[i+1].get("content", "") if i+1 < len(history) else ""483                pairs.append((u, b))484            generate_and_store_summary(session_id, pairs)485 486        return history, "", audio_out, session_id487 488    # ── Build message list for LLM ──489    messages = [490        {491            "role": "system",492            "content": memory_context + DEFAULT_SYSTEM_PROMPT,493        }494    ]495    for msg in history:496        messages.append({"role": msg["role"], "content": msg["content"]})497    messages.append({"role": "user", "content": user_input})498 499    # ── Stage 1: Thinking phase (internal monologue) ──500    thinking_prompt = messages + [{501        "role": "user",502        "content": (503            "Think through your reasoning before responding. "504            "What should you consider before replying to the user?"505        ),506    }]507    thinking_response = llm.create_chat_completion(thinking_prompt)508    thinking_text = thinking_response["choices"][0]["message"]["content"]509    print(f"\n[Thinking Phase]:\n{thinking_text}\n")510 511    # ── Stage 2: Final response informed by thinking ──512    final_messages = truncate_history(messages) + [{513        "role": "system",514        "content": f"Here's your thought process:\n{thinking_text}\nNow generate the final reply.",515    }]516 517    final_response = llm.create_chat_completion(final_messages)518    reply = final_response["choices"][0]["message"]["content"].strip()519 520    # ── Persist ──521    store_message(session_id, "user",      user_input)522    store_message(session_id, "assistant", reply)523    history.append({"role": "user",      "content": user_input})524    history.append({"role": "assistant", "content": reply})525 526    # ── TTS ──527    audio_out = generate_tts(reply)528 529    # ── End-session summary ──530    if user_input.lower().strip() in ["end session", "exit", "bye"]:531        pairs = []532        for i in range(0, len(history) - 1, 2):533            u = history[i].get("content", "")534            b = history[i+1].get("content", "") if i+1 < len(history) else ""535            pairs.append((u, b))536        generate_and_store_summary(session_id, pairs)537        print("[Session summarized and stored]")538 539    return history, "", audio_out, session_id540 541 542def clear_chat(session_id: str):543    new_session = str(int(time.time()))544    if session_id in _greeting_done:545        del _greeting_done[session_id]546    return [], "", None, new_session547 548 549# ──────────────────────────────────────────────550# GRADIO UI551# ──────────────────────────────────────────────552 553css = """554body { font-family: 'Segoe UI', sans-serif; }555#chatbot { height: 480px; overflow-y: auto; }556footer { display: none !important; }557"""558 559with gr.Blocks(title="Conversational AI") as demo:560 561    gr.Markdown("## 🎙️ Conversational AI Companion")562    gr.Markdown(563        "Talk to your AI companion via **voice** or **text**. "564        "Say *'end session'* / *'bye'* to save a session summary."565    )566 567    session_id_state = gr.State(str(int(time.time())))568 569    chatbot = gr.Chatbot(label="Conversation", elem_id="chatbot")570 571    with gr.Row():572        audio_input = gr.Audio(573            sources=["microphone"],574            type="filepath",575            label="🎤 Speak",576        )577        audio_output = gr.Audio(578            label="🔊 Response",579            type="filepath",580            interactive=False,581            autoplay=True,582        )583 584    with gr.Row():585        text_input = gr.Textbox(586            placeholder="Type your message here…",587            label="Message",588            scale=5,589            show_label=False,590        )591        submit_btn = gr.Button("Send", variant="primary", scale=1)592        clear_btn  = gr.Button("Clear", scale=1)593 594    # ── Event wiring ──595    submit_btn.click(596        chat_pipeline,597        inputs=[audio_input, text_input, chatbot, session_id_state],598        outputs=[chatbot, text_input, audio_output, session_id_state],599    )600    text_input.submit(601        chat_pipeline,602        inputs=[audio_input, text_input, chatbot, session_id_state],603        outputs=[chatbot, text_input, audio_output, session_id_state],604    )605    clear_btn.click(606        clear_chat,607        inputs=[session_id_state],608        outputs=[chatbot, text_input, audio_output, session_id_state],609    )610 611demo.launch(css=css,ssr_mode=False)