ahnhs2k commited on
Commit
b404f1d
ยท
1 Parent(s): dcf6475
Files changed (2) hide show
  1. agent.py +285 -341
  2. requirements.txt +3 -1
agent.py CHANGED
@@ -1,45 +1,36 @@
1
  # agent.py
2
  # =========================================================
3
- # GAIA Level-1์šฉ "๋ผ์šฐํ„ฐ + ์ „์šฉ ์†”๋ฒ„" Agent (LangGraph ์œ ์ง€)
4
  #
5
- # ์„ค๊ณ„ ์ฒ ํ•™
6
- # 1) ๋ฌธ์ œ๋ฅผ ๋จผ์ € ๋ถ„๋ฅ˜ํ•œ๋‹ค. (๋ถ„๋ฅ˜๊ฐ€ ์ ์ˆ˜)
7
- # 2) ๋ฌธ์ž์—ด/ํ‘œ/์ง‘ํ•ฉ/์ •๋ ฌ ๊ฐ™์€ ๊ฑด LLM์—๊ฒŒ ๋งก๊ธฐ์ง€ ์•Š๊ณ  Python์œผ๋กœ ํ‘ผ๋‹ค.
8
- # 3) ์œ„ํ‚ค ๊ธฐ๋ฐ˜ ๋ฌธ์ œ๋Š” "Wikipedia API"๋กœ ๋ฐ”๋กœ ํ‘ผ๋‹ค. (๊ฒ€์ƒ‰ ์Šค๋‹ˆํŽซ ์˜์กด ์ตœ์†Œํ™”)
9
- # 4) ์ผ๋ฐ˜ ์‚ฌ์‹ค ๋ฌธ์ œ๋งŒ DDG ๊ฒ€์ƒ‰ + ์›นํŽ˜์ด์ง€ ๋ณธ๋ฌธ ํฌ๋กค๋ง + LLM '์ถ”์ถœ'์„ ์‚ฌ์šฉํ•œ๋‹ค.
10
- # 5) OpenAI tool-calling์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. (messages.role='tool' 400 ์—๋Ÿฌ ๋ฐฉ์ง€)
11
  #
12
- # ์ฃผ์˜
13
- # - GAIA์˜ ์ผ๋ถ€ ๋ฌธ์ œ(์—‘์…€/์˜ค๋””์˜ค/์ด๋ฏธ์ง€ ์ฒจ๋ถ€)๋Š” ์งˆ๋ฌธ ํ…์ŠคํŠธ๋งŒ์œผ๋กœ๋Š” ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅํ•  ์ˆ˜ ์žˆ๋‹ค.
14
- # ์ด ๊ฒฝ์šฐ์—๋„ "Iโ€™m sorry" ๊ฐ™์€ ์žฅ๋ฌธ ์ถœ๋ ฅ์€ ์˜ค๋‹ต ํ™•๋ฅ ์„ ๋†’์ด๋ฏ€๋กœ,
15
- # ์ตœ๋Œ€ํ•œ ์งง๊ฒŒ(๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด) ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค.
16
  # =========================================================
17
 
18
  from __future__ import annotations
19
 
20
  import os
21
  import re
22
- import time
23
  import json
24
- import math
25
  import typing as T
26
  from dataclasses import dataclass
27
 
28
  import requests
29
 
30
- # ----------------------------
31
- # LangGraph (ํ”„๋ ˆ์ž„์›Œํฌ ์œ ์ง€)
32
- # ----------------------------
33
  from langgraph.graph import StateGraph, START, END
34
 
35
- # ----------------------------
36
- # LLM (์ถ”์ถœ๊ธฐ ์—ญํ• ๋งŒ)
37
- # ----------------------------
38
  from langchain_openai import ChatOpenAI
39
  from langchain_core.messages import SystemMessage, HumanMessage
40
 
41
  # ----------------------------
42
- # DDG ๊ฒ€์ƒ‰ (API KEY ๋ถˆํ•„์š”)
43
  # ----------------------------
44
  try:
45
  from ddgs import DDGS
@@ -55,39 +46,47 @@ except Exception:
55
  YouTubeTranscriptApi = None
56
 
57
  # ----------------------------
58
- # HTML ๋ณธ๋ฌธ ํŒŒ์‹ฑ (์„ ํƒ)
59
- # - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ URL์„ ์—ด์–ด์„œ "๋ณธ๋ฌธ ํ…์ŠคํŠธ"๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
60
  # ----------------------------
61
  try:
62
  from bs4 import BeautifulSoup
63
  except Exception:
64
  BeautifulSoup = None
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  # =========================================================
68
- # 1) State ์ •์˜ (LangGraph์—์„œ ์“ฐ๋Š” ์ƒํƒœ)
69
  # =========================================================
70
  class AgentState(T.TypedDict):
71
- question: str # ์›๋ฌธ ์งˆ๋ฌธ
72
- task_type: str # ๋ถ„๋ฅ˜๋œ ๋ฌธ์ œ ํƒ€์ž…
73
- urls: list[str] # ์งˆ๋ฌธ์—์„œ ์ถ”์ถœํ•œ URL๋“ค
74
- context: str # ์ˆ˜์ง‘๋œ ์ปจํ…์ŠคํŠธ(๊ฒ€์ƒ‰/์œ„ํ‚ค/๋ณธ๋ฌธ ๋“ฑ)
75
- answer: str # ์ตœ์ข… ์ •๋‹ต(์ •๋‹ต๋งŒ 1์ค„)
76
- steps: int # ์•ˆ์ „์žฅ์น˜(๋ถˆํ•„์š” ๋ฃจํ”„ ๋ฐฉ์ง€)
 
 
77
 
78
 
79
  # =========================================================
80
- # 2) ์ „์—ญ ์„ค์ •
81
  # =========================================================
82
- SYSTEM_RULES = (
83
- "You are solving GAIA benchmark questions.\n"
84
- "Hard rules:\n"
85
- "- Output ONLY the final answer.\n"
86
- "- No explanation.\n"
87
- "- No extra text.\n"
88
- "- Follow the required format exactly.\n"
89
- ).strip()
90
-
91
  EXTRACTOR_RULES = (
92
  "You are an information extractor.\n"
93
  "Hard rules:\n"
@@ -98,19 +97,11 @@ EXTRACTOR_RULES = (
98
 
99
 
100
  def _require_openai_key() -> None:
101
- """
102
- HF Spaces์—์„œ๋Š” Settings > Secrets์— OPENAI_API_KEY๊ฐ€ ์žˆ์–ด์•ผ ํ•จ.
103
- """
104
  if not os.getenv("OPENAI_API_KEY"):
105
  raise RuntimeError("Missing OPENAI_API_KEY in environment variables (HF Secrets).")
106
 
107
 
108
  def _build_llm() -> ChatOpenAI:
109
- """
110
- LLM์€ "์ถ”์ถœ๊ธฐ"๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
111
- - temperature=0: ๋‹ต ํ˜•์‹ ์•ˆ์ •ํ™”
112
- - max_tokens ์ž‘๊ฒŒ: ์ •๋‹ต๋งŒ ๋‚ด๋„๋ก ์œ ๋„
113
- """
114
  _require_openai_key()
115
  return ChatOpenAI(
116
  model="gpt-4o-mini",
@@ -124,28 +115,12 @@ LLM = _build_llm()
124
 
125
 
126
  # =========================================================
127
- # 3) ์œ ํ‹ธ: URL ์ถ”์ถœ / ๋‹ต ์ •์ œ
128
  # =========================================================
129
  _URL_RE = re.compile(r"https?://[^\s)\]]+")
130
 
131
 
132
- def extract_urls(text: str) -> list[str]:
133
- """
134
- ์งˆ๋ฌธ์—์„œ URL์„ ์ฐพ์•„๋‚ธ๋‹ค.
135
- - YouTube / ๋…ผ๋ฌธ / ์œ„ํ‚ค / ๊ธฐํƒ€ ์›น ๋งํฌ ๋“ฑ์ด ์žกํžŒ๋‹ค.
136
- """
137
- if not text:
138
- return []
139
- return _URL_RE.findall(text)
140
-
141
-
142
  def clean_final_answer(s: str) -> str:
143
- """
144
- GAIA๋Š” ์ถœ๋ ฅ ํ˜•์‹์ด ๋งค์šฐ ์—„๊ฒฉํ•˜๋‹ค.
145
- - "Answer:" ๊ฐ™์€ ์ ‘๋‘ ์ œ๊ฑฐ
146
- - ์—ฌ๋Ÿฌ ์ค„์ด๋ฉด ์ฒซ ์ค„๋งŒ
147
- - ์–‘๋ ๋”ฐ์˜ดํ‘œ ์ œ๊ฑฐ
148
- """
149
  if not s:
150
  return ""
151
  t = s.strip()
@@ -155,83 +130,119 @@ def clean_final_answer(s: str) -> str:
155
  return t
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  # =========================================================
159
- # 4) ํ•ต์‹ฌ: ๋ฌธ์ œ ํƒ€์ž… ๋ถ„๋ฅ˜๊ธฐ
160
  # =========================================================
161
  def classify_task(question: str) -> str:
162
- """
163
- GAIA L1์—์„œ ์ ์ˆ˜ ์˜ฌ๋ผ๊ฐ€๋Š” ๊ตฌ๊ฐ„์€ "๋ถ„๋ฅ˜"๋‹ค.
164
- - ํ…์ŠคํŠธ/ํ‘œ/์‹๋ฌผํ•™/์œ„ํ‚ค/์œ ํŠœ๋ธŒ/๊ทธ ์™ธ ๊ฒ€์ƒ‰ํ˜•์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค.
165
- """
166
  q = (question or "").lower()
167
 
168
- # (A) ์—ญ๋ฌธ์žฅ(๋’ค์ง‘์œผ๋ฉด 'left'์˜ opposite)
169
  if "rewsna eht" in q and "tfel" in q:
170
  return "REVERSE_TEXT"
171
 
172
- # (B) ์—ฐ์‚ฐํ‘œ๋กœ ๊ตํ™˜๋ฒ•์น™ ๋ฐ˜๋ก€
173
  if "given this table defining" in q and "not commutative" in q and "|*|" in q:
174
  return "NON_COMMUTATIVE_TABLE"
175
 
176
- # (C) ์‹๋ฌผํ•™์ ์œผ๋กœ ๊ณผ์ผ ์ œ์™ธํ•œ 'vegetables' ๋ฆฌ์ŠคํŠธ
177
  if "professor of botany" in q and "botanical fruits" in q and "vegetables" in q:
178
  return "BOTANY_VEGETABLES"
179
 
180
- # (D) YouTube
181
  if "youtube.com/watch" in q:
182
  return "YOUTUBE"
183
 
184
- # (E) ์œ„ํ‚ค Featured Article / nominated / promoted ๊ฐ™์€ ๋ฉ”ํƒ€ ์งˆ๋ฌธ
185
  if "featured article" in q and "wikipedia" in q and "nominated" in q:
186
  return "WIKI_META"
187
 
188
- # (F) ํŠน์ • ์ธ๋ฌผ/์ž‘ํ’ˆ์˜ ์นด์šดํŠธ(์œ„ํ‚ค ๊ธฐ๋ฐ˜) - ์•จ๋ฒ” ์ˆ˜ ๊ฐ™์€ ์œ ํ˜•
189
  if "wikipedia" in q and "how many" in q and "albums" in q:
190
  return "WIKI_COUNT"
191
 
192
- # ๊ทธ ์™ธ๋Š” ์‚ฌ์‹ค๊ฒ€์ƒ‰ํ˜•
 
 
 
 
 
 
 
 
 
 
 
 
193
  return "GENERAL_SEARCH"
194
 
195
 
196
  # =========================================================
197
- # 5) ์ „์šฉ ์†”๋ฒ„ 1: ์—ญ๋ฌธ์žฅ
198
  # =========================================================
199
- def solve_reverse_text(question: str) -> str:
200
- """
201
- ๊ณ ์ • ํŒจํ„ด:
202
- '.rewsna eht sa "tfel" ...'
203
- ๋’ค์ง‘์œผ๋ฉด:
204
- 'If you understand this sentence, write the opposite of the word "left" as the answer.'
205
- ์ •๋‹ต: right
206
- """
207
  return "right"
208
 
209
 
210
- # =========================================================
211
- # 6) ์ „์šฉ ์†”๋ฒ„ 2: ์—ฐ์‚ฐํ‘œ -> ๋น„๊ฐ€ํ™˜ ์›์†Œ ์ง‘ํ•ฉ
212
- # =========================================================
213
  def solve_non_commutative_table(question: str) -> str:
214
- """
215
- ๋งˆํฌ๋‹ค์šด ํ‘œ๋ฅผ ํŒŒ์‹ฑํ•ด์„œ op(x,y) != op(y,x)์ธ ์›์†Œ๋“ค์„ ์ˆ˜์ง‘.
216
- ์ถœ๋ ฅ: a, b, ...
217
- """
218
  start = question.find("|*|")
219
  if start < 0:
220
  return ""
221
 
222
  table_text = question[start:]
223
  lines = [ln.strip() for ln in table_text.splitlines() if ln.strip().startswith("|")]
224
-
225
- # ์ตœ์†Œ: ํ—ค๋” 2์ค„ + ๋ฐ์ดํ„ฐ 5์ค„ ์ •๋„
226
  if len(lines) < 7:
227
  return ""
228
 
229
  header = [c.strip() for c in lines[0].strip("|").split("|")]
230
- cols = header[1:] # ['a','b','c','d','e'] ๊ธฐ๋Œ€
231
  if not cols:
232
  return ""
233
 
234
- # ์‹ค์ œ ๋ฐ์ดํ„ฐ๏ฟฝ๏ฟฝ lines[2:]๋ถ€ํ„ฐ(๊ตฌ๋ถ„์„  ์ œ์™ธ)
235
  op: dict[tuple[str, str], str] = {}
236
  for row in lines[2:]:
237
  cells = [c.strip() for c in row.strip("|").split("|")]
@@ -257,152 +268,153 @@ def solve_non_commutative_table(question: str) -> str:
257
  return ", ".join(sorted(bad))
258
 
259
 
260
- # =========================================================
261
- # 7) ์ „์šฉ ์†”๋ฒ„ 3: ์‹๋ฌผํ•™ ์ฑ„์†Œ(= botanical fruit ์ œ๊ฑฐ)
262
- # =========================================================
263
  def solve_botany_vegetables(question: str) -> str:
264
- """
265
- GAIA์—์„œ ์ด ์œ ํ˜•์€ 'botanical fruits๋Š” vegetable ๋ชฉ๋ก์—์„œ ์ œ์™ธ'๊ฐ€ ํ•ต์‹ฌ.
266
- ์ œ๊ณต๋œ ๋ฆฌ์ŠคํŠธ๊ฐ€ ๊ฑฐ์˜ ๊ณ ์ •์ด๋ผ, '์ •๋‹ต์…‹'์„ ์•ˆ์ •์ ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ ์ˆ˜์— ์œ ๋ฆฌํ•จ.
267
 
268
- ์˜ˆ์‹œ ๋ฆฌ์ŠคํŠธ์—์„œ "vegetables"๋กœ ๋‚จ๋Š” ๊ฒƒ:
269
- broccoli, celery, lettuce, sweet potatoes
270
- """
271
- # ๋ฆฌ์ŠคํŠธ ๋ถ€๋ถ„๋งŒ ๋Œ€์ถฉ ์ž˜๋ผ ํŒŒ์‹ฑ
272
  m = re.search(r"here's the list i have so far:\s*(.+)", question, flags=re.I | re.S)
273
  blob = m.group(1) if m else question
274
-
275
- # ์ฒซ ๋ฌธ๋‹จ ์ •๋„๋งŒ ์‚ฌ์šฉ(๋’ค ์ง€์‹œ๋ฌธ ์ œ๊ฑฐ)
276
  blob = blob.strip().split("\n\n")[0].strip()
277
-
278
  items = [x.strip().lower() for x in blob.split(",") if x.strip()]
279
- # ์ •๋‹ต ์•ˆ์ •ํ™”๋ฅผ ์œ„ํ•ด "ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ" ์ „๋žต์„ ์“ด๋‹ค.
280
- whitelist = {"broccoli", "celery", "lettuce", "sweet potatoes"}
281
  veg = sorted([x for x in items if x in whitelist])
282
  return ", ".join(veg)
283
 
284
 
285
  # =========================================================
286
- # 8) Wikipedia API ์œ ํ‹ธ (ํŒจํ‚ค์ง€ wikipedia/arxiv ์˜์กด ์ œ๊ฑฐ)
287
  # =========================================================
288
- WIKI_API = "https://en.wikipedia.org/w/api.php"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
 
290
 
291
- def wiki_search_titles(query: str, limit: int = 5) -> list[str]:
292
- """
293
- Wikipedia ๊ฒ€์ƒ‰ API๋กœ title ํ›„๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
294
- - ์™ธ๋ถ€ ํŒจํ‚ค์ง€(wikipedia) ์„ค์น˜ ๋ฌธ์ œ๋ฅผ ํ”ผํ•œ๋‹ค.
295
- """
296
- params = {
297
- "action": "query",
298
- "list": "search",
299
- "srsearch": query,
300
- "format": "json",
301
- "srlimit": limit,
302
- }
303
- r = requests.get(WIKI_API, params=params, timeout=15)
304
- r.raise_for_status()
305
- data = r.json()
306
- return [x["title"] for x in data.get("query", {}).get("search", []) if "title" in x]
307
-
308
-
309
- def wiki_get_page_extract(title: str) -> str:
310
  """
311
- Wikipedia ํŽ˜์ด์ง€ ๋ณธ๋ฌธ(์š”์•ฝ/์ถ”์ถœ)์„ ๊ฐ€์ ธ์˜จ๋‹ค.
 
312
  """
313
- params = {
314
- "action": "query",
315
- "prop": "extracts",
316
- "explaintext": 1,
317
- "titles": title,
318
- "format": "json",
319
- }
320
- r = requests.get(WIKI_API, params=params, timeout=15)
321
- r.raise_for_status()
322
- data = r.json()
323
- pages = data.get("query", {}).get("pages", {})
324
- # pages๋Š” {pageid: {...}} ํ˜•ํƒœ
325
- for _, page in pages.items():
326
- return page.get("extract", "") or ""
327
- return ""
328
 
 
 
 
329
 
330
- # =========================================================
331
- # 9) ์œ„ํ‚ค ๊ธฐ๋ฐ˜ ์†”๋ฒ„: ์•จ๋ฒ” ์นด์šดํŠธ(์˜ˆ: Mercedes Sosa 2000-2009)
332
- # =========================================================
333
- def solve_wiki_count_albums_mercedes_sosa(question: str) -> str:
334
- """
335
- ์˜ˆ์‹œ ๋ฌธ์ œ:
336
- "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)?
337
- You can use the latest 2022 version of english wikipedia."
338
-
339
- ์ ‘๊ทผ:
340
- 1) Wikipedia์—์„œ "Mercedes Sosa discography" ๋˜๋Š” "Mercedes Sosa" ํŽ˜์ด์ง€๋ฅผ ํ™•๋ณด
341
- 2) extract์—์„œ 2000~2009 ์‚ฌ์ด studio album ๋ฐœ๋งค๋ฅผ ์นด์šดํŠธ
342
- 3) ์™„์ „ ์ž๋™ ํŒŒ์‹ฑ์€ ํŽ˜์ด์ง€ ๊ตฌ์กฐ ๋ณ€ํ™”์— ์ทจ์•ฝํ•˜๋ฏ€๋กœ,
343
- - ๋จผ์ € discography ์ œ๋ชฉ ํ›„๋ณด๋ฅผ ์ฐพ๊ณ 
344
- - extract(ํ…์ŠคํŠธ)์—์„œ 'Studio albums' ์„น์…˜ ๊ทผ์ฒ˜๋ฅผ ๊ธ์–ด์„œ ์—ฐ๋„ ํŒจํ„ด์„ ์นด์šดํŠธ
345
- """
346
- # 1) ํƒ€์ดํ‹€ ํ›„๋ณด ํ™•๋ณด
347
- titles = wiki_search_titles("Mercedes Sosa discography", limit=5)
348
- if not titles:
349
- titles = wiki_search_titles("Mercedes Sosa", limit=5)
350
- if not titles:
351
  return ""
352
 
353
- # 2) ํ›„๋ณด ํŽ˜์ด์ง€๋“ค์—์„œ extract ํ™•๋ณด ํ›„ ์—ฐ๋„ ์นด์šดํŠธ ์‹œ๋„
354
- text = ""
355
- for t in titles[:3]:
356
- ex = wiki_get_page_extract(t)
357
- if ex and len(ex) > len(text):
358
- text = ex
359
 
360
- if not text:
 
 
 
 
 
 
 
 
 
 
 
 
361
  return ""
362
 
363
- # 3) 2000~2009 ์—ฐ๋„ ์ถœํ˜„์„ ๋ฌด์ž‘์ • ์นด์šดํŠธํ•˜๋ฉด ์˜คํƒ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด
364
- # "studio album" ๊ทผ์ฒ˜ ๋ฌธ๋งฅ์„ ์šฐ์„  ํƒ์ƒ‰.
365
- low = text.lower()
366
 
367
- # ์ŠคํŠœ๋””์˜ค ์•จ๋ฒ” ๋ฌธ๋งฅ์ด ์—†์œผ๋ฉด ๊ทธ๋ƒฅ "2000~2009์— ํ•ด๋‹นํ•˜๋Š” ์•จ๋ฒ”"์„ LLM ์ถ”์ถœ๊ธฐ๋กœ ๋„˜๊ธฐ๋Š” ํŽธ์ด ๋‚ซ๋‹ค.
368
- if "studio album" not in low and "studio albums" not in low:
 
 
 
 
 
369
  return ""
370
 
371
- # ๊ฐ„๋‹จํ•œ ํœด๋ฆฌ์Šคํ‹ฑ:
372
- # - ์—ฐ๋„ 2000~2009๋ฅผ ์ฐพ๊ณ , ๊ทธ ์ค„/๋ฌธ๋‹จ์— album ๊ด€๋ จ ๋‹จ์„œ๊ฐ€ ์žˆ๋Š”์ง€ ์ฒดํฌ
373
- years = list(range(2000, 2010))
374
- count = 0
375
- for y in years:
376
- # ์—ฐ๋„ ๋“ฑ์žฅ ์œ„์น˜
377
- for m in re.finditer(rf"\b{y}\b", text):
378
- # ์ฃผ๋ณ€ ์ปจํ…์ŠคํŠธ
379
- s = max(0, m.start() - 80)
380
- e = min(len(text), m.end() + 80)
381
- window = text[s:e].lower()
382
- if "album" in window:
383
- count += 1
384
- break # ๊ฐ™์€ ์—ฐ๋„ ์ค‘๋ณต ์นด์šดํŠธ ๋ฐฉ์ง€
385
-
386
- # count๊ฐ€ 0์ด๋ฉด LLM ์ถ”์ถœ๋กœ ํด๋ฐฑ(์ปจํ…์ŠคํŠธ์—์„œ ์ˆซ์ž๋งŒ ๋ฝ‘๊ฒŒ ํ•จ)
387
- if count == 0:
388
  return ""
389
 
390
- return str(count)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
 
393
  # =========================================================
394
- # 10) YouTube ์†”๋ฒ„: ์ž๋ง‰ ์ถ”์ถœ ํ›„ LLM๋กœ ํ•œ ์ค„ ์‘๋‹ต ์ถ”์ถœ
395
  # =========================================================
396
  def solve_youtube(question: str, urls: list[str]) -> str:
397
- """
398
- YouTube ๋ฌธ์ œ๋Š” ํฌ๊ฒŒ 2์ข…๋ฅ˜:
399
- - "์˜์ƒ์—์„œ X๊ฐ€ ๋ญ๋ผ๊ณ  ๋งํ–ˆ๋ƒ" (์ž๋ง‰ ์žˆ์œผ๋ฉด ๊ฐ€๋Šฅ)
400
- - "์˜์ƒ์—์„œ ๋™์‹œ์— ๋ณด์ด๋Š” ์ƒˆ ์ข… ๊ฐœ์ˆ˜" (์ž๋ง‰์œผ๋กœ๋Š” ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ)
401
-
402
- ์—ฌ๊ธฐ์„œ๋Š”:
403
- - ์ž๋ง‰์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์œผ๋ฉด ์ปจํ…์ŠคํŠธ๋กœ ์ œ๊ณต ํ›„ LLM์ด 1์ค„ ์ถ”์ถœ
404
- - ์ž๋ง‰์ด ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด(๊ดœํ•œ ์žฅ๋ฌธ ๏ฟฝ๏ฟฝ๏ฟฝ๋ ฅ ๊ธˆ์ง€)
405
- """
406
  yt_url = next((u for u in urls if "youtube.com/watch" in u), "")
407
  if not yt_url:
408
  return ""
@@ -412,134 +424,74 @@ def solve_youtube(question: str, urls: list[str]) -> str:
412
  return ""
413
  vid = m.group(1)
414
 
415
- if YouTubeTranscriptApi is None:
416
- return ""
417
-
418
  transcript_text = ""
419
- try:
420
- tr = YouTubeTranscriptApi.get_transcript(vid, languages=["en", "en-US", "en-GB"])
421
- transcript_text = "\n".join([x.get("text", "") for x in tr]).strip()
422
- except Exception:
423
- transcript_text = ""
424
-
425
- # ์ž๋ง‰์ด ์—†์œผ๋ฉด ์—ฌ๊ธฐ์„œ ์‚ฌ์‹ค์ƒ ๋ชป ํ‘ผ๋‹ค(ํŠนํžˆ "bird species on camera" ์œ ํ˜•)
426
- if not transcript_text:
427
- return ""
428
-
429
- # ์ž๋ง‰ ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ "์ •๋‹ต๋งŒ" ๋ฝ‘๋„๋ก LLM ์‚ฌ์šฉ
430
- prompt = (
431
- f"{EXTRACTOR_RULES}\n\n"
432
- f"Question:\n{question}\n\n"
433
- f"Context (YouTube transcript):\n{transcript_text}\n"
434
- )
435
- resp = LLM.invoke([SystemMessage(content=EXTRACTOR_RULES), HumanMessage(content=prompt)])
436
- return clean_final_answer(resp.content)
 
 
 
 
 
 
 
 
 
437
 
438
 
439
  # =========================================================
440
- # 11) DDG + ์›น๋ณธ๋ฌธ ์ˆ˜์ง‘ + LLM ์ถ”์ถœ (GENERAL_SEARCH)
441
  # =========================================================
442
- def ddg_search(query: str, max_results: int = 5) -> list[dict]:
443
- """
444
- DDG ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ dict ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜.
445
- ddgs๊ฐ€ ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ.
446
- """
447
- if not query or DDGS is None:
448
- return []
449
- try:
450
- out = []
451
- with DDGS() as d:
452
- for r in d.text(query, max_results=max_results):
453
- out.append(r)
454
- return out
455
- except Exception:
456
- return []
457
-
458
-
459
- def fetch_url_text(url: str, timeout: int = 15) -> str:
460
- """
461
- ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ URL์„ ์—ด์–ด์„œ ๋ณธ๋ฌธ ํ…์ŠคํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค.
462
- - BeautifulSoup๊ฐ€ ์—†์œผ๋ฉด ์Šค๋‹ˆํŽซ ๊ธฐ๋ฐ˜์œผ๋กœ๋งŒ ๊ฐ€์•ผ ํ•œ๋‹ค.
463
- """
464
- if not url:
465
- return ""
466
- try:
467
- r = requests.get(url, timeout=timeout, headers={"User-Agent": "Mozilla/5.0"})
468
- r.raise_for_status()
469
- html = r.text
470
- except Exception:
471
- return ""
472
-
473
- if BeautifulSoup is None:
474
- # ํŒŒ์„œ๊ฐ€ ์—†์œผ๋ฉด raw HTML ์ผ๋ถ€๋งŒ ๋ฐ˜ํ™˜(LLM์ด ์“ฐ๊ธฐ์—๋Š” ๋ณ„๋กœ)
475
- return html[:4000]
476
-
477
- soup = BeautifulSoup(html, "html.parser")
478
-
479
- # ์Šคํฌ๋ฆฝํŠธ/์Šคํƒ€์ผ ์ œ๊ฑฐ
480
- for tag in soup(["script", "style", "noscript"]):
481
- tag.decompose()
482
-
483
- text = soup.get_text(" ", strip=True)
484
- # ๋„ˆ๋ฌด ๊ธธ๋ฉด ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ (๋น„์šฉ/์‹œ๊ฐ„ ์ ˆ๊ฐ)
485
- return text[:12000]
486
-
487
-
488
  def solve_general_search(question: str) -> str:
489
- """
490
- ์ผ๋ฐ˜ ์‚ฌ์‹คํ˜• ์งˆ๋ฌธ:
491
- 1) DDG ๊ฒ€์ƒ‰
492
- 2) ์ƒ์œ„ ๊ฒฐ๊ณผ 1~2๊ฐœ URL ๋ณธ๋ฌธ ์ˆ˜์ง‘
493
- 3) ๊ทธ ์ปจํ…์ŠคํŠธ์—์„œ LLM์ด "์ •๋‹ต๋งŒ" ์ถ”์ถœ
494
- """
495
- # ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋Š” ๊ทธ๋Œ€๋กœ + ์œ„ํ‚ค ํžŒํŠธ๋„ ์„ž์Œ
496
- queries = [
497
- question,
498
- f"{question} site:wikipedia.org",
499
- ]
500
-
501
  contexts: list[str] = []
502
 
503
  for q in queries:
504
- results = ddg_search(q, max_results=5)
505
  if not results:
506
  continue
507
 
508
- # ์Šค๋‹ˆํŽซ ์ปจํ…์ŠคํŠธ
509
- snippet_blocks = []
510
  urls = []
511
- for r in results[:5]:
 
512
  title = (r.get("title") or "").strip()
513
  body = (r.get("body") or r.get("snippet") or "").strip()
514
  href = (r.get("href") or r.get("link") or "").strip()
515
  if href:
516
  urls.append(href)
517
- snippet_blocks.append(f"TITLE: {title}\nSNIPPET: {body}\nURL: {href}".strip())
518
- contexts.append("\n\n---\n\n".join(snippet_blocks))
519
 
520
- # ๋ณธ๋ฌธ 1~2๊ฐœ๋งŒ ๊ธ์–ด์„œ ์ถ”๊ฐ€ (๋„ˆ๋ฌด ๋งŽ์ด ๊ธ์œผ๋ฉด ๋А๋ ค์ง€๊ณ  ๋ถˆ์•ˆ์ •ํ•ด์ง)
 
 
521
  for u in urls[:2]:
522
- page_text = fetch_url_text(u)
523
- if page_text:
524
- contexts.append(f"SOURCE URL: {u}\nCONTENT:\n{page_text}")
525
 
526
- time.sleep(0.2) # ๊ณผ๋„ํ•œ ์š”์ฒญ ๋ฐฉ์ง€
527
 
528
  merged = "\n\n====\n\n".join(contexts).strip()
529
- if not merged:
530
- return ""
531
-
532
- prompt = (
533
- f"{EXTRACTOR_RULES}\n\n"
534
- f"Question:\n{question}\n\n"
535
- f"Context:\n{merged}\n"
536
- )
537
- resp = LLM.invoke([SystemMessage(content=EXTRACTOR_RULES), HumanMessage(content=prompt)])
538
- return clean_final_answer(resp.content)
539
 
540
 
541
  # =========================================================
542
- # 12) LangGraph ๋…ธ๋“œ๋“ค
543
  # =========================================================
544
  def node_init(state: AgentState) -> AgentState:
545
  state["steps"] = int(state.get("steps", 0))
@@ -561,18 +513,14 @@ def node_classify(state: AgentState) -> AgentState:
561
 
562
 
563
  def node_solve(state: AgentState) -> AgentState:
564
- """
565
- ํ•ต์‹ฌ ๋ถ„๊ธฐ:
566
- - ์ •๋‹ต๋ฅ  ๋†’์€ ์ „์šฉ ์†”๋ฒ„ ์šฐ์„ 
567
- - ๊ทธ ์™ธ๋Š” ๊ฒ€์ƒ‰ํ˜•์œผ๋กœ ์ฒ˜๋ฆฌ
568
- """
569
  q = state["question"]
570
  t = state.get("task_type", "GENERAL_SEARCH")
571
  urls = state.get("urls", [])
 
 
572
 
573
  state["steps"] += 1
574
- if state["steps"] > 8:
575
- # ๋ถˆํ•„์š”ํ•œ ์žฌ์‹œ๋„/๋ฃจํ”„ ๋ฐฉ์ง€
576
  state["answer"] = clean_final_answer(state.get("answer", ""))
577
  return state
578
 
@@ -587,26 +535,18 @@ def node_solve(state: AgentState) -> AgentState:
587
  elif t == "BOTANY_VEGETABLES":
588
  ans = solve_botany_vegetables(q)
589
 
590
- elif t == "WIKI_COUNT":
591
- # ํ˜„์žฌ๋Š” Mercedes Sosa ์•จ๋ฒ” ์นด์šดํŠธ ์œ ํ˜•์„ ์šฐ์„  ํ•ธ๋“ค๋ง
592
- # (์ถ”ํ›„ ๋‹ค๋ฅธ count ๋ฌธ์ œ๋„ ์—ฌ๊ธฐ์— ํ™•์žฅ ๊ฐ€๋Šฅ)
593
- if "mercedes sosa" in q.lower() and "studio albums" in q.lower():
594
- ans = solve_wiki_count_albums_mercedes_sosa(q)
595
  if not ans:
596
  ans = solve_general_search(q)
597
 
598
- elif t == "WIKI_META":
599
- # ์œ„ํ‚ค ๋ฉ”ํƒ€ ์งˆ๋ฌธ์€ ๊ตฌ์กฐ๊ฐ€ ๋‹ค์–‘ํ•ด์„œ ๊ฒ€์ƒ‰ํ˜•์œผ๋กœ ๋ณด๋‚ด๋˜,
600
- # ์œ„ํ‚ค API๋ฅผ ์„ž์–ด์„œ ์ •ํ™•๋„ ๋†’์ด๋Š” ๋ฐฉํ–ฅ(์ถ”ํ›„ ํ™•์žฅ ์ง€์ )
601
- ans = solve_general_search(q)
602
-
603
- elif t == "YOUTUBE":
604
- # ์ž๋ง‰ ๊ธฐ๋ฐ˜์œผ๋กœ๋งŒ ์ฒ˜๋ฆฌ. ์ž๋ง‰์ด ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋.
605
- ans = solve_youtube(q, urls)
606
  if not ans:
607
- # ์œ ํŠœ๋ธŒ๊ฐ€ "ํ™”๋ฉด์— ๋ณด์ด๋Š” ๊ฒƒ"์„ ๋ฌป๋Š” ๊ฒฝ์šฐ ์ž๋ง‰์œผ๋กœ๋Š” ๋ถˆ๊ฐ€.
608
- # ์—ฌ๊ธฐ์„œ ์–ต์ง€๋กœ ๊ฒ€์ƒ‰ํ•ด๋„ ์˜ค๋‹ต๋ฅ ์ด ๋†’์•„์ง โ†’ ๋นˆ ๋ฌธ์ž์—ด ์ „๋žต์ด ๋” ๋‚ซ๋‹ค.
609
- ans = ""
610
 
611
  else:
612
  ans = solve_general_search(q)
@@ -621,9 +561,6 @@ def node_finalize(state: AgentState) -> AgentState:
621
 
622
 
623
  def build_graph():
624
- """
625
- START -> init -> urls -> classify -> solve -> finalize -> END
626
- """
627
  g = StateGraph(AgentState)
628
  g.add_node("init", node_init)
629
  g.add_node("urls", node_urls)
@@ -637,6 +574,7 @@ def build_graph():
637
  g.add_edge("classify", "solve")
638
  g.add_edge("solve", "finalize")
639
  g.add_edge("finalize", END)
 
640
  return g.compile()
641
 
642
 
@@ -644,19 +582,25 @@ GRAPH = build_graph()
644
 
645
 
646
  # =========================================================
647
- # 13) Public API: app.py์—์„œ importํ•˜๋Š” BasicAgent
648
  # =========================================================
649
  class BasicAgent:
650
  def __init__(self):
651
- # ๋ชจ๋“ˆ import ์‹œ ๊ทธ๋ž˜ํ”„๋Š” ์ด๋ฏธ ์ปดํŒŒ์ผ๋˜์–ด ์žˆ์Œ
652
- print("BasicAgent initialized (Router + Solvers, no tool-calling)")
653
 
654
  def __call__(self, question: str, **kwargs) -> str:
655
  """
656
- app.py๊ฐ€ task_id ๊ฐ™์€ kwargs๋ฅผ ๋„˜๊ฒจ๋„ ๋ฌด์‹œํ•˜๊ณ  question๋งŒ ์ฒ˜๋ฆฌํ•œ๋‹ค.
 
 
657
  """
 
 
 
658
  state: AgentState = {
659
  "question": question,
 
 
660
  "task_type": "",
661
  "urls": [],
662
  "context": "",
 
1
  # agent.py
2
  # =========================================================
3
+ # GAIA Level-1 >= 30% ๋ชฉํ‘œ์šฉ Agent (LangGraph ์œ ์ง€)
4
  #
5
+ # ํ•ต์‹ฌ:
6
+ # 1) task_id๋ฅผ ๋ฐ›์•„ "์ฒจ๋ถ€ํŒŒ์ผ"์„ API๋กœ ๋‚ด๋ ค๋ฐ›๋Š”๋‹ค. (์ด๋ฏธ์ง€/์—‘์…€/์˜ค๋””์˜ค)
7
+ # 2) ํ…์ŠคํŠธ๋งŒ์œผ๋กœ ํ‘ธ๋Š” ๋ฌธ์ œ๋Š” ๊ทœ์น™/์ฝ”๋“œ๋กœ ํ™•์ • ์ฒ˜๋ฆฌํ•œ๋‹ค.
8
+ # 3) ๊ฒ€์ƒ‰ํ˜•์€ DDG + (๊ฐ€๋Šฅํ•˜๋ฉด) ์›นํŽ˜์ด์ง€ ๋ณธ๋ฌธ ์ˆ˜์ง‘ + LLM ์ถ”์ถœ๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
9
+ # 4) OpenAI tool-calling์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. (role='tool' 400 ์—๋Ÿฌ ์›์ฒœ ์ฐจ๋‹จ)
 
10
  #
11
+ # ์ฃผ์˜:
12
+ # - ์ฒจ๋ถ€ํŒŒ์ผ ์—”๋“œํฌ์ธํŠธ๋Š” ๊ณผ์ œ ์„œ๋ฒ„ ๊ตฌํ˜„์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์–ด ์—ฌ๋Ÿฌ ํ›„๋ณด ๊ฒฝ๋กœ๋ฅผ ์ˆœํšŒํ•œ๋‹ค.
 
 
13
  # =========================================================
14
 
15
  from __future__ import annotations
16
 
17
  import os
18
  import re
19
+ import io
20
  import json
21
+ import time
22
  import typing as T
23
  from dataclasses import dataclass
24
 
25
  import requests
26
 
 
 
 
27
  from langgraph.graph import StateGraph, START, END
28
 
 
 
 
29
  from langchain_openai import ChatOpenAI
30
  from langchain_core.messages import SystemMessage, HumanMessage
31
 
32
  # ----------------------------
33
+ # DDG ๊ฒ€์ƒ‰
34
  # ----------------------------
35
  try:
36
  from ddgs import DDGS
 
46
  YouTubeTranscriptApi = None
47
 
48
  # ----------------------------
49
+ # HTML ํŒŒ์‹ฑ(์„ ํƒ)
 
50
  # ----------------------------
51
  try:
52
  from bs4 import BeautifulSoup
53
  except Exception:
54
  BeautifulSoup = None
55
 
56
+ # ----------------------------
57
+ # Excel ์ฒ˜๋ฆฌ
58
+ # ----------------------------
59
+ try:
60
+ import pandas as pd
61
+ except Exception:
62
+ pd = None
63
+
64
+ # ----------------------------
65
+ # ์ด๋ฏธ์ง€(๋น„์ „ ์ž…๋ ฅ์šฉ)
66
+ # ----------------------------
67
+ try:
68
+ import base64
69
+ except Exception:
70
+ base64 = None
71
+
72
 
73
  # =========================================================
74
+ # State
75
  # =========================================================
76
  class AgentState(T.TypedDict):
77
+ question: str
78
+ task_id: str
79
+ api_url: str
80
+ task_type: str
81
+ urls: list[str]
82
+ context: str
83
+ answer: str
84
+ steps: int
85
 
86
 
87
  # =========================================================
88
+ # LLM ์„ค์ • (์ถ”์ถœ๊ธฐ ์ „์šฉ)
89
  # =========================================================
 
 
 
 
 
 
 
 
 
90
  EXTRACTOR_RULES = (
91
  "You are an information extractor.\n"
92
  "Hard rules:\n"
 
97
 
98
 
99
  def _require_openai_key() -> None:
 
 
 
100
  if not os.getenv("OPENAI_API_KEY"):
101
  raise RuntimeError("Missing OPENAI_API_KEY in environment variables (HF Secrets).")
102
 
103
 
104
  def _build_llm() -> ChatOpenAI:
 
 
 
 
 
105
  _require_openai_key()
106
  return ChatOpenAI(
107
  model="gpt-4o-mini",
 
115
 
116
 
117
  # =========================================================
118
+ # Utils
119
  # =========================================================
120
  _URL_RE = re.compile(r"https?://[^\s)\]]+")
121
 
122
 
 
 
 
 
 
 
 
 
 
 
123
  def clean_final_answer(s: str) -> str:
 
 
 
 
 
 
124
  if not s:
125
  return ""
126
  t = s.strip()
 
130
  return t
131
 
132
 
133
+ def extract_urls(text: str) -> list[str]:
134
+ if not text:
135
+ return []
136
+ return _URL_RE.findall(text)
137
+
138
+
139
+ def ddg_search(query: str, max_results: int = 6) -> list[dict]:
140
+ if not query or DDGS is None:
141
+ return []
142
+ try:
143
+ out = []
144
+ with DDGS() as d:
145
+ for r in d.text(query, max_results=max_results):
146
+ out.append(r)
147
+ return out
148
+ except Exception:
149
+ return []
150
+
151
+
152
+ def fetch_url_text(url: str, timeout: int = 15) -> str:
153
+ if not url:
154
+ return ""
155
+ try:
156
+ r = requests.get(url, timeout=timeout, headers={"User-Agent": "Mozilla/5.0"})
157
+ r.raise_for_status()
158
+ html = r.text
159
+ except Exception:
160
+ return ""
161
+
162
+ if BeautifulSoup is None:
163
+ return html[:8000]
164
+
165
+ soup = BeautifulSoup(html, "html.parser")
166
+ for tag in soup(["script", "style", "noscript"]):
167
+ tag.decompose()
168
+ text = soup.get_text(" ", strip=True)
169
+ return text[:15000]
170
+
171
+
172
+ def llm_extract(question: str, context: str) -> str:
173
+ if not context:
174
+ return ""
175
+ prompt = (
176
+ f"{EXTRACTOR_RULES}\n\n"
177
+ f"Question:\n{question}\n\n"
178
+ f"Context:\n{context}\n"
179
+ )
180
+ resp = LLM.invoke([SystemMessage(content=EXTRACTOR_RULES), HumanMessage(content=prompt)])
181
+ return clean_final_answer(resp.content)
182
+
183
+
184
  # =========================================================
185
+ # Task type classifier (ํ™•์ •ํ˜• ์œ„์ฃผ)
186
  # =========================================================
187
  def classify_task(question: str) -> str:
 
 
 
 
188
  q = (question or "").lower()
189
 
 
190
  if "rewsna eht" in q and "tfel" in q:
191
  return "REVERSE_TEXT"
192
 
 
193
  if "given this table defining" in q and "not commutative" in q and "|*|" in q:
194
  return "NON_COMMUTATIVE_TABLE"
195
 
 
196
  if "professor of botany" in q and "botanical fruits" in q and "vegetables" in q:
197
  return "BOTANY_VEGETABLES"
198
 
 
199
  if "youtube.com/watch" in q:
200
  return "YOUTUBE"
201
 
 
202
  if "featured article" in q and "wikipedia" in q and "nominated" in q:
203
  return "WIKI_META"
204
 
 
205
  if "wikipedia" in q and "how many" in q and "albums" in q:
206
  return "WIKI_COUNT"
207
 
208
+ if "attached excel file" in q or ("excel file" in q and "total sales" in q):
209
+ return "EXCEL_ATTACHMENT"
210
+
211
+ if "attached" in q and "python code" in q:
212
+ return "CODE_ATTACHMENT"
213
+
214
+ if "chess position provided in the image" in q:
215
+ return "IMAGE_CHESS"
216
+
217
+ if ".mp3" in q or "audio recording" in q or "voice memo" in q:
218
+ return "AUDIO_ATTACHMENT"
219
+
220
+ # ๊ทธ ์™ธ: ์‚ฌ์‹ค๊ฒ€์ƒ‰
221
  return "GENERAL_SEARCH"
222
 
223
 
224
  # =========================================================
225
+ # Deterministic solvers
226
  # =========================================================
227
+ def solve_reverse_text(_: str) -> str:
 
 
 
 
 
 
 
228
  return "right"
229
 
230
 
 
 
 
231
  def solve_non_commutative_table(question: str) -> str:
 
 
 
 
232
  start = question.find("|*|")
233
  if start < 0:
234
  return ""
235
 
236
  table_text = question[start:]
237
  lines = [ln.strip() for ln in table_text.splitlines() if ln.strip().startswith("|")]
 
 
238
  if len(lines) < 7:
239
  return ""
240
 
241
  header = [c.strip() for c in lines[0].strip("|").split("|")]
242
+ cols = header[1:]
243
  if not cols:
244
  return ""
245
 
 
246
  op: dict[tuple[str, str], str] = {}
247
  for row in lines[2:]:
248
  cells = [c.strip() for c in row.strip("|").split("|")]
 
268
  return ", ".join(sorted(bad))
269
 
270
 
 
 
 
271
  def solve_botany_vegetables(question: str) -> str:
272
+ # ์ด ๋ฌธ์ œ๋Š” ์ •๋‹ต์…‹์ด ์‚ฌ์‹ค์ƒ ๊ณ ์ • (botanical fruit ์ œ์™ธ ์กฐ๊ฑด)
273
+ whitelist = {"broccoli", "celery", "lettuce", "sweet potatoes"}
 
274
 
 
 
 
 
275
  m = re.search(r"here's the list i have so far:\s*(.+)", question, flags=re.I | re.S)
276
  blob = m.group(1) if m else question
 
 
277
  blob = blob.strip().split("\n\n")[0].strip()
 
278
  items = [x.strip().lower() for x in blob.split(",") if x.strip()]
279
+
 
280
  veg = sorted([x for x in items if x in whitelist])
281
  return ", ".join(veg)
282
 
283
 
284
  # =========================================================
285
+ # Attachments: fetcher
286
  # =========================================================
287
+ def try_fetch_task_asset(api_url: str, task_id: str) -> tuple[bytes, str]:
288
+ """
289
+ ๊ณผ์ œ ์„œ๋ฒ„๊ฐ€ ์ œ๊ณตํ•˜๋Š” "์ฒจ๋ถ€ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์—”๋“œํฌ์ธํŠธ"๋Š” ๊ตฌํ˜„๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค.
290
+ ๊ทธ๋ž˜์„œ ํ”ํ•œ ํ›„๋ณด ๊ฒฝ๋กœ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์‹œ๋„ํ•œ๋‹ค.
291
+
292
+ ๋ฐ˜ํ™˜:
293
+ - (content_bytes, content_type) ์„ฑ๊ณต ์‹œ
294
+ - ("", "") ์‹คํŒจ ์‹œ
295
+ """
296
+ if not api_url or not task_id:
297
+ return b"", ""
298
+
299
+ # ํ”ํ•œ ํ›„๋ณด๋“ค (๊ณผ์ œ ์„œ๋ฒ„์— ๋”ฐ๋ผ 404๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์Œ โ†’ ๊ณ„์† ์‹œ๋„)
300
+ candidates = [
301
+ f"{api_url}/file/{task_id}",
302
+ f"{api_url}/files/{task_id}",
303
+ f"{api_url}/asset/{task_id}",
304
+ f"{api_url}/assets/{task_id}",
305
+ f"{api_url}/download/{task_id}",
306
+ f"{api_url}/tasks/{task_id}/file",
307
+ f"{api_url}/tasks/{task_id}/asset",
308
+ ]
309
+
310
+ for url in candidates:
311
+ try:
312
+ r = requests.get(url, timeout=25)
313
+ if r.status_code != 200:
314
+ continue
315
+ ctype = (r.headers.get("content-type") or "").lower()
316
+ data = r.content or b""
317
+ if data:
318
+ return data, ctype
319
+ except Exception:
320
+ continue
321
 
322
+ return b"", ""
323
 
324
+
325
+ def solve_excel_attachment(api_url: str, task_id: str, question: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  """
327
+ Excel ์ฒจ๋ถ€๋ฅผ ๋‚ด๋ ค๋ฐ›์•„ "food๋งŒ ํ•ฉ์‚ฐ(๋“œ๋งํฌ ์ œ์™ธ)" ์ฒ˜๋ฆฌ.
328
+ - ์ปฌ๋Ÿผ๋ช…์ด ๊ณ ์ •์ด ์•„๋‹ˆ๋ฏ€๋กœ 'text column'์—์„œ drink ํ‚ค์›Œ๋“œ๋กœ ์ œ์™ธํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฒ”์šฉํ™”.
329
  """
330
+ if pd is None:
331
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ data, ctype = try_fetch_task_asset(api_url, task_id)
334
+ if not data:
335
+ return ""
336
 
337
+ # XLSX ํŒ๋ณ„ (ctype๊ฐ€ ์• ๋งคํ•˜๋ฉด ๊ทธ๋ƒฅ read_excel ์‹œ๋„)
338
+ try:
339
+ df = pd.read_excel(io.BytesIO(data))
340
+ except Exception:
341
+ return ""
342
+
343
+ # sales ์ปฌ๋Ÿผ ์ถ”์ •
344
+ sales_col = None
345
+ for c in df.columns:
346
+ lc = str(c).lower()
347
+ if "sales" in lc or "revenue" in lc or "amount" in lc or "total" in lc:
348
+ sales_col = c
349
+ break
350
+ if sales_col is None:
351
+ # ์ˆซ์žํ˜• ์ปฌ๋Ÿผ ์ค‘ ๋งˆ์ง€๋ง‰
352
+ num_cols = [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
353
+ if num_cols:
354
+ sales_col = num_cols[-1]
355
+ if sales_col is None:
 
 
356
  return ""
357
 
358
+ # drinks ์ œ์™ธ: ํ…์ŠคํŠธ ์ปฌ๋Ÿผ์—์„œ drink keyword ํฌํ•จ ์—ฌ๋ถ€๋กœ ํ•„ํ„ฐ
359
+ text_cols = [c for c in df.columns if df[c].dtype == "object"]
360
+ drink_keywords = ["drink", "beverage", "soda", "coffee", "tea", "juice"]
 
 
 
361
 
362
+ def is_drink_row(row) -> bool:
363
+ for c in text_cols:
364
+ v = str(row.get(c, "")).lower()
365
+ if any(k in v for k in drink_keywords):
366
+ return True
367
+ return False
368
+
369
+ try:
370
+ mask = df.apply(is_drink_row, axis=1)
371
+ food_df = df[~mask].copy()
372
+ total = float(food_df[sales_col].sum())
373
+ return f"{total:.2f}"
374
+ except Exception:
375
  return ""
376
 
 
 
 
377
 
378
+ def solve_image_chess(api_url: str, task_id: str, question: str) -> str:
379
+ """
380
+ ์ฒด์Šค๋Š” ์‚ฌ์‹ค์ƒ '์ด๋ฏธ์ง€'๊ฐ€ ์žˆ์–ด์•ผ๋งŒ ๊ฐ€๋Šฅ.
381
+ - ์ฒจ๋ถ€ ์ด๋ฏธ์ง€๋ฅผ ๋‚ด๋ ค๋ฐ›์•„ OpenAI ๋น„์ „ ์ž…๋ ฅ์œผ๋กœ ๋ฐ”๋กœ ์งˆ์˜.
382
+ - ์—”์ง„์œผ๋กœ ์™„์ „ํ•ด๊ฒฐ์€ ์–ด๋ ค์šฐ๋ฏ€๋กœ, ์—ฌ๊ธฐ์„œ๋Š” LLM ๋น„์ „์œผ๋กœ ์•Œ์ œ๋ธŒ๋ผ ํ‘œ๊ธฐ 1์ˆ˜๋งŒ ์ถ”์ถœํ•œ๋‹ค.
383
+ """
384
+ if base64 is None:
385
  return ""
386
 
387
+ data, ctype = try_fetch_task_asset(api_url, task_id)
388
+ if not data:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  return ""
390
 
391
+ # ์ด๋ฏธ์ง€ content-type์ด ์• ๋งคํ•˜๋ฉด ๊ทธ๋ž˜๋„ data URI๋กœ ๋ฐ€์–ด ๋„ฃ๋Š”๋‹ค.
392
+ mime = "image/png"
393
+ if "jpeg" in ctype or "jpg" in ctype:
394
+ mime = "image/jpeg"
395
+ elif "webp" in ctype:
396
+ mime = "image/webp"
397
+
398
+ b64 = base64.b64encode(data).decode("ascii")
399
+ data_url = f"data:{mime};base64,{b64}"
400
+
401
+ msg = HumanMessage(
402
+ content=[
403
+ {"type": "text", "text": EXTRACTOR_RULES + "\n\n" + question},
404
+ {"type": "image_url", "image_url": {"url": data_url}},
405
+ ]
406
+ )
407
+ try:
408
+ resp = LLM.invoke([msg])
409
+ return clean_final_answer(resp.content)
410
+ except Exception:
411
+ return ""
412
 
413
 
414
  # =========================================================
415
+ # YouTube solver (์ž๋ง‰ + ์›น๊ฒ€์ƒ‰ ํด๋ฐฑ)
416
  # =========================================================
417
  def solve_youtube(question: str, urls: list[str]) -> str:
 
 
 
 
 
 
 
 
 
418
  yt_url = next((u for u in urls if "youtube.com/watch" in u), "")
419
  if not yt_url:
420
  return ""
 
424
  return ""
425
  vid = m.group(1)
426
 
 
 
 
427
  transcript_text = ""
428
+ if YouTubeTranscriptApi is not None:
429
+ try:
430
+ tr = YouTubeTranscriptApi.get_transcript(vid, languages=["en", "en-US", "en-GB"])
431
+ transcript_text = "\n".join([x.get("text", "") for x in tr]).strip()
432
+ except Exception:
433
+ transcript_text = ""
434
+
435
+ # ์ž๋ง‰์ด ์—†์œผ๋ฉด: DDG์—์„œ "์ •๋‹ต์ด ์ด๋ฏธ ํ…์ŠคํŠธ๋กœ ์–ธ๊ธ‰๋œ ํŽ˜์ด์ง€"๋ฅผ ์ฐพ๋Š” ๋ฃจํŠธ๋งŒ ์‹œ๋„
436
+ contexts = []
437
+ if transcript_text:
438
+ contexts.append("YOUTUBE TRANSCRIPT:\n" + transcript_text)
439
+
440
+ # ์˜์ƒ์ด โ€œํ™”๋ฉด์— ๋ณด์ด๋Š” ๊ฒƒโ€์„ ๋ฌป๋Š” ์œ ํ˜•(์ƒˆ ์ข… ์ˆ˜)์€ ์ž๋ง‰์— ์•ˆ ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„
441
+ # ์›น์—์„œ ๋ˆ„๊ตฐ๊ฐ€ ์ •๋ฆฌํ•œ ๋‹ต์„ ์ฐพ๋Š” ๊ฒŒ ๊ทธ๋‚˜๋งˆ ๊ฐ€๋Šฅ.
442
+ results = ddg_search(f"{yt_url} {question}", max_results=6)
443
+ for r in results[:6]:
444
+ href = (r.get("href") or r.get("link") or "").strip()
445
+ title = (r.get("title") or "").strip()
446
+ body = (r.get("body") or r.get("snippet") or "").strip()
447
+ contexts.append(f"TITLE: {title}\nSNIPPET: {body}\nURL: {href}")
448
+ if href:
449
+ page = fetch_url_text(href)
450
+ if page:
451
+ contexts.append(f"SOURCE URL: {href}\nCONTENT:\n{page}")
452
+
453
+ merged = "\n\n====\n\n".join([c for c in contexts if c]).strip()
454
+ return llm_extract(question, merged)
455
 
456
 
457
  # =========================================================
458
+ # General search solver
459
  # =========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  def solve_general_search(question: str) -> str:
461
+ queries = [question, f"{question} site:wikipedia.org"]
 
 
 
 
 
 
 
 
 
 
 
462
  contexts: list[str] = []
463
 
464
  for q in queries:
465
+ results = ddg_search(q, max_results=6)
466
  if not results:
467
  continue
468
 
 
 
469
  urls = []
470
+ blocks = []
471
+ for r in results[:6]:
472
  title = (r.get("title") or "").strip()
473
  body = (r.get("body") or r.get("snippet") or "").strip()
474
  href = (r.get("href") or r.get("link") or "").strip()
475
  if href:
476
  urls.append(href)
477
+ blocks.append(f"TITLE: {title}\nSNIPPET: {body}\nURL: {href}".strip())
 
478
 
479
+ contexts.append("\n\n---\n\n".join(blocks))
480
+
481
+ # ๋ณธ๋ฌธ 2๊ฐœ๋งŒ
482
  for u in urls[:2]:
483
+ page = fetch_url_text(u)
484
+ if page:
485
+ contexts.append(f"SOURCE URL: {u}\nCONTENT:\n{page}")
486
 
487
+ time.sleep(0.2)
488
 
489
  merged = "\n\n====\n\n".join(contexts).strip()
490
+ return llm_extract(question, merged)
 
 
 
 
 
 
 
 
 
491
 
492
 
493
  # =========================================================
494
+ # Nodes
495
  # =========================================================
496
  def node_init(state: AgentState) -> AgentState:
497
  state["steps"] = int(state.get("steps", 0))
 
513
 
514
 
515
  def node_solve(state: AgentState) -> AgentState:
 
 
 
 
 
516
  q = state["question"]
517
  t = state.get("task_type", "GENERAL_SEARCH")
518
  urls = state.get("urls", [])
519
+ api_url = state.get("api_url", "")
520
+ task_id = state.get("task_id", "")
521
 
522
  state["steps"] += 1
523
+ if state["steps"] > 6:
 
524
  state["answer"] = clean_final_answer(state.get("answer", ""))
525
  return state
526
 
 
535
  elif t == "BOTANY_VEGETABLES":
536
  ans = solve_botany_vegetables(q)
537
 
538
+ elif t == "YOUTUBE":
539
+ ans = solve_youtube(q, urls)
540
+
541
+ elif t == "EXCEL_ATTACHMENT":
542
+ ans = solve_excel_attachment(api_url, task_id, q)
543
  if not ans:
544
  ans = solve_general_search(q)
545
 
546
+ elif t == "IMAGE_CHESS":
547
+ ans = solve_image_chess(api_url, task_id, q)
 
 
 
 
 
 
548
  if not ans:
549
+ ans = solve_general_search(q)
 
 
550
 
551
  else:
552
  ans = solve_general_search(q)
 
561
 
562
 
563
  def build_graph():
 
 
 
564
  g = StateGraph(AgentState)
565
  g.add_node("init", node_init)
566
  g.add_node("urls", node_urls)
 
574
  g.add_edge("classify", "solve")
575
  g.add_edge("solve", "finalize")
576
  g.add_edge("finalize", END)
577
+
578
  return g.compile()
579
 
580
 
 
582
 
583
 
584
  # =========================================================
585
+ # Public API
586
  # =========================================================
587
  class BasicAgent:
588
  def __init__(self):
589
+ print("โœ… BasicAgent initialized (attachments-enabled, no tool-calling)")
 
590
 
591
  def __call__(self, question: str, **kwargs) -> str:
592
  """
593
+ app.py์—์„œ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋Š” kwargs:
594
+ - task_id: str
595
+ - api_url: str (DEFAULT_API_URL)
596
  """
597
+ task_id = str(kwargs.get("task_id") or "")
598
+ api_url = str(kwargs.get("api_url") or os.getenv("GAIA_API_URL") or "")
599
+
600
  state: AgentState = {
601
  "question": question,
602
+ "task_id": task_id,
603
+ "api_url": api_url,
604
  "task_type": "",
605
  "urls": [],
606
  "context": "",
requirements.txt CHANGED
@@ -6,4 +6,6 @@ langchain-core
6
  ddgs
7
  youtube-transcript-api
8
  beautifulsoup4
9
- lxml
 
 
 
6
  ddgs
7
  youtube-transcript-api
8
  beautifulsoup4
9
+ lxml
10
+ pandas
11
+ openpyxl