| import os |
| import json |
| import datetime |
| import re |
| import math |
| import random |
|
|
| |
| 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() |
|
|
| |
| 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 = { |
| "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", |
| }, |
| } |
|
|
| |
| |
| |
|
|
| 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"]}, |
| }}, |
| ] |
|
|
| |
| |
| |
|
|
| 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)}" |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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"" 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 |
|
|
| |
| |
| |
|
|
| 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(""" |
| <div class="app-title"> |
| <h1>🚀 FreeToChat Clone</h1> |
| <p>🔒 Privatno · 🛠️ Tool Calling · 📄 Analiza · 🎨 Slike · 8 Providera</p> |
| </div> |
| """) |
|
|
| 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(""" |
| <div style="font-size:0.85em"> |
| <b>🔑 API ključevi (besplatno):</b><br> |
| · <a href="https://console.groq.com/keys" target="_blank">Groq</a><br> |
| · <a href="https://aistudio.google.com/app/apikey" target="_blank">Gemini</a><br> |
| · <a href="https://cloud.sambanova.ai/" target="_blank">SambaNova</a><br> |
| · <a href="https://console.mistral.ai/" target="_blank">Mistral</a><br> |
| · <a href="https://dashboard.cohere.com/" target="_blank">Cohere</a><br> |
| · <a href="https://build.nvidia.com/" target="_blank">NVIDIA</a><br> |
| · <a href="https://github.com/settings/tokens" target="_blank">GitHub PAT</a> |
| <p style="font-size:0.75em;color:#888;text-align:center;margin-top:8px">🔒 Sve poruke ostaju na tvom uređaju</p> |
| </div> |
| """) |
|
|
| 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(""" |
| <div class="toolbar"> |
| <span>🤖 Alati: 🕐 Vrijeme · 🧮 Kalkulator · 🔍 Web · 🎲 Random · 📄 Dokument</span> |
| <span>🔌 Groq · Gemini · OpenRouter · SambaNova · Mistral · Cohere · NVIDIA · GitHub</span> |
| </div> |
| """) |
|
|
| |
| 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 |
|
|
| |
| |
| |
|
|
| 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) |
|
|