ahnhs2k commited on
Commit
598f8d9
ยท
1 Parent(s): 8c1ae17
Files changed (2) hide show
  1. agent.py +68 -63
  2. requirements.txt +15 -18
agent.py CHANGED
@@ -4,10 +4,12 @@
4
  # - OpenAI GPT-4o-mini ์‚ฌ์šฉ
5
  # - Wikipedia / Arxiv / DuckDuckGo ๊ฒ€์ƒ‰ ๋„๊ตฌ ํฌํ•จ
6
  # - HuggingFace Embedding + FAISS Retriever ์œ ์ง€
 
7
  # =========================================================
8
 
9
  import os
10
  import pickle
 
11
 
12
  # ---------------------------------------------------------
13
  # LangChain / LangGraph ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ
@@ -23,22 +25,27 @@ from langchain_core.messages import SystemMessage, HumanMessage
23
  from langchain_core.tools import tool
24
  from langchain_community.tools import DuckDuckGoSearchRun
25
 
26
- from langgraph.graph import START, StateGraph, MessagesState
27
  from langgraph.prebuilt import ToolNode, tools_condition
28
 
29
 
 
 
 
 
 
 
 
 
 
30
  # ---------------------------------------------------------
31
- # DuckDuckGo ๊ฒ€์ƒ‰ ์ธ์Šคํ„ด์Šค
32
- # - API Key ๋ถˆํ•„์š”
33
- # - Space ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
34
  # ---------------------------------------------------------
35
  ddg = DuckDuckGoSearchRun()
36
 
37
 
38
  # =========================================================
39
  # TOOLS ์ •์˜
40
- # โ€ป LangChain ์ตœ์‹  ๋ฒ„์ „์—์„œ๋Š”
41
- # @tool ํ•จ์ˆ˜์— "docstring"์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ
42
  # =========================================================
43
 
44
  @tool
@@ -47,12 +54,8 @@ def wiki_search(query: str) -> dict:
47
  Search Wikipedia for factual information related to the query.
48
  Returns raw page content as text.
49
  """
50
- # Wikipedia์—์„œ ์ตœ๋Œ€ 2๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ
51
  docs = WikipediaLoader(query=query, load_max_docs=2).load()
52
-
53
- # ๋ฌธ์„œ ๋‚ด์šฉ์„ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ‘ํ•ฉ
54
  text = "\n\n".join(d.page_content for d in docs)
55
-
56
  return {"wiki": text}
57
 
58
 
@@ -62,12 +65,8 @@ def arxiv_search(query: str) -> dict:
62
  Search Arxiv for academic papers related to the query.
63
  Returns truncated paper content.
64
  """
65
- # Arxiv์—์„œ ์ตœ๋Œ€ 2๊ฐœ ๋…ผ๋ฌธ ๋กœ๋“œ
66
  docs = ArxivLoader(query=query, load_max_docs=2).load()
67
-
68
- # ๋…ผ๋ฌธ์€ ๊ธธ๊ธฐ ๋•Œ๋ฌธ์— ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉ
69
  text = "\n\n".join(d.page_content[:1000] for d in docs)
70
-
71
  return {"arxiv": text}
72
 
73
 
@@ -78,21 +77,16 @@ def web_search(query: str) -> dict:
78
  No API key required. Suitable for Hugging Face Spaces.
79
  """
80
  try:
81
- # DuckDuckGo ๊ฒ€์ƒ‰ ์‹คํ–‰
82
- result = ddg.run(query)
83
- return {"web": result}
84
  except Exception:
85
- # ๊ฒ€์ƒ‰ ์‹คํŒจ ์‹œ ๋นˆ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ (๊ทธ๋ž˜ํ”„ ์ค‘๋‹จ ๋ฐฉ์ง€)
86
  return {"web": ""}
87
 
88
 
89
- # LangGraph์—์„œ ์‚ฌ์šฉํ•  Tool ๋ชฉ๋ก
90
  TOOLS = [wiki_search, arxiv_search, web_search]
91
 
92
 
93
  # =========================================================
94
- # SYSTEM PROMPT
95
- # - GAIA ํ‰๊ฐ€ ๊ทœ์น™์— ๋งž๊ฒŒ ์ถœ๋ ฅ ํ˜•์‹ ๊ฐ•์ œ
96
  # =========================================================
97
  SYSTEM_PROMPT = """
98
  You are solving GAIA benchmark questions.
@@ -105,36 +99,27 @@ You MUST:
105
  - No extra text.
106
  """.strip()
107
 
108
- # SystemMessage ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
109
  SYS_MSG = SystemMessage(content=SYSTEM_PROMPT)
110
 
111
 
112
  # =========================================================
113
- # RETRIEVER (FAISS + HuggingFace Embeddings)
114
- # - ์ด์ „ ์งˆ๋ฌธ๊ณผ ์œ ์‚ฌํ•œ ํŒจํ„ด์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„
115
  # =========================================================
116
-
117
- # ๋ฌธ์žฅ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ (768์ฐจ์›)
118
  embeddings = HuggingFaceEmbeddings(
119
  model_name="sentence-transformers/all-mpnet-base-v2"
120
  )
121
 
122
- # FAISS ์ธ๋ฑ์Šค๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋กœ๋“œ
123
  if os.path.exists("faiss.pkl"):
124
  with open("faiss.pkl", "rb") as f:
125
  vector_store = pickle.load(f)
126
  else:
127
- # ์ตœ์ดˆ ์‹คํ–‰ ์‹œ ๊ธฐ๋ณธ ์‹œ๋“œ ๋ฌธ์„œ ์ƒ์„ฑ
128
  seed_docs = [
129
  Document(page_content="GAIA questions require factual exact answers."),
130
  ]
131
  vector_store = FAISS.from_documents(seed_docs, embeddings)
132
-
133
- # ๋‹ค์Œ ์‹คํ–‰์„ ์œ„ํ•ด ์ €์žฅ
134
  with open("faiss.pkl", "wb") as f:
135
  pickle.dump(vector_store, f)
136
 
137
- # Retriever Tool (ํ˜„์žฌ ๊ทธ๋ž˜ํ”„์—์„œ๋Š” ๋ณด์กฐ ์—ญํ• )
138
  retriever_tool = create_retriever_tool(
139
  retriever=vector_store.as_retriever(),
140
  name="question_retriever",
@@ -143,73 +128,93 @@ retriever_tool = create_retriever_tool(
143
 
144
 
145
  # =========================================================
146
- # LANGGRAPH ๊ทธ๋ž˜ํ”„ ๋นŒ๋”
147
  # =========================================================
148
  def build_agent():
149
  """
150
- LangGraph StateGraph๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ
151
- - System Prompt ์ฃผ์ž…
152
- - LLM โ†’ Tool โ†’ LLM ๋ฐ˜๋ณต ํ๋ฆ„์„ ์ƒ์„ฑ
153
  """
154
 
155
- # OpenAI GPT-4o-mini (Space์—์„œ ์•ˆ์ •์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
156
  llm = ChatOpenAI(
157
  model="gpt-4o-mini",
158
- temperature=0, # deterministic
159
  max_tokens=128,
160
  )
161
 
162
- # LLM์— Tool ์‚ฌ์šฉ ๊ถŒํ•œ ๋ถ€์—ฌ
163
  llm_with_tools = llm.bind_tools(TOOLS)
164
 
165
  # ---------------------
166
- # Graph Node ์ •์˜
167
  # ---------------------
168
- def retriever(state: MessagesState):
169
- # ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ํ•ญ์ƒ ๋ฉ”์‹œ์ง€ ๋งจ ์•ž์— ์‚ฝ์ž…
170
- return {"messages": [SYS_MSG] + state["messages"]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- def assistant(state: MessagesState):
173
- # LLM ์‹คํ–‰ (Tool ํ˜ธ์ถœ ์—ฌ๋ถ€๋Š” tools_condition์ด ํŒ๋‹จ)
174
- return {"messages": [llm_with_tools.invoke(state["messages"])]}
 
 
 
 
 
 
 
 
175
 
176
  # ---------------------
177
- # Graph ๊ตฌ์„ฑ
178
  # ---------------------
179
- builder = StateGraph(MessagesState)
180
 
181
- builder.add_node("retriever", retriever)
182
  builder.add_node("assistant", assistant)
183
  builder.add_node("tools", ToolNode(TOOLS))
184
 
185
- # START โ†’ retriever โ†’ assistant
186
- builder.add_edge(START, "retriever")
187
- builder.add_edge("retriever", "assistant")
188
 
189
- # assistant๊ฐ€ tool ํ˜ธ์ถœ์„ ์›ํ•˜๋ฉด tools๋กœ ๋ถ„๊ธฐ
190
- builder.add_conditional_edges("assistant", tools_condition)
 
 
191
 
192
- # tool ์‹คํ–‰ ํ›„ ๋‹ค์‹œ assistant๋กœ ๋ณต๊ท€
193
  builder.add_edge("tools", "assistant")
194
 
195
- return builder.compile()
 
196
 
197
 
198
  # =========================================================
199
- # ์™ธ๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋Š” PUBLIC API
200
- # - app.py์—์„œ ์ด ํด๋ž˜์Šค๋งŒ import
201
  # =========================================================
202
  class BasicAgent:
203
  def __init__(self):
204
- # LangGraph ์ปดํŒŒ์ผ
205
  self.graph = build_agent()
206
- print("โœ… LangGraph GPT-4o-mini Agent initialized")
207
 
208
  def __call__(self, question: str) -> str:
209
- # ์งˆ๋ฌธ์„ HumanMessage๋กœ ๊ฐ์‹ธ Graph ์‹คํ–‰
210
  result = self.graph.invoke(
211
- {"messages": [HumanMessage(content=question)]}
 
 
 
212
  )
213
-
214
- # ์ตœ์ข… assistant ๋ฉ”์‹œ์ง€๋งŒ ๋ฐ˜ํ™˜
215
  return result["messages"][-1].content.strip()
 
4
  # - OpenAI GPT-4o-mini ์‚ฌ์šฉ
5
  # - Wikipedia / Arxiv / DuckDuckGo ๊ฒ€์ƒ‰ ๋„๊ตฌ ํฌํ•จ
6
  # - HuggingFace Embedding + FAISS Retriever ์œ ์ง€
7
+ # - Tool ๋ฌดํ•œ ๋ฃจํ”„ ์ฐจ๋‹จ (์ค‘์š”)
8
  # =========================================================
9
 
10
  import os
11
  import pickle
12
+ from typing import TypedDict
13
 
14
  # ---------------------------------------------------------
15
  # LangChain / LangGraph ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ
 
25
  from langchain_core.tools import tool
26
  from langchain_community.tools import DuckDuckGoSearchRun
27
 
28
+ from langgraph.graph import START, END, StateGraph
29
  from langgraph.prebuilt import ToolNode, tools_condition
30
 
31
 
32
+ # =========================================================
33
+ # State ์ •์˜
34
+ # - tool์„ ์ด๋ฏธ ์‚ฌ์šฉํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๊ธฐ๋ก
35
+ # =========================================================
36
+ class AgentState(TypedDict):
37
+ messages: list
38
+ used_tools: bool
39
+
40
+
41
  # ---------------------------------------------------------
42
+ # DuckDuckGo ๊ฒ€์ƒ‰ ์ธ์Šคํ„ด์Šค (API Key ๋ถˆํ•„์š”)
 
 
43
  # ---------------------------------------------------------
44
  ddg = DuckDuckGoSearchRun()
45
 
46
 
47
  # =========================================================
48
  # TOOLS ์ •์˜
 
 
49
  # =========================================================
50
 
51
  @tool
 
54
  Search Wikipedia for factual information related to the query.
55
  Returns raw page content as text.
56
  """
 
57
  docs = WikipediaLoader(query=query, load_max_docs=2).load()
 
 
58
  text = "\n\n".join(d.page_content for d in docs)
 
59
  return {"wiki": text}
60
 
61
 
 
65
  Search Arxiv for academic papers related to the query.
66
  Returns truncated paper content.
67
  """
 
68
  docs = ArxivLoader(query=query, load_max_docs=2).load()
 
 
69
  text = "\n\n".join(d.page_content[:1000] for d in docs)
 
70
  return {"arxiv": text}
71
 
72
 
 
77
  No API key required. Suitable for Hugging Face Spaces.
78
  """
79
  try:
80
+ return {"web": ddg.run(query)}
 
 
81
  except Exception:
 
82
  return {"web": ""}
83
 
84
 
 
85
  TOOLS = [wiki_search, arxiv_search, web_search]
86
 
87
 
88
  # =========================================================
89
+ # SYSTEM PROMPT (GAIA ๊ทœ์น™ ๊ณ ์ •)
 
90
  # =========================================================
91
  SYSTEM_PROMPT = """
92
  You are solving GAIA benchmark questions.
 
99
  - No extra text.
100
  """.strip()
101
 
 
102
  SYS_MSG = SystemMessage(content=SYSTEM_PROMPT)
103
 
104
 
105
  # =========================================================
106
+ # FAISS Retriever (์œ ์ง€)
 
107
  # =========================================================
 
 
108
  embeddings = HuggingFaceEmbeddings(
109
  model_name="sentence-transformers/all-mpnet-base-v2"
110
  )
111
 
 
112
  if os.path.exists("faiss.pkl"):
113
  with open("faiss.pkl", "rb") as f:
114
  vector_store = pickle.load(f)
115
  else:
 
116
  seed_docs = [
117
  Document(page_content="GAIA questions require factual exact answers."),
118
  ]
119
  vector_store = FAISS.from_documents(seed_docs, embeddings)
 
 
120
  with open("faiss.pkl", "wb") as f:
121
  pickle.dump(vector_store, f)
122
 
 
123
  retriever_tool = create_retriever_tool(
124
  retriever=vector_store.as_retriever(),
125
  name="question_retriever",
 
128
 
129
 
130
  # =========================================================
131
+ # LangGraph ๋นŒ๋”
132
  # =========================================================
133
  def build_agent():
134
  """
135
+ Tool์„ ์ตœ๋Œ€ 1ํšŒ๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐ•์ œํ•œ LangGraph.
136
+ ๋ฌดํ•œ ๋ฃจํ”„ / recursion ์—๋Ÿฌ ์›์ฒœ ์ฐจ๋‹จ.
 
137
  """
138
 
 
139
  llm = ChatOpenAI(
140
  model="gpt-4o-mini",
141
+ temperature=0,
142
  max_tokens=128,
143
  )
144
 
 
145
  llm_with_tools = llm.bind_tools(TOOLS)
146
 
147
  # ---------------------
148
+ # Nodes
149
  # ---------------------
150
+ def inject_system(state: AgentState):
151
+ return {
152
+ "messages": [SYS_MSG] + state["messages"],
153
+ "used_tools": state.get("used_tools", False),
154
+ }
155
+
156
+ def assistant(state: AgentState):
157
+ msg = llm_with_tools.invoke(state["messages"])
158
+ return {
159
+ "messages": [msg],
160
+ "used_tools": state.get("used_tools", False),
161
+ }
162
+
163
+ def tools_node(state: AgentState):
164
+ # Tool์„ ์‚ฌ์šฉํ•œ ์ˆœ๊ฐ„ used_tools = True
165
+ return {
166
+ "messages": state["messages"],
167
+ "used_tools": True,
168
+ }
169
 
170
+ # ---------------------
171
+ # Conditional (ํ•ต์‹ฌ)
172
+ # ---------------------
173
+ def tool_condition_once(state: AgentState):
174
+ """
175
+ Tool์€ ๋”ฑ ํ•œ ๋ฒˆ๋งŒ ํ—ˆ์šฉ.
176
+ ์ด๋ฏธ ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด ๋ฌด์กฐ๊ฑด END.
177
+ """
178
+ if state.get("used_tools"):
179
+ return END
180
+ return tools_condition(state)
181
 
182
  # ---------------------
183
+ # Graph
184
  # ---------------------
185
+ builder = StateGraph(AgentState)
186
 
187
+ builder.add_node("inject_system", inject_system)
188
  builder.add_node("assistant", assistant)
189
  builder.add_node("tools", ToolNode(TOOLS))
190
 
191
+ builder.add_edge(START, "inject_system")
192
+ builder.add_edge("inject_system", "assistant")
 
193
 
194
+ builder.add_conditional_edges(
195
+ "assistant",
196
+ tool_condition_once
197
+ )
198
 
 
199
  builder.add_edge("tools", "assistant")
200
 
201
+ # recursion_limit์€ ์•ˆ์ „์žฅ์น˜
202
+ return builder.compile(recursion_limit=10)
203
 
204
 
205
  # =========================================================
206
+ # Public API (app.py์—์„œ ์‚ฌ์šฉ)
 
207
  # =========================================================
208
  class BasicAgent:
209
  def __init__(self):
 
210
  self.graph = build_agent()
211
+ print("โœ… LangGraph GPT-4o-mini Agent initialized (tool loop fixed)")
212
 
213
  def __call__(self, question: str) -> str:
 
214
  result = self.graph.invoke(
215
+ {
216
+ "messages": [HumanMessage(content=question)],
217
+ "used_tools": False,
218
+ }
219
  )
 
 
220
  return result["messages"][-1].content.strip()
requirements.txt CHANGED
@@ -1,18 +1,15 @@
1
- gradio==5.25.2
2
- requests>=2.31.0
3
-
4
- langgraph>=0.2.40
5
- langchain-core>=0.3.30
6
- langchain-community>=0.3.15
7
- langchain-openai>=0.3.0
8
- langchain-huggingface>=0.1.2
9
-
10
- sentence-transformers>=3.0.0
11
-
12
- duckduckgo-search>=6.3.0
13
- ddgs>=6.1.0
14
-
15
- faiss-cpu>=1.8.0
16
-
17
- wikipedia>=1.4.0
18
- arxiv>=2.1.0
 
1
+ gradio
2
+ requests
3
+ langgraph
4
+ langchain
5
+ langchain_core
6
+ langchain-community
7
+ langchain_openai
8
+ langchain-huggingface
9
+ sentence-transformers
10
+ faiss-cpu
11
+ ddgs
12
+ duckduckgo-search
13
+ wikipedia
14
+ arxiv
15
+ pymupdf