Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -16,7 +16,7 @@ import operator
|
|
| 16 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage
|
| 17 |
from langchain_core.tools import tool
|
| 18 |
from langgraph.graph import StateGraph, END
|
| 19 |
-
from langgraph.prebuilt import ToolNode
|
| 20 |
from langchain_core.utils.function_calling import convert_to_openai_function
|
| 21 |
|
| 22 |
# 其他工具依赖
|
|
@@ -27,14 +27,16 @@ from youtube_transcript_api import YouTubeTranscriptApi
|
|
| 27 |
# 配置常量
|
| 28 |
# =============================================================================
|
| 29 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 30 |
-
|
|
|
|
| 31 |
AGICTO_API_KEY = os.getenv("AGICTO_API_KEY", "")
|
| 32 |
QWEN_MODEL = "qwen3.5-35b-a3b"
|
| 33 |
|
| 34 |
# =============================================================================
|
| 35 |
-
# 进度监控器
|
| 36 |
# =============================================================================
|
| 37 |
class ProgressMonitor:
|
|
|
|
| 38 |
def __init__(self):
|
| 39 |
self.current = 0
|
| 40 |
self.total = 0
|
|
@@ -78,13 +80,17 @@ class ProgressMonitor:
|
|
| 78 |
return html
|
| 79 |
|
| 80 |
# =============================================================================
|
| 81 |
-
# Qwen LLM 封装(
|
| 82 |
# =============================================================================
|
| 83 |
class QwenLLM:
|
| 84 |
def __init__(self, model=QWEN_MODEL):
|
| 85 |
self.model = model
|
| 86 |
self.api_key = AGICTO_API_KEY
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
if not self.api_key:
|
| 89 |
print("⚠️ 未设置 AGICTO_API_KEY,请检查环境变量")
|
| 90 |
|
|
@@ -102,8 +108,11 @@ class QwenLLM:
|
|
| 102 |
if functions:
|
| 103 |
body["tools"] = [{"type": "function", "function": f} for f in functions]
|
| 104 |
body["tool_choice"] = "auto"
|
|
|
|
|
|
|
|
|
|
| 105 |
try:
|
| 106 |
-
resp = requests.post(
|
| 107 |
resp.raise_for_status()
|
| 108 |
return resp.json()
|
| 109 |
except Exception as e:
|
|
@@ -181,13 +190,19 @@ class QwenLLM:
|
|
| 181 |
return formatted
|
| 182 |
|
| 183 |
# =============================================================================
|
| 184 |
-
# 工具定义
|
| 185 |
# =============================================================================
|
| 186 |
api_url_tasks = DEFAULT_API_URL
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
@tool
|
| 189 |
def web_search(query: str) -> str:
|
| 190 |
-
|
| 191 |
try:
|
| 192 |
url = "https://api.duckduckgo.com/"
|
| 193 |
params = {"q": query, "format": "json", "no_html": 1}
|
|
@@ -205,7 +220,7 @@ def web_search(query: str) -> str:
|
|
| 205 |
|
| 206 |
@tool
|
| 207 |
def web_scraper(url: str) -> str:
|
| 208 |
-
|
| 209 |
try:
|
| 210 |
headers = {"User-Agent": "Mozilla/5.0"}
|
| 211 |
resp = requests.get(url, headers=headers, timeout=15)
|
|
@@ -220,7 +235,7 @@ def web_scraper(url: str) -> str:
|
|
| 220 |
|
| 221 |
@tool
|
| 222 |
def calculator(expression: str) -> str:
|
| 223 |
-
|
| 224 |
try:
|
| 225 |
import math
|
| 226 |
allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
|
|
@@ -244,7 +259,10 @@ def analyze_image(image_data: str) -> str:
|
|
| 244 |
]}],
|
| 245 |
"max_tokens": 800
|
| 246 |
}
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
| 248 |
if resp.status_code == 200:
|
| 249 |
return resp.json()["choices"][0]["message"]["content"]
|
| 250 |
return f"图片分析失败: {resp.status_code}"
|
|
@@ -263,7 +281,9 @@ def transcribe_audio(audio_path: str) -> str:
|
|
| 263 |
else:
|
| 264 |
audio_data = open(audio_path, "rb")
|
| 265 |
files = {"file": audio_data, "model": (None, "whisper-1")}
|
| 266 |
-
|
|
|
|
|
|
|
| 267 |
if resp.status_code == 200:
|
| 268 |
return resp.json()["text"]
|
| 269 |
return f"转录失败: {resp.status_code}"
|
|
@@ -272,7 +292,7 @@ def transcribe_audio(audio_path: str) -> str:
|
|
| 272 |
|
| 273 |
@tool
|
| 274 |
def get_youtube_transcript(video_url: str) -> str:
|
| 275 |
-
|
| 276 |
try:
|
| 277 |
if "watch?v=" in video_url:
|
| 278 |
vid = video_url.split("v=")[1].split("&")[0]
|
|
@@ -287,7 +307,7 @@ def get_youtube_transcript(video_url: str) -> str:
|
|
| 287 |
|
| 288 |
@tool
|
| 289 |
def download_file_for_task(task_id: str) -> str:
|
| 290 |
-
|
| 291 |
try:
|
| 292 |
url = f"{api_url_tasks}/files/{task_id}"
|
| 293 |
resp = requests.get(url, timeout=20)
|
|
@@ -310,25 +330,15 @@ def download_file_for_task(task_id: str) -> str:
|
|
| 310 |
return f"文件下载失败: {e}"
|
| 311 |
|
| 312 |
# =============================================================================
|
| 313 |
-
# LangGraph Agent
|
| 314 |
# =============================================================================
|
| 315 |
class AgentState(TypedDict):
|
| 316 |
messages: Annotated[Sequence[BaseMessage], operator.add]
|
| 317 |
final_answer: str
|
| 318 |
task_id: str
|
| 319 |
|
| 320 |
-
tools = [
|
| 321 |
-
|
| 322 |
-
web_scraper,
|
| 323 |
-
calculator,
|
| 324 |
-
analyze_image,
|
| 325 |
-
transcribe_audio,
|
| 326 |
-
get_youtube_transcript,
|
| 327 |
-
download_file_for_task
|
| 328 |
-
]
|
| 329 |
-
|
| 330 |
-
# 创建 ToolNode(直接基于工具列表)
|
| 331 |
-
tool_node = ToolNode(tools) # 新版 LangGraph 使用 ToolNode
|
| 332 |
|
| 333 |
llm = QwenLLM()
|
| 334 |
functions = [convert_to_openai_function(t) for t in tools]
|
|
@@ -363,28 +373,19 @@ def build_graph():
|
|
| 363 |
workflow.add_node("agent", agent_node)
|
| 364 |
workflow.add_node("tools", tool_node)
|
| 365 |
workflow.add_node("finish", finish_node)
|
| 366 |
-
|
| 367 |
workflow.set_entry_point("agent")
|
| 368 |
workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", "finish": "finish"})
|
| 369 |
workflow.add_edge("tools", "agent")
|
| 370 |
workflow.add_edge("finish", END)
|
| 371 |
-
|
| 372 |
return workflow.compile()
|
| 373 |
|
| 374 |
-
# =============================================================================
|
| 375 |
-
# Agent 类
|
| 376 |
-
# =============================================================================
|
| 377 |
class LangGraphAgent:
|
| 378 |
def __init__(self):
|
| 379 |
self.graph = build_graph()
|
| 380 |
print("LangGraphAgent 初始化完成,使用模型:", QWEN_MODEL)
|
| 381 |
|
| 382 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 383 |
-
state = {
|
| 384 |
-
"messages": [HumanMessage(content=question)],
|
| 385 |
-
"final_answer": "",
|
| 386 |
-
"task_id": task_id
|
| 387 |
-
}
|
| 388 |
try:
|
| 389 |
final_state = self.graph.invoke(state)
|
| 390 |
return final_state["final_answer"]
|
|
@@ -393,8 +394,10 @@ class LangGraphAgent:
|
|
| 393 |
return f"Error: {e}"
|
| 394 |
|
| 395 |
# =============================================================================
|
| 396 |
-
# 主运行函数
|
| 397 |
# =============================================================================
|
|
|
|
|
|
|
| 398 |
def run_and_submit_all(profile: gr.OAuthProfile | None) -> Generator:
|
| 399 |
space_id = os.getenv("SPACE_ID")
|
| 400 |
if not profile:
|
|
@@ -412,7 +415,6 @@ def run_and_submit_all(profile: gr.OAuthProfile | None) -> Generator:
|
|
| 412 |
yield f"<div>Agent 初始化失败: {e}</div>", f"Agent 初始化失败: {e}", pd.DataFrame()
|
| 413 |
return
|
| 414 |
|
| 415 |
-
# 获取题目
|
| 416 |
try:
|
| 417 |
resp = requests.get(f"{api_url}/questions", timeout=15)
|
| 418 |
resp.raise_for_status()
|
|
@@ -448,11 +450,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None) -> Generator:
|
|
| 448 |
yield monitor.get_html(), "没有答案可提交", pd.DataFrame(results_log)
|
| 449 |
return
|
| 450 |
|
| 451 |
-
submission = {
|
| 452 |
-
"username": username.strip(),
|
| 453 |
-
"agent_code": agent_code,
|
| 454 |
-
"answers": answers_payload
|
| 455 |
-
}
|
| 456 |
try:
|
| 457 |
resp = requests.post(f"{api_url}/submit", json=submission, timeout=60)
|
| 458 |
resp.raise_for_status()
|
|
@@ -472,28 +470,27 @@ def run_and_submit_all(profile: gr.OAuthProfile | None) -> Generator:
|
|
| 472 |
# =============================================================================
|
| 473 |
# Gradio 界面
|
| 474 |
# =============================================================================
|
| 475 |
-
import pandas as pd
|
| 476 |
-
|
| 477 |
with gr.Blocks(title="GAIA Agent") as demo:
|
| 478 |
gr.Markdown("""
|
| 479 |
# 🤖 GAIA Level 1 Agent (LangGraph + Qwen)
|
| 480 |
**模型:** Qwen3.5-35B-A3B | **API:** agicto.com
|
| 481 |
点击按钮获取题目,Agent 自动调用工具并回答,最后提交评分。
|
| 482 |
""")
|
| 483 |
-
|
| 484 |
gr.LoginButton()
|
| 485 |
-
|
| 486 |
run_btn = gr.Button("🚀 运行评测并提交", variant="primary")
|
| 487 |
-
|
| 488 |
progress_html = gr.HTML(label="实时进度")
|
| 489 |
status_output = gr.Textbox(label="提交结果 / 总分", lines=5, interactive=False)
|
| 490 |
results_table = gr.DataFrame(label="题目与 Agent 答案", wrap=True)
|
| 491 |
-
|
| 492 |
run_btn.click(
|
| 493 |
fn=run_and_submit_all,
|
| 494 |
outputs=[progress_html, status_output, results_table]
|
| 495 |
)
|
| 496 |
|
| 497 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
print("启动 Gradio App...")
|
| 499 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 16 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage, SystemMessage
|
| 17 |
from langchain_core.tools import tool
|
| 18 |
from langgraph.graph import StateGraph, END
|
| 19 |
+
from langgraph.prebuilt import ToolNode
|
| 20 |
from langchain_core.utils.function_calling import convert_to_openai_function
|
| 21 |
|
| 22 |
# 其他工具依赖
|
|
|
|
| 27 |
# 配置常量
|
| 28 |
# =============================================================================
|
| 29 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 30 |
+
# AGICTO_BASE_URL 请设置为 https://api.agicto.cn (不含 /v1)
|
| 31 |
+
AGICTO_BASE_URL = os.getenv("AGICTO_BASE_URL", "https://api.agicto.cn")
|
| 32 |
AGICTO_API_KEY = os.getenv("AGICTO_API_KEY", "")
|
| 33 |
QWEN_MODEL = "qwen3.5-35b-a3b"
|
| 34 |
|
| 35 |
# =============================================================================
|
| 36 |
+
# 进度监控器
|
| 37 |
# =============================================================================
|
| 38 |
class ProgressMonitor:
|
| 39 |
+
# ... 保持不变 ...
|
| 40 |
def __init__(self):
|
| 41 |
self.current = 0
|
| 42 |
self.total = 0
|
|
|
|
| 80 |
return html
|
| 81 |
|
| 82 |
# =============================================================================
|
| 83 |
+
# Qwen LLM 封装(修正 URL 拼接)
|
| 84 |
# =============================================================================
|
| 85 |
class QwenLLM:
|
| 86 |
def __init__(self, model=QWEN_MODEL):
|
| 87 |
self.model = model
|
| 88 |
self.api_key = AGICTO_API_KEY
|
| 89 |
+
# 规范化 base_url,确保末尾没有多余斜杠,并去掉可能存在的 /v1
|
| 90 |
+
base = AGICTO_BASE_URL.rstrip('/')
|
| 91 |
+
if base.endswith('/v1'):
|
| 92 |
+
base = base[:-3] # 去掉 /v1
|
| 93 |
+
self.base_url = base
|
| 94 |
if not self.api_key:
|
| 95 |
print("⚠️ 未设置 AGICTO_API_KEY,请检查环境变量")
|
| 96 |
|
|
|
|
| 108 |
if functions:
|
| 109 |
body["tools"] = [{"type": "function", "function": f} for f in functions]
|
| 110 |
body["tool_choice"] = "auto"
|
| 111 |
+
|
| 112 |
+
# 统一使用 /v1/chat/completions 路径
|
| 113 |
+
url = f"{self.base_url}/v1/chat/completions"
|
| 114 |
try:
|
| 115 |
+
resp = requests.post(url, headers=headers, json=body, timeout=60)
|
| 116 |
resp.raise_for_status()
|
| 117 |
return resp.json()
|
| 118 |
except Exception as e:
|
|
|
|
| 190 |
return formatted
|
| 191 |
|
| 192 |
# =============================================================================
|
| 193 |
+
# 工具定义(analyze_image / transcribe_audio 也需要使用修正后的 URL)
|
| 194 |
# =============================================================================
|
| 195 |
api_url_tasks = DEFAULT_API_URL
|
| 196 |
|
| 197 |
+
def _get_api_base():
|
| 198 |
+
base = AGICTO_BASE_URL.rstrip('/')
|
| 199 |
+
if base.endswith('/v1'):
|
| 200 |
+
base = base[:-3]
|
| 201 |
+
return base
|
| 202 |
+
|
| 203 |
@tool
|
| 204 |
def web_search(query: str) -> str:
|
| 205 |
+
# ... 不变 ...
|
| 206 |
try:
|
| 207 |
url = "https://api.duckduckgo.com/"
|
| 208 |
params = {"q": query, "format": "json", "no_html": 1}
|
|
|
|
| 220 |
|
| 221 |
@tool
|
| 222 |
def web_scraper(url: str) -> str:
|
| 223 |
+
# ... 不变 ...
|
| 224 |
try:
|
| 225 |
headers = {"User-Agent": "Mozilla/5.0"}
|
| 226 |
resp = requests.get(url, headers=headers, timeout=15)
|
|
|
|
| 235 |
|
| 236 |
@tool
|
| 237 |
def calculator(expression: str) -> str:
|
| 238 |
+
# ... 不变 ...
|
| 239 |
try:
|
| 240 |
import math
|
| 241 |
allowed = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
|
|
|
|
| 259 |
]}],
|
| 260 |
"max_tokens": 800
|
| 261 |
}
|
| 262 |
+
# 使用修正后的基地址
|
| 263 |
+
base = _get_api_base()
|
| 264 |
+
url = f"{base}/v1/chat/completions"
|
| 265 |
+
resp = requests.post(url, headers=headers, json=body, timeout=30)
|
| 266 |
if resp.status_code == 200:
|
| 267 |
return resp.json()["choices"][0]["message"]["content"]
|
| 268 |
return f"图片分析失败: {resp.status_code}"
|
|
|
|
| 281 |
else:
|
| 282 |
audio_data = open(audio_path, "rb")
|
| 283 |
files = {"file": audio_data, "model": (None, "whisper-1")}
|
| 284 |
+
base = _get_api_base()
|
| 285 |
+
url = f"{base}/v1/audio/transcriptions"
|
| 286 |
+
resp = requests.post(url, headers=headers, files=files, timeout=60)
|
| 287 |
if resp.status_code == 200:
|
| 288 |
return resp.json()["text"]
|
| 289 |
return f"转录失败: {resp.status_code}"
|
|
|
|
| 292 |
|
| 293 |
@tool
|
| 294 |
def get_youtube_transcript(video_url: str) -> str:
|
| 295 |
+
# ... 不变 ...
|
| 296 |
try:
|
| 297 |
if "watch?v=" in video_url:
|
| 298 |
vid = video_url.split("v=")[1].split("&")[0]
|
|
|
|
| 307 |
|
| 308 |
@tool
|
| 309 |
def download_file_for_task(task_id: str) -> str:
|
| 310 |
+
# ... 不变 ...
|
| 311 |
try:
|
| 312 |
url = f"{api_url_tasks}/files/{task_id}"
|
| 313 |
resp = requests.get(url, timeout=20)
|
|
|
|
| 330 |
return f"文件下载失败: {e}"
|
| 331 |
|
| 332 |
# =============================================================================
|
| 333 |
+
# LangGraph Agent
|
| 334 |
# =============================================================================
|
| 335 |
class AgentState(TypedDict):
|
| 336 |
messages: Annotated[Sequence[BaseMessage], operator.add]
|
| 337 |
final_answer: str
|
| 338 |
task_id: str
|
| 339 |
|
| 340 |
+
tools = [web_search, web_scraper, calculator, analyze_image, transcribe_audio, get_youtube_transcript, download_file_for_task]
|
| 341 |
+
tool_node = ToolNode(tools)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
llm = QwenLLM()
|
| 344 |
functions = [convert_to_openai_function(t) for t in tools]
|
|
|
|
| 373 |
workflow.add_node("agent", agent_node)
|
| 374 |
workflow.add_node("tools", tool_node)
|
| 375 |
workflow.add_node("finish", finish_node)
|
|
|
|
| 376 |
workflow.set_entry_point("agent")
|
| 377 |
workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", "finish": "finish"})
|
| 378 |
workflow.add_edge("tools", "agent")
|
| 379 |
workflow.add_edge("finish", END)
|
|
|
|
| 380 |
return workflow.compile()
|
| 381 |
|
|
|
|
|
|
|
|
|
|
| 382 |
class LangGraphAgent:
|
| 383 |
def __init__(self):
|
| 384 |
self.graph = build_graph()
|
| 385 |
print("LangGraphAgent 初始化完成,使用模型:", QWEN_MODEL)
|
| 386 |
|
| 387 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 388 |
+
state = {"messages": [HumanMessage(content=question)], "final_answer": "", "task_id": task_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
try:
|
| 390 |
final_state = self.graph.invoke(state)
|
| 391 |
return final_state["final_answer"]
|
|
|
|
| 394 |
return f"Error: {e}"
|
| 395 |
|
| 396 |
# =============================================================================
|
| 397 |
+
# 主运行函数
|
| 398 |
# =============================================================================
|
| 399 |
+
import pandas as pd
|
| 400 |
+
|
| 401 |
def run_and_submit_all(profile: gr.OAuthProfile | None) -> Generator:
|
| 402 |
space_id = os.getenv("SPACE_ID")
|
| 403 |
if not profile:
|
|
|
|
| 415 |
yield f"<div>Agent 初始化失败: {e}</div>", f"Agent 初始化失败: {e}", pd.DataFrame()
|
| 416 |
return
|
| 417 |
|
|
|
|
| 418 |
try:
|
| 419 |
resp = requests.get(f"{api_url}/questions", timeout=15)
|
| 420 |
resp.raise_for_status()
|
|
|
|
| 450 |
yield monitor.get_html(), "没有答案可提交", pd.DataFrame(results_log)
|
| 451 |
return
|
| 452 |
|
| 453 |
+
submission = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
try:
|
| 455 |
resp = requests.post(f"{api_url}/submit", json=submission, timeout=60)
|
| 456 |
resp.raise_for_status()
|
|
|
|
| 470 |
# =============================================================================
|
| 471 |
# Gradio 界面
|
| 472 |
# =============================================================================
|
|
|
|
|
|
|
| 473 |
with gr.Blocks(title="GAIA Agent") as demo:
|
| 474 |
gr.Markdown("""
|
| 475 |
# 🤖 GAIA Level 1 Agent (LangGraph + Qwen)
|
| 476 |
**模型:** Qwen3.5-35B-A3B | **API:** agicto.com
|
| 477 |
点击按钮获取题目,Agent 自动调用工具并回答,最后提交评分。
|
| 478 |
""")
|
|
|
|
| 479 |
gr.LoginButton()
|
|
|
|
| 480 |
run_btn = gr.Button("🚀 运行评测并提交", variant="primary")
|
|
|
|
| 481 |
progress_html = gr.HTML(label="实时进度")
|
| 482 |
status_output = gr.Textbox(label="提交结果 / 总分", lines=5, interactive=False)
|
| 483 |
results_table = gr.DataFrame(label="题目与 Agent 答案", wrap=True)
|
|
|
|
| 484 |
run_btn.click(
|
| 485 |
fn=run_and_submit_all,
|
| 486 |
outputs=[progress_html, status_output, results_table]
|
| 487 |
)
|
| 488 |
|
| 489 |
if __name__ == "__main__":
|
| 490 |
+
# 检查必要环境变量
|
| 491 |
+
if not AGICTO_API_KEY:
|
| 492 |
+
print("❌ 错误:AGICTO_API_KEY 未设置!请在 Space 的 Settings -> Repository Secrets 中添加。")
|
| 493 |
+
if "v1" in AGICTO_BASE_URL:
|
| 494 |
+
print("⚠️ 提示:AGICTO_BASE_URL 不应包含 /v1,已自动去除。请考虑设置为 https://api.agicto.cn")
|
| 495 |
print("启动 Gradio App...")
|
| 496 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|