Add main.py
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Main entry point for the Structured Output Union events API task.
|
||||
|
||||
This script demonstrates how to parse a raw log containing multiple HTTP events
|
||||
using Pydantic v2 models and LangChain structured output. It can be run as a
|
||||
stand‑alone CLI or imported as a module.
|
||||
|
||||
Requirements (see requirements.txt):
|
||||
- langchain-core>=1.0.0
|
||||
- langchain-openai
|
||||
- pydantic>=2.0
|
||||
- python-dotenv
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from langchain_core.output_parsers import PydanticOutputParser
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Models
|
||||
# ---------------------------------------------------------------------------
|
||||
class HttpOkEvent(BaseModel):
|
||||
"""Represents a successful HTTP request."""
|
||||
|
||||
kind: str = Field("ok", description="Discriminator for the union.")
|
||||
status: int = Field(200, description="HTTP status code (must be 200).")
|
||||
path: str = Field(..., description="Requested URL path.")
|
||||
duration_ms: int = Field(..., description="Duration of the request in milliseconds.")
|
||||
|
||||
class HttpErrorEvent(BaseModel):
|
||||
"""Represents a failed HTTP request."""
|
||||
|
||||
kind: str = Field("error", description="Discriminator for the union.")
|
||||
status: int = Field(..., ge=400, le=599, description="HTTP error status code.")
|
||||
path: str = Field(..., description="Requested URL path.")
|
||||
error_message: str = Field(..., description="Human readable error message.")
|
||||
|
||||
# Union with discriminator ``kind``. Pydantic v2 automatically uses the field
|
||||
# named ``kind`` to decide which model to instantiate.
|
||||
ApiEvent = Union[HttpOkEvent, HttpErrorEvent]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser helper
|
||||
# ---------------------------------------------------------------------------
|
||||
parser = PydanticOutputParser(pydantic_object=ApiEvent)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM wrapper
|
||||
# ---------------------------------------------------------------------------
|
||||
load_dotenv() # Load JARVIS API key from .env if present.
|
||||
llm = ChatOpenAI(
|
||||
model="openai/gpt-oss-20b:free",
|
||||
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
|
||||
api_key=os.getenv("JOURNAL_MCP_PAT"),
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core logic
|
||||
# ---------------------------------------------------------------------------
|
||||
def parse_event(raw: str) -> ApiEvent:
|
||||
"""Parse a single raw log line into an :class:`ApiEvent`.
|
||||
|
||||
The function sends the raw text to the LLM with a structured output prompt
|
||||
and then validates the result using Pydantic. If validation fails, the
|
||||
exception is propagated so that callers can decide how to handle it.
|
||||
"""
|
||||
# Build a simple prompt that instructs the model to return JSON matching
|
||||
# one of the two event schemas.
|
||||
prompt = f"Parse the following log line into JSON:\n{raw}\nJSON:" # noqa: E501
|
||||
response = llm.invoke([prompt])
|
||||
json_text = parser.parse(response.content)
|
||||
try:
|
||||
return ApiEvent.model_validate(json_text) # type: ignore[arg-type]
|
||||
except ValidationError as exc: # pragma: no cover - defensive
|
||||
raise ValueError(f"Failed to validate event: {exc}") from exc
|
||||
|
||||
|
||||
def parse_log(raw_log: str) -> List[ApiEvent]:
|
||||
"""Parse a multiline log into a list of :class:`ApiEvent` objects.
|
||||
|
||||
The function splits the input on newlines and ignores empty lines.
|
||||
Each non‑empty line is parsed individually.
|
||||
"""
|
||||
events: List[ApiEvent] = []
|
||||
for line in raw_log.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
event = parse_event(line)
|
||||
events.append(event)
|
||||
except Exception as exc: # pragma: no cover - log and skip
|
||||
print(f"Warning: could not parse line '{line}': {exc}", file=sys.stderr)
|
||||
return events
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
def main() -> None:
|
||||
parser_cli = argparse.ArgumentParser(description="Parse raw HTTP logs into structured events.")
|
||||
parser_cli.add_argument(
|
||||
"--log-file",
|
||||
type=Path,
|
||||
help="Path to a file containing the raw log. If omitted, a sample log is used.",
|
||||
)
|
||||
args = parser_cli.parse_args()
|
||||
|
||||
if args.log_file and args.log_file.exists():
|
||||
raw_log = args.log_file.read_text(encoding="utf-8")
|
||||
else:
|
||||
# Sample log with mixed success and error events.
|
||||
raw_log = """
|
||||
GET /api/users 200 OK duration=123ms
|
||||
POST /api/login 404 Not Found: user not found
|
||||
PUT /api/items/42 500 Internal Server Error: database timeout
|
||||
GET /health 200 OK duration=45ms
|
||||
"""
|
||||
print("Using sample log:\n", raw_log)
|
||||
|
||||
events = parse_log(raw_log)
|
||||
if not events:
|
||||
print("No valid events parsed.")
|
||||
sys.exit(1)
|
||||
|
||||
# Pretty‑print the results as a table.
|
||||
header = f"{'Kind':<6} | {'Path':<20} | {'Status':<6} | Details"
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
for ev in events:
|
||||
if isinstance(ev, HttpOkEvent):
|
||||
details = f"duration={ev.duration_ms}ms"
|
||||
else: # HttpErrorEvent
|
||||
details = f"error='{ev.error_message}'"
|
||||
print(f"{ev.kind:<6} | {ev.path:<20} | {ev.status:<6} | {details}")
|
||||
|
||||
# Also output the raw Pydantic model dumps for debugging.
|
||||
print("\nModel dumps:\n")
|
||||
for ev in events:
|
||||
print(ev.model_dump_json(indent=2))
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - entry point
|
||||
main()
|
||||
Reference in New Issue
Block a user