-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
161 lines (135 loc) · 5.2 KB
/
app.py
File metadata and controls
161 lines (135 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import os, json, logging
from typing import List, TypedDict, Optional
try:
import requests
_HAVE_REQUESTS = True
except ModuleNotFoundError:
import urllib.request, urllib.error
_HAVE_REQUESTS = False
from groq import Groq
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
app.logger.setLevel(logging.INFO)
# ---------- Models ----------
class SearchItem(TypedDict):
title: str
url: str
snippet: str
# ---------- HTTP helper ----------
def _post_json(url: str, headers: dict, payload: dict) -> dict:
if _HAVE_REQUESTS:
r = requests.post(url, headers=headers, json=payload, timeout=12)
r.raise_for_status()
return r.json()
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=12) as resp: # nosec
return json.loads(resp.read().decode("utf-8"))
# ---------- Tavily search ----------
def tavily_search(query: str, k: int = 5, depth: str = "basic") -> List[SearchItem]:
"""
depth: 'basic' (1 credit) or 'advanced' (2 credits).
"""
api_key = os.getenv("TAVILY_API_KEY", "")
if not api_key:
raise RuntimeError("TAVILY_API_KEY not set")
# Respect ~400 char query limit (hard trim as a guard).
q = (query or "").strip()
if len(q) > 400:
q = q[:400]
payload = {
"query": q,
"search_depth": depth, # 'basic' or 'advanced'
"include_answer": False, # we let Groq synthesize
"include_raw_content": False,
"max_results": max(1, min(k, 8)),
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = _post_json("https://api.tavily.com/search", headers, payload)
items: List[SearchItem] = []
for r in data.get("results", [])[:k]:
items.append({
"title": (r.get("title") or "").strip(),
"url": r.get("url") or "",
"snippet": (r.get("content") or "").strip()[:500],
})
return items
# ---------- Groq LLM ----------
def _groq() -> Optional[Groq]:
key = os.getenv("GROQ_API_KEY", "")
return Groq(api_key=key) if key else None
def generate_ai_response(prompt: str) -> str: # unchanged pure-LLM path
client = _groq()
if not client:
return "GROQ_API_KEY is not set on the server – AI mode is unavailable."
r = client.chat.completions.create(
model="openai/gpt-oss-120b",
messages=[
{"role": "system", "content": "You are Boog – concise, helpful."},
{"role": "user", "content": prompt},
],
temperature=0.6,
)
return (r.choices[0].message.content or "").strip() or "(No response)"
def answer_with_web_search(query: str, k: int = 5, depth: str = "basic") -> str:
try:
results = tavily_search(query, k=k, depth=depth)
except Exception as exc:
app.logger.error("Tavily error: %s", exc)
return "Web search is temporarily unavailable."
if not results:
return "No results found."
# Build grounded prompt
sources_txt = []
for i, r in enumerate(results, 1):
title = r["title"] or r["url"]
sources_txt.append(
f"[{i}] {title}\nURL: {r['url']}\nSnippet: {r['snippet']}"
)
prompt = (
"Use the numbered sources to answer. Cite like [1], [2]. "
"Only include supported claims; if unclear, say so.\n\n"
f"USER QUESTION:\n{query}\n\nSOURCES:\n" + "\n\n".join(sources_txt)
)
client = _groq()
if not client:
return "GROQ_API_KEY is not set on the server – AI mode is unavailable."
r = client.chat.completions.create(
model="openai/gpt-oss-120b",
messages=[
{"role": "system", "content": "Ground answers in the sources and cite."},
{"role": "user", "content": prompt},
],
temperature=0.3,
)
answer = (r.choices[0].message.content or "").strip()
links = "\n".join(
f"- [{i}] {it['title'] or it['url']} — {it['url']}"
for i, it in enumerate(results, 1)
)
return f"{answer}\n\n---\n**Sources (links):**\n{links}"
# ---------- Flask Routes ----------------------------------------------------
@app.route("/")
def index():
return render_template("index.html")
@app.route("/chat", methods=["POST"])
def chat():
payload = request.get_json(silent=True) or {}
user_input: str = (payload.get("message") or "").strip()
mode: str = (payload.get("mode") or "ai").lower() # "ai" | "web"
if not user_input:
return jsonify(response="Please provide a message.")
if mode in ("web", "web-search", "search"):
# You can flip to depth="advanced" for tougher queries (costs 2 credits).
resp = answer_with_web_search(user_input, k=5, depth="basic")
else:
resp = generate_ai_response(user_input)
return jsonify(response=resp)
# ---------- Entrypoint ------------------------------------------------------
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
# Setting threaded=True plays nicer with Groq and HTTP requests concurrency
app.run(host="0.0.0.0", port=port, threaded=True)