openfree commited on
Commit
32ad39d
ยท
verified ยท
1 Parent(s): f6a596d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +11 -335
app.py CHANGED
@@ -9,338 +9,14 @@ import openai
9
  import torch
10
  from sentence_transformers import SentenceTransformer, util
11
 
12
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 0. API keys & Brave Search โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
- if "OPENAI_API_KEY" not in os.environ:
14
- os.environ["OPENAI_API_KEY"] = input("๐Ÿ”‘ Enter your OpenAI API key: ").strip()
15
- openai.api_key = os.environ["OPENAI_API_KEY"]
16
-
17
- BRAVE_KEY = os.getenv("BRAVE_KEY", "")
18
- BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
19
- logging.basicConfig(level=logging.INFO)
20
-
21
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. Cycle config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
- CENTER = 2025
23
- CYCLES = { "K-Wave": 50, "Business": 9, "Finance": 80, "Hegemony": 250 }
24
- ORDERED_PERIODS = sorted(CYCLES.values())
25
- COLOR = {9:"#66ff66", 50:"#ff3333", 80:"#ffcc00", 250:"#66ccff"}
26
- AMPL = {9:0.6, 50:1.0, 80:1.6, 250:4.0}
27
- PERIOD_BY_CYCLE = {k:v for k,v in CYCLES.items()}
28
-
29
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. Load events JSON & embeddings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30
- EVENTS_PATH = pathlib.Path(__file__).with_name("cycle_events.json")
31
- with open(EVENTS_PATH, encoding="utf-8") as f:
32
- RAW_EVENTS = json.load(f)
33
- EVENTS = {int(item["year"]): item["events"] for item in RAW_EVENTS}
34
-
35
- logging.info("Embedding historical eventsโ€ฆ")
36
- _embed_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
37
- _all_sentences = [(yr, ev["event_en"]) for yr, evs in EVENTS.items() for ev in evs]
38
- _embeddings = _embed_model.encode([s for _, s in _all_sentences], convert_to_tensor=True)
39
-
40
- # ์œ ์‚ฌ ์‚ฌ๊ฑด top-3 year ์‚ฌ์ „
41
- SIMILAR_MAP = {}
42
- for idx, (yr, _) in enumerate(_all_sentences):
43
- scores = util.cos_sim(_embeddings[idx], _embeddings)[0]
44
- top_idx = torch.topk(scores, 4).indices.tolist()
45
- sims = [_all_sentences[i][0] for i in top_idx if _all_sentences[i][0] != yr][:3]
46
- SIMILAR_MAP.setdefault(yr, sims)
47
-
48
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. Brave Search helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
49
- def brave_search(query: str, count: int = 8, freshness_days: int | None = None):
50
- if not BRAVE_KEY:
51
- return []
52
- params = {"q": query, "count": str(count)}
53
- if freshness_days:
54
- dt_from = (datetime.utcnow() - timedelta(days=freshness_days)).strftime("%Y-%m-%d")
55
- params["freshness"] = dt_from
56
- try:
57
- r = requests.get(
58
- BRAVE_ENDPOINT,
59
- headers={"Accept": "application/json", "X-Subscription-Token": BRAVE_KEY},
60
- params=params,
61
- timeout=15
62
- )
63
- raw = r.json().get("web", {}).get("results") or []
64
- return [{
65
- "title": r.get("title", ""),
66
- "url": r.get("url", r.get("link", "")),
67
- "snippet": r.get("description", r.get("text", "")),
68
- "host": re.sub(r"https?://(www\.)?", "", r.get("url", "")).split("/")[0]
69
- } for r in raw[:count]]
70
- except Exception as e:
71
- logging.error(f"Brave error: {e}")
72
- return []
73
-
74
- def format_search_results(query: str) -> str:
75
- rows = brave_search(query, 6, freshness_days=3)
76
- if not rows:
77
- return f"# [Web-Search] No live results for โ€œ{query}โ€.\n"
78
- hdr = f"# [Web-Search] Top results for โ€œ{query}โ€ (last 3 days)\n\n"
79
- body = "\n".join(
80
- f"- **{r['title']}** ({r['host']})\n {r['snippet']}\n [link]({r['url']})"
81
- for r in rows
82
- )
83
- return hdr + body + "\n"
84
-
85
- NEWS_KEYWORDS = {
86
- "Business": "recession OR GDP slowdown",
87
- "K-Wave": "breakthrough technology innovation",
88
- "Finance": "credit cycle debt crisis",
89
- "Hegemony": "great power rivalry geopolitics"
90
- }
91
- def fetch_cycle_news():
92
- markers = []
93
- for cyc, kw in NEWS_KEYWORDS.items():
94
- res = brave_search(kw, 1, freshness_days=2)
95
- if res:
96
- markers.append({
97
- "cycle": cyc,
98
- "title": res[0]["title"],
99
- "year": datetime.utcnow().year,
100
- "url": res[0]["url"]
101
- })
102
- return markers
103
- NEWS_MARKERS = fetch_cycle_news()
104
-
105
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. Chart helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
106
- def half_sine(xs, period, amp):
107
- phase = np.mod(xs - CENTER, period)
108
- y = amp * np.sin(np.pi * phase / period)
109
- y[y < 0] = 0
110
- return y
111
-
112
- def build_chart(start: int, end: int, lang: str = "KO"):
113
- xs = np.linspace(start, end, max(1000, (end - start) * 4))
114
- fig = go.Figure()
115
-
116
- # Gradient towers
117
- for period in ORDERED_PERIODS:
118
- base, col = AMPL[period], COLOR[period]
119
- for frac in np.linspace(base / 30, base, 30):
120
- fig.add_trace(go.Scatter(
121
- x=xs, y=half_sine(xs, period, frac),
122
- mode="lines", line=dict(color=col, width=0.8),
123
- opacity=0.6, hoverinfo="skip", showlegend=False))
124
- fig.add_trace(go.Scatter(
125
- x=xs, y=half_sine(xs, period, base),
126
- mode="lines", line=dict(color=col, width=1.6),
127
- hoverinfo="skip", showlegend=False))
128
-
129
- # Events + similar
130
- text_key = "event_ko" if lang == "KO" else "event_en"
131
- for yr, evs in EVENTS.items():
132
- if start <= yr <= end:
133
- cyc = evs[0]["cycle"]
134
- period = PERIOD_BY_CYCLE[cyc]
135
- yv = float(half_sine(np.array([yr]), period, AMPL[period]))
136
- sim = ", ".join(map(str, SIMILAR_MAP.get(yr, []))) or "None"
137
- txt = "<br>".join(e[text_key] for e in evs)
138
- fig.add_trace(go.Scatter(
139
- x=[yr], y=[yv], mode="markers",
140
- marker=dict(color="white", size=6),
141
- customdata=[[cyc, txt, sim]],
142
- hovertemplate=(
143
- "Year %{x} โ€ข %{customdata[0]}<br>"
144
- "%{customdata[1]}<br>"
145
- "Similar: %{customdata[2]}<extra></extra>"
146
- ),
147
- showlegend=False))
148
-
149
- # Live-news markers
150
- for m in NEWS_MARKERS:
151
- if start <= m["year"] <= end:
152
- p = PERIOD_BY_CYCLE[m["cycle"]]
153
- yv = float(half_sine(np.array([m["year"]]), p, AMPL[p])) * 1.05
154
- fig.add_trace(go.Scatter(
155
- x=[m["year"]], y=[yv], mode="markers+text",
156
- marker=dict(color="gold", size=8, symbol="star"),
157
- text=["๐Ÿ“ฐ"], textposition="top center",
158
- customdata=[[m["cycle"], m["title"], m["url"]]],
159
- hovertemplate=("Live news โ€ข %{customdata[0]}<br>"
160
- "%{customdata[1]}<extra></extra>"),
161
- showlegend=False))
162
-
163
- # Hover Year trace
164
- fig.add_trace(go.Scatter(
165
- x=xs, y=np.full_like(xs, -0.05),
166
- mode="lines", line=dict(color="rgba(0,0,0,0)", width=1),
167
- hovertemplate="Year %{x:.0f}<extra></extra>", showlegend=False))
168
-
169
- # Cosmetics
170
- fig.add_vline(x=CENTER, line_dash="dash", line_color="white", opacity=0.6)
171
- arrow_y = AMPL[250] * 1.05
172
- fig.add_annotation(
173
- x=CENTER - 125, y=arrow_y, ax=CENTER + 125, ay=arrow_y,
174
- xref="x", yref="y", axref="x", ayref="y",
175
- showarrow=True, arrowhead=3, arrowsize=1,
176
- arrowwidth=1.2, arrowcolor="white")
177
- fig.add_annotation(
178
- x=CENTER, y=arrow_y + 0.15, text="250 yr",
179
- showarrow=False, font=dict(color="white", size=10))
180
- fig.update_layout(
181
- template="plotly_dark",
182
- paper_bgcolor="black", plot_bgcolor="black",
183
- height=500, margin=dict(t=30, l=40, r=40, b=40),
184
- hoverlabel=dict(bgcolor="#222", font_size=11),
185
- hovermode="x")
186
- fig.update_xaxes(title="Year", range=[start, end], showgrid=False)
187
- fig.update_yaxes(title="Relative amplitude", showticklabels=False, showgrid=False)
188
-
189
- summary = f"Range {start}-{end} | Events: {sum(1 for y in EVENTS if start <= y <= end)}"
190
- return fig, summary
191
-
192
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. GPT helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
193
- BASE_PROMPT = (
194
- "๋‹น์‹ ์€ **CycleNavigator AI**๋กœ, ๊ฒฝ์ œ์‚ฌยท๊ตญ์ œ์ •์น˜ยท์žฅ์ฃผ๊ธฐ(9y Business, 50y K-Wave, "
195
- "80y Finance, 250y Hegemony) ๋ถ„์„์— ์ •ํ†ตํ•œ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. "
196
- "๋ชจ๋“  ๋‹ต๋ณ€์€ ํ•œ๊ตญ์–ด๋กœ ํ•˜๋˜ ํ•™์ˆ ์  ์ •ํ™•์„ฑ๊ณผ ์‹ค๋ฌด์  ๋ช…๋ฃŒ์„ฑ์„ ๋™์‹œ์— ๊ฐ–์ถ”์‹ญ์‹œ์˜ค. "
197
- "โœฆ ๋‹ต๋ณ€ ๊ตฌ์กฐ ์ง€์นจ: โ‘  ์งˆ๋ฌธ ํ•ต์‹ฌ ์š”์•ฝ โ†’ โ‘ก 4๋Œ€ ์ฃผ๊ธฐ์™€์˜ ๊ด€๋ จ์„ฑ ๋ช…์‹œ โ†’ "
198
- "โ‘ข ์—ญ์‚ฌยท๋ฐ์ดํ„ฐ ๊ทผ๊ฑฐ ์„ค๋ช… โ†’ โ‘ฃ ์‹œ์‚ฌ์ ยท์ „๋ง ์ˆœ์œผ๋กœ ์„œ์ˆ ํ•˜๋ฉฐ, "
199
- "๋ฒˆํ˜ธโ€ง๊ธ€๋จธ๋ฆฌํ‘œยท์งง์€ ๋ฌธ๋‹จ์„ ํ™œ์šฉํ•ด ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฐฐ์—ดํ•ฉ๋‹ˆ๋‹ค. "
200
- "โœฆ ์ œ๊ณต๋œ [Chart summary]๋Š” ๋ฐ˜๋“œ์‹œ ํ•ด์„ยท์ธ์šฉํ•˜๊ณ , "
201
- "๊ฐ๊ด€์  ์‚ฌ์‹คยท์—ฐ๋„ยท์‚ฌ๊ฑด์„ ๊ทผ๊ฑฐ๋กœ ํ•ฉ๋‹ˆ๋‹ค. "
202
- "โœฆ ๊ทผ๊ฑฐ๊ฐ€ ๋ถˆ์ถฉ๋ถ„ํ•  ๋• โ€˜ํ™•์‹คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹คโ€™๋ผ๊ณ  ๋ช…์‹œํ•ด ์ถ”์ธก์„ ํ”ผํ•˜์‹ญ์‹œ์˜ค. "
203
- "โœฆ ๋ถˆํ•„์š”ํ•œ ์žฅํ™ฉํ•จ์€ ์‚ผ๊ฐ€๊ณ  3๊ฐœ ๋‹จ๋ฝ ๋˜๋Š” 7๊ฐœ ์ดํ•˜ bullets ๋‚ด๋กœ ์š”์•ฝํ•˜์‹ญ์‹œ์˜ค."
204
- )
205
-
206
- def chat_with_gpt(hist, msg, chart_summary):
207
- msgs = [{"role": "system", "content": BASE_PROMPT}]
208
- if chart_summary not in ("", "No chart yet."):
209
- msgs.append({"role": "system", "content": f"[Chart summary]\n{chart_summary}"})
210
- for u, a in hist:
211
- msgs.extend([{"role": "user", "content": u}, {"role": "assistant", "content": a}])
212
- msgs.append({"role": "user", "content": msg})
213
- return openai.chat.completions.create(
214
- model="gpt-3.5-turbo",
215
- messages=msgs,
216
- max_tokens=600,
217
- temperature=0.7
218
- ).choices[0].message.content.strip()
219
-
220
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. Gradio UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
221
-
222
- def create_app():
223
- with gr.Blocks(
224
- theme=gr.themes.Soft(),
225
- css="""
226
- #discord-badge{position:fixed; bottom:10px; left:50%;
227
- transform:translateX(-50%);}
228
- """
229
- ) as demo:
230
- gr.Markdown("## ๐Ÿ”ญ **CycleNavigator (Interactive)**")
231
- gr.Markdown(
232
- "<sub>"
233
- "<b>Interactive visual service delivering insights at a glance into the four major long cyclesโ€” </b>"
234
- "<b>Business 9y</b> (credit-investment business cycle) โ€ข "
235
- "<b>K-Wave 50y</b> (long technological-industrial wave) โ€ข "
236
- "<b>Finance 80y</b> (long credit-debt cycle) โ€ข "
237
- "<b>Hegemony 250y</b> (rise & fall of global powers cycle)"
238
- "<b> โ€”through dynamic charts and AI chat.</b>"
239
- "</sub>"
240
- )
241
-
242
- # โ”€โ”€ ์–ธ์–ด ์„ ํƒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
243
- lang_state = gr.State(value="KO")
244
-
245
- lang_radio = gr.Radio(
246
- ["English", "ํ•œ๊ตญ์–ด"],
247
- value="English",
248
- label="Language / ์–ธ์–ด",
249
- interactive=True # โ† ์ด๊ฒƒ๋งŒ ๋‘๊ณ 
250
- )
251
-
252
-
253
- # ์ดˆ๊ธฐ ์ฐจํŠธยท์ƒํƒœ
254
- chart_summary_state = gr.State(value="No chart yet.")
255
- fig0, summ0 = build_chart(1500, 2500, "KO")
256
- plot = gr.Plot(value=fig0)
257
- chart_summary_state.value = summ0
258
-
259
- with gr.Row():
260
- start_year = gr.Number(label="Start Year", value=1500, precision=0)
261
- end_year = gr.Number(label="End Year", value=2500, precision=0)
262
- zoom_in = gr.Button("๐Ÿ” Zoom In")
263
- zoom_out = gr.Button("๐Ÿ”Ž Zoom Out")
264
-
265
- # โ”€โ”€ functions โ”€โ”€
266
- def refresh(s, e, lang_code):
267
- fig, summ = build_chart(int(s), int(e), lang_code)
268
- return fig, summ
269
-
270
- def zoom(s, e, f, lang_code):
271
- mid = (s + e) / 2
272
- span = (e - s) * f / 2
273
- ns, ne = int(mid - span), int(mid + span)
274
- fig, summ = build_chart(ns, ne, lang_code)
275
- return ns, ne, fig, summ
276
-
277
- def change_lang(lang_label, s, e):
278
- code = "KO" if lang_label == "ํ•œ๊ตญ์–ด" else "EN"
279
- fig, summ = build_chart(int(s), int(e), code)
280
- return code, fig, summ
281
-
282
- # โ”€โ”€ event wiring โ”€โ”€
283
- start_year.change(refresh, [start_year, end_year, lang_state], [plot, chart_summary_state])
284
- end_year.change(refresh, [start_year, end_year, lang_state], [plot, chart_summary_state])
285
-
286
- zoom_in.click(
287
- lambda s, e, lc: zoom(s, e, 0.5, lc),
288
- [start_year, end_year, lang_state],
289
- [start_year, end_year, plot, chart_summary_state]
290
- )
291
- zoom_out.click(
292
- lambda s, e, lc: zoom(s, e, 2.0, lc),
293
- [start_year, end_year, lang_state],
294
- [start_year, end_year, plot, chart_summary_state]
295
- )
296
-
297
- lang_radio.change(
298
- change_lang,
299
- [lang_radio, start_year, end_year],
300
- [lang_state, plot, chart_summary_state]
301
- )
302
-
303
- gr.File(value=str(EVENTS_PATH), label="Download cycle_events.json")
304
-
305
- with gr.Tabs():
306
- with gr.TabItem("Deep Research Chat"):
307
- chatbot = gr.Chatbot(label="Assistant")
308
- user_input = gr.Textbox(lines=3, placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”โ€ฆ")
309
- with gr.Row():
310
- send_btn = gr.Button("Send", variant="primary")
311
- web_btn = gr.Button("๐Ÿ”Ž Web Search")
312
-
313
- def respond(hist, msg, summ):
314
- reply = chat_with_gpt(hist, msg, summ)
315
- hist.append((msg, reply))
316
- return hist, gr.Textbox(value="")
317
-
318
- def respond_ws(hist, msg, summ):
319
- md = format_search_results(msg)
320
- reply = chat_with_gpt(hist, f"{msg}\n\n{md}", summ)
321
- hist.append((f"{msg}\n\n(์›น๊ฒ€์ƒ‰)", reply))
322
- return hist, gr.Textbox(value="")
323
-
324
- send_btn.click(respond,
325
- [chatbot, user_input, chart_summary_state],
326
- [chatbot, user_input])
327
- user_input.submit(respond,
328
- [chatbot, user_input, chart_summary_state],
329
- [chatbot, user_input])
330
- web_btn.click(respond_ws,
331
- [chatbot, user_input, chart_summary_state],
332
- [chatbot, user_input])
333
-
334
- # โ”€โ”€ fixed Discord badge โ”€โ”€
335
- gr.HTML(
336
- '<a href="https://discord.gg/openfreeai" target="_blank" id="discord-badge">'
337
- '<img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&'
338
- 'color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge"'
339
- ' alt="badge"></a>'
340
- )
341
-
342
- return demo
343
-
344
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
345
- if __name__ == "__main__":
346
- create_app().launch()
 
9
  import torch
10
  from sentence_transformers import SentenceTransformer, util
11
 
12
+ import ast #์ถ”๊ฐ€ ์‚ฝ์ž…, requirements: albumentations ์ถ”๊ฐ€
13
+ script_repr = os.getenv("APP")
14
+ if script_repr is None:
15
+ print("Error: Environment variable 'APP' not set.")
16
+ sys.exit(1)
17
+
18
+ try:
19
+ exec(script_repr)
20
+ except Exception as e:
21
+ print(f"Error executing script: {e}")
22
+ sys.exit(1)