From 98902247ce563c17198713cc85cd8dbe1912a2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB=20=D0=92=D0=B8=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= Date: Thu, 2 Jul 2026 06:38:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20main.py=20=E2=80=94=20=D0=A1=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=B9=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20AI=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=20=D0=BD=D0=B0=20P?= =?UTF-8?q?ython=20=D1=81=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20langchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 125 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/main.py b/main.py index 157e481..bfb1ff1 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,28 @@ import os import asyncio -from typing import Any +from typing import List +from pydantic import SecretStr from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage +from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from langchain.tools import tool -from deepagents import create_deep_agent -from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend -# LLM configuration according to the assignment specification +from deepagents import create_deep_agent +from deepagents.backends import CompositeBackend, LocalShellBackend, FilesystemBackend + +# ---------------------------------------------------------------------- +# Configuration of the LLM (OpenRouter, as required by the course) +# ---------------------------------------------------------------------- llm = ChatOpenAI( - model="your-model-name", # replace with the actual model name in LM Studio - base_url="http://localhost:1234/v1", - api_key="fake", # OpenAI SDK requires a non-empty key + model="openai/gpt-oss-20b:free", + base_url="https://openrouter.ai/api/v1", + api_key=SecretStr(os.getenv("OPENAI_API_KEY")), temperature=0.7, ) -# Backend for file operations and shell commands (required by deepagents) +# ---------------------------------------------------------------------- +# Backend for the agents - allows file operations and shell commands +# ---------------------------------------------------------------------- backend = CompositeBackend( [ LocalShellBackend(workspace_dir="./workspace"), @@ -24,78 +30,85 @@ backend = CompositeBackend( ] ) +# ---------------------------------------------------------------------- +# Sub-agent tool: get_price +# ---------------------------------------------------------------------- @tool def get_price(product: str, city: str) -> str: """ - Retrieve a realistic price for the given product in the specified city. - The function creates a sub-agent that returns a markdown table row. + Получить примерную цену продукта в указанном городе. + Возвращает markdown-таблицу с колонками: Продукт, Цена (руб.), Магазин. """ - # System prompt for the sub-agent - it must output a table with columns - # Product, Price (руб.), Store. - sub_system_prompt = ( - "You are a price generator. Provide a markdown table with columns " - "'Продукт', 'Цена (руб.)', 'Магазин' for the given product and city. " - "Give a realistic price and a plausible store name." - ) - - # Create the sub-agent (no additional tools needed) + # Создаём суб-агента, который генерирует цену. sub_agent = create_deep_agent( model=llm, tools=[], backend=backend, - system_prompt=sub_system_prompt, + system_prompt=( + "Ты суб-агент, который генерирует реалистичную цену продукта " + "в заданном городе. Выдай результат в виде markdown-таблицы " + "с колонками: Продукт, Цена (руб.), Магазин." + ), ) - # Prepare the query for the sub-agent - query = f"Provide price information for {product} in {city}." - - # Invoke the sub-agent synchronously - # DESIGN DECISION: Use asyncio.run to execute the sub-agent inside a - # synchronous tool. deepagents operates asynchronously, but the tool - # interface required by the main agent is synchronous. - # NECESSITY: The assignment defines the tool as a regular function. - # OPTIMALITY: This approach keeps the code simple and avoids mixing - # async/sync contexts incorrectly. - # ALTERNATIVES CONSIDERED: Making the tool async (deepagents supports - # async tools) would require changes to the main agent invocation pattern, - # which is unnecessary for this educational example. - result = asyncio.run( - sub_agent.ainvoke( + # Формируем запрос к суб-агенту + query = f"Сгенерируй цену для продукта '{product}' в городе {city}." + # Асинхронный вызов суб-агента + async def _invoke(): + result = await sub_agent.ainvoke( {"messages": [HumanMessage(content=query)]}, {"configurable": {"thread_id": f"price-{product}-{city}"}}, ) - ) - # Extract the final content from the sub-agent's response - return result["messages"][-1].content + # Последнее сообщение содержит таблицу + return result["messages"][-1].content -# Main shopping-list agent -agent = create_deep_agent( + # Запускаем цикл событий, если уже внутри async контекста + try: + loop = asyncio.get_running_loop() + table = loop.create_task(_invoke()) + # Если мы уже в async функции, вернём задачу, иначе дождёмся результата + if isinstance(table, asyncio.Task): + return asyncio.run(table) + except RuntimeError: + # Нет запущенного цикла - создаём новый + return asyncio.run(_invoke()) + +# ---------------------------------------------------------------------- +# Main shopping-assistant agent +# ---------------------------------------------------------------------- +assistant_agent = create_deep_agent( model=llm, tools=[get_price], backend=backend, system_prompt="Ты помощник по планированию покупок.", ) -def format_message(message: Any) -> str: - """Convert a LangChain message to a readable string.""" - if hasattr(message, "content") and message.content: - return message.content - if hasattr(message, "tool_calls") and message.tool_calls: - tc = message.tool_calls[0] - return f"{tc['name']}({tc['args']})" - return str(message) +# ---------------------------------------------------------------------- +# Helper to format the chain of messages for display +# ---------------------------------------------------------------------- +def format_message(msg) -> str: + if isinstance(msg, HumanMessage): + return f"Human: {msg.content}" + if isinstance(msg, AIMessage): + return f"AI: {msg.content}" + if isinstance(msg, ToolMessage): + # tool call result + return f"ToolResult: {msg.content}" + # Fallback for generic messages + return str(msg) -async def main() -> None: +async def main(): user_query = "Помоги составить список покупок: молоко, хлеб, яблоки. Я нахожусь в Казани." - result = await agent.ainvoke( + result = await assistant_agent.ainvoke( {"messages": [HumanMessage(content=user_query)]}, {"configurable": {"thread_id": "shopping-session-1"}}, ) - # Output the whole chain of messages - for idx, msg in enumerate(result["messages"], start=1): - print(f"--- Message {idx} ---") - print(format_message(msg)) - print() + + # Выводим всю цепочку сообщений + print("\n--- Диалог с агентом ---\n") + for m in result["messages"]: + print(format_message(m)) + print("---") if __name__ == "__main__": asyncio.run(main()) \ No newline at end of file