Files changed (1) hide show
  1. agent.py +242 -0
agent.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LangGraph Agent"""
2
+ import os
3
+ import requests as req
4
+ from dotenv import load_dotenv
5
+ from langgraph.graph import START, StateGraph, MessagesState
6
+ from langgraph.prebuilt import tools_condition
7
+ from langgraph.prebuilt import ToolNode
8
+ from langchain_google_genai import ChatGoogleGenerativeAI
9
+ from langchain_openai import ChatOpenAI
10
+ from langchain_groq import ChatGroq
11
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint, HuggingFaceEmbeddings
12
+ from langchain_community.tools.tavily_search import TavilySearchResults
13
+ from langchain_community.document_loaders import WikipediaLoader
14
+ from langchain_community.document_loaders import ArxivLoader
15
+ from langchain_community.vectorstores import SupabaseVectorStore
16
+ from langchain_core.messages import SystemMessage, HumanMessage
17
+ from langchain_core.tools import tool
18
+ from langchain.tools.retriever import create_retriever_tool
19
+ from supabase.client import Client, create_client
20
+ import pytesseract
21
+ from PIL import Image
22
+
23
+ load_dotenv()
24
+
25
+ # ─── Math tools ────────────────────────────────────────────────────────────────
26
+
27
+ @tool
28
+ def multiply(a: int, b: int) -> int:
29
+ """Multiply two numbers.
30
+ Args:
31
+ a: first int
32
+ b: second int
33
+ """
34
+ return a * b
35
+
36
+ @tool
37
+ def add(a: int, b: int) -> int:
38
+ """Add two numbers.
39
+ Args:
40
+ a: first int
41
+ b: second int
42
+ """
43
+ return a + b
44
+
45
+ @tool
46
+ def subtract(a: int, b: int) -> int:
47
+ """Subtract two numbers.
48
+ Args:
49
+ a: first int
50
+ b: second int
51
+ """
52
+ return a - b
53
+
54
+ @tool
55
+ def divide(a: int, b: int) -> float:
56
+ """Divide two numbers.
57
+ Args:
58
+ a: first int
59
+ b: second int
60
+ """
61
+ if b == 0:
62
+ raise ValueError("Cannot divide by zero.")
63
+ return a / b
64
+
65
+ @tool
66
+ def modulus(a: int, b: int) -> int:
67
+ """Get the modulus of two numbers.
68
+ Args:
69
+ a: first int
70
+ b: second int
71
+ """
72
+ return a % b
73
+
74
+ # ─── Search tools ──────────────────────────────────────────────────────────────
75
+
76
+ @tool
77
+ def wiki_search(query: str) -> dict:
78
+ """Search Wikipedia for a query and return maximum 2 results.
79
+ Args:
80
+ query: The search query.
81
+ """
82
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
83
+ formatted = "\n\n---\n\n".join(
84
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
85
+ for doc in search_docs
86
+ )
87
+ return {"wiki_results": formatted}
88
+
89
+ @tool
90
+ def web_search(query: str) -> dict:
91
+ """Search Tavily for a query and return maximum 3 results.
92
+ Args:
93
+ query: The search query.
94
+ """
95
+ search_docs = TavilySearchResults(max_results=3).invoke(query=query)
96
+ formatted = "\n\n---\n\n".join(
97
+ f'<Document source="{doc["url"]}"/>\n{doc["content"]}\n</Document>'
98
+ for doc in search_docs
99
+ )
100
+ return {"web_results": formatted}
101
+
102
+ @tool
103
+ def arxiv_search(query: str) -> dict:
104
+ """Search Arxiv for a query and return maximum 3 results.
105
+ Args:
106
+ query: The search query.
107
+ """
108
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
109
+ formatted = "\n\n---\n\n".join(
110
+ f'<Document source="{doc.metadata["source"]}"/>\n{doc.page_content[:1000]}\n</Document>'
111
+ for doc in search_docs
112
+ )
113
+ return {"arxiv_results": formatted}
114
+
115
+ # ─── Image / OCR tools ─────────────────────────────────────────────────────────
116
+
117
+ @tool
118
+ def download_image(task_id: str) -> dict:
119
+ """Download an image file attached to a task from the evaluation API.
120
+ Use this first when the question mentions an image or attached file.
121
+ Args:
122
+ task_id: The task ID whose file should be downloaded.
123
+ """
124
+ api_url = "https://agents-course-unit4-scoring.hf.space"
125
+ file_url = f"{api_url}/files/{task_id}"
126
+ try:
127
+ response = req.get(file_url, timeout=30)
128
+ response.raise_for_status()
129
+ # Detect extension from Content-Type header
130
+ content_type = response.headers.get("content-type", "")
131
+ ext_map = {
132
+ "image/jpeg": ".jpg",
133
+ "image/png": ".png",
134
+ "image/gif": ".gif",
135
+ "image/webp": ".webp",
136
+ }
137
+ ext = next((v for k, v in ext_map.items() if k in content_type), ".jpg")
138
+ save_path = f"/tmp/{task_id}{ext}"
139
+ with open(save_path, "wb") as f:
140
+ f.write(response.content)
141
+ return {"image_path": save_path, "message": f"Image saved to {save_path}"}
142
+ except Exception as e:
143
+ return {"error": str(e)}
144
+
145
+ @tool
146
+ def extract_text_from_image(image_path: str, language: str = "eng") -> dict:
147
+ """Extract text from a local image file using OCR (pytesseract).
148
+ Use this after download_image to read text inside an image.
149
+ Args:
150
+ image_path: Local path to the image file (e.g. /tmp/task123.jpg).
151
+ language: Tesseract language code β€” 'eng' for English, 'vie' for Vietnamese.
152
+ """
153
+ try:
154
+ image = Image.open(image_path)
155
+ text = pytesseract.image_to_string(image, lang=language)
156
+ return {"ocr_result": text.strip()}
157
+ except Exception as e:
158
+ return {"error": str(e)}
159
+
160
+ # ─── System prompt ─────────────────────────────────────────────────────────────
161
+
162
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
163
+ system_prompt = f.read()
164
+
165
+ sys_msg = SystemMessage(content=system_prompt)
166
+
167
+ # ─── Vector store / retriever ──────────────────────────────────────────────────
168
+
169
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
170
+ supabase: Client = create_client(
171
+ os.environ.get("SUPABASE_URL"),
172
+ os.environ.get("SUPABASE_SERVICE_KEY"),
173
+ )
174
+ vector_store = SupabaseVectorStore(
175
+ client=supabase,
176
+ embedding=embeddings,
177
+ table_name="documents",
178
+ query_name="match_documents_langchain",
179
+ )
180
+ retriever_tool = create_retriever_tool(
181
+ retriever=vector_store.as_retriever(),
182
+ name="Question Search",
183
+ description="Retrieve similar questions from the vector store.",
184
+ )
185
+
186
+ # ─── Tool list ─────────────────────────────────────────────────────────────────
187
+
188
+ tools = [
189
+ multiply,
190
+ add,
191
+ subtract,
192
+ divide,
193
+ modulus,
194
+ wiki_search,
195
+ web_search,
196
+ arxiv_search,
197
+ download_image,
198
+ extract_text_from_image,
199
+ ]
200
+
201
+ # ─── Graph builder ─────────────────────────────────────────────────────────────
202
+
203
+ def build_graph(provider: str = "groq"):
204
+ """Build the LangGraph agent graph."""
205
+ if provider == "google":
206
+ llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
207
+ elif provider == "groq":
208
+ llm = ChatGroq(model="qwen-qwq-32b", temperature=0)
209
+ elif provider == "openai":
210
+ llm = ChatOpenAI(model="gpt-4o", temperature=0)
211
+ elif provider == "huggingface":
212
+ llm = ChatHuggingFace(
213
+ llm=HuggingFaceEndpoint(
214
+ url="https://api-inference.huggingface.co/models/Meta-DeepLearning/llama-2-7b-chat-hf",
215
+ temperature=0,
216
+ ),
217
+ )
218
+ else:
219
+ raise ValueError("Invalid provider. Choose 'google', 'groq', 'openai', or 'huggingface'.")
220
+
221
+ llm_with_tools = llm.bind_tools(tools)
222
+
223
+ def assistant(state: MessagesState):
224
+ return {"messages": [llm_with_tools.invoke(state["messages"])]}
225
+
226
+ def retriever_node(state: MessagesState):
227
+ results = vector_store.similarity_search(state["messages"][0].content)
228
+ example_msg = HumanMessage(
229
+ content=f"Here I provide a similar question and answer for reference:\n\n{results[0].page_content}"
230
+ )
231
+ return {"messages": [sys_msg] + state["messages"] + [example_msg]}
232
+
233
+ builder = StateGraph(MessagesState)
234
+ builder.add_node("retriever", retriever_node)
235
+ builder.add_node("assistant", assistant)
236
+ builder.add_node("tools", ToolNode(tools))
237
+ builder.add_edge(START, "retriever")
238
+ builder.add_edge("retriever", "assistant")
239
+ builder.add_conditional_edges("assistant", tools_condition)
240
+ builder.add_edge("tools", "assistant")
241
+
242
+ return builder.compile()