Freechat / app.py
Nerdur's picture
Update app.py
3d963db verified
Raw
History Blame Contribute Delete
23.5 kB
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("""
<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>
""")
# === 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)