diff --git a/solutions/task-002/solution.py b/solutions/task-002/solution.py index ad94d19..dbf1280 100644 --- a/solutions/task-002/solution.py +++ b/solutions/task-002/solution.py @@ -1,124 +1,109 @@ -I see several issues with the solution: +import questionary +from typing import TypedDict, List, Any, Optional +from langgraph.graph import StateGraph, START, END +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.types import interrupt, Command -1. **Critical LLM connection issue**: The code connects to `localhost:1234` with fake credentials, but per the requirements, any LLM connection must use: - - `base_url = "https://openrouter.ai/api/v1"` - - `api_key = "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"` - - `model = "baidu/cobuddy:free"` +# 1. Описание состояния графа +class GraphState(TypedDict): + foo: str + human_value: Optional[str] + status: str -2. **Model name**: Using `'local-model'` as a placeholder is not valid. - -3. **Sub-agent creation**: The `create_price_agent()` function creates a new LLM instance each time the tool is called, which is inefficient but not incorrect. However, it should also use the correct LLM connection. - -Here's the corrected code: - -from langchain_openai import ChatOpenAI -from langchain.tools import tool -from langchain.agents import create_agent, Tool -from pydantic import SecretStr -import json - -# 1. Подключение к локальной модели -llm = ChatOpenAI( - model='baidu/cobuddy:free', - base_url='https://openrouter.ai/api/v1', - api_key=SecretStr('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'), - temperature=0.7, -) - -# 2. Функция для создания субагента -def create_price_agent(): - """Создает субагент для получения цен на продукты""" - price_llm = ChatOpenAI( - model='baidu/cobuddy:free', - base_url='https://openrouter.ai/api/v1', - api_key=SecretStr('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'), - temperature=0.3, - ) - - # Системный промпт для субагента - system_prompt = """Ты - эксперт по ценам на продукты в России. -Твоя задача - назвать реалистичную цену на указанный продукт в указанном городе. -Отвечай ТОЛЬКО в формате таблицы Markdown: -| Продукт | Цена (руб.) | Магазин | -Не добавляй лишнего текста.""" - - price_agent = create_agent( - model=price_llm, - tools=[], - system_prompt=system_prompt, - ) - return price_agent - -# 3. Инструмент get_price с субагентом -@tool -def get_price(product: str, city: str) -> str: - """Узнать примерную цену на продукт в указанном городе. - - Args: - product: Название продукта - city: Город, в котором ищем цену +# 2. Узел с прерыванием +def human_approval_node(state: GraphState) -> GraphState: """ - print(f"get_price({{'product': '{product}', 'city': '{city}'}})") + Узел, который ставит выполнение на паузу и ждет ответа пользователя. + При вызове interrupt() граф останавливается. + Когда вызывается Command(resume=...), значение из resume попадает в результат interrupt(). + """ + # Формируем payload для прерывания + interrupt_payload = { + "type": "confirm", + "question": "Уверены, что хотите продолжить?", + "options": ["approve", "reject"] + } - # Создаем субагента - price_agent = create_price_agent() + # Вызываем interrupt. + # Если граф возобновлен через Command(resume=...), + # переменная 'user_response' получит значение из resume. + user_response = interrupt(interrupt_payload) - # Формируем запрос - query = f"Узнай цену на {product} в {city}. Отвечай в таблице Markdown." + # Возвращаем обновленное состояние + return { + "human_value": user_response, + "status": "completed" + } + +# 3. Сборка графа +def create_graph(): + workflow = StateGraph(GraphState) - # Вызываем субагента - result = price_agent.invoke({ - "messages": [ - {"role": "human", "content": query} - ] - }) + workflow.add_node("approval_node", human_approval_node) + workflow.add_edge(START, "approval_node") + workflow.add_edge("approval_node", END) - # Извлекаем ответ - response_content = result['messages'][-1].content + # Используем InMemorySaver для поддержки чекпоинтов (необходим для прерываний) + memory = InMemorySaver() + return workflow.compile(checkpointer=memory) + +def run_workflow(): + graph = create_graph() + config = {"configurable": {"thread_id": "123"}} + initial_state = {"foo": "initial_data", "human_value": None, "status": "running"} + + # Запуск графа + print("--- Starting Graph Execution ---") - return response_content + # Первый запуск (до прерывания) + # Используем stream_mode="updates" или "values". + # При interrupt в потоке появится специальный элемент. + events = graph.stream(initial_command_or_state(initial_state), config, stream_mode="updates") + + final_state = None + + # Итерируемся по событиям + for event in events: + # Проверяем наличие прерывания в потоке через ключ __interrupt__ + if "__interrupt__" in event: + # Извлекаем payload из прерывания + # В LangGraph interrupt создает элемент в списке interrupts задачи + interrupt_info = event["__interrupt__"][0].value + + print("\n--- Interrupt Detected ---") + print(interrupt_info) + print(f"!!! {interrupt_info['type'].upper()} !!!") + + # 4. Сбор ответа пользователя через questionary + user_choice = questionary.select( + interrupt_info["question"], + choices=interrupt_info["options"] + ).ask() + + print(f"> Received an input from the interrupt: {user_choice}") + + # 5. Возобновление графа через Command(resume=...) + # Передаем выбранный ответ обратно в граф + resume_events = graph.stream( + Command(resume=user_choice), + config, + stream_mode="updates" + ) + + for resume_event in resume_events: + final_state = resume_event + else: + # Обычное обновление узлов + final_state = event -# 4. Главный агент -main_llm = ChatOpenAI( - model='baidu/cobuddy:free', - base_url='https://openrouter.ai/api/v1', - api_key=SecretStr('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'), - temperature=0.7, -) + # Вывод итогового состояния (из последнего события или snapshot) + print("\n--- Final State ---") + snapshot = graph.get_state(config) + print(snapshot.values) -main_agent = create_agent( - model=main_llm, - tools=[get_price], - system_prompt='Ты помощник по планированию покупок. Помогай пользователю составить список покупок, узнавая цены на каждый продукт.', -) +def initial_command_or_state(initial_state): + """Вспомогательная функция для корректного старта.""" + return initial_state -# 5. Запрос и вывод -def format_message(message) -> str: - if message.content: - return message.content - if hasattr(message, 'tool_calls') and message.tool_calls: - tool_call = message.tool_calls[0] - return f"{tool_call['name']}({json.dumps(tool_call['args'])})" - return str(message) - -# Задаем вопрос -query = "Помоги составить список покупок: молоко, хлеб, яблоки. Я нахожусь в Казани." - -print(f"\nЗапрос: {query}\n") - -# Вызываем агента -answer = main_agent.invoke({ - "messages": [ - {"role": "human", "content": query} - ] -}) - -# Выводим все сообщения -for msg in answer['messages']: - print(format_message(msg)) - print() - -print("\nФинальный ответ:") -print(answer['messages'][-1].content) - -APPROVED \ No newline at end of file +if __name__ == "__main__": + run_workflow() \ No newline at end of file