From 1e3762524be38fa8d0e896207d33d667da50955d Mon Sep 17 00:00:00 2001 From: lonpatovaadelina Date: Tue, 2 Jun 2026 06:02:36 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B2=D1=82=D0=BE=D1=80=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD:=20St?= =?UTF-8?q?ructured=20output=20=E2=80=94=20Union=20=D1=81=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D0=B9=20API:=20solution.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- solution.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 solution.py diff --git a/solution.py b/solution.py new file mode 100644 index 0000000..3ff3ce1 --- /dev/null +++ b/solution.py @@ -0,0 +1,130 @@ +import os +import argparse +from typing import Annotated, Literal, Union, List + +from pydantic import BaseModel, Field, ValidationError +from langchain_core.output_parsers import PydanticOutputParser +from langchain_core.prompts import PromptTemplate +from langchain_openai import ChatOpenAI + +# --------------------------------------------------------------------------- # +# 1. Модели событий +# --------------------------------------------------------------------------- # + +class HttpOkEvent(BaseModel): + kind: Literal["ok"] = Field(..., description="Тип события: OK") + status: Literal[200] = Field(..., description="HTTP статус 200") + path: str = Field(..., description="Запрошенный путь") + duration_ms: int = Field(..., description="Время выполнения в миллисекундах") + +class HttpErrorEvent(BaseModel): + kind: Literal["error"] = Field(..., description="Тип события: ошибка") + status: int = Field(..., description="HTTP статус 4xx/5xx") + path: str = Field(..., description="Запрошенный путь") + error_message: str = Field(..., description="Текст ошибки") + +ApiEvent = Annotated[ + Union[HttpOkEvent, HttpErrorEvent], + Field(discriminator="kind") +] + +# --------------------------------------------------------------------------- # +# 2. Парсер LLM + Pydantic +# --------------------------------------------------------------------------- # + +parser = PydanticOutputParser(pydantic_object=ApiEvent) + +prompt_template = PromptTemplate( + input_variables=["log_line"], + template=( + "Найди в следующем логе событие API и выведи его в формате JSON, " + "соответствующем одной из следующих схем:\n" + "1. {ok_schema}\n" + "2. {error_schema}\n" + "Укажи поле \"kind\" как \"ok\" или \"error\".\n" + "Лог: {log_line}\n" + "Ответ в чистом JSON без комментариев." + ), +) + +# --------------------------------------------------------------------------- # +# 3. Функция обработки одной строки +# --------------------------------------------------------------------------- # + +def parse_line(line: str, llm: ChatOpenAI) -> ApiEvent: + ok_schema = HttpOkEvent.model_json_schema() + error_schema = HttpErrorEvent.model_json_schema() + prompt = prompt_template.format( + log_line=line, + ok_schema=ok_schema, + error_schema=error_schema, + ) + response = llm.invoke(prompt) + try: + event = parser.parse(response.content) + except ValidationError as e: + raise ValueError(f"LLM не смог распарсить строку: {line}\n{e}") from e + return event + +# --------------------------------------------------------------------------- # +# 4. Обработка всего лога +# --------------------------------------------------------------------------- # + +def parse_log(text: str, llm: ChatOpenAI) -> List[ApiEvent]: + events: List[ApiEvent] = [] + # Разделяем по строкам, игнорируем пустые + for line in text.strip().splitlines(): + line = line.strip() + if not line: + continue + event = parse_line(line, llm) + events.append(event) + return events + +# --------------------------------------------------------------------------- # +# 5. CLI +# --------------------------------------------------------------------------- # + +def main(): + parser_cli = argparse.ArgumentParser(description="Parse API logs into structured events.") + parser_cli.add_argument( + "--log", + type=str, + default=None, + help="Путь к файлу с логом. Если не задан, используется пример.", + ) + args = parser_cli.parse_args() + + if args.log: + with open(args.log, "r", encoding="utf-8") as f: + log_text = f.read() + else: + log_text = """\ +2023-10-01 12:00:01 INFO /api/users 200 123ms +2023-10-01 12:00:02 ERROR /api/orders 404 Not Found +2023-10-01 12:00:03 WARN /api/payments 500 Internal Server Error +""" + + # Инициализируем LLM + openai_api_key = os.getenv("OPENAI_API_KEY") + if not openai_api_key: + raise RuntimeError("Переменная окружения OPENAI_API_KEY не найдена") + llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, api_key=openai_api_key) + + events = parse_log(log_text, llm) + + # Выводим каждый объект + for ev in events: + print(ev.model_dump()) + + # Таблица + print("\n| kind | path | status |") + print("|------|------|--------|") + for ev in events: + if ev.kind == "ok": + print(f"| {ev.kind} | {ev.path} | {ev.status} |") + else: + print(f"| {ev.kind} | {ev.path} | {ev.status} |") + +if __name__ == "__main__": + main() \ No newline at end of file