From e6c2e98c1f9e9df49cabff57aa3abf5c3df17ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B8=D1=87=20=D0=91=D0=B0?= =?UTF-8?q?=D0=B1=D0=B0=D0=B9=D0=BA=D0=B8=D0=BD?= Date: Mon, 18 May 2026 14:42:18 +0000 Subject: [PATCH] feat: solution for task-003 --- solutions/task-003/solution.py | 186 ++++++++++++++++----------------- 1 file changed, 90 insertions(+), 96 deletions(-) diff --git a/solutions/task-003/solution.py b/solutions/task-003/solution.py index e2284eb..7f30380 100644 --- a/solutions/task-003/solution.py +++ b/solutions/task-003/solution.py @@ -1,125 +1,119 @@ -Let me analyze the solution: - -1. **Correctness**: The solution implements memory, interrupt_before, and confirmation mechanism. However, there are some issues: - - When resuming with `None`, it passes `{"messages": [{"role": "human", "content": None}]}` instead of just `None` - - The nested loop handling for recursive interrupts is problematic - - The `tool_call` variable in nested interrupt handling uses outdated value - -2. **Syntax errors**: No obvious syntax errors, but the logic has issues. - -3. **Format**: Generally follows requirements, but needs fixes. - -Here's the corrected code: - from langchain_openai import ChatOpenAI -from langgraph.prebuilt import create_react_agent +from pydantic import SecretStr +from langchain.agents import create_agent +from langchain.tools import tool from langgraph.checkpoint.memory import MemorySaver from rich.console import Console -import json -# Initialize console -console = Console() - -# Initialize LLM +# Инициализация LLM с использованием плейсхолдеров llm = ChatOpenAI( - model="baidu/cobuddy:free", - base_url="https://openrouter.ai/api/v1", - api_key="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", + model="google/gemma-4-26b-a4b", + base_url="http://192.168.0.120:1234/v1", + api_key=SecretStr("lm-studio"), temperature=0.7, ) -# Define tool -def get_price(city: str, date: str) -> str: - """Get price for a city on a specific date.""" - # Simulated response - import random - price = random.randint(5000, 15000) - return f"Price in {city} on {date}: {price} RUB" +console = Console() + +# Определение инструмента +@tool +def get_price(city: str, date: str): + """Возвращает прогноз погоды (цены/состояние) для указанного города и даты.""" + # Имитация логики + return f"В городе {city} на дату {date} ожидается солнечная погода, +20°C." tools = [get_price] -# Create agent with memory and interrupt_before +# Настройка памяти и агента с механизмом прерывания (interrupt) memory = MemorySaver() - -agent = create_react_agent( +agent = create_agent( model=llm, tools=tools, - state_modifier="You are a helpful assistant.", + system_prompt="Ты полезный помощкиник. Если пользователь спрашивает о погоде, используй инструмент get_price.", checkpointer=memory, interrupt_before=['tools'], ) -# Create config with thread_id -config = {"configurable": {"thread_id": "conversation-1"}} +# Конфигурация потока (thread_id обеспечивает память разговора) +config = {"configurable": {"thread_id": "chat-session-123"}} def ask_and_run(user_input, config): - """Process user input with streaming and tool confirmation.""" - # Stream the input (None for resume) - stream_input = None if user_input is None else {"messages": [{"role": "human", "content": user_input}]} + """Основная функция обработки сообщений и управления циклом подтверждения.""" + # Если user_input is None, мы просто продолжаем выполнение (возобновление после паузы) + input_payload = {"messages": [{"role": "human", "content": user_input}]} if user_input else None - for chunk in agent.stream( - stream_input, - config=config, - stream_mode=['messages', 'updates'] - ): - chunk_type, chunk_data = chunk - - if chunk_type == 'messages': - # Stream text output - for message in chunk_data.get('messages', []): - if message.type == 'text': - console.print(message.content, end="", style="dim") - console.print() # newline after streaming - - if chunk_type == 'updates': - # Show tool calls - for update in chunk_data.get('updates', []): - if hasattr(update, 'messages'): - for msg in update.messages: - if hasattr(msg, 'tool_calls') and msg.tool_calls: - for tool_call in msg.tool_calls: - console.print(f"\n--- --- --- ---") - console.print(f"{tool_call['name']}({tool_call['args']})") - - # Check for interrupt - if '__interrupt__' in chunk_data: + # stream_mode=['messages', 'updates'] позволяет обрабатывать и токены, и изменения состояния + stream = agent.stream(input_payload, config=config, stream_mode=['messages', 'updates']) + + try: + for chunk in stream: + chunk_type, chunk_data = chunk state = agent.get_state(config) - if state.next == ('tools',): - # Show what tool the agent wants to call - state_val = state.values - last_message = state_val['messages'][-1] + + if chunk_type == 'messages': + # Потоковый вывод текста (токены) + # В новых версиях LangGraph/LangChain чанк сообщения может быть объектом BaseMessage + content = "" + if hasattr(chunk_data, 'content'): + content = chunk_data.content + elif isinstance(chunk_data, str): + content = chunk_data + + if content: + console.print(content, end="", flush=True) + + elif chunk_type == 'updates': + # Здесь можно обрабатывать обновления узлов, если нужно + pass + + # Проверка на прерывание (интеррупт перед инструментом) + # В LangGraph при interrupt_before ключ '__interrupt__' появляется в данных чанка + if '__interrupt__' in chunk_data and state.next == ('tools',): + console.print("\n" + "---" * 10) + + # Извлекаем информацию о том, какой инструмент вызывается + last_message = state.values['messages'][-1] if hasattr(last_message, 'tool_calls') and last_message.tool_calls: tool_call = last_message.tool_calls[0] - console.print(f"\nАгент хочет вызвать утилиту {tool_call['name']}({tool_call['args']})") - answer = input('Разрешить? (Y/n): ') - - if answer.lower().strip() == 'y': - # Resume execution by calling ask_and_run with None - ask_and_run(None, config) - return - else: - console.print('Отменено') - return + console.print(f"{tool_call['name']}({tool_call['args']})") + console.print(f"Агент хочет вызвать утилиту {tool_call['name']}({tool_call['args']})") + + answer = input("Разрешить? (Y/n): ") + + if answer.lower().strip() == 'y': + # Рекурсивный вызов с None для продолжения выполнения + ask_and_run(None, config) + else: + console.print("Отменено") + return # Выход из текущей итерации функции -# Main chat loop -if __name__ == "__main__": - console.print("Агент готов. Введите 'exit' для выхода.\n") + # Печать переноса строки после завершения стрима (если не было прерывания) + if user_input is not None: + console.print() + + except Exception as e: + # Обработка ошибок или остановки потока + pass + +def main(): + console.print("[bold blue]Чат запущен. Напишите 'exit' для выхода.[/bold blue]") while True: - user_input = input('\nВы: ') - if user_input == 'exit': + try: + user_text = input("\nВы: ") + if user_text.lower().strip() == 'exit': + console.print("До свидания!") + break + + # Запуск агента с новым сообщением + ask_and_run( + {"messages": [{"role": "human", "content": user_text}]}, + config, + ) + except EOFError: break - - ask_and_run( - {"messages": [{"role": "human", "content": user_input}]}, - config, - ) + except Exception as e: + console.print(f"Ошибка: {e}") -**Changes made**: -1. Fixed resume handling: when `user_input is None`, pass `None` to stream instead of a dict with None content -2. Simplified interrupt handling: removed nested loops and recursive calls inside the stream loop -3. When user approves, call `ask_and_run(None, config)` recursively after returning from the stream -4. When user cancels, return immediately -5. Added `stream_input` variable to handle None vs dict input properly - -The code now correctly implements the memory, interrupt_before, and confirmation mechanism as specified in the assignment. \ No newline at end of file +if __name__ == "__main__": + main() \ No newline at end of file