Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import requests | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| # ========= CONFIG ========= | |
| CHAT_MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct" | |
| ROUTER_URL = "https://router.huggingface.co/v1/chat/completions" | |
| ASR_MODEL_ID = "openai/whisper-large-v3" | |
| DEFAULT_SYSTEM_PROMPT = "You are a helpful AI assistant." | |
| MAX_FILE_CHARS = 40_000 | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| if not HF_TOKEN: | |
| raise RuntimeError("HF_TOKEN is missing. Add it in Space Settings β Secrets as HF_TOKEN.") | |
| HEADERS = { | |
| "Authorization": f"Bearer {HF_TOKEN}", | |
| "Content-Type": "application/json", | |
| } | |
| asr_client = InferenceClient(api_key=HF_TOKEN) | |
| # ========= HELPERS ========= | |
| def normalize_whitespace(s: str) -> str: | |
| return re.sub(r"\s+", " ", (s or "")).strip() | |
| def safe_read_text_file(path: str) -> str: | |
| with open(path, "r", encoding="utf-8", errors="ignore") as f: | |
| return f.read() | |
| def extract_text_from_pdf(path: str) -> str: | |
| try: | |
| from pypdf import PdfReader | |
| except Exception: | |
| return "PDF support not installed. Add 'pypdf' to requirements.txt." | |
| reader = PdfReader(path) | |
| parts = [] | |
| for page in reader.pages[:50]: | |
| parts.append(page.extract_text() or "") | |
| return "\n".join(parts) | |
| def extract_file_text(file_path: str) -> str: | |
| ext = os.path.splitext(file_path)[1].lower() | |
| if ext in [".txt", ".md", ".csv", ".json", ".py", ".js", ".html", ".css", ".log", ".yaml", ".yml"]: | |
| return safe_read_text_file(file_path) | |
| if ext == ".pdf": | |
| return extract_text_from_pdf(file_path) | |
| return "" | |
| def call_llm(messages, temperature=0.7, max_tokens=400): | |
| payload = { | |
| "model": CHAT_MODEL_ID, | |
| "messages": messages, | |
| "temperature": float(temperature), | |
| "max_tokens": int(max_tokens), | |
| } | |
| resp = requests.post(ROUTER_URL, headers=HEADERS, json=payload, timeout=120) | |
| if resp.status_code != 200: | |
| try: | |
| err = resp.json() | |
| except Exception: | |
| err = resp.text | |
| raise RuntimeError(f"HTTP {resp.status_code}: {err}") | |
| data = resp.json() | |
| return data["choices"][0]["message"]["content"] | |
| def chat_to_llm_messages(chat_messages, system_prompt: str, file_context: str, user_text: str): | |
| msgs = [{"role": "system", "content": (system_prompt.strip() or DEFAULT_SYSTEM_PROMPT)}] | |
| if (file_context or "").strip(): | |
| msgs.append({ | |
| "role": "system", | |
| "content": "User uploaded file content (use this as reference):\n" + file_context | |
| }) | |
| for m in (chat_messages or []): | |
| role = (m.get("role") or "").lower() | |
| content = m.get("content") or "" | |
| if role in ("user", "assistant") and content: | |
| msgs.append({"role": role, "content": content}) | |
| msgs.append({"role": "user", "content": user_text}) | |
| return msgs | |
| # ========= ACTIONS ========= | |
| def on_upload_file(chat_messages, file_obj, file_context): | |
| if file_obj is None: | |
| return chat_messages, file_context, "No file selected" | |
| file_path = getattr(file_obj, "name", None) or str(file_obj) | |
| filename = os.path.basename(file_path) | |
| file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 | |
| extracted = extract_file_text(file_path) | |
| if extracted: | |
| extracted = extracted[:MAX_FILE_CHARS] | |
| file_context = f"[{filename}]\n{extracted}\n\n" + (file_context or "") | |
| chat_messages = (chat_messages or []) + [ | |
| {"role": "user", "content": f"π Uploaded file: {filename}"}, | |
| {"role": "assistant", "content": f"β **{filename}** loaded ({file_size:,} bytes). Ask me anything about this document!"}, | |
| ] | |
| file_info = f"β Loaded: {filename} ({file_size:,} bytes)" | |
| else: | |
| chat_messages = (chat_messages or []) + [ | |
| {"role": "user", "content": f"π Uploaded file: {filename}"}, | |
| {"role": "assistant", "content": f"π **{filename}** received. I'll answer based on its content."}, | |
| ] | |
| file_info = f"π Uploaded: {filename}" | |
| return chat_messages, file_context, file_info | |
| def on_record_audio(audio_path): | |
| if not audio_path: | |
| return "" | |
| try: | |
| out = asr_client.automatic_speech_recognition(audio_path, model=ASR_MODEL_ID) | |
| text = getattr(out, "text", "") if out is not None else "" | |
| return normalize_whitespace(text) | |
| except Exception as e: | |
| return f"[Voice recognition error: {str(e)[:50]}]" | |
| def on_send(chat_messages, user_text, system_prompt, temperature, max_tokens, file_context): | |
| user_text = (user_text or "").strip() | |
| if not user_text: | |
| return chat_messages, "" | |
| chat_messages = chat_messages or [] | |
| chat_messages.append({"role": "user", "content": user_text}) | |
| try: | |
| llm_messages = chat_to_llm_messages(chat_messages, system_prompt, file_context or "", user_text) | |
| assistant = call_llm(llm_messages, temperature=temperature, max_tokens=max_tokens) | |
| chat_messages.append({"role": "assistant", "content": assistant}) | |
| return chat_messages, "" | |
| except Exception as e: | |
| chat_messages.append({"role": "assistant", "content": f"β οΈ Error: {type(e).__name__}: {str(e)[:100]}"}) | |
| return chat_messages, "" | |
| def on_clear(): | |
| return [], "", "No file selected" | |
| # ========= SIMPLE UI ========= | |
| with gr.Blocks(title="AI Chat Assistant") as demo: | |
| file_context_state = gr.State("") | |
| chat_state = gr.State([]) | |
| # Header | |
| with gr.Column(): | |
| gr.Markdown("# π€ AI Chat Assistant") | |
| gr.Markdown("### Your intelligent assistant powered by Llama 3.1") | |
| gr.Markdown(f"**Model:** `{CHAT_MODEL_ID.split('/')[-1]}`") | |
| # Chat display | |
| chatbot = gr.Chatbot(height=400, label="Chat History") | |
| # File upload section | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π Upload Document") | |
| upload = gr.File( | |
| label="", | |
| file_count="single", | |
| file_types=[".txt", ".pdf", ".md", ".csv", ".json", ".py"] | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π€ Voice Input") | |
| mic = gr.Audio( | |
| sources=["microphone"], | |
| type="filepath", | |
| label="" | |
| ) | |
| with gr.Column(scale=2): | |
| file_info_display = gr.Textbox( | |
| label="π File Status", | |
| value="No file selected", | |
| interactive=False | |
| ) | |
| # Settings in accordion | |
| with gr.Accordion("βοΈ Settings", open=False): | |
| system_prompt = gr.Textbox( | |
| value=DEFAULT_SYSTEM_PROMPT, | |
| label="System Prompt", | |
| lines=3 | |
| ) | |
| with gr.Row(): | |
| temperature = gr.Slider( | |
| minimum=0.0, | |
| maximum=1.5, | |
| value=0.7, | |
| step=0.05, | |
| label="Temperature (Creativity)" | |
| ) | |
| max_tokens = gr.Slider( | |
| minimum=64, | |
| maximum=1024, | |
| value=400, | |
| step=64, | |
| label="Max Response Length" | |
| ) | |
| # Input area | |
| with gr.Row(): | |
| user_input = gr.Textbox( | |
| placeholder="Type your message here...", | |
| label="Your Message", | |
| lines=3, | |
| scale=4 | |
| ) | |
| with gr.Column(scale=1): | |
| send_btn = gr.Button("π Send", variant="primary", size="lg") | |
| clear_btn = gr.Button("ποΈ Clear Chat", variant="secondary") | |
| # ========= EVENT HANDLERS ========= | |
| # File upload | |
| upload.change( | |
| fn=on_upload_file, | |
| inputs=[chat_state, upload, file_context_state], | |
| outputs=[chat_state, file_context_state, file_info_display] | |
| ).then( | |
| fn=lambda x: x, | |
| inputs=[chat_state], | |
| outputs=[chatbot] | |
| ) | |
| # Voice input | |
| mic.change( | |
| fn=on_record_audio, | |
| inputs=[mic], | |
| outputs=[user_input] | |
| ) | |
| # Send message (button) | |
| send_btn.click( | |
| fn=on_send, | |
| inputs=[chat_state, user_input, system_prompt, temperature, max_tokens, file_context_state], | |
| outputs=[chat_state, user_input] | |
| ).then( | |
| fn=lambda x: x, | |
| inputs=[chat_state], | |
| outputs=[chatbot] | |
| ) | |
| # Send message (enter) | |
| user_input.submit( | |
| fn=on_send, | |
| inputs=[chat_state, user_input, system_prompt, temperature, max_tokens, file_context_state], | |
| outputs=[chat_state, user_input] | |
| ).then( | |
| fn=lambda x: x, | |
| inputs=[chat_state], | |
| outputs=[chatbot] | |
| ) | |
| # Clear chat | |
| clear_btn.click( | |
| fn=on_clear, | |
| inputs=[], | |
| outputs=[chat_state, file_context_state, file_info_display] | |
| ).then( | |
| fn=lambda: [], | |
| inputs=[], | |
| outputs=[chatbot] | |
| ) | |
| # Launch without CSS to avoid issues | |
| demo.launch(share=False, server_name="0.0.0.0", server_port=7860) |