Spaces:
Sleeping
Sleeping
Upload 15 files
Browse files- .python-version +1 -1
- agent.py +274 -56
- app.py +12 -4
- config.yaml +46 -0
- create_vector_database.ipynb +43 -41
- create_vector_database.py +52 -0
- data/metadata.jsonl +0 -0
- prompts/prompt.yaml +53 -0
- prompts/vlm_prompt.yaml +15 -0
- pyproject.toml +51 -4
- requirements.txt +52 -13
- states.py +25 -0
- tools.py +549 -6
- utils.py +70 -2
- uv.lock +0 -0
.python-version
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
3.
|
|
|
|
| 1 |
+
3.13
|
agent.py
CHANGED
|
@@ -1,81 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
|
|
|
|
|
|
|
|
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
-
|
| 4 |
-
from langgraph.
|
| 5 |
-
from langgraph.prebuilt import ToolNode
|
| 6 |
-
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
-
from langchain_core.
|
| 10 |
from supabase.client import Client, create_client
|
| 11 |
-
|
| 12 |
-
from
|
|
|
|
|
|
|
| 13 |
|
| 14 |
load_dotenv()
|
|
|
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
embedding= embeddings,
|
| 23 |
-
table_name="gaia_documents",
|
| 24 |
-
query_name="match_documents_langchain",
|
| 25 |
-
)
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
|
|
|
|
| 37 |
llm = HuggingFaceEndpoint(
|
| 38 |
-
repo_id=
|
| 39 |
-
temperature=
|
| 40 |
-
repetition_penalty=
|
| 41 |
-
provider="
|
| 42 |
-
huggingfacehub_api_token=
|
| 43 |
)
|
| 44 |
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
agent_with_tools = agent.bind_tools(tools)
|
| 48 |
|
| 49 |
-
def retriever_node(state:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
def agent_graph():
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
#
|
|
|
|
| 68 |
workflow.add_node("retriever_node", retriever_node)
|
|
|
|
| 69 |
workflow.add_node("processor_node", processor_node)
|
| 70 |
-
workflow.add_node("tools", ToolNode(
|
| 71 |
-
|
| 72 |
-
## Add edges
|
| 73 |
-
workflow.add_edge(START, "retriever_node")
|
| 74 |
-
workflow.add_edge("retriever_node", "processor_node")
|
| 75 |
-
workflow.add_conditional_edges("processor_node", tools_condition)
|
| 76 |
-
workflow.add_edge("tools", "processor_node")
|
| 77 |
-
|
| 78 |
-
# Compile graph
|
| 79 |
-
graph = workflow.compile()
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GAIA Agent with Multi-Modal File Processing and Hybrid Retrieval.
|
| 3 |
+
|
| 4 |
+
This module defines a LangGraph agent that can:
|
| 5 |
+
1. Retrieve similar questions using Hybrid Search (Vector + BM25) and Reranking
|
| 6 |
+
2. Process files using tools (PDF, XLSX, MP3, etc.)
|
| 7 |
+
3. Answer questions using web search, calculator, and other tools
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
import os
|
| 11 |
+
import bm25s
|
| 12 |
+
import requests
|
| 13 |
+
from pathlib import Path
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
+
|
| 16 |
+
from langgraph.graph import START, END, StateGraph
|
| 17 |
+
from langgraph.prebuilt import tools_condition, ToolNode
|
| 18 |
+
|
| 19 |
+
from sentence_transformers import SentenceTransformer, CrossEncoder
|
| 20 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 21 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 22 |
from supabase.client import Client, create_client
|
| 23 |
+
|
| 24 |
+
from utils import load_config, load_prompt, init_bm25_index, reciprocal_rank_fusion
|
| 25 |
+
from tools import tools_list
|
| 26 |
+
from states import AgentState
|
| 27 |
|
| 28 |
load_dotenv()
|
| 29 |
+
config = load_config()
|
| 30 |
|
| 31 |
+
# Environment details and others
|
| 32 |
+
hf_key = os.getenv("HF_INFERENCE_KEY")
|
| 33 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 34 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 35 |
|
| 36 |
+
# ============================================
|
| 37 |
+
# Model & Embeddings Setup
|
| 38 |
+
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
enable_keyword_search = config["retrievers"]["enable_keyword_search"]
|
| 41 |
+
enable_vector_search = config["retrievers"]["enable_vector_search"]
|
| 42 |
+
|
| 43 |
+
# BM25 Retriever
|
| 44 |
+
bm25_retriever, bm25_corpus, bm25_ids = None, None, None
|
| 45 |
+
if enable_keyword_search:
|
| 46 |
+
bm25_retriever, bm25_corpus, bm25_ids = init_bm25_index(corpus_file=config["data"])
|
| 47 |
+
|
| 48 |
+
bm25_id_to_text = {}
|
| 49 |
+
if bm25_corpus and bm25_ids:
|
| 50 |
+
bm25_id_to_text = dict(zip(bm25_ids, bm25_corpus))
|
| 51 |
+
|
| 52 |
+
embeddings, supabase = None, None
|
| 53 |
+
if enable_vector_search:
|
| 54 |
+
# Embeddings for Vector Search
|
| 55 |
+
embeddings = SentenceTransformer(model_name_or_path=config["models"]["embeddings"]["model_name"], cache_folder=config["models"]["cache_folder"])
|
| 56 |
|
| 57 |
+
# Supabase Vector Store
|
| 58 |
+
supabase: Client = create_client(supabase_url, supabase_key)
|
| 59 |
|
| 60 |
+
# Reranker Model (ModernBERT Cross-Encoder)
|
| 61 |
+
reranker = CrossEncoder(config["models"]["reranker"]["model_name"], cache_folder=config["models"]["cache_folder"])
|
| 62 |
|
| 63 |
+
# LLM for Agent
|
| 64 |
llm = HuggingFaceEndpoint(
|
| 65 |
+
repo_id=config["models"]["llm"]["model_name"],
|
| 66 |
+
temperature=config["models"]["llm"]["parameters"]["temperature"],
|
| 67 |
+
repetition_penalty=config["models"]["llm"]["parameters"]["repetition_penalty"],
|
| 68 |
+
provider=config["models"]["llm"]["parameters"]["provider"],
|
| 69 |
+
huggingfacehub_api_token=hf_key
|
| 70 |
)
|
| 71 |
|
| 72 |
+
agent_llm = ChatHuggingFace(llm=llm)
|
| 73 |
+
agent_with_tools = agent_llm.bind_tools(tools_list)
|
| 74 |
+
|
| 75 |
+
_system_prompt = load_prompt("prompts/prompt.yaml")
|
| 76 |
+
_thinking_enabled = config["models"]["llm"]["parameters"].get("thinking_enabled", True)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ============================================
|
| 80 |
+
# Graph Nodes
|
| 81 |
+
# ============================================
|
| 82 |
+
|
| 83 |
+
def file_downloader_node(state: AgentState) -> AgentState:
|
| 84 |
+
"""
|
| 85 |
+
Download the task file from the scoring API if one is associated with the question.
|
| 86 |
+
Saves to a local directory and stores the path in state.
|
| 87 |
+
"""
|
| 88 |
+
print("--- FILE DOWNLOADER NODE ---")
|
| 89 |
+
file_name = state.get("file_name", "")
|
| 90 |
+
task_id = state.get("task_id", "")
|
| 91 |
+
|
| 92 |
+
if not file_name or not task_id:
|
| 93 |
+
return {"file_path": ""}
|
| 94 |
+
|
| 95 |
+
safe_name = Path(file_name).name
|
| 96 |
+
if not safe_name:
|
| 97 |
+
print(f"File download skipped: invalid file_name '{file_name}'")
|
| 98 |
+
return {"file_path": ""}
|
| 99 |
+
|
| 100 |
+
save_dir = Path(config["api"]["files_dir"]) / task_id
|
| 101 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
local_path = save_dir / safe_name
|
| 103 |
+
|
| 104 |
+
if local_path.exists():
|
| 105 |
+
print(f"File already cached: {local_path}")
|
| 106 |
+
return {"file_path": str(local_path)}
|
| 107 |
+
|
| 108 |
+
file_url = f"{config['api']['base_url']}/files/{task_id}"
|
| 109 |
+
try:
|
| 110 |
+
response = requests.get(file_url, timeout=30)
|
| 111 |
+
response.raise_for_status()
|
| 112 |
+
if not response.content:
|
| 113 |
+
print(f"File download failed ({file_url}): empty response body")
|
| 114 |
+
return {"file_path": ""}
|
| 115 |
+
local_path.write_bytes(response.content)
|
| 116 |
+
print(f"Downloaded: {safe_name} → {local_path}")
|
| 117 |
+
return {"file_path": str(local_path)}
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"File download failed ({file_url}): {e}")
|
| 120 |
+
return {"file_path": ""}
|
| 121 |
|
|
|
|
| 122 |
|
| 123 |
+
def retriever_node(state: AgentState) -> AgentState:
|
| 124 |
+
"""
|
| 125 |
+
Hybrid Search Node: Retrieve docs via Vector Search + BM25, combine with RRF.
|
| 126 |
+
"""
|
| 127 |
+
print("--- RETRIEVER NODE ---")
|
| 128 |
+
messages = state.get("messages", [])
|
| 129 |
+
if not messages:
|
| 130 |
+
return {"retrieved_docs": []}
|
| 131 |
+
|
| 132 |
+
question_content = messages[0].content
|
| 133 |
+
|
| 134 |
+
if not enable_vector_search and not enable_keyword_search:
|
| 135 |
+
print("No retrieval method enabled.")
|
| 136 |
+
return {"retrieved_docs": []}
|
| 137 |
+
|
| 138 |
+
# 1. Vector Search
|
| 139 |
+
vector_docs = []
|
| 140 |
+
if supabase and embeddings:
|
| 141 |
+
try:
|
| 142 |
+
response = supabase.rpc(
|
| 143 |
+
config["retrievers"]["vector_store"]["query"],
|
| 144 |
+
{"query_embedding": embeddings.encode(question_content).tolist(),
|
| 145 |
+
"match_count": config["retrievers"]["vector_store"]["k"],
|
| 146 |
+
"match_threshold": config["retrievers"]["vector_store"]["threshold"]
|
| 147 |
+
}
|
| 148 |
+
).execute()
|
| 149 |
|
| 150 |
+
vector_docs = response.data
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f"Vector search error: {e}")
|
| 154 |
+
|
| 155 |
+
# 2. BM25 Search
|
| 156 |
+
bm25_docs = []
|
| 157 |
+
if bm25_retriever and bm25_corpus and bm25_ids:
|
| 158 |
+
try:
|
| 159 |
+
query_tokens = bm25s.tokenize([question_content], stopwords="en")
|
| 160 |
+
results, scores = bm25_retriever.retrieve(query_tokens, k=config["retrievers"]["bm25"]["k"])
|
| 161 |
+
indices = results[0]
|
| 162 |
+
|
| 163 |
+
for i, idx in enumerate(indices):
|
| 164 |
+
content = bm25_corpus[idx]
|
| 165 |
+
task_id = bm25_ids[idx]
|
| 166 |
+
score = scores[0][i]
|
| 167 |
+
bm25_dict = {"content":content, "metadata": {"source": "bm25_search", "task_id": task_id, "score": score}}
|
| 168 |
+
bm25_docs.append(bm25_dict)
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f"BM25 search error: {e}")
|
| 171 |
+
|
| 172 |
+
# 3. RRF Fusion
|
| 173 |
+
final_candidates = []
|
| 174 |
+
if vector_docs and bm25_docs:
|
| 175 |
+
fused = reciprocal_rank_fusion([vector_docs, bm25_docs])
|
| 176 |
+
final_candidates = [id for id, doc, score in fused]
|
| 177 |
+
else:
|
| 178 |
+
final_candidates = vector_docs + bm25_docs
|
| 179 |
+
final_candidates = [doc["metadata"]["task_id"] for doc in final_candidates]
|
| 180 |
|
| 181 |
+
top_candidates = final_candidates[:20]
|
| 182 |
+
|
| 183 |
+
return {"retrieved_docs": top_candidates}
|
| 184 |
|
| 185 |
+
|
| 186 |
+
def reranker_node(state: AgentState) -> AgentState:
|
| 187 |
+
"""
|
| 188 |
+
Reranker Node: Re-order candidates using Cross-Encoder and return top 3.
|
| 189 |
+
"""
|
| 190 |
+
print("--- RERANKER NODE ---")
|
| 191 |
+
candidates = state.get("retrieved_docs", [])
|
| 192 |
+
messages = state.get("messages", [])
|
| 193 |
+
|
| 194 |
+
if not candidates or not messages:
|
| 195 |
+
return {"messages": []}
|
| 196 |
+
|
| 197 |
+
question = messages[0].content
|
| 198 |
+
|
| 199 |
+
# Deduplicate candidates — candidates are task_id strings; resolve to text via corpus lookup
|
| 200 |
+
unique_candidates = []
|
| 201 |
+
seen_content = set()
|
| 202 |
+
for task_id in candidates:
|
| 203 |
+
text = bm25_id_to_text.get(task_id)
|
| 204 |
+
if text and text not in seen_content:
|
| 205 |
+
unique_candidates.append(text)
|
| 206 |
+
seen_content.add(text)
|
| 207 |
+
|
| 208 |
+
if not unique_candidates:
|
| 209 |
+
return {"messages": []}
|
| 210 |
+
|
| 211 |
+
pairs = [[question, doc_text] for doc_text in unique_candidates]
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
scores = reranker.predict(pairs)
|
| 215 |
+
|
| 216 |
+
scored_docs = sorted(
|
| 217 |
+
zip(unique_candidates, scores),
|
| 218 |
+
key=lambda x: x[1],
|
| 219 |
+
reverse=True
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
top_k = config["retrievers"]["final_rrf_k"]
|
| 223 |
+
top_results = scored_docs[:top_k]
|
| 224 |
+
|
| 225 |
+
context_str = "Here are similar questions and answers for reference:\n\n"
|
| 226 |
+
for i, (doc_text, score) in enumerate(top_results):
|
| 227 |
+
context_str += f"--- Example {i+1} (Score: {score:.2f}) ---\n{doc_text}\n\n"
|
| 228 |
+
|
| 229 |
+
context_message = HumanMessage(content=context_str)
|
| 230 |
+
|
| 231 |
+
return {"messages": [context_message]}
|
| 232 |
+
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"Reranker error: {e}")
|
| 235 |
+
if unique_candidates:
|
| 236 |
+
fallback_msg = HumanMessage(content=f"Reference (Fallback):\n{unique_candidates[0]}")
|
| 237 |
+
return {"messages": [fallback_msg]}
|
| 238 |
+
|
| 239 |
+
return {"messages": []}
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def processor_node(state: AgentState) -> AgentState:
|
| 243 |
+
"""
|
| 244 |
+
Processor Node: Main LLM agent that answers the question.
|
| 245 |
+
"""
|
| 246 |
+
prompt_content = _system_prompt.content + ("" if _thinking_enabled else "\n/no_think")
|
| 247 |
+
system_prompt = SystemMessage(content=prompt_content)
|
| 248 |
+
messages = state.get("messages", [])
|
| 249 |
+
file_name = state.get("file_name", "")
|
| 250 |
+
file_path = state.get("file_path", "")
|
| 251 |
+
|
| 252 |
+
full_messages = [system_prompt]
|
| 253 |
+
|
| 254 |
+
if file_name:
|
| 255 |
+
if file_path:
|
| 256 |
+
file_msg = HumanMessage(
|
| 257 |
+
content=f"Note: A file named '{file_name}' is associated with this question. It is available at path: {file_path}"
|
| 258 |
+
)
|
| 259 |
+
else:
|
| 260 |
+
file_msg = HumanMessage(
|
| 261 |
+
content=f"Note: A file named '{file_name}' is associated with this question, but it could not be downloaded."
|
| 262 |
+
)
|
| 263 |
+
full_messages.append(file_msg)
|
| 264 |
+
|
| 265 |
+
full_messages.extend(messages)
|
| 266 |
+
|
| 267 |
+
response = agent_with_tools.invoke(full_messages)
|
| 268 |
+
|
| 269 |
+
return {"messages": [response]}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# ============================================
|
| 273 |
+
# Graph Construction
|
| 274 |
+
# ============================================
|
| 275 |
|
| 276 |
def agent_graph():
|
| 277 |
+
"""
|
| 278 |
+
Build and compile the agent graph.
|
| 279 |
+
"""
|
| 280 |
+
workflow = StateGraph(AgentState)
|
| 281 |
|
| 282 |
+
# Add nodes
|
| 283 |
+
workflow.add_node("file_downloader_node", file_downloader_node)
|
| 284 |
workflow.add_node("retriever_node", retriever_node)
|
| 285 |
+
workflow.add_node("reranker_node", reranker_node)
|
| 286 |
workflow.add_node("processor_node", processor_node)
|
| 287 |
+
workflow.add_node("tools", ToolNode(tools_list))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
+
# Add edges
|
| 290 |
+
workflow.add_edge(START, "file_downloader_node")
|
| 291 |
+
workflow.add_edge("file_downloader_node", "retriever_node")
|
| 292 |
+
workflow.add_edge("retriever_node", "reranker_node")
|
| 293 |
+
workflow.add_edge("reranker_node", "processor_node")
|
| 294 |
+
workflow.add_edge("tools", "processor_node")
|
| 295 |
+
workflow.add_conditional_edges("processor_node", tools_condition, {"tools": "tools", END: END})
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
compiled = workflow.compile()
|
| 299 |
+
return compiled.with_config({"recursion_limit": config["graph"]["recursion_limit"]})
|
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
import requests
|
| 4 |
import inspect
|
|
@@ -16,10 +17,16 @@ class BasicAgent:
|
|
| 16 |
def __init__(self):
|
| 17 |
self.agent = agent_graph()
|
| 18 |
print("BasicAgent initialized.")
|
| 19 |
-
def __call__(self, question: str) -> str:
|
| 20 |
messages = [HumanMessage(content=question)]
|
| 21 |
-
response = self.agent.invoke({
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 24 |
return fixed_answer
|
| 25 |
|
|
@@ -80,11 +87,12 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 80 |
for item in questions_data:
|
| 81 |
task_id = item.get("task_id")
|
| 82 |
question_text = item.get("question")
|
|
|
|
| 83 |
if not task_id or question_text is None:
|
| 84 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 85 |
continue
|
| 86 |
try:
|
| 87 |
-
submitted_answer = agent(question_text)
|
| 88 |
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 89 |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 90 |
except Exception as e:
|
|
|
|
| 1 |
import os
|
| 2 |
+
import re
|
| 3 |
import gradio as gr
|
| 4 |
import requests
|
| 5 |
import inspect
|
|
|
|
| 17 |
def __init__(self):
|
| 18 |
self.agent = agent_graph()
|
| 19 |
print("BasicAgent initialized.")
|
| 20 |
+
def __call__(self, question: str, task_id: str = "", file_name: str = "") -> str:
|
| 21 |
messages = [HumanMessage(content=question)]
|
| 22 |
+
response = self.agent.invoke({
|
| 23 |
+
"messages": messages,
|
| 24 |
+
"task_id": task_id,
|
| 25 |
+
"file_name": file_name
|
| 26 |
+
})
|
| 27 |
+
content = response['messages'][-1].content
|
| 28 |
+
match = re.search(r'FINAL ANSWER:\s*(.*)', content, re.DOTALL)
|
| 29 |
+
fixed_answer = match.group(1).strip() if match else content.strip()
|
| 30 |
print(f"Agent returning fixed answer: {fixed_answer}")
|
| 31 |
return fixed_answer
|
| 32 |
|
|
|
|
| 87 |
for item in questions_data:
|
| 88 |
task_id = item.get("task_id")
|
| 89 |
question_text = item.get("question")
|
| 90 |
+
file_name = item.get("file_name", "")
|
| 91 |
if not task_id or question_text is None:
|
| 92 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 93 |
continue
|
| 94 |
try:
|
| 95 |
+
submitted_answer = agent(question_text, task_id=task_id, file_name=file_name)
|
| 96 |
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 97 |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 98 |
except Exception as e:
|
config.yaml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project: LangGraph HF Agent for GAIA
|
| 2 |
+
# config.yaml
|
| 3 |
+
data: "data/metadata.jsonl" # Path to the GAIA documents dataset
|
| 4 |
+
retrievers:
|
| 5 |
+
enable_vector_search: true # Enable vector-based document retrieval
|
| 6 |
+
enable_keyword_search: true # Enable keyword-based document retrieval
|
| 7 |
+
final_rrf_k: 3 # Number of top documents to consider after reciprocal rank fusion
|
| 8 |
+
vector_store:
|
| 9 |
+
table: "gaia_documents" # Type of vector store (e.g., faiss, chroma)
|
| 10 |
+
query: "match_documents" # Method to query the vector store
|
| 11 |
+
k: 10 # Number of top documents to retrieve
|
| 12 |
+
threshold: 0.5 # Similarity threshold for document retrieval
|
| 13 |
+
bm25:
|
| 14 |
+
k: 5 # Number of top documents to retrieve using keyword search
|
| 15 |
+
models:
|
| 16 |
+
cache_folder: "./models/hf_cache" # Directory to cache Hugging Face models
|
| 17 |
+
embeddings:
|
| 18 |
+
model_name: "Alibaba-NLP/gte-modernbert-base" # Hugging Face embedding model ID
|
| 19 |
+
reranker:
|
| 20 |
+
model_name: "Alibaba-NLP/gte-reranker-modernbert-base" # Hugging Face model ID for reranking
|
| 21 |
+
llm:
|
| 22 |
+
model_name: "Qwen/Qwen3-32B-Instruct" # Hugging Face model ID
|
| 23 |
+
parameters:
|
| 24 |
+
temperature: 0
|
| 25 |
+
repetition_penalty: 1.3
|
| 26 |
+
provider: "auto"
|
| 27 |
+
thinking_enabled: false
|
| 28 |
+
vlm:
|
| 29 |
+
model_name: "Qwen/Qwen3-VL-32B-Instruct" # Hugging Face model ID
|
| 30 |
+
asr:
|
| 31 |
+
model_name: "distil-whisper/distil-large-v3" # Hugging Face model ID
|
| 32 |
+
#device: "cuda" # cpu, cuda, or mps (for Mac)
|
| 33 |
+
#parameters:
|
| 34 |
+
# temperature: 0.7
|
| 35 |
+
# max_new_tokens: 512
|
| 36 |
+
# repetition_penalty: 1.1
|
| 37 |
+
|
| 38 |
+
graph:
|
| 39 |
+
recursion_limit: 20 # Max steps before the graph terminates
|
| 40 |
+
thread_id: "default-user" # Default session identifier
|
| 41 |
+
memory_type: "sqlite" # Persistence method for checkpointers
|
| 42 |
+
|
| 43 |
+
api:
|
| 44 |
+
base_url: "https://agents-course-unit4-scoring.hf.space"
|
| 45 |
+
files_dir: "./data/task_files" # Local directory for downloaded task files
|
| 46 |
+
|
create_vector_database.ipynb
CHANGED
|
@@ -2,78 +2,80 @@
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "code",
|
| 5 |
-
"execution_count":
|
| 6 |
"id": "a9f7a25f",
|
| 7 |
"metadata": {},
|
| 8 |
-
"outputs": [
|
| 9 |
-
{
|
| 10 |
-
"name": "stderr",
|
| 11 |
-
"output_type": "stream",
|
| 12 |
-
"text": [
|
| 13 |
-
"/home/kpatelis/projects/Agents_Course_Assignment/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
| 14 |
-
" from .autonotebook import tqdm as notebook_tqdm\n"
|
| 15 |
-
]
|
| 16 |
-
}
|
| 17 |
-
],
|
| 18 |
"source": [
|
|
|
|
| 19 |
"import os\n",
|
| 20 |
"import json\n",
|
| 21 |
"from dotenv import load_dotenv\n",
|
| 22 |
"from supabase.client import Client, create_client\n",
|
| 23 |
-
"from
|
| 24 |
-
"from
|
| 25 |
"\n",
|
| 26 |
-
"load_dotenv()"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
"metadata": {},
|
| 34 |
-
"outputs": [],
|
| 35 |
-
"source": [
|
| 36 |
-
"supabase: Client = create_client(\n",
|
| 37 |
-
" os.environ.get(\"SUPABASE_URL\"), \n",
|
| 38 |
-
" os.environ.get(\"SUPABASE_SERVICE_KEY\"))\n",
|
| 39 |
"\n",
|
| 40 |
-
"
|
|
|
|
| 41 |
]
|
| 42 |
},
|
| 43 |
{
|
| 44 |
"cell_type": "code",
|
| 45 |
-
"execution_count":
|
| 46 |
"id": "f2c5492b",
|
| 47 |
"metadata": {},
|
| 48 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
"source": [
|
| 50 |
-
"
|
|
|
|
| 51 |
" json_list = list(jsonl_file)\n",
|
| 52 |
"\n",
|
| 53 |
"documents = []\n",
|
| 54 |
"for json_str in json_list:\n",
|
| 55 |
" json_data = json.loads(json_str)\n",
|
| 56 |
-
" content = f\"
|
| 57 |
-
" embedding = embeddings.
|
| 58 |
" document = {\n",
|
| 59 |
-
" \"content\"
|
| 60 |
-
" \"metadata\"
|
| 61 |
-
" \"source\"
|
|
|
|
| 62 |
" },\n",
|
| 63 |
-
" \"embedding\"
|
| 64 |
" }\n",
|
| 65 |
" documents.append(document)"
|
| 66 |
]
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"cell_type": "code",
|
| 70 |
-
"execution_count":
|
| 71 |
"id": "26ddbafd",
|
| 72 |
"metadata": {},
|
| 73 |
"outputs": [],
|
| 74 |
"source": [
|
| 75 |
-
"#
|
| 76 |
-
"
|
|
|
|
|
|
|
| 77 |
"try:\n",
|
| 78 |
" response = (\n",
|
| 79 |
" supabase.table(\"gaia_documents\")\n",
|
|
@@ -87,7 +89,7 @@
|
|
| 87 |
],
|
| 88 |
"metadata": {
|
| 89 |
"kernelspec": {
|
| 90 |
-
"display_name": "
|
| 91 |
"language": "python",
|
| 92 |
"name": "python3"
|
| 93 |
},
|
|
@@ -101,7 +103,7 @@
|
|
| 101 |
"name": "python",
|
| 102 |
"nbconvert_exporter": "python",
|
| 103 |
"pygments_lexer": "ipython3",
|
| 104 |
-
"version": "3.
|
| 105 |
}
|
| 106 |
},
|
| 107 |
"nbformat": 4,
|
|
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
"id": "a9f7a25f",
|
| 7 |
"metadata": {},
|
| 8 |
+
"outputs": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"source": [
|
| 10 |
+
"# Loading environment variables and initializing Supabase client and SentenceTransformer model\n",
|
| 11 |
"import os\n",
|
| 12 |
"import json\n",
|
| 13 |
"from dotenv import load_dotenv\n",
|
| 14 |
"from supabase.client import Client, create_client\n",
|
| 15 |
+
"from sentence_transformers import SentenceTransformer\n",
|
| 16 |
+
"from utils import load_config\n",
|
| 17 |
"\n",
|
| 18 |
+
"load_dotenv()\n",
|
| 19 |
+
"\n",
|
| 20 |
+
"config = load_config()\n",
|
| 21 |
+
"data = config[\"data\"]\n",
|
| 22 |
+
"\n",
|
| 23 |
+
"supabase_url = os.getenv(\"SUPABASE_URL\")\n",
|
| 24 |
+
"supabase_key = os.getenv(\"SUPABASE_SERVICE_KEY\")\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
"\n",
|
| 26 |
+
"supabase: Client = create_client(supabase_url, supabase_key)\n",
|
| 27 |
+
"embeddings = SentenceTransformer(model_name_or_path=config[\"vector_store\"][\"embedding_model_name\"], cache_folder=config[\"models\"][\"cache_folder\"])"
|
| 28 |
]
|
| 29 |
},
|
| 30 |
{
|
| 31 |
"cell_type": "code",
|
| 32 |
+
"execution_count": 2,
|
| 33 |
"id": "f2c5492b",
|
| 34 |
"metadata": {},
|
| 35 |
+
"outputs": [
|
| 36 |
+
{
|
| 37 |
+
"name": "stderr",
|
| 38 |
+
"output_type": "stream",
|
| 39 |
+
"text": [
|
| 40 |
+
"/home/kpatelis/projects/gaia/.venv/lib/python3.13/site-packages/torch/_dynamo/guards.py:1114: RuntimeWarning: Guards may run slower on Python 3.13.0. Consider upgrading to Python 3.13.1+.\n",
|
| 41 |
+
" warnings.warn(\n",
|
| 42 |
+
"/home/kpatelis/projects/gaia/.venv/lib/python3.13/site-packages/torch/_dynamo/guards.py:1114: RuntimeWarning: Guards may run slower on Python 3.13.0. Consider upgrading to Python 3.13.1+.\n",
|
| 43 |
+
" warnings.warn(\n"
|
| 44 |
+
]
|
| 45 |
+
}
|
| 46 |
+
],
|
| 47 |
"source": [
|
| 48 |
+
"# Reading JSONL file and creating documents with embeddings\n",
|
| 49 |
+
"with open(data, 'r') as jsonl_file:\n",
|
| 50 |
" json_list = list(jsonl_file)\n",
|
| 51 |
"\n",
|
| 52 |
"documents = []\n",
|
| 53 |
"for json_str in json_list:\n",
|
| 54 |
" json_data = json.loads(json_str)\n",
|
| 55 |
+
" content = f\"{json_data['Question']}\"\n",
|
| 56 |
+
" embedding = embeddings.encode(content, normalize_embeddings=True).tolist()\n",
|
| 57 |
" document = {\n",
|
| 58 |
+
" \"content\": content,\n",
|
| 59 |
+
" \"metadata\": {\n",
|
| 60 |
+
" \"source\": \"vector_search\",\n",
|
| 61 |
+
" \"task_id\": json_data['task_id']\n",
|
| 62 |
" },\n",
|
| 63 |
+
" \"embedding\": embedding,\n",
|
| 64 |
" }\n",
|
| 65 |
" documents.append(document)"
|
| 66 |
]
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"cell_type": "code",
|
| 70 |
+
"execution_count": 3,
|
| 71 |
"id": "26ddbafd",
|
| 72 |
"metadata": {},
|
| 73 |
"outputs": [],
|
| 74 |
"source": [
|
| 75 |
+
"# Inserting documents into Supabase\n",
|
| 76 |
+
"\n",
|
| 77 |
+
"# Note1: pgvector needs to be enabled, to turn to vector database\n",
|
| 78 |
+
"# Note2: Table needs to be created beforehand in Supabase, with column types\n",
|
| 79 |
"try:\n",
|
| 80 |
" response = (\n",
|
| 81 |
" supabase.table(\"gaia_documents\")\n",
|
|
|
|
| 89 |
],
|
| 90 |
"metadata": {
|
| 91 |
"kernelspec": {
|
| 92 |
+
"display_name": "gaia",
|
| 93 |
"language": "python",
|
| 94 |
"name": "python3"
|
| 95 |
},
|
|
|
|
| 103 |
"name": "python",
|
| 104 |
"nbconvert_exporter": "python",
|
| 105 |
"pygments_lexer": "ipython3",
|
| 106 |
+
"version": "3.13.0"
|
| 107 |
}
|
| 108 |
},
|
| 109 |
"nbformat": 4,
|
create_vector_database.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Populate the Supabase vector store with GAIA benchmark embeddings.
|
| 3 |
+
|
| 4 |
+
Prerequisites:
|
| 5 |
+
- SUPABASE_URL and SUPABASE_SERVICE_KEY in .env
|
| 6 |
+
- pgvector extension enabled in Supabase
|
| 7 |
+
- gaia_documents table created with columns: content (text), metadata (jsonb), embedding (vector)
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from supabase.client import Client, create_client
|
| 13 |
+
from sentence_transformers import SentenceTransformer
|
| 14 |
+
from utils import load_config
|
| 15 |
+
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
config = load_config()
|
| 19 |
+
data_path = config["data"]
|
| 20 |
+
|
| 21 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 22 |
+
supabase_key = os.getenv("SUPABASE_SERVICE_KEY")
|
| 23 |
+
|
| 24 |
+
supabase: Client = create_client(supabase_url, supabase_key)
|
| 25 |
+
embeddings = SentenceTransformer(
|
| 26 |
+
model_name_or_path=config["vector_store"]["embedding_model_name"],
|
| 27 |
+
cache_folder=config["models"]["cache_folder"],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
with open(data_path, "r") as jsonl_file:
|
| 31 |
+
json_list = list(jsonl_file)
|
| 32 |
+
|
| 33 |
+
documents = []
|
| 34 |
+
for json_str in json_list:
|
| 35 |
+
json_data = json.loads(json_str)
|
| 36 |
+
content = json_data["Question"]
|
| 37 |
+
embedding = embeddings.encode(content, normalize_embeddings=True).tolist()
|
| 38 |
+
documents.append({
|
| 39 |
+
"content": content,
|
| 40 |
+
"metadata": {
|
| 41 |
+
"source": "vector_search",
|
| 42 |
+
"task_id": json_data["task_id"],
|
| 43 |
+
},
|
| 44 |
+
"embedding": embedding,
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
print(f"Inserting {len(documents)} documents into Supabase...")
|
| 48 |
+
try:
|
| 49 |
+
response = supabase.table("gaia_documents").insert(documents).execute()
|
| 50 |
+
print("Done.")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print("Error inserting data into Supabase:", e)
|
data/metadata.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
prompts/prompt.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: "GAIA Agent"
|
| 2 |
+
prompt: |
|
| 3 |
+
# CONTEXT & PERSONA
|
| 4 |
+
You are an elite autonomous agent designed to solve the GAIA benchmark. Your goal is to solve complex, multi-step problems that require reasoning, file processing, and internet research. You have access to a suite of powerful tools to interact with the world and process data.
|
| 5 |
+
|
| 6 |
+
# TOOL USAGE STRATEGY
|
| 7 |
+
1. **File First**: If the user mentions a file (e.g., "attached", "spreadsheet", "image"), YOU MUST PROCESS IT IMMEDIATELY.
|
| 8 |
+
- Look for file path information provided in the context.
|
| 9 |
+
- Select the specific tool for the file type (e.g., `read_excel` for .xlsx, `read_pdf` for .pdf).
|
| 10 |
+
- If the specific tool fails, fallback to the generic `read_file` tool.
|
| 11 |
+
- For images, use `analyze_image` with a specific question about what you need to extract from the image.
|
| 12 |
+
2. **Search Smart**: If you need external information, use `duck_web_search` or `tavily_web_search`.
|
| 13 |
+
- Be specific in your queries.
|
| 14 |
+
- Use `wiki_search` for broad factual context.
|
| 15 |
+
- If a search result points to a specific URL that likely contains the answer, use `fetch_webpage` to read the full page — do not rely on the search snippet alone.
|
| 16 |
+
3. **Calculate Precisely**: Use the `calculator` tool for math. Note that `read_excel` and `read_csv` already include column statistics (sum, min, max, mean) — check those first before reaching for `calculator`.
|
| 17 |
+
4. **Run Code Directly**: If a question asks what a Python script outputs, use `python_eval` to execute it. Do not try to trace execution mentally.
|
| 18 |
+
|
| 19 |
+
# REASONING FRAMEWORK
|
| 20 |
+
Follow this Chain-of-Thought (CoT) process for every step:
|
| 21 |
+
1. **Analyze**: What is the core question? What data do I have? What is missing?
|
| 22 |
+
2. **Plan**: What is the next best tool to use? Why?
|
| 23 |
+
3. **Execute**: Call the tool.
|
| 24 |
+
4. **Observe**: Analyze the tool output. Does it answer the question?
|
| 25 |
+
5. **Refine**: If the output is insufficient, adjust the plan and try a different angle.
|
| 26 |
+
|
| 27 |
+
# CRITICAL OUTPUT RULES
|
| 28 |
+
The automated scoring system is EXTREMELY STRICT. You must follow these formatting rules exactly:
|
| 29 |
+
- **Numeric Answers**:
|
| 30 |
+
- Output ONLY the number.
|
| 31 |
+
- NO commas (e.g., write `1000000`, NOT `1,000,000`).
|
| 32 |
+
- NO units or symbols (e.g., write `50`, NOT `$50` or `50%`, unless explicitly asked for the unit string).
|
| 33 |
+
- **String Answers**:
|
| 34 |
+
- Be concise.
|
| 35 |
+
- NO articles (a, an, the).
|
| 36 |
+
- NO abbreviations usually, unless standard (e.g., 'USA' might be okay, but 'Sept' for September is risky).
|
| 37 |
+
- **Final Format**:
|
| 38 |
+
- Your final line MUST be exactly: `FINAL ANSWER: <your_answer>`
|
| 39 |
+
- Do not put proper sentences in the final answer, just the raw value.
|
| 40 |
+
|
| 41 |
+
# EXAMPLE SCENARIOS
|
| 42 |
+
- Question: "What is the sum of the 'Total' column in the attached file.xlsx?"
|
| 43 |
+
Thought: I need to read the excel file first using `read_excel`. Then I will sum the values using `calculator`.
|
| 44 |
+
Action: `read_excel(file_path="...")`
|
| 45 |
+
Observation: (Dataframe output...)
|
| 46 |
+
Thought: I have the numbers. Sum is 500 + 200...
|
| 47 |
+
Action: `calculator(a=500, b=200, type="addition")`
|
| 48 |
+
FINAL ANSWER: 700
|
| 49 |
+
|
| 50 |
+
- Question: "Which city is the capital of France?"
|
| 51 |
+
FINAL ANSWER: Paris
|
| 52 |
+
|
| 53 |
+
Failure to follow these rules will result in a score of 0. Take a deep breath and think step by step.
|
prompts/vlm_prompt.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: "GAIA VLM Assistant"
|
| 2 |
+
prompt: |
|
| 3 |
+
# ROLE
|
| 4 |
+
You are an expert visual assistant AI capable of analyzing images with high precision. Your goal is to extract visual information to answer a SPECIFIC question provided by the user.
|
| 5 |
+
|
| 6 |
+
# INSTRUCTION
|
| 7 |
+
- Analyze the provided image thoroughly.
|
| 8 |
+
- Focus specifically on the information needed to answer the user's question.
|
| 9 |
+
- Provide a clear, detailed description of the relevant visual elements.
|
| 10 |
+
- If the image contains text (documents, charts, screenshots), transcribe or summarize it accurately.
|
| 11 |
+
- Do NOT generate unrelated descriptions. Stick to the context of the question.
|
| 12 |
+
|
| 13 |
+
# FORMAT
|
| 14 |
+
- Start with a direct answer or observation related to the question.
|
| 15 |
+
- Follow with supporting details from the image.
|
pyproject.toml
CHANGED
|
@@ -1,7 +1,54 @@
|
|
| 1 |
[project]
|
| 2 |
-
name = "
|
| 3 |
version = "0.1.0"
|
| 4 |
-
description = "
|
| 5 |
readme = "README.md"
|
| 6 |
-
requires-python = ">=3.
|
| 7 |
-
dependencies = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
[project]
|
| 2 |
+
name = "gaia"
|
| 3 |
version = "0.1.0"
|
| 4 |
+
description = "GAIA Benchmark Agent with Multi-Modal File Processing"
|
| 5 |
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.13"
|
| 7 |
+
dependencies = [
|
| 8 |
+
# Core LangChain
|
| 9 |
+
"langchain>=1.1.3",
|
| 10 |
+
"langchain-huggingface>=1.1.0",
|
| 11 |
+
"langchain-community>=0.4.1",
|
| 12 |
+
"langgraph>=1.0.4",
|
| 13 |
+
# HuggingFace
|
| 14 |
+
"huggingface-hub>=0.36.0",
|
| 15 |
+
"transformers>=4.46.0",
|
| 16 |
+
"accelerate>=1.0.0",
|
| 17 |
+
# Vector store
|
| 18 |
+
"sentence-transformers>=5.2.0",
|
| 19 |
+
"supabase>=2.25.1",
|
| 20 |
+
# Document processing
|
| 21 |
+
"pypdf>=4.0.0",
|
| 22 |
+
"python-docx>=1.1.0",
|
| 23 |
+
"python-pptx>=0.6.23",
|
| 24 |
+
# Data processing (polars)
|
| 25 |
+
"polars>=1.0.0",
|
| 26 |
+
"xlsx2csv>=0.8.0",
|
| 27 |
+
# Science
|
| 28 |
+
"biopython>=1.82",
|
| 29 |
+
"numpy>=2.0.0",
|
| 30 |
+
# Image
|
| 31 |
+
"pillow>=10.0.0",
|
| 32 |
+
# Audio
|
| 33 |
+
"librosa>=0.10.0",
|
| 34 |
+
"soundfile>=0.12.0",
|
| 35 |
+
# Web tools
|
| 36 |
+
"ddgs>=9.0.0",
|
| 37 |
+
"tavily-python>=0.5.0",
|
| 38 |
+
"wikipedia>=1.4.0",
|
| 39 |
+
"arxiv>=2.1.0",
|
| 40 |
+
"trafilatura>=1.6.0",
|
| 41 |
+
"openpyxl>=3.1.0",
|
| 42 |
+
# Utilities
|
| 43 |
+
"python-dotenv>=1.2.1",
|
| 44 |
+
"pyyaml>=6.0.0",
|
| 45 |
+
"requests>=2.31.0",
|
| 46 |
+
"tqdm>=4.67.1",
|
| 47 |
+
"gradio>=4.0.0",
|
| 48 |
+
"ipykernel>=7.1.0",
|
| 49 |
+
"bm25s>=0.2.14",
|
| 50 |
+
"jieba>=0.42.1",
|
| 51 |
+
"jupyter>=1.1.1",
|
| 52 |
+
"ipywidgets>=8.1.8",
|
| 53 |
+
"torch>=2.9.1",
|
| 54 |
+
]
|
requirements.txt
CHANGED
|
@@ -1,16 +1,55 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
python-dotenv
|
| 4 |
-
pgvector
|
| 5 |
-
supabase
|
| 6 |
-
huggingface_hub
|
| 7 |
-
sentence-transformers
|
| 8 |
-
langchain
|
| 9 |
langchain-core
|
| 10 |
-
langchain-huggingface
|
|
|
|
| 11 |
langchain-tavily
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
pymupdf
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core LangChain
|
| 2 |
+
langchain>=1.1.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
langchain-core
|
| 4 |
+
langchain-huggingface>=1.1.0
|
| 5 |
+
langchain-community>=0.4.1
|
| 6 |
langchain-tavily
|
| 7 |
+
langgraph>=1.0.4
|
| 8 |
+
|
| 9 |
+
# HuggingFace
|
| 10 |
+
huggingface-hub>=0.36.0
|
| 11 |
+
transformers>=4.46.0
|
| 12 |
+
accelerate>=1.0.0
|
| 13 |
+
|
| 14 |
+
# Vector store
|
| 15 |
+
sentence-transformers>=5.2.0
|
| 16 |
+
supabase>=2.25.1
|
| 17 |
+
pgvector
|
| 18 |
+
|
| 19 |
+
# Document processing
|
| 20 |
+
pypdf>=4.0.0
|
| 21 |
pymupdf
|
| 22 |
+
python-docx>=1.1.0
|
| 23 |
+
python-pptx>=0.6.23
|
| 24 |
+
|
| 25 |
+
# Data processing (polars)
|
| 26 |
+
polars>=1.0.0
|
| 27 |
+
xlsx2csv>=0.8.0
|
| 28 |
+
|
| 29 |
+
# Science
|
| 30 |
+
biopython>=1.82
|
| 31 |
+
numpy>=2.0.0
|
| 32 |
+
|
| 33 |
+
# Image
|
| 34 |
+
pillow>=10.0.0
|
| 35 |
+
|
| 36 |
+
# Audio
|
| 37 |
+
librosa>=0.10.0
|
| 38 |
+
soundfile>=0.12.0
|
| 39 |
+
|
| 40 |
+
# Web tools
|
| 41 |
+
ddgs>=9.0.0
|
| 42 |
+
tavily-python>=0.5.0
|
| 43 |
+
wikipedia>=1.4.0
|
| 44 |
+
arxiv>=2.1.0
|
| 45 |
+
trafilatura>=1.6.0
|
| 46 |
+
|
| 47 |
+
# Excel
|
| 48 |
+
openpyxl>=3.1.0
|
| 49 |
+
|
| 50 |
+
# Utilities
|
| 51 |
+
python-dotenv>=1.2.1
|
| 52 |
+
pyyaml>=6.0.0
|
| 53 |
+
requests>=2.31.0
|
| 54 |
+
tqdm>=4.67.1
|
| 55 |
+
gradio>=4.0.0
|
states.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, TypedDict, List
|
| 2 |
+
from langgraph.graph.message import add_messages
|
| 3 |
+
from langchain_core.messages import BaseMessage
|
| 4 |
+
from langchain_core.documents import Document
|
| 5 |
+
|
| 6 |
+
# ============================================
|
| 7 |
+
# State Definition
|
| 8 |
+
# ============================================
|
| 9 |
+
|
| 10 |
+
class AgentState(TypedDict):
|
| 11 |
+
"""
|
| 12 |
+
State schema for the GAIA agent graph.
|
| 13 |
+
|
| 14 |
+
Attributes:
|
| 15 |
+
messages: List of conversation messages (auto-accumulated via add_messages)
|
| 16 |
+
task_id: The GAIA task identifier for the current question
|
| 17 |
+
file_name: Name of the attached file (empty string if no file)
|
| 18 |
+
file_path: Local filesystem path to the downloaded file (empty if no file or download failed)
|
| 19 |
+
retrieved_docs: List of candidate documents from the retriever node
|
| 20 |
+
"""
|
| 21 |
+
messages: Annotated[list[BaseMessage], add_messages]
|
| 22 |
+
task_id: str
|
| 23 |
+
file_name: str
|
| 24 |
+
file_path: str
|
| 25 |
+
retrieved_docs: List[Document]
|
tools.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
| 1 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from langchain_community.tools import DuckDuckGoSearchRun
|
| 4 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
|
@@ -6,6 +12,35 @@ from langchain_community.document_loaders import WikipediaLoader
|
|
| 6 |
from langchain_community.document_loaders import ArxivLoader
|
| 7 |
from langchain_core.tools import tool
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
@tool
|
| 10 |
def calculator(a: float, b: float, type: str) -> float:
|
| 11 |
"""Performs mathematical calculations, addition, subtraction, multiplication, division, modulus.
|
|
@@ -26,9 +61,9 @@ def calculator(a: float, b: float, type: str) -> float:
|
|
| 26 |
raise ValueError("Cannot divide by zero.")
|
| 27 |
return a / b
|
| 28 |
elif type == "modulus":
|
| 29 |
-
a % b
|
| 30 |
else:
|
| 31 |
-
TypeError(f"{type} is not an option for type, choose one of addition, subtraction, multiplication, division, modulus")
|
| 32 |
|
| 33 |
@tool
|
| 34 |
def duck_web_search(query: str) -> str:
|
|
@@ -37,7 +72,7 @@ def duck_web_search(query: str) -> str:
|
|
| 37 |
Args:
|
| 38 |
query: The search query.
|
| 39 |
"""
|
| 40 |
-
search =
|
| 41 |
|
| 42 |
return {"duckduckgo_web_search": search}
|
| 43 |
|
|
@@ -75,11 +110,519 @@ def tavily_web_search(query: str) -> str:
|
|
| 75 |
|
| 76 |
Args:
|
| 77 |
query: The search query."""
|
| 78 |
-
|
| 79 |
-
search_documents = search_engine.invoke(input=query)
|
| 80 |
web_results = "\n\n---\n\n".join(
|
| 81 |
[
|
| 82 |
f'Document title: {document["title"]}. Contents: {document["content"]}. Relevance Score: {document["score"]}'
|
| 83 |
for document in search_documents
|
| 84 |
])
|
| 85 |
-
return {"web_results": web_results}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
import json
|
| 3 |
+
import base64
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
load_dotenv()
|
| 8 |
|
| 9 |
from langchain_community.tools import DuckDuckGoSearchRun
|
| 10 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
|
|
|
| 12 |
from langchain_community.document_loaders import ArxivLoader
|
| 13 |
from langchain_core.tools import tool
|
| 14 |
|
| 15 |
+
from huggingface_hub import InferenceClient
|
| 16 |
+
|
| 17 |
+
from utils import load_config, load_prompt
|
| 18 |
+
|
| 19 |
+
_config = load_config()
|
| 20 |
+
_vlm_model_name = _config["models"]["vlm"]["model_name"]
|
| 21 |
+
_vlm_system_prompt = load_prompt("prompts/vlm_prompt.yaml").content
|
| 22 |
+
_asr_model_name = _config["models"]["asr"]["model_name"]
|
| 23 |
+
_hf_client = InferenceClient(token=os.getenv("HF_INFERENCE_KEY"))
|
| 24 |
+
|
| 25 |
+
_ddg_search = None
|
| 26 |
+
_tavily_search = None
|
| 27 |
+
|
| 28 |
+
def _get_ddg():
|
| 29 |
+
global _ddg_search
|
| 30 |
+
if _ddg_search is None:
|
| 31 |
+
_ddg_search = DuckDuckGoSearchRun()
|
| 32 |
+
return _ddg_search
|
| 33 |
+
|
| 34 |
+
def _get_tavily():
|
| 35 |
+
global _tavily_search
|
| 36 |
+
if _tavily_search is None:
|
| 37 |
+
_tavily_search = TavilySearchResults(max_results=3)
|
| 38 |
+
return _tavily_search
|
| 39 |
+
|
| 40 |
+
# ============================================
|
| 41 |
+
# Basic Tools
|
| 42 |
+
# ============================================
|
| 43 |
+
|
| 44 |
@tool
|
| 45 |
def calculator(a: float, b: float, type: str) -> float:
|
| 46 |
"""Performs mathematical calculations, addition, subtraction, multiplication, division, modulus.
|
|
|
|
| 61 |
raise ValueError("Cannot divide by zero.")
|
| 62 |
return a / b
|
| 63 |
elif type == "modulus":
|
| 64 |
+
return a % b
|
| 65 |
else:
|
| 66 |
+
raise TypeError(f"{type} is not an option for type, choose one of addition, subtraction, multiplication, division, modulus")
|
| 67 |
|
| 68 |
@tool
|
| 69 |
def duck_web_search(query: str) -> str:
|
|
|
|
| 72 |
Args:
|
| 73 |
query: The search query.
|
| 74 |
"""
|
| 75 |
+
search = _get_ddg().invoke(query=query)
|
| 76 |
|
| 77 |
return {"duckduckgo_web_search": search}
|
| 78 |
|
|
|
|
| 110 |
|
| 111 |
Args:
|
| 112 |
query: The search query."""
|
| 113 |
+
search_documents = _get_tavily().invoke(input=query)
|
|
|
|
| 114 |
web_results = "\n\n---\n\n".join(
|
| 115 |
[
|
| 116 |
f'Document title: {document["title"]}. Contents: {document["content"]}. Relevance Score: {document["score"]}'
|
| 117 |
for document in search_documents
|
| 118 |
])
|
| 119 |
+
return {"web_results": web_results}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@tool
|
| 123 |
+
def fetch_webpage(url: str) -> str:
|
| 124 |
+
"""
|
| 125 |
+
Fetch and extract the main text content from a webpage.
|
| 126 |
+
Use this when a search result points to a specific URL you need to read in full.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
url: The full URL of the page to fetch.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
The extracted text content of the page.
|
| 133 |
+
"""
|
| 134 |
+
import trafilatura
|
| 135 |
+
try:
|
| 136 |
+
downloaded = trafilatura.fetch_url(url)
|
| 137 |
+
if downloaded is None:
|
| 138 |
+
return f"Error: Could not fetch {url}"
|
| 139 |
+
text = trafilatura.extract(downloaded, include_tables=True, include_links=False)
|
| 140 |
+
if text is None:
|
| 141 |
+
return f"Error: Could not extract content from {url}"
|
| 142 |
+
return f"Page content from {url}:\n\n{text}"
|
| 143 |
+
except Exception as e:
|
| 144 |
+
return f"Error fetching webpage: {e}"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@tool
|
| 148 |
+
def python_eval(code: str) -> str:
|
| 149 |
+
"""
|
| 150 |
+
Execute a Python code snippet and return its stdout output.
|
| 151 |
+
Use this when a question asks what a script outputs, or when computation requires running code.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
code: Python source code to execute.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
The stdout output of the code, or an error/timeout message.
|
| 158 |
+
"""
|
| 159 |
+
import subprocess
|
| 160 |
+
import tempfile
|
| 161 |
+
try:
|
| 162 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
| 163 |
+
f.write(code)
|
| 164 |
+
tmp_path = f.name
|
| 165 |
+
result = subprocess.run(
|
| 166 |
+
['python3', tmp_path],
|
| 167 |
+
capture_output=True, text=True, timeout=30
|
| 168 |
+
)
|
| 169 |
+
os.unlink(tmp_path)
|
| 170 |
+
if result.returncode == 0:
|
| 171 |
+
return f"Output:\n{result.stdout}"
|
| 172 |
+
return f"Error (exit {result.returncode}):\n{result.stderr}"
|
| 173 |
+
except subprocess.TimeoutExpired:
|
| 174 |
+
return "Error: execution timed out (30s limit)"
|
| 175 |
+
except Exception as e:
|
| 176 |
+
return f"Error: {e}"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# ============================================
|
| 180 |
+
# VLM Tool
|
| 181 |
+
# ============================================
|
| 182 |
+
|
| 183 |
+
@tool
|
| 184 |
+
def analyze_image(image_path: str, question: str) -> str:
|
| 185 |
+
"""
|
| 186 |
+
Analyze an image using a Vision Language Model (VLM) to answer a specific question.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
image_path: Path to the image file (JPG, PNG).
|
| 190 |
+
question: The specific question to answer about the image.
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
A detailed description or answer based on the visual content.
|
| 194 |
+
"""
|
| 195 |
+
try:
|
| 196 |
+
if not os.path.exists(image_path):
|
| 197 |
+
return f"Error: Image file not found at {image_path}"
|
| 198 |
+
|
| 199 |
+
with open(image_path, "rb") as img_file:
|
| 200 |
+
image_data = base64.b64encode(img_file.read()).decode("utf-8")
|
| 201 |
+
ext = Path(image_path).suffix.lower().lstrip(".")
|
| 202 |
+
mime_type = "image/jpeg" if ext in ("jpg", "jpeg") else f"image/{ext}"
|
| 203 |
+
image_url = f"data:{mime_type};base64,{image_data}"
|
| 204 |
+
|
| 205 |
+
messages = [
|
| 206 |
+
{
|
| 207 |
+
"role": "user",
|
| 208 |
+
"content": [
|
| 209 |
+
{"type": "image_url", "image_url": {"url": image_url}},
|
| 210 |
+
{"type": "text", "text": f"{_vlm_system_prompt}\n\nQuestion: {question}"}
|
| 211 |
+
]
|
| 212 |
+
}
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
output = _hf_client.chat_completion(
|
| 216 |
+
messages=messages,
|
| 217 |
+
model=_vlm_model_name,
|
| 218 |
+
max_tokens=1000
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
return output.choices[0].message.content
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
return f"Error analyzing image with VLM: {str(e)}"
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# ============================================
|
| 228 |
+
# Document Processing Tools
|
| 229 |
+
# ============================================
|
| 230 |
+
|
| 231 |
+
@tool
|
| 232 |
+
def read_pdf(file_path: str) -> str:
|
| 233 |
+
"""
|
| 234 |
+
Extract text content from a PDF file.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
file_path: Path to the PDF file to read.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
The text content of the PDF, with page separators.
|
| 241 |
+
"""
|
| 242 |
+
from pypdf import PdfReader
|
| 243 |
+
|
| 244 |
+
try:
|
| 245 |
+
reader = PdfReader(file_path)
|
| 246 |
+
text = []
|
| 247 |
+
for i, page in enumerate(reader.pages):
|
| 248 |
+
page_text = page.extract_text()
|
| 249 |
+
if page_text:
|
| 250 |
+
text.append(f"--- Page {i+1} ---\n{page_text}")
|
| 251 |
+
|
| 252 |
+
return "\n\n".join(text) if text else "[Empty PDF]"
|
| 253 |
+
except Exception as e:
|
| 254 |
+
return f"Error reading PDF: {e}"
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@tool
|
| 258 |
+
def read_docx(file_path: str) -> str:
|
| 259 |
+
"""
|
| 260 |
+
Extract text content from a Word document (.docx).
|
| 261 |
+
|
| 262 |
+
Args:
|
| 263 |
+
file_path: Path to the Word document to read.
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
The text content of the document.
|
| 267 |
+
"""
|
| 268 |
+
from docx import Document
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
doc = Document(file_path)
|
| 272 |
+
text_parts = []
|
| 273 |
+
|
| 274 |
+
paragraphs = [para.text for para in doc.paragraphs if para.text.strip()]
|
| 275 |
+
if paragraphs:
|
| 276 |
+
text_parts.append("\n".join(paragraphs))
|
| 277 |
+
|
| 278 |
+
for i, table in enumerate(doc.tables):
|
| 279 |
+
rows = [" | ".join(cell.text.strip() for cell in row.cells) for row in table.rows]
|
| 280 |
+
rows = [r for r in rows if r.strip()]
|
| 281 |
+
if rows:
|
| 282 |
+
text_parts.append(f"--- Table {i+1} ---\n" + "\n".join(rows))
|
| 283 |
+
|
| 284 |
+
return "\n\n".join(text_parts) if text_parts else "[Empty document]"
|
| 285 |
+
except Exception as e:
|
| 286 |
+
return f"Error reading DOCX: {e}"
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
@tool
|
| 290 |
+
def read_pptx(file_path: str) -> str:
|
| 291 |
+
"""
|
| 292 |
+
Extract text content from a PowerPoint presentation (.pptx).
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
file_path: Path to the PowerPoint file to read.
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
The text content from all slides.
|
| 299 |
+
"""
|
| 300 |
+
from pptx import Presentation
|
| 301 |
+
|
| 302 |
+
try:
|
| 303 |
+
prs = Presentation(file_path)
|
| 304 |
+
text = []
|
| 305 |
+
|
| 306 |
+
for slide_num, slide in enumerate(prs.slides, 1):
|
| 307 |
+
slide_text = [f"--- Slide {slide_num} ---"]
|
| 308 |
+
for shape in slide.shapes:
|
| 309 |
+
if hasattr(shape, "text") and shape.text.strip():
|
| 310 |
+
slide_text.append(shape.text)
|
| 311 |
+
if len(slide_text) > 1:
|
| 312 |
+
text.append("\n".join(slide_text))
|
| 313 |
+
|
| 314 |
+
return "\n\n".join(text) if text else "[Empty presentation]"
|
| 315 |
+
except Exception as e:
|
| 316 |
+
return f"Error reading PPTX: {e}"
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
@tool
|
| 320 |
+
def read_text_file(file_path: str) -> str:
|
| 321 |
+
"""
|
| 322 |
+
Read content from a plain text file (.txt).
|
| 323 |
+
|
| 324 |
+
Args:
|
| 325 |
+
file_path: Path to the text file to read.
|
| 326 |
+
|
| 327 |
+
Returns:
|
| 328 |
+
The content of the text file.
|
| 329 |
+
"""
|
| 330 |
+
try:
|
| 331 |
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
| 332 |
+
return f.read()
|
| 333 |
+
except Exception as e:
|
| 334 |
+
return f"Error reading text file: {e}"
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# ============================================
|
| 338 |
+
# Data Processing Tools (using polars)
|
| 339 |
+
# ============================================
|
| 340 |
+
|
| 341 |
+
@tool
|
| 342 |
+
def read_csv(file_path: str) -> str:
|
| 343 |
+
"""
|
| 344 |
+
Read and analyze a CSV file using polars.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
file_path: Path to the CSV file to read.
|
| 348 |
+
|
| 349 |
+
Returns:
|
| 350 |
+
Summary of the CSV including schema, row count, and data preview.
|
| 351 |
+
"""
|
| 352 |
+
import polars as pl
|
| 353 |
+
|
| 354 |
+
try:
|
| 355 |
+
df = pl.read_csv(file_path)
|
| 356 |
+
|
| 357 |
+
output = f"CSV File — {len(df)} rows, {len(df.columns)} columns\n"
|
| 358 |
+
output += f"Columns: {df.columns}\n\n"
|
| 359 |
+
output += f"Column Statistics:\n{df.describe()}\n\n"
|
| 360 |
+
output += f"Data (first 20 rows):\n{df.head(20)}"
|
| 361 |
+
if len(df) <= 50:
|
| 362 |
+
output += f"\n\nComplete data:\n{df}"
|
| 363 |
+
return output
|
| 364 |
+
except Exception as e:
|
| 365 |
+
return f"Error reading CSV: {e}"
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@tool
|
| 369 |
+
def read_excel(file_path: str, sheet_id: int = 0) -> str:
|
| 370 |
+
"""
|
| 371 |
+
Read and analyze an Excel file (.xlsx) using polars.
|
| 372 |
+
|
| 373 |
+
Args:
|
| 374 |
+
file_path: Path to the Excel file to read.
|
| 375 |
+
sheet_id: The sheet index to read (0-based). Default is 0 (first sheet).
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
Summary of the Excel sheet including schema, row count, and data preview.
|
| 379 |
+
"""
|
| 380 |
+
import polars as pl
|
| 381 |
+
import openpyxl
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
wb = openpyxl.load_workbook(file_path, read_only=True)
|
| 385 |
+
sheet_names = wb.sheetnames
|
| 386 |
+
wb.close()
|
| 387 |
+
except Exception:
|
| 388 |
+
sheet_names = []
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
df = pl.read_excel(file_path, sheet_id=sheet_id)
|
| 392 |
+
sheet_label = sheet_names[sheet_id] if sheet_id < len(sheet_names) else str(sheet_id)
|
| 393 |
+
|
| 394 |
+
output = f"Excel File — Available sheets: {sheet_names}\n\n"
|
| 395 |
+
output += f"Sheet {sheet_id} ('{sheet_label}') — {len(df)} rows, {len(df.columns)} columns\n"
|
| 396 |
+
output += f"Columns: {df.columns}\n\n"
|
| 397 |
+
output += f"Column Statistics:\n{df.describe()}\n\n"
|
| 398 |
+
output += f"Data (first 20 rows):\n{df.head(20)}"
|
| 399 |
+
if len(df) <= 50:
|
| 400 |
+
output += f"\n\nComplete data:\n{df}"
|
| 401 |
+
return output
|
| 402 |
+
except Exception as e:
|
| 403 |
+
return f"Error reading Excel: {e}"
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
@tool
|
| 407 |
+
def read_jsonld(file_path: str) -> str:
|
| 408 |
+
"""
|
| 409 |
+
Read and parse a JSON-LD file.
|
| 410 |
+
|
| 411 |
+
Args:
|
| 412 |
+
file_path: Path to the JSON-LD file to read.
|
| 413 |
+
|
| 414 |
+
Returns:
|
| 415 |
+
The formatted JSON content.
|
| 416 |
+
"""
|
| 417 |
+
try:
|
| 418 |
+
with open(file_path, 'r') as f:
|
| 419 |
+
data = json.load(f)
|
| 420 |
+
return f"JSON-LD Content:\n{json.dumps(data, indent=2)}"
|
| 421 |
+
except Exception as e:
|
| 422 |
+
return f"Error reading JSON-LD: {e}"
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@tool
|
| 426 |
+
def read_pdb(file_path: str) -> str:
|
| 427 |
+
"""
|
| 428 |
+
Read and analyze a PDB (Protein Data Bank) file for protein structure analysis.
|
| 429 |
+
|
| 430 |
+
Args:
|
| 431 |
+
file_path: Path to the PDB file to read.
|
| 432 |
+
|
| 433 |
+
Returns:
|
| 434 |
+
Analysis of the protein structure including atoms, chains, and coordinates.
|
| 435 |
+
"""
|
| 436 |
+
from Bio.PDB import PDBParser
|
| 437 |
+
import numpy as np
|
| 438 |
+
|
| 439 |
+
try:
|
| 440 |
+
parser = PDBParser(QUIET=True)
|
| 441 |
+
structure = parser.get_structure("protein", file_path)
|
| 442 |
+
|
| 443 |
+
info = ["=== PDB Structure Analysis ==="]
|
| 444 |
+
|
| 445 |
+
atoms = list(structure.get_atoms())
|
| 446 |
+
info.append(f"Total atoms: {len(atoms)}")
|
| 447 |
+
|
| 448 |
+
for model in structure:
|
| 449 |
+
info.append(f"\nModel {model.id}:")
|
| 450 |
+
for chain in model:
|
| 451 |
+
residues = list(chain.get_residues())
|
| 452 |
+
info.append(f" Chain {chain.id}: {len(residues)} residues")
|
| 453 |
+
|
| 454 |
+
if len(atoms) >= 2:
|
| 455 |
+
info.append("\nFirst atoms (for distance calculations):")
|
| 456 |
+
for i, atom in enumerate(atoms[:5]):
|
| 457 |
+
coord = atom.get_coord()
|
| 458 |
+
info.append(
|
| 459 |
+
f" Atom {i+1}: {atom.get_name()} at "
|
| 460 |
+
f"[{coord[0]:.4f}, {coord[1]:.4f}, {coord[2]:.4f}]"
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
dist = np.linalg.norm(atoms[0].get_coord() - atoms[1].get_coord())
|
| 464 |
+
info.append(f"\nDistance between first two atoms: {dist:.4f} Angstroms")
|
| 465 |
+
|
| 466 |
+
return "\n".join(info)
|
| 467 |
+
except Exception as e:
|
| 468 |
+
return f"Error reading PDB: {e}"
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# ============================================
|
| 472 |
+
# Audio Processing Tools
|
| 473 |
+
# ============================================
|
| 474 |
+
|
| 475 |
+
@tool
|
| 476 |
+
def transcribe_audio(file_path: str) -> str:
|
| 477 |
+
"""
|
| 478 |
+
Transcribe an audio file (MP3, WAV, etc.) to text using Whisper.
|
| 479 |
+
|
| 480 |
+
Args:
|
| 481 |
+
file_path: Path to the audio file to transcribe.
|
| 482 |
+
|
| 483 |
+
Returns:
|
| 484 |
+
The transcribed text from the audio.
|
| 485 |
+
"""
|
| 486 |
+
try:
|
| 487 |
+
result = _hf_client.automatic_speech_recognition(audio=file_path, model=_asr_model_name)
|
| 488 |
+
return f"Audio Transcription:\n{result.text}"
|
| 489 |
+
except Exception as e:
|
| 490 |
+
return f"Error transcribing audio: {e}"
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ============================================
|
| 494 |
+
# Code Processing Tools
|
| 495 |
+
# ============================================
|
| 496 |
+
|
| 497 |
+
@tool
|
| 498 |
+
def read_python_file(file_path: str) -> str:
|
| 499 |
+
"""
|
| 500 |
+
Read a Python source code file.
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
file_path: Path to the Python file to read.
|
| 504 |
+
|
| 505 |
+
Returns:
|
| 506 |
+
The Python code content.
|
| 507 |
+
"""
|
| 508 |
+
try:
|
| 509 |
+
with open(file_path, 'r') as f:
|
| 510 |
+
code = f.read()
|
| 511 |
+
return f"Python Code:\n```python\n{code}\n```"
|
| 512 |
+
except Exception as e:
|
| 513 |
+
return f"Error reading Python file: {e}"
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
# ============================================
|
| 517 |
+
# Archive Processing Tools
|
| 518 |
+
# ============================================
|
| 519 |
+
|
| 520 |
+
@tool
|
| 521 |
+
def extract_zip(file_path: str) -> str:
|
| 522 |
+
"""
|
| 523 |
+
Extract a ZIP archive and list its contents.
|
| 524 |
+
|
| 525 |
+
Args:
|
| 526 |
+
file_path: Path to the ZIP file to extract.
|
| 527 |
+
|
| 528 |
+
Returns:
|
| 529 |
+
List of files extracted from the archive with their paths.
|
| 530 |
+
"""
|
| 531 |
+
import zipfile
|
| 532 |
+
|
| 533 |
+
try:
|
| 534 |
+
extract_dir = Path(file_path).parent / Path(file_path).stem
|
| 535 |
+
extract_dir.mkdir(exist_ok=True)
|
| 536 |
+
|
| 537 |
+
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
| 538 |
+
zip_ref.extractall(extract_dir)
|
| 539 |
+
|
| 540 |
+
results = [f"ZIP Archive extracted to: {extract_dir}\n\nContents:"]
|
| 541 |
+
|
| 542 |
+
for root, dirs, files in os.walk(extract_dir):
|
| 543 |
+
for file in files:
|
| 544 |
+
full_path = os.path.join(root, file)
|
| 545 |
+
rel_path = os.path.relpath(full_path, extract_dir)
|
| 546 |
+
file_size = os.path.getsize(full_path)
|
| 547 |
+
results.append(f" - {rel_path} ({file_size} bytes)")
|
| 548 |
+
|
| 549 |
+
results.append(f"\nUse the appropriate read tool on the extracted files at: {extract_dir}/")
|
| 550 |
+
|
| 551 |
+
return "\n".join(results)
|
| 552 |
+
except Exception as e:
|
| 553 |
+
return f"Error extracting ZIP: {e}"
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
# ============================================
|
| 557 |
+
# Generic File Processing
|
| 558 |
+
# ============================================
|
| 559 |
+
|
| 560 |
+
@tool
|
| 561 |
+
def read_file(file_path: str) -> str:
|
| 562 |
+
"""
|
| 563 |
+
Automatically read a file based on its extension.
|
| 564 |
+
|
| 565 |
+
Supported formats: PDF, DOCX, PPTX, TXT, CSV, XLSX, JSON-LD, PDB, Python, ZIP, JPG, JPEG, PNG, MP3, WAV, FLAC, OGG, M4A
|
| 566 |
+
|
| 567 |
+
Args:
|
| 568 |
+
file_path: Path to the file to read.
|
| 569 |
+
|
| 570 |
+
Returns:
|
| 571 |
+
The processed content of the file.
|
| 572 |
+
"""
|
| 573 |
+
ext = Path(file_path).suffix.lower()
|
| 574 |
+
|
| 575 |
+
processors = {
|
| 576 |
+
'.pdf': lambda p: read_pdf.invoke(p),
|
| 577 |
+
'.docx': lambda p: read_docx.invoke(p),
|
| 578 |
+
'.pptx': lambda p: read_pptx.invoke(p),
|
| 579 |
+
'.txt': lambda p: read_text_file.invoke(p),
|
| 580 |
+
'.csv': lambda p: read_csv.invoke(p),
|
| 581 |
+
'.xlsx': lambda p: read_excel.invoke(p),
|
| 582 |
+
'.jsonld': lambda p: read_jsonld.invoke(p),
|
| 583 |
+
'.pdb': lambda p: read_pdb.invoke(p),
|
| 584 |
+
'.py': lambda p: read_python_file.invoke(p),
|
| 585 |
+
'.mp3': lambda p: transcribe_audio.invoke(p),
|
| 586 |
+
'.wav': lambda p: transcribe_audio.invoke(p),
|
| 587 |
+
'.flac': lambda p: transcribe_audio.invoke(p),
|
| 588 |
+
'.ogg': lambda p: transcribe_audio.invoke(p),
|
| 589 |
+
'.m4a': lambda p: transcribe_audio.invoke(p),
|
| 590 |
+
'.zip': lambda p: extract_zip.invoke(p),
|
| 591 |
+
'.jpg': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 592 |
+
'.jpeg': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 593 |
+
'.png': lambda p: analyze_image.invoke({"image_path": p, "question": "Describe this image in detail."}),
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
processor = processors.get(ext)
|
| 597 |
+
|
| 598 |
+
if processor:
|
| 599 |
+
return processor(file_path)
|
| 600 |
+
|
| 601 |
+
return f"[Unsupported file type: {ext}]"
|
| 602 |
+
|
| 603 |
+
# ============================================
|
| 604 |
+
# List of all tools
|
| 605 |
+
# ============================================
|
| 606 |
+
|
| 607 |
+
tools_list = [
|
| 608 |
+
calculator,
|
| 609 |
+
duck_web_search,
|
| 610 |
+
wiki_search,
|
| 611 |
+
arxiv_search,
|
| 612 |
+
tavily_web_search,
|
| 613 |
+
fetch_webpage,
|
| 614 |
+
python_eval,
|
| 615 |
+
read_pdf,
|
| 616 |
+
read_docx,
|
| 617 |
+
read_pptx,
|
| 618 |
+
read_text_file,
|
| 619 |
+
read_csv,
|
| 620 |
+
read_excel,
|
| 621 |
+
read_jsonld,
|
| 622 |
+
read_pdb,
|
| 623 |
+
transcribe_audio,
|
| 624 |
+
read_python_file,
|
| 625 |
+
extract_zip,
|
| 626 |
+
analyze_image,
|
| 627 |
+
read_file,
|
| 628 |
+
]
|
utils.py
CHANGED
|
@@ -1,10 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import yaml
|
|
|
|
| 2 |
from langchain_core.messages import SystemMessage
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
with open(prompt_location) as f:
|
| 6 |
try:
|
| 7 |
prompt = yaml.safe_load(f)["prompt"]
|
| 8 |
return SystemMessage(content=prompt)
|
| 9 |
except yaml.YAMLError as exc:
|
| 10 |
-
print(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import bm25s
|
| 4 |
import yaml
|
| 5 |
+
from pathlib import Path
|
| 6 |
from langchain_core.messages import SystemMessage
|
| 7 |
|
| 8 |
+
|
| 9 |
+
def load_config(path="config.yaml"):
|
| 10 |
+
with open(path, "r") as f:
|
| 11 |
+
return yaml.safe_load(f)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_prompt(prompt_location: str) -> SystemMessage:
|
| 15 |
+
"""Load system prompt from YAML file."""
|
| 16 |
with open(prompt_location) as f:
|
| 17 |
try:
|
| 18 |
prompt = yaml.safe_load(f)["prompt"]
|
| 19 |
return SystemMessage(content=prompt)
|
| 20 |
except yaml.YAMLError as exc:
|
| 21 |
+
print(exc)
|
| 22 |
+
return SystemMessage(content="You are a helpful assistant.")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def init_bm25_index(corpus_file = "data/metadata.jsonl"):
|
| 27 |
+
"""BM25 Index Initialization (Local Corpus)"""
|
| 28 |
+
try:
|
| 29 |
+
if not os.path.exists(corpus_file):
|
| 30 |
+
print(f"Warning: {corpus_file} not found. BM25 will use empty index.")
|
| 31 |
+
return None, [], []
|
| 32 |
+
|
| 33 |
+
search_texts = [] # question-only — used for BM25 indexing
|
| 34 |
+
corpus_texts = [] # Q+A+Steps — returned for context injection
|
| 35 |
+
corpus_ids = []
|
| 36 |
+
with open(corpus_file, "r") as f:
|
| 37 |
+
for line in f:
|
| 38 |
+
item = json.loads(line)
|
| 39 |
+
question = item.get('Question', '')
|
| 40 |
+
answer = item.get('Final answer', '')
|
| 41 |
+
steps = item.get('Annotator Metadata', {}).get('Steps', '')
|
| 42 |
+
search_texts.append(question)
|
| 43 |
+
parts = [f"Question: {question}"]
|
| 44 |
+
if answer:
|
| 45 |
+
parts.append(f"Final Answer: {answer}")
|
| 46 |
+
if steps:
|
| 47 |
+
parts.append(f"Solution Steps: {steps}")
|
| 48 |
+
corpus_texts.append("\n".join(parts))
|
| 49 |
+
corpus_ids.append(item.get('task_id', ''))
|
| 50 |
+
|
| 51 |
+
corpus_tokens = bm25s.tokenize(search_texts, stopwords="en", stemmer=None)
|
| 52 |
+
|
| 53 |
+
retriever_bm25 = bm25s.BM25()
|
| 54 |
+
retriever_bm25.index(corpus_tokens)
|
| 55 |
+
|
| 56 |
+
print(f"BM25 Index initialized with {len(corpus_texts)} documents.")
|
| 57 |
+
return retriever_bm25, corpus_texts, corpus_ids
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Error initializing BM25: {e}")
|
| 60 |
+
return None, [], []
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def reciprocal_rank_fusion(results: list[list[dict]], k=60) -> list[tuple[dict, float]]:
|
| 64 |
+
"""
|
| 65 |
+
Fuse multiple ranked lists using Reciprocal Rank Fusion (RRF).
|
| 66 |
+
"""
|
| 67 |
+
fused_scores = {}
|
| 68 |
+
|
| 69 |
+
for rank_list in results:
|
| 70 |
+
for rank, doc in enumerate(rank_list):
|
| 71 |
+
doc_id = doc["metadata"]["task_id"]
|
| 72 |
+
doc_content = doc["content"]
|
| 73 |
+
if doc_id not in fused_scores:
|
| 74 |
+
fused_scores[doc_id] = {"id": doc_id, "content": doc_content, "score": 0.0}
|
| 75 |
+
fused_scores[doc_id]["score"] += 1.0 / (k + rank + 1)
|
| 76 |
+
|
| 77 |
+
sorted_results = sorted(fused_scores.values(), key=lambda x: x["score"], reverse=True)
|
| 78 |
+
return [(item["id"], item["content"], item["score"]) for item in sorted_results]
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|