diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..71969d7 --- /dev/null +++ b/parser.py @@ -0,0 +1,102 @@ +import os +from typing import List +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_openai import ChatOpenAI +# Для локальной разработки без OpenAI: +# from langchain_ollama import ChatOllama + +from models import ApiEvent, HttpOkEvent, HttpErrorEvent, ApiEventAdapter + +# Системный промпт для извлечения структурированных событий +SYSTEM_PROMPT = """Ты — парсер логов HTTP API. Из входного текста извлеки структурированные события. + +Правила: +1. Каждая строка лога — отдельное событие +2. Если статус 2xx (200, 201 и т.д.) — создавай HttpOkEvent (kind='ok') +3. Если статус 4xx или 5xx — создавай HttpErrorEvent (kind='error') +4. Всегда заполняй все поля: kind, status, path, duration_ms (для ok) или error_message (для error) +5. Если длительности нет в логе, укажи 0.0 +6. Если сообщения об ошибке нет, составь стандартное: "HTTP {status} error" + +Входной формат лога может быть разным, но старайся извлечь: +- status: число 200, 404, 500 и т.д. +- path: строка типа /api/users, /products/123 +- duration_ms: число +- error_message: текст ошибки (если есть) +""" + +class LogEventParser: + def __init__(self, model_name: str = "gpt-3.5-turbo", temperature: float = 0.0, openai_api_key: str = None): + """Инициализация парсера с LangChain structured output""" + # Настройка OpenAI (можно заменить на Ollama) + kwargs = { + "model": model_name, + "temperature": temperature, + } + if openai_api_key: + kwargs["api_key"] = openai_api_key + elif os.getenv("OPENAI_API_KEY"): + kwargs["api_key"] = os.getenv("OPENAI_API_KEY") + + self.llm = ChatOpenAI(**kwargs) + + # Привязываем структуру вывода напрямую к LLM (LangChain >=0.2) + self.structured_llm = self.llm.with_structured_output(ApiEvent, method="function_calling") + + def parse_single_event(self, log_line: str) -> ApiEvent: + """Парсит одну строку лога в ApiEvent""" + messages = [ + SystemMessage(content=SYSTEM_PROMPT), + HumanMessage(content=f"Извлеки событие из строки лога:\n{log_line}") + ] + + # LangChain автоматически вернёт нужный объект благодаря discriminated union + event = self.structured_llm.invoke(messages) + return event + + def parse_multiple_events(self, log_text: str, separator: str = "\n") -> List[ApiEvent]: + """Парсит несколько событий из текста (по строкам или разделителю)""" + events = [] + + # Разбиваем на блоки + blocks = log_text.strip().split(separator) + + for block in blocks: + if not block.strip(): + continue + try: + event = self.parse_single_event(block) + events.append(event) + except Exception as e: + print(f"⚠️ Ошибка парсинга строки '{block[:50]}...': {e}") + continue + + return events + +# Альтернативный парсер через PydanticOutputParser (без function calling) +from langchain_core.output_parsers import PydanticOutputParser +from langchain_core.prompts import PromptTemplate + +class LogEventParserWithParser: + """Версия с PydanticOutputParser (работает без function calling)""" + + def __init__(self, model_name: str = "gpt-3.5-turbo", temperature: float = 0.0): + self.llm = ChatOpenAI(model=model_name, temperature=temperature) + self.parser = PydanticOutputParser(pydantic_object=ApiEventAdapter) # Используем адаптер + + self.prompt = PromptTemplate( + template="""Извлеки событие из строки лога. +{format_instructions} + +Лог: {log_line} + +Верни только JSON с полем kind и соответствующими полями для события.""", + input_variables=["log_line"], + partial_variables={"format_instructions": self.parser.get_format_instructions()} + ) + + self.chain = self.prompt | self.llm | self.parser + + def parse_single_event(self, log_line: str) -> ApiEvent: + result = self.chain.invoke({"log_line": log_line}) + return result \ No newline at end of file