Повторный экзамен: Structured output — Union событий API: solution.py

This commit is contained in:
2026-06-02 06:02:36 +00:00
parent 653d818651
commit 1e3762524b
+130
View File
@@ -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()