yoshizen commited on
Commit
f836dd5
·
verified ·
1 Parent(s): b5494eb

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +272 -0
  2. requirements.txt +10 -0
app.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Финальный агент для Agent Challenge (LangGraph)
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import re
8
+ import math
9
+ import requests
10
+ from typing import List, Dict, Any, Optional, TypedDict, Annotated, Literal, Union
11
+ from datetime import datetime
12
+
13
+ # Импорт необходимых компонентов LangGraph
14
+ from langgraph.graph import StateGraph, END
15
+ from langgraph.prebuilt import ToolNode, tools_condition
16
+
17
+ # Импорт инструментов LangChain
18
+ from langchain_core.tools import tool
19
+
20
+ # Безопасная обработка токена Hugging Face
21
+ # Токен должен быть установлен как переменная окружения HUGGINGFACE_TOKEN
22
+ # или передан через Secrets в Hugging Face Spaces
23
+ HUGGINGFACE_TOKEN = os.environ.get("HUGGINGFACE_TOKEN")
24
+
25
+ # Инициализация клиента Hugging Face
26
+ client = None
27
+ try:
28
+ from huggingface_hub import InferenceClient
29
+ client = InferenceClient(
30
+ model="mistralai/Mixtral-8x7B-Instruct-v0.1", # Рекомендуемая модель
31
+ token=HUGGINGFACE_TOKEN,
32
+ timeout=120 # Увеличим таймаут для больших моделей
33
+ )
34
+ except ImportError:
35
+ print("Ошибка: библиотека huggingface_hub не установлена. Установите: pip install huggingface_hub")
36
+ except Exception as e:
37
+ print(f"Ошибка инициализации InferenceClient: {e}. Проверьте токен и доступность модели.")
38
+
39
+ # --- Определение инструментов ---
40
+
41
+ @tool
42
+ def calculator(expression: str) -> str:
43
+ """Выполняет математические вычисления.
44
+ Пример входа: "(2 + 3) * 4 / 2"
45
+ Возвращает результат вычисления или сообщение об ошибке.
46
+ """
47
+ try:
48
+ # Ограничение на доступные функции для безопасности
49
+ allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
50
+ allowed_names["abs"] = abs
51
+ allowed_names["round"] = round
52
+ allowed_names["max"] = max
53
+ allowed_names["min"] = min
54
+
55
+ # Удаление потенциально опасных символов (хотя eval все равно рискован)
56
+ safe_expression = re.sub(r"[^0-9\.\+\-\*\/\(\)\s]|\b(import|exec|eval|open|lambda|\_\_)\b", "", expression)
57
+
58
+ if safe_expression != expression:
59
+ return "Ошибка: Обнаружены недопустимые символы в выражении."
60
+
61
+ result = eval(safe_expression, {"__builtins__": {}}, allowed_names)
62
+ return f"Результат: {result}"
63
+ except Exception as e:
64
+ return f"Ошибка в вычислении: {str(e)}"
65
+
66
+ @tool
67
+ def web_search(query: str) -> str:
68
+ """Выполняет поиск в интернете по заданному запросу.
69
+ Пример входа: "прогноз погоды в Париже"
70
+ Возвращает результаты поиска (симуляция).
71
+ Для реального использования замените на API поисковой системы (например, Tavily, Serper).
72
+ """
73
+ print(f"--- Выполняется поиск: {query} ---")
74
+ # --- Симуляция поиска ---
75
+ # В реальном приложении здесь будет вызов API поисковика
76
+ try:
77
+ # Пример использования requests (закомментировано, т.к. нет реального API)
78
+ # headers = {"X-API-KEY": os.environ.get("SEARCH_API_KEY"), "Content-Type": "application/json"}
79
+ # data = json.dumps({"query": query, "max_results": 3})
80
+ # response = requests.post("https://api.tavily.com/search", headers=headers, data=data)
81
+ # response.raise_for_status()
82
+ # results = response.json()["results"]
83
+ # return json.dumps(results)
84
+
85
+ # Простая симуляция для теста
86
+ if "погода" in query.lower():
87
+ return json.dumps([{"title": "Прогноз погоды", "content": "В городе, который вы ищете, сегодня солнечно, +25C."}])
88
+ elif "hugging face" in query.lower():
89
+ return json.dumps([{"title": "Hugging Face", "content": "Hugging Face - это платформа и сообщество для работы с моделями машинного обучения."}])
90
+ elif "langgraph" in query.lower():
91
+ return json.dumps([{"title": "LangGraph", "content": "LangGraph - это библиотека для создания агентов с состоянием на основе LangChain."}])
92
+ else:
93
+ return json.dumps([{"title": "Результат поиска", "content": f"По вашему запросу '{query}' найдена общая информация."}])
94
+
95
+ except requests.exceptions.RequestException as e:
96
+ return f"Ошибка сети при поиске: {e}"
97
+ except Exception as e:
98
+ return f"Ошибка при выполнении поиска: {str(e)}"
99
+
100
+ @tool
101
+ def get_current_datetime() -> str:
102
+ """Возвращает текущую дату и время.
103
+ Не требует входных данных.
104
+ """
105
+ now = datetime.now()
106
+ return f"Текущая дата и время: {now.strftime('%Y-%m-%d %H:%M:%S')}"
107
+
108
+ # Список инструментов для агента
109
+ tools_list = [calculator, web_search, get_current_datetime]
110
+
111
+ # --- Определение состояния и графа ---
112
+
113
+ class AgentState(TypedDict):
114
+ """Состояние агента LangGraph."""
115
+ messages: List[Union[Dict[str, str], Any]] # История сообщений (включая вызовы инструментов)
116
+
117
+ # Узел агента (LLM для принятия решений)
118
+ def agent_node(state: AgentState) -> Dict[str, Any]:
119
+ """Вызывает LLM для определения следующего шага (вызов инструмента или финальный ответ)."""
120
+ if client is None:
121
+ raise ValueError("Клиент Hugging Face не инициализирован.")
122
+
123
+ # Формируем промпт для LLM
124
+ # Важно: Промпт должен быть адаптирован под конкретную модель (Mixtral)
125
+ # и формат вывода инструментов LangChain/LangGraph
126
+
127
+ # Преобразуем state["messages"] в формат, понятный Mixtral
128
+ prompt_messages = []
129
+ for msg in state["messages"]:
130
+ if isinstance(msg, dict) and "role" in msg and "content" in msg:
131
+ prompt_messages.append(msg)
132
+ elif hasattr(msg, "type") and msg.type == "human":
133
+ prompt_messages.append({"role": "user", "content": msg.content})
134
+ elif hasattr(msg, "type") and msg.type == "ai":
135
+ # Обработка вызовов инструментов в ответе AI
136
+ content = msg.content
137
+ if hasattr(msg, "tool_calls") and msg.tool_calls:
138
+ tool_calls_str = json.dumps([tc["name"] for tc in msg.tool_calls])
139
+ content += f"\n(Вызов инструментов: {tool_calls_str})"
140
+ prompt_messages.append({"role": "assistant", "content": content})
141
+ elif hasattr(msg, "type") and msg.type == "tool":
142
+ prompt_messages.append({
143
+ "role": "tool",
144
+ "content": f"Результат инструмента {msg.name}: {msg.content}",
145
+ "name": msg.name # Добавляем имя инструмента для контекста
146
+ })
147
+ else:
148
+ # Пропускаем или логируем неизвестные типы сообщений
149
+ print(f"Пропущено сообщение неизвестного типа: {type(msg)}")
150
+
151
+ print("--- Промпт для LLM ---")
152
+ # print(json.dumps(prompt_messages, indent=2, ensure_ascii=False))
153
+ print("...") # Не выводим весь промпт, может быть большим
154
+
155
+ # Вызов LLM
156
+ response = client.chat_completion(
157
+ messages=prompt_messages,
158
+ tool_choice="auto", # Позволяем модели решать, использовать ли инструмент
159
+ tools=[tool.get_input_schema().schema() for tool in tools_list], # Передаем схему инструментов
160
+ temperature=0.1, # Низкая температура для более предсказуемых вызовов
161
+ max_tokens=1500
162
+ )
163
+
164
+ ai_message = response["choices"][0]["message"]
165
+
166
+ print("--- Ответ LLM ---")
167
+ print(ai_message)
168
+
169
+ # Возвращаем сообщение для добавления в состояние графом
170
+ return {"messages": [ai_message]}
171
+
172
+ # Создание графа
173
+ workflow = StateGraph(AgentState)
174
+
175
+ # Добавление узлов
176
+ workflow.add_node("agent", agent_node)
177
+ workflow.add_node("tools", ToolNode(tools_list))
178
+
179
+ # Определение ребер
180
+ workflow.set_entry_point("agent")
181
+
182
+ # Условное ребро: после агента решаем, вызывать ли инструменты или завершать
183
+ workflow.add_conditional_edges(
184
+ "agent",
185
+ # Функция conditions.tools_condition проверяет, есть ли tool_calls в последнем сообщении
186
+ tools_condition,
187
+ # Если есть вызовы -> к узлу tools, иначе -> к концу (END)
188
+ {
189
+ "tools": "tools",
190
+ END: END,
191
+ },
192
+ )
193
+
194
+ # Ребро от узла инструментов обратно к агенту для обработки результата
195
+ workflow.add_edge("tools", "agent")
196
+
197
+ # Компиляция графа
198
+ agent_graph = workflow.compile()
199
+
200
+ # --- Функция для запуска агента ---
201
+ def run_final_agent(query: str) -> str:
202
+ """Запускает финального агента LangGraph для ответа на вопрос."""
203
+ if agent_graph is None:
204
+ return "Ошибка: Граф агента не скомпилирован."
205
+
206
+ # Начальное состояние с запросом пользователя
207
+ initial_state = {"messages": [{"role": "user", "content": query}]}
208
+
209
+ final_state = None
210
+ try:
211
+ # Запуск графа
212
+ final_state = agent_graph.invoke(initial_state, {"recursion_limit": 10})
213
+
214
+ except Exception as e:
215
+ print(f"Ошибка выполнения графа: {e}")
216
+ return f"Произошла ошибка во время обработки запроса: {e}"
217
+
218
+ # Извлечение финального ответа из состояния
219
+ if final_state and "messages" in final_state and final_state["messages"]:
220
+ # Ищем последнее сообщение от ассистента без вызова инструментов
221
+ for msg in reversed(final_state["messages"]):
222
+ # Проверяем, что это сообщение от AI и нет активных tool_calls
223
+ is_ai = (isinstance(msg, dict) and msg.get("role") == "assistant") or (hasattr(msg, "type") and msg.type == "ai")
224
+ has_tool_calls = (isinstance(msg, dict) and msg.get("tool_calls")) or (hasattr(msg, "tool_calls") and msg.tool_calls)
225
+
226
+ if is_ai and not has_tool_calls:
227
+ return msg.get("content") if isinstance(msg, dict) else msg.content
228
+
229
+ # Если не нашли чистого ответа, возвращаем последнее сообщение AI
230
+ last_ai_msg = next((m for m in reversed(final_state["messages"]) if (isinstance(m, dict) and m.get("role") == "assistant") or (hasattr(m, "type") and m.type == "ai")), None)
231
+ if last_ai_msg:
232
+ return last_ai_msg.get("content") if isinstance(last_ai_msg, dict) else last_ai_msg.content
233
+
234
+ return "Не удалось получить финальный ответ от агента."
235
+
236
+ # --- API для Hugging Face Spaces ---
237
+ # Если запускается как веб-приложение
238
+ if __name__ == "__main__":
239
+ from fastapi import FastAPI, Request
240
+ from fastapi.responses import JSONResponse
241
+ import uvicorn
242
+
243
+ app = FastAPI(title="Agent Challenge - Финальный агент")
244
+
245
+ @app.get("/")
246
+ async def root():
247
+ return {"message": "Агент готов к работе! Отправьте POST запрос на /agent с JSON {'query': 'ваш вопрос'}"}
248
+
249
+ @app.post("/agent")
250
+ async def agent_endpoint(request: Request):
251
+ try:
252
+ data = await request.json()
253
+ query = data.get("query", "")
254
+
255
+ if not query:
256
+ return JSONResponse(
257
+ status_code=400,
258
+ content={"error": "Запрос должен содержать поле 'query'"}
259
+ )
260
+
261
+ response = run_final_agent(query)
262
+ return {"answer": response}
263
+
264
+ except Exception as e:
265
+ return JSONResponse(
266
+ status_code=500,
267
+ content={"error": f"Ошибка обработки запроса: {str(e)}"}
268
+ )
269
+
270
+ # Для локального запуска (не используется в Hugging Face Spaces)
271
+ if os.environ.get("RUN_LOCAL") == "true":
272
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ huggingface_hub>=0.19.0
2
+ transformers>=4.35.0
3
+ langchain>=0.0.335
4
+ langchain-core>=0.1.0
5
+ langgraph>=0.0.15
6
+ fastapi>=0.104.1
7
+ uvicorn>=0.24.0
8
+ pydantic>=2.4.2
9
+ requests>=2.31.0
10
+ python-dotenv>=1.0.0