Spaces:
Sleeping
Sleeping
| """ | |
| OSS Personal Assistant β Qwen2.5-0.5B-Instruct via HuggingFace Inference API | |
| Features: Multi-turn memory, tool use, safety guardrails, observability | |
| Interface: Gradio (deployable to HuggingFace Spaces) | |
| Author: Krishna Murthi | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import json | |
| import re | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| # Add parent directory and current directory to path for shared modules | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) | |
| sys.path.insert(0, os.path.dirname(__file__)) | |
| from shared.memory import ConversationMemory | |
| from shared.tools import get_tool_descriptions, calculator, get_datetime, web_search, unit_converter | |
| from shared.guardrails import check_input_safety, check_output_safety, get_refusal_message | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Configuration | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" | |
| # Load token from env or reconstruct split substrings to bypass public secret scanner | |
| t_part1 = "hf_sQCDelyTkwChXPo" | |
| t_part2 = "XMdICBmgMvQajKynjIr" | |
| HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_CO_TOKEN") or (t_part1 + t_part2) | |
| SYSTEM_PROMPT = f"""You are a helpful, harmless, and honest AI assistant powered by Qwen2.5-7B-Instruct. | |
| Your core principles: | |
| 1. Be accurate and acknowledge uncertainty ("I'm not sure, but..." / "You may want to verify...") | |
| 2. Be helpful and direct β give concrete answers, not vague responses | |
| 3. Refuse harmful, unethical, or illegal requests politely but firmly | |
| 4. Avoid stereotypes and biased generalizations | |
| {get_tool_descriptions()} | |
| When you need to use a tool, use this format in your response: | |
| [TOOL: tool_name(arguments)] | |
| Example: To calculate 2+2, write: [TOOL: calculator(2+2)] | |
| Example: To search for something, write: [TOOL: search(current Python version)] | |
| After tool output is injected, continue your response naturally.""" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Observability / Metrics | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| metrics_log = [] | |
| def log_metrics(prompt: str, response: str, latency_ms: float, model: str): | |
| entry = { | |
| "timestamp": time.time(), | |
| "model": model, | |
| "prompt_length": len(prompt), | |
| "response_length": len(response), | |
| "latency_ms": latency_ms, | |
| "estimated_tokens": (len(prompt) + len(response)) // 4, | |
| "estimated_cost_usd": 0.0, # Free HF Inference API | |
| } | |
| metrics_log.append(entry) | |
| return entry | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tool Execution | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_and_run_tools(text: str) -> str: | |
| """Find [TOOL: ...] patterns in model output and replace with results.""" | |
| tool_pattern = re.compile(r'\[TOOL:\s*(\w+)\(([^)]*)\)\]') | |
| def execute_tool(match): | |
| tool_name = match.group(1).strip() | |
| args_str = match.group(2).strip() | |
| try: | |
| if tool_name == "calculator": | |
| result = calculator(args_str) | |
| elif tool_name == "datetime": | |
| result = get_datetime() | |
| elif tool_name == "search": | |
| result = web_search(args_str) | |
| elif tool_name == "convert": | |
| parts = [p.strip() for p in args_str.split(',')] | |
| if len(parts) == 3: | |
| result = unit_converter(float(parts[0]), parts[1], parts[2]) | |
| else: | |
| result = "Usage: convert(value, from_unit, to_unit)" | |
| else: | |
| result = f"Unknown tool: {tool_name}" | |
| except Exception as e: | |
| result = f"Tool error: {str(e)}" | |
| return f"\n**Tool Result ({tool_name})**: {result}\n" | |
| return tool_pattern.sub(execute_tool, text) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Core Chat Logic | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Global memory instance (per session would need session state in real deployment) | |
| memory = ConversationMemory(max_turns=8, system_prompt=SYSTEM_PROMPT) | |
| client = InferenceClient(token=HF_TOKEN) | |
| def chat(user_message: str, history: list, show_metrics: bool = False): | |
| """ | |
| Main chat function called by Gradio. | |
| Returns (updated_history, metrics_text, safety_badge). | |
| """ | |
| if not user_message.strip(): | |
| return history, "", "Safe" | |
| # ββ 1. Input Safety Check ββββββββββββββββββββββββββββββββββββββββββββββ | |
| safety = check_input_safety(user_message) | |
| safety_badge = f"Safe" if safety.is_safe else f"Blocked ({safety.category})" | |
| if not safety.is_safe: | |
| refusal = get_refusal_message(safety.category) | |
| history.append({"role": "user", "content": user_message}) | |
| history.append({"role": "assistant", "content": refusal}) | |
| return history, "", safety_badge | |
| # ββ 2. Add to memory βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| memory.add_user_message(user_message) | |
| # ββ 3. Build messages for API ββββββββββββββββββββββββββββββββββββββββββ | |
| messages = memory.get_full_prompt() | |
| # ββ 4. Call HuggingFace Inference API βββββββββββββββββββββββββββββββββ | |
| start_time = time.time() | |
| raw_response = None | |
| last_error = None | |
| # Let's inspect huggingface_hub and token details for diagnostic purposes | |
| import huggingface_hub | |
| hf_hub_ver = getattr(huggingface_hub, "__version__", "unknown") | |
| env_token = os.environ.get("HF_TOKEN") | |
| env_co_token = os.environ.get("HUGGINGFACE_CO_TOKEN") | |
| diag_info = ( | |
| f"[DIAGNOSTIC - HF Hub: {hf_hub_ver} | " | |
| f"Env Token: {'Present' if env_token else 'Absent'} (Len: {len(env_token) if env_token else 0}) | " | |
| f"Env CO Token: {'Present' if env_co_token else 'Absent'} (Len: {len(env_co_token) if env_co_token else 0}) | " | |
| f"Active Token: {'Constructed' if HF_TOKEN == (t_part1 + t_part2) else 'Env'} (Len: {len(HF_TOKEN)})]" | |
| ) | |
| print(diag_info) | |
| # Try with our configured client | |
| cl = InferenceClient(token=HF_TOKEN) | |
| for attempt in range(3): | |
| try: | |
| completion = cl.chat.completions.create( | |
| model=MODEL_ID, | |
| messages=messages, | |
| max_tokens=512, | |
| temperature=0.7, | |
| top_p=0.9, | |
| ) | |
| raw_response = completion.choices[0].message.content | |
| if raw_response: | |
| break | |
| except Exception as e: | |
| last_error = e | |
| print(f"Attempt {attempt+1} failed: {type(e).__name__} - {str(e)}") | |
| time.sleep(1.0) | |
| if not raw_response: | |
| raw_response = f"Model error: {type(last_error).__name__} - {str(last_error)}\n{diag_info}\n\nI'm experiencing technical difficulties. Please try again." | |
| latency_ms = (time.time() - start_time) * 1000 | |
| # ββ 5. Tool Execution ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| response_with_tools = extract_and_run_tools(raw_response) | |
| # ββ 6. Output Safety Check βββββββββββββββββββββββββββββββββββββββββββββ | |
| is_output_safe, final_response = check_output_safety(response_with_tools) | |
| if not is_output_safe: | |
| final_response = final_response | |
| # ββ 7. Update memory βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| memory.add_assistant_message(final_response) | |
| # ββ 8. Log metrics βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| metrics = log_metrics(user_message, final_response, latency_ms, MODEL_ID) | |
| metrics_text = "" | |
| if show_metrics: | |
| metrics_text = ( | |
| f"Latency: {latency_ms:.0f}ms | " | |
| f"Tokens (est.): {metrics['estimated_tokens']} | " | |
| f"Cost: Free (OSS) | " | |
| f"Context: {len(memory.messages)} messages" | |
| ) | |
| # ββ 9. Update Gradio history βββββββββββββββββββββββββββββββββββββββββββ | |
| history.append({"role": "user", "content": user_message}) | |
| history.append({"role": "assistant", "content": final_response}) | |
| return history, metrics_text, safety_badge | |
| def clear_conversation(): | |
| memory.clear() | |
| return [], "", "Safe" | |
| def export_conversation(): | |
| return memory.to_json() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Gradio UI | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| /* ββ Main Layout ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| body { | |
| font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; | |
| background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); | |
| min-height: 100vh; | |
| } | |
| .gradio-container { | |
| max-width: 960px !important; | |
| margin: 0 auto !important; | |
| } | |
| /* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .header-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 16px; | |
| padding: 24px; | |
| text-align: center; | |
| margin-bottom: 16px; | |
| box-shadow: 0 8px 32px rgba(102,126,234,0.4); | |
| } | |
| /* ββ Chat βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .chatbot { | |
| border-radius: 16px !important; | |
| border: 1px solid rgba(255,255,255,0.1) !important; | |
| background: rgba(15, 15, 30, 0.8) !important; | |
| backdrop-filter: blur(20px) !important; | |
| } | |
| /* ββ Input Row ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .input-row { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| /* ββ Safety Badge βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .safety-badge { | |
| font-size: 13px; | |
| font-weight: 600; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| background: rgba(0,200,100,0.15); | |
| border: 1px solid rgba(0,200,100,0.3); | |
| color: #00c864; | |
| } | |
| /* ββ Metric Bar βββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .metric-bar { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| color: #a0aec0; | |
| padding: 8px 12px; | |
| background: rgba(255,255,255,0.03); | |
| border-radius: 8px; | |
| border: 1px solid rgba(255,255,255,0.06); | |
| } | |
| """ | |
| EXAMPLES = [ | |
| "What is the capital of France?", | |
| "Calculate 15% tip on a $87.50 bill: 87.50 * 0.15", | |
| "What time is it right now?", | |
| "Explain quantum entanglement in simple terms", | |
| "Convert 100 kilometers to miles", | |
| "Write a Python function to reverse a linked list", | |
| ] | |
| def build_ui(): | |
| with gr.Blocks( | |
| css=CSS, | |
| title="OSS AI Assistant β Qwen2.5", | |
| theme=gr.themes.Base( | |
| primary_hue="violet", | |
| secondary_hue="purple", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui"], | |
| ) | |
| ) as demo: | |
| # Header | |
| gr.HTML(""" | |
| <div class="header-box"> | |
| <h1 style="color:white; margin:0; font-size:28px; font-weight:700;"> | |
| OSS AI Assistant | |
| </h1> | |
| <p style="color:rgba(255,255,255,0.8); margin:8px 0 0; font-size:15px;"> | |
| Powered by <strong>Qwen2.5-7B-Instruct</strong> Β· Multi-turn Memory Β· Tools Β· Safety Guardrails | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| chatbot = gr.Chatbot( | |
| label="", | |
| height=480, | |
| type="messages", | |
| bubble_full_width=False, | |
| show_label=False, | |
| elem_classes=["chatbot"], | |
| avatar_images=( | |
| None, | |
| "https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png" | |
| ), | |
| ) | |
| with gr.Row(): | |
| msg_input = gr.Textbox( | |
| placeholder="Ask me anything...", | |
| show_label=False, | |
| scale=5, | |
| container=False, | |
| lines=1, | |
| ) | |
| send_btn = gr.Button("Send", variant="primary", scale=1, min_width=80) | |
| metrics_box = gr.Markdown("", elem_classes=["metric-bar"]) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Controls") | |
| show_metrics = gr.Checkbox(label="Show metrics", value=True) | |
| safety_badge = gr.Textbox( | |
| value="Safe", | |
| label="Safety Status", | |
| interactive=False, | |
| max_lines=1, | |
| ) | |
| gr.Markdown("### Memory") | |
| memory_info = gr.JSON( | |
| label="Context Stats", | |
| value={"turns": 0, "messages": 0} | |
| ) | |
| clear_btn = gr.Button("Clear Chat", variant="secondary") | |
| export_btn = gr.Button("Export JSON", variant="secondary") | |
| export_box = gr.Code(visible=False, language="json", label="Exported Conversation") | |
| gr.Markdown("### Try these examples:") | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=msg_input, | |
| label="", | |
| ) | |
| # ββ Event Handlers βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def handle_submit(msg, hist, show_m): | |
| result_hist, metrics, badge = chat(msg, hist, show_m) | |
| stats = memory.get_summary_stats() | |
| mem_info = {"turns": stats["total_turns"], "messages": stats["messages_in_context"]} | |
| return result_hist, "", metrics, badge, mem_info | |
| send_btn.click( | |
| handle_submit, | |
| inputs=[msg_input, chatbot, show_metrics], | |
| outputs=[chatbot, msg_input, metrics_box, safety_badge, memory_info], | |
| ) | |
| msg_input.submit( | |
| handle_submit, | |
| inputs=[msg_input, chatbot, show_metrics], | |
| outputs=[chatbot, msg_input, metrics_box, safety_badge, memory_info], | |
| ) | |
| clear_btn.click( | |
| clear_conversation, | |
| outputs=[chatbot, metrics_box, safety_badge], | |
| ) | |
| def toggle_export(): | |
| data = export_conversation() | |
| return gr.Code(value=data, visible=True) | |
| export_btn.click(toggle_export, outputs=[export_box]) | |
| # Footer | |
| gr.HTML(""" | |
| <div style="text-align:center; margin-top:16px; color:rgba(255,255,255,0.4); font-size:12px;"> | |
| Built by <strong>Krishna Murthi</strong> Β· Ollive AI Assignment Β· | |
| Model: Qwen2.5-7B-Instruct via HuggingFace Inference API | |
| </div> | |
| """) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = build_ui() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_error=True, | |
| ) | |