import os import json import datetime import re import math import random # === PATCH: HfFolder za Gradio (HF Spaces fix) === import huggingface_hub if not hasattr(huggingface_hub, 'HfFolder'): class HfFolder: @staticmethod def get_token(): return os.getenv('HF_TOKEN') @staticmethod def save_token(token): pass @staticmethod def delete_token(): pass huggingface_hub.HfFolder = HfFolder # ================================================ from dotenv import load_dotenv import requests import openai import gradio as gr load_dotenv() # === API KLJUCEVI === GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") SAMBANOVA_API_KEY = os.getenv("SAMBANOVA_API_KEY", "") MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY", "") COHERE_API_KEY = os.getenv("COHERE_API_KEY", "") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "") # ============================================================ # PROVIDERS # ============================================================ PROVIDERS = { "Groq": { "models": { "llama-3.3-70b-versatile": {"tools": True, "ctx": 128000}, "llama-3.1-8b-instant": {"tools": True, "ctx": 128000}, "mixtral-8x7b-32768": {"tools": True, "ctx": 32768}, }, "key": GROQ_API_KEY, "env": "GROQ_API_KEY", "type": "groq", }, "Google Gemini": { "models": { "gemini-1.5-flash": {"tools": True, "ctx": 1048576}, "gemini-1.5-flash-8b": {"tools": True, "ctx": 1048576}, "gemini-2.0-flash-exp": {"tools": True, "ctx": 1048576}, }, "key": GOOGLE_API_KEY, "env": "GOOGLE_API_KEY", "type": "gemini", }, "OpenRouter": { "models": { "meta-llama/llama-3.2-3b-instruct:free": {"tools": True, "ctx": 32768}, "google/gemma-2-9b-it:free": {"tools": False, "ctx": 8192}, }, "key": OPENROUTER_API_KEY, "env": "OPENROUTER_API_KEY", "type": "oai", "url": "https://openrouter.ai/api/v1", }, "SambaNova": { "models": { "Meta-Llama-3.3-70B-Instruct": {"tools": True, "ctx": 128000}, "DeepSeek-V3.1": {"tools": True, "ctx": 131072}, "Qwen2.5-72B-Instruct": {"tools": True, "ctx": 131072}, }, "key": SAMBANOVA_API_KEY, "env": "SAMBANOVA_API_KEY", "type": "oai", "url": "https://api.sambanova.ai/v1", }, "Mistral AI": { "models": { "mistral-small-latest": {"tools": True, "ctx": 32768}, "mistral-nemo-latest": {"tools": True, "ctx": 131072}, "codestral-latest": {"tools": True, "ctx": 256000}, }, "key": MISTRAL_API_KEY, "env": "MISTRAL_API_KEY", "type": "oai", "url": "https://api.mistral.ai/v1", }, "Cohere": { "models": { "command-r-plus": {"tools": True, "ctx": 128000}, "command-r": {"tools": True, "ctx": 128000}, "command-r7b": {"tools": True, "ctx": 128000}, }, "key": COHERE_API_KEY, "env": "COHERE_API_KEY", "type": "cohere", }, "NVIDIA NIM": { "models": { "meta/llama-3.3-70b-instruct": {"tools": True, "ctx": 131072}, "mistralai/mistral-7b-instruct-v0.3": {"tools": True, "ctx": 32768}, "nvidia/llama-3.1-nemotron-70b-instruct": {"tools": True, "ctx": 131072}, }, "key": NVIDIA_API_KEY, "env": "NVIDIA_API_KEY", "type": "oai", "url": "https://integrate.api.nvidia.com/v1", }, "GitHub Models": { "models": { "gpt-4o-mini": {"tools": True, "ctx": 128000}, "Phi-4": {"tools": True, "ctx": 16384}, "Mistral-large-2411": {"tools": True, "ctx": 131072}, }, "key": GITHUB_TOKEN, "env": "GITHUB_TOKEN", "type": "oai", "url": "https://models.inference.ai.azure.com", }, } # ============================================================ # TOOL DEFINICIJE # ============================================================ TOOLS = [ {"type": "function", "function": { "name": "get_current_time", "description": "Trenutno vrijeme u bilo kojoj vremenskoj zoni", "parameters": {"type": "object", "properties": { "timezone": {"type": "string", "description": "Npr. Europe/Sarajevo"} }}, }}, {"type": "function", "function": { "name": "calculate", "description": "Matematički izraz", "parameters": {"type": "object", "properties": { "expression": {"type": "string", "description": "Izraz npr. 2+2"} }, "required": ["expression"]}, }}, {"type": "function", "function": { "name": "search_web", "description": "Pretraži internet", "parameters": {"type": "object", "properties": { "query": {"type": "string", "description": "Upit"} }, "required": ["query"]}, }}, {"type": "function", "function": { "name": "get_random_number", "description": "Nasumični broj", "parameters": {"type": "object", "properties": { "min": {"type": "number", "description": "Min"}, "max": {"type": "number", "description": "Max"}, }}, }}, {"type": "function", "function": { "name": "analyze_document", "description": "Analiziraj učitani dokument", "parameters": {"type": "object", "properties": { "question": {"type": "string", "description": "Pitanje"} }, "required": ["question"]}, }}, ] # ============================================================ # TOOL EXECUTION # ============================================================ def run_tool(name, args, file_text=""): try: if name == "get_current_time": tz = args.get("timezone", "Europe/Sarajevo") try: import pytz now = datetime.datetime.now(pytz.timezone(tz)) return f"Vrijeme u {tz}: {now.strftime('%H:%M:%S, %d.%m.%Y.')}" except: return f"UTC: {datetime.datetime.utcnow().strftime('%H:%M:%S, %d.%m.%Y.')} UTC" elif name == "calculate": safe = {"__builtins__": {k: v for k, v in __builtins__.items() if k in ("abs","round","int","float","str","bool","min","max","sum","len","True","False","None")}} safe.update({"sqrt":math.sqrt,"sin":math.sin,"cos":math.cos,"tan":math.tan, "log":math.log,"pi":math.pi,"e":math.e,"floor":math.floor,"ceil":math.ceil}) return f"Rezultat: {eval(args.get('expression',''), safe, safe)}" elif name == "search_web": from duckduckgo_search import DDGS q = args.get("query","") with DDGS() as ddgs: res = list(ddgs.text(q, max_results=4)) if res: out = f"Pretraga za '{q}':\n\n" for i, r in enumerate(res, 1): out += f"{i}. **{r['title']}**\n{r['body'][:200]}...\n\n" return out return f"Nema rezultata." elif name == "get_random_number": mn, mx = int(args.get("min",1)), int(args.get("max",100)) if mn > mx: mn, mx = mx, mn return f"🎲 {random.randint(mn, mx)}" elif name == "analyze_document": if not file_text: return "Nema dokumenta." q = args.get("question","").lower() sents = [s.strip() for s in re.split(r'[.!?\n]+', file_text) if any(w in s.lower() for w in q.split())] return "Iz dokumenta:\n\n" + "\n".join(sents[:5]) if sents else f"Dokument: {len(file_text.split())} riječi." except Exception as e: return f"Greška: {str(e)}" # ============================================================ # LLM FUNKCIJE # ============================================================ def call_groq(model, messages, tools_on, file_text): from groq import Groq client = Groq(api_key=GROQ_API_KEY) msgs = [{"role": m["role"], "content": m["content"]} for m in messages] kw = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} if tools_on: kw.update({"tools": TOOLS, "tool_choice": "auto"}) try: r = client.chat.completions.create(**kw) c = r.choices[0] if c.finish_reason == "tool_calls" and c.message.tool_calls: tc = c.message.tool_calls[0] name = tc.function.name args = json.loads(tc.function.arguments) if tc.function.arguments else {} result = run_tool(name, args, file_text) msgs.append({"role": "assistant", "content": c.message.content or "", "tool_calls": [{"id": tc.id, "type": "function", "function": {"name": name, "arguments": tc.function.arguments}}]}) msgs.append({"role": "tool", "tool_call_id": tc.id, "content": result}) f = client.chat.completions.create(**{**kw, "messages": msgs}) return f.choices[0].message.content or "", result, name return c.message.content or "", None, None except Exception as e: return f"**Groq greška:** {str(e)}", None, None def call_gemini(model, messages, tools_on, file_text): import google.generativeai as genai genai.configure(api_key=GOOGLE_API_KEY) m = f"models/{model}" hist = [{"role": "user" if m.get("role") in ("user","tool") else "model", "parts": [m["content"]]} for m in messages[:-1] if m.get("content")] last = messages[-1]["content"] if messages else "" try: if tools_on: fl = [genai.protos.FunctionDeclaration(name=t["function"]["name"], description=t["function"].get("description",""), parameters=t["function"]["parameters"]) for t in TOOLS] chat = genai.GenerativeModel(m, tools=fl).start_chat(history=hist) r = chat.send_message(last) if r.candidates[0].content.parts[0].function_call: fc = r.candidates[0].content.parts[0].function_call args = {k: v for k, v in fc.args.items()} result = run_tool(fc.name, args, file_text) r2 = chat.send_message(genai.protos.Content(parts=[genai.protos.Part( function_response=genai.protos.FunctionResponse(name=fc.name, response={"result": result}))])) return r2.text, result, fc.name return r.text, None, None return genai.GenerativeModel(m).start_chat(history=hist).send_message(last).text, None, None except Exception as e: return f"**Gemini greška:** {str(e)}", None, None def call_oai(model, messages, base_url, api_key, tools_on, file_text): client = openai.OpenAI(base_url=base_url, api_key=api_key) msgs = [{"role": m["role"], "content": m["content"]} for m in messages] kw = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} if tools_on: kw.update({"tools": TOOLS, "tool_choice": "auto"}) try: r = client.chat.completions.create(**kw) c = r.choices[0] if c.finish_reason == "tool_calls" and c.message.tool_calls: tc = c.message.tool_calls[0] name = tc.function.name args = json.loads(tc.function.arguments) if tc.function.arguments else {} result = run_tool(name, args, file_text) msgs.append({"role": "assistant", "content": c.message.content or "", "tool_calls": [{"id": tc.id, "type": "function", "function": {"name": name, "arguments": tc.function.arguments}}]}) msgs.append({"role": "tool", "tool_call_id": tc.id, "content": result}) f = client.chat.completions.create(**{**kw, "messages": msgs}) return f.choices[0].message.content or "", result, name return c.message.content or "", None, None except Exception as e: return f"**Greška:** {str(e)}", None, None def call_cohere(model, messages, tools_on, file_text): headers = {"Authorization": f"Bearer {COHERE_API_KEY}", "Content-Type": "application/json"} msgs = [{"role": m["role"], "content": m["content"]} for m in messages] payload = {"model": model, "messages": msgs, "temperature": 0.7, "max_tokens": 4096} if tools_on: payload["tools"] = TOOLS try: r = requests.post("https://api.cohere.com/v2/chat", headers=headers, json=payload, timeout=30) r.raise_for_status() d = r.json() msg = d.get("message", {}) if "tool_calls" in msg: tc = msg["tool_calls"][0] name = tc.get("function",{}).get("name","") args = tc.get("function",{}).get("arguments",{}) result = run_tool(name, args, file_text) payload["messages"].append({"role": "assistant", "content": msg.get("content","")}) payload["messages"].append({"role": "tool", "content": result, "tool_call_id": tc.get("id","")}) r2 = requests.post("https://api.cohere.com/v2/chat", headers=headers, json=payload, timeout=30) d2 = r2.json() txt = d2.get("message",{}).get("content","") or d2.get("text","") return txt, result, name return msg.get("content","") or d.get("text",""), None, None except Exception as e: return f"**Cohere greška:** {str(e)}", None, None # ============================================================ # FILE / IMAGE # ============================================================ def read_file(file): if file is None: return "" name = getattr(file, 'name', '') or getattr(file, 'orig_name', '') try: if name.lower().endswith('.pdf'): import PyPDF2 return "\n".join(p.extract_text() for p in PyPDF2.PdfReader(file).pages) elif name.lower().endswith('.docx'): from docx import Document return "\n".join(p.text for p in Document(file).paragraphs) elif name.lower().endswith('.txt'): return file.read().decode('utf-8', errors='ignore') return f"[Nepodržan: {name}]" except Exception as e: return f"[Greška: {str(e)}]" def gen_image(prompt): try: return f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width=1024&height=1024&nologo=true" except: return None # ============================================================ # CHAT FUNKCIJA # ============================================================ def chat_fn(msg, history, provider, model, uploaded_file, tools_on, img_mode): if history is None: history = [] file_text = read_file(uploaded_file) if uploaded_file else "" if img_mode and msg.strip(): url = gen_image(msg.strip()) history.append({"role": "user", "content": msg}) history.append({"role": "assistant", "content": f"![Slika]({url})" if url else "❌ Greška"}) return history, history cfg = PROVIDERS.get(provider, {}) if not cfg.get("key"): err = f"⚠️ **API ključ nije podešen za {provider}!**\n\nPostavi `{cfg.get('env','?')}` u HF Secrets." history.append({"role": "user", "content": msg}) history.append({"role": "assistant", "content": err}) return history, history msgs = [] for h in history: if isinstance(h, dict) and h.get("content"): msgs.append({"role": h["role"], "content": h["content"]}) elif isinstance(h, (list,tuple)) and len(h)==2: if h[0]: msgs.append({"role": "user", "content": str(h[0])}) if h[1]: msgs.append({"role": "assistant", "content": str(h[1])}) full_msg = msg.strip() if file_text and not file_text.startswith("["): full_msg += f"\n\n---\nSadržaj fajla:\n```\n{file_text[:3000]}\n```" msgs.append({"role": "user", "content": full_msg}) try: t = cfg.get("type","") if t == "groq": resp, tres, tname = call_groq(model, msgs, tools_on, file_text) elif t == "gemini": resp, tres, tname = call_gemini(model, msgs, tools_on, file_text) elif t == "cohere": resp, tres, tname = call_cohere(model, msgs, tools_on, file_text) elif t == "oai": resp, tres, tname = call_oai(model, msgs, cfg.get("url",""), cfg["key"], tools_on, file_text) else: resp, tres, tname = f"Nepoznat tip: {t}", None, None final = resp if tname and tres: emoji = {"get_current_time":"🕐","calculate":"🧮","search_web":"🔍","get_random_number":"🎲","analyze_document":"📄"}.get(tname,"🛠️") final += f"\n\n---\n{emoji} **Alat:** `{tname}`\n```\n{tres}\n```" history.append({"role": "user", "content": msg}) history.append({"role": "assistant", "content": final}) except Exception as e: history.append({"role": "user", "content": msg}) history.append({"role": "assistant", "content": f"❌ **Greška:**\n```\n{str(e)}\n```"}) return history, history def share_chat(history): if not history: return None ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") txt = f"# 💬 FreeToChat Clone\n📅 {ts}\n\n---\n\n" for h in history: if isinstance(h, dict): r, c = h.get("role",""), h.get("content","") if r == "user": txt += f"**👤 User:**\n{c}\n\n" elif r == "assistant": txt += f"**🤖 AI:**\n{c}\n\n---\n\n" elif isinstance(h, (list,tuple)) and len(h)==2: if h[0]: txt += f"**👤 User:**\n{h[0]}\n\n" if h[1]: txt += f"**🤖 AI:**\n{h[1]}\n\n---\n\n" p = "/tmp/chat_export.md" with open(p, "w", encoding="utf-8") as f: f.write(txt) return p # ============================================================ # UI # ============================================================ def create_ui(): plist = list(PROVIDERS.keys()) avail = [p for p, c in PROVIDERS.items() if c["key"]] default = avail[0] if avail else plist[0] css = """ .app-title { text-align: center; margin-bottom: 12px; } .app-title h1 { margin: 0; font-size: 2.2em; background: linear-gradient(135deg,#667eea,#764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .app-title p { color: #888; margin: 4px 0; font-size: 0.9em; } .toolbar { display: flex; justify-content: space-between; font-size: 0.75em; color: #888; padding: 4px 0; } """ with gr.Blocks(title="FreeToChat Clone", css=css, theme=gr.themes.Soft(primary_hue="violet", secondary_hue="blue", neutral_hue="slate")) as demo: chat_state = gr.State([]) gr.HTML("""

🚀 FreeToChat Clone

🔒 Privatno · 🛠️ Tool Calling · 📄 Analiza · 🎨 Slike · 8 Providera

""") with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=240): gr.Markdown("### ⚙️ Provider") prov_dd = gr.Dropdown(choices=plist, value=default, label="🤖 Provider") model_dd = gr.Dropdown( choices=list(PROVIDERS[default]["models"].keys()), value=list(PROVIDERS[default]["models"].keys())[0], label="🧠 Model") with gr.Row(): tools_cb = gr.Checkbox(value=True, label="🛠️ Alati") img_cb = gr.Checkbox(value=False, label="🎨 Slike") gr.Markdown("---") gr.Markdown("### 📂 Fajl") file_up = gr.File(file_count="single", file_types=[".pdf",".docx",".txt"], label="📎 Upload") gr.Markdown("---") gr.Markdown("### 💬 Opcije") gr.Button("🗑️ Novo", variant="secondary", size="sm").click( fn=lambda: ([],[],None), inputs=[], outputs=[chat_state, gr.skip(), file_up]) share_btn = gr.Button("📤 Podijeli", variant="secondary", size="sm") share_file = gr.File(label="Download", visible=True) share_btn.click(fn=share_chat, inputs=[chat_state], outputs=[share_file]) gr.Markdown("---") gr.HTML("""
🔑 API ključevi (besplatno):
· Groq
· Gemini
· SambaNova
· Mistral
· Cohere
· NVIDIA
· GitHub PAT

🔒 Sve poruke ostaju na tvom uređaju

""") with gr.Column(scale=3): chatbot = gr.Chatbot(label="💬 Chat", height=500, bubble_full_width=False, show_copy_button=True, avatar_images=(None, "🤖"), type="messages") with gr.Row(): msg_tb = gr.Textbox(label="Poruka", placeholder="Napiši poruku... (ili uključi Image Mode za slike)", scale=4, container=True) send_btn = gr.Button("➡️", variant="primary", scale=1, min_width=60) gr.HTML("""
🤖 Alati: 🕐 Vrijeme · 🧮 Kalkulator · 🔍 Web · 🎲 Random · 📄 Dokument 🔌 Groq · Gemini · OpenRouter · SambaNova · Mistral · Cohere · NVIDIA · GitHub
""") # === Events === def upd_models(p): m = list(PROVIDERS.get(p,{}).get("models",{}).keys()) return gr.Dropdown(choices=m, value=m[0] if m else None) prov_dd.change(fn=upd_models, inputs=[prov_dd], outputs=[model_dd]) def upd_ph(img): return gr.update(placeholder="Opiši sliku za generisanje..." if img else "Napiši poruku...") img_cb.change(fn=upd_ph, inputs=[img_cb], outputs=[msg_tb]) inputs = [msg_tb, chat_state, prov_dd, model_dd, file_up, tools_cb, img_cb] outputs = [chat_state, chatbot, msg_tb] send_btn.click(fn=chat_fn, inputs=inputs, outputs=outputs) msg_tb.submit(fn=chat_fn, inputs=inputs, outputs=outputs) return demo # ============================================================ # MAIN # ============================================================ if __name__ == "__main__": print("=" * 60) print(" 🚀 FreeToChat Clone — 8 Providera") print(" Groq · Gemini · OpenRouter · SambaNova") print(" Mistral · Cohere · NVIDIA · GitHub") print("=" * 60) if not any([GROQ_API_KEY, GOOGLE_API_KEY]): print("⚠️ Postavi API ključeve u HF Spaces → Secrets!") demo = create_ui() port = int(os.getenv("SPACE_PORT", 7860)) demo.launch(server_name="0.0.0.0", server_port=port, share=False)