diff --git a/solution.py b/solution.py new file mode 100644 index 0000000..8489e9e --- /dev/null +++ b/solution.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +solution.py + +Простой скрипт, который принимает одну строку с описанием задания, +передаёт её модели OpenAI через LangChain и получает валидированный объект +TaskCard (Pydantic). Затем выводит сериализованную форму и краткую сводку. +""" + +import os +import sys +from typing import List + +from pydantic import BaseModel, Field +from langchain_core.prompts import PromptTemplate +from langchain_openai import ChatOpenAI +from langchain_core.output_parsers import PydanticOutputParser + + +# --------------------------------------------------------------------------- # +# 1. Модель данных карточки задания +# --------------------------------------------------------------------------- # +class TaskCard(BaseModel): + """ + Карточка задачи, которую генерирует модель. + """ + + title: str = Field( + ..., + description="Краткое название задачи (например, «Мини‑отчёт по LangChain»).", + ) + subject: str | None = Field( + None, + description="Тема или предмет, к которому относится задача.", + ) + deadline_hint: str | None = Field( + None, + description=( + "Неформальное указание срока сдачи (например, «к пятнице»)." + ), + ) + deliverable_type: str | None = Field( + None, + description="Тип сдаваемого материала (отчёт, код, презентация и т.п.).", + ) + grading_hints: List[str] | None = Field( + None, + description=( + "Ключевые критерии оценки. Каждый элемент списка – отдельный пункт." + ), + ) + + +# --------------------------------------------------------------------------- # +# 2. Настройка LLM и парсера +# --------------------------------------------------------------------------- # +# Параметры модели можно менять через переменные окружения. +OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") +TEMPERATURE = float(os.getenv("OPENAI_TEMPERATURE", "0")) + +llm = ChatOpenAI( + model=OPENAI_MODEL, + temperature=TEMPERATURE, +) + +parser = PydanticOutputParser(pydantic_object=TaskCard) + + +# --------------------------------------------------------------------------- # +# 3. Шаблон промпта +# --------------------------------------------------------------------------- # +prompt_template = PromptTemplate( + template=""" +Вы — помощник, который преобразует неформальное описание задания в структурированный формат. +Пожалуйста, выведите только JSON‑объект с полями, которые описаны ниже. + +{format_instructions} + +Входное описание: +"{input_text}" +""", + input_variables=["input_text"], + partial_variables={"format_instructions": parser.get_format_instructions()}, +) + +# --------------------------------------------------------------------------- # +# 4. Цепочка (prompt → LLM → парсер) +# --------------------------------------------------------------------------- # +chain = prompt_template | llm | parser + + +# --------------------------------------------------------------------------- # +# 5. Вспомогательная функция для вывода сводки +# --------------------------------------------------------------------------- # +def summarize(card: TaskCard) -> str: + """ + Формирует краткую человекочитаемую сводку из объекта TaskCard. + """ + parts = [f"**{card.title}**"] + if card.subject: + parts.append(f"(предмет: {card.subject})") + if card.deadline_hint: + parts.append(f"Срок сдачи: {card.deadline_hint}") + if card.deliverable_type: + parts.append(f"Тип сдаваемого материала: {card.deliverable_type}") + if card.grading_hints: + hints = ", ".join(card.grading_hints) + parts.append(f"Критерии оценки: {hints}") + return " | ".join(parts) + + +# --------------------------------------------------------------------------- # +# 6. Точка входа +# --------------------------------------------------------------------------- # +def main() -> None: + if len(sys.argv) > 1: + # Если передан аргумент, считаем его описанием задания + input_text = " ".join(sys.argv[1:]) + else: + # Иначе читаем из stdin (первую строку) + print("Введите описание задания:") + input_text = sys.stdin.readline().strip() + + if not input_text: + print("Ошибка: входной текст пуст.") + sys.exit(1) + + try: + card: TaskCard = chain.invoke({"input_text": input_text}) + except Exception as exc: + print(f"Не удалось получить структуру от модели: {exc}") + sys.exit(1) + + # Вывод валидированного объекта + print("\n=== Валидированный объект ===") + print(card.model_dump(indent=2)) + + # Краткая сводка + print("\n=== Сводка задачи ===") + print(summarize(card)) + + +if __name__ == "__main__": + main() \ No newline at end of file