diff --git a/main.py b/main.py index 30e65fc..0210f77 100644 --- a/main.py +++ b/main.py @@ -1,108 +1,74 @@ -import os -import asyncio -from typing import Any, Dict, List - -from pydantic import SecretStr +import json from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from langchain.tools import tool -from deepagents import create_deep_agent -from deepagents.backends import CompositeBackend, LocalShellBackend, FilesystemBackend +from langchain.agents import create_agent +from pydantic import SecretStr -# DESIGN DECISION: Use OpenRouter LLM as required by the technical constraints. -# NECESSITY: The course forbids local LM endpoints and mandates OpenRouter for all LLM calls. -# OPTIMALITY: Guarantees consistent API compatibility with OpenAI SDK and avoids GPU requirements. -# ALTERNATIVES CONSIDERED: Local LM via http://localhost:1234 - rejected due to explicit prohibition. +# Конфигурация локальной модели LM Studio llm = ChatOpenAI( - model="openai/gpt-oss-20b:free", - base_url="https://openrouter.ai/api/v1", - api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-4o-mini", + base_url="http://localhost:1234/v1", + api_key=SecretStr("fake"), temperature=0.7, ) -# Backend required by deepagents - combines a shell and filesystem workspace. -backend = CompositeBackend( - [ - LocalShellBackend(workspace_dir="./workspace"), - FilesystemBackend(), - ] -) - @tool def get_price(product: str, city: str) -> str: - """ - Получить примерную цену продукта в указанном городе. - Возвращает таблицу в markdown-формате: - | Продукт | Цена (руб.) | Магазин | - """ - # DESIGN DECISION: Sub-agent is created inside the tool using the same LLM. - # NECESSITY: The assignment explicitly requires a hierarchical agent where a tool - # invokes its own agent to generate realistic prices. - # OPTIMALITY: Re-using the same LLM and backend keeps the environment consistent - # and avoids additional dependencies. - # ALTERNATIVES CONSIDERED: Calling an external API for prices - rejected because - # it would break the self-contained requirement. - sub_agent = create_deep_agent( + """Получить примерную цену продукта в указанном городе.""" + # Создаём субагента, который генерирует таблицу цен + sub_agent = create_agent( model=llm, - tools=[], # No further tools needed for price generation - backend=backend, + tools=[], system_prompt=( - "Ты суб-агент, который генерирует реалистичную цену продукта в заданном городе. " - "Ответ дай в виде markdown-таблицы с колонками: Продукт, Цена (руб.), Магазин." + f"Ты генератор цены для продукта {product} в городе {city}. " + "Сгенерируй таблицу в формате:\n" + "| Продукт | Цена (руб.) | Магазин |\n" + "|---------|-------------|---------|\n" + f"Например:\n" + f"| {product} | 100 | Магнит |\n" + "Ответ должен содержать только таблицу без лишних слов." ), ) - - # Формируем запрос к суб-агенту - query = f"Сгенерируй цену для продукта '{product}' в городе '{city}'." - # Асинхронный вызов суб-агента - async def invoke_sub() -> Dict[str, Any]: - return await sub_agent.ainvoke( - {"messages": [HumanMessage(content=query)]}, - {"configurable": {"thread_id": f"price-{product}-{city}"}}, - ) - - # Запускаем цикл событий, если уже внутри async контекста - try: - loop = asyncio.get_running_loop() - result = loop.create_task(invoke_sub()) - sub_result = asyncio.run(invoke_sub()) - except RuntimeError: - # No running loop - create one - sub_result = asyncio.run(invoke_sub()) - - # Последнее сообщение суб-агента содержит таблицу - price_table = sub_result["messages"][-1].content - return price_table + # Запускаем субагента + result = sub_agent.invoke( + {"messages": [{"role": "user", "content": f"Сгенерируй цену для {product} в {city}"}]} + ) + # Извлекаем текст ответа + content = result["messages"][-1]["content"] + return content # Главный агент -agent = create_deep_agent( +main_agent = create_agent( model=llm, tools=[get_price], - backend=backend, system_prompt="Ты помощник по планированию покупок.", ) -def format_message(msg: Any) -> str: - """Привести сообщение к читаемому виду.""" - if isinstance(msg, AIMessage) or isinstance(msg, HumanMessage): - return f"{msg.type.upper()}: {msg.content}" - if isinstance(msg, ToolMessage): - return f"TOOL CALL: {msg.name}({msg.args}) -> {msg.content}" - # Fallback - return str(msg) +def format_message(message: dict) -> str: + """Форматируем сообщение для вывода.""" + if "content" in message and message["content"]: + return message["content"] + if "tool_calls" in message and message["tool_calls"]: + calls = [] + for call in message["tool_calls"]: + name = call["name"] + args_str = call.get("arguments", "{}") + try: + args = json.loads(args_str) + except json.JSONDecodeError: + args = args_str + calls.append(f"{name}({args})") + return " | ".join(calls) + return "" -async def main() -> None: +def main(): user_query = "Помоги составить список покупок: молоко, хлеб, яблоки. Я нахожусь в Казани." - result = await agent.ainvoke( - {"messages": [HumanMessage(content=user_query)]}, - {"configurable": {"thread_id": "session-1"}}, + result = main_agent.invoke( + {"messages": [{"role": "human", "content": user_query}]} ) - - # Вывод всей цепочки сообщений - for i, message in enumerate(result["messages"]): - print(f"--- Message {i + 1} ---") - print(format_message(message)) - print() + for msg in result["messages"]: + print(format_message(msg)) + print("---") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + main() \ No newline at end of file