Add src/main.py
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
|||||||
|
"""CLI tool to parse raw API logs into structured events using LangChain and Pydantic.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m src.main [--log LOG_TEXT]
|
||||||
|
|
||||||
|
If LOG_TEXT is omitted, a default example log is used.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from .models import ApiEvent
|
||||||
|
|
||||||
|
# Default example log: mix of 200 and error lines
|
||||||
|
DEFAULT_LOG = (
|
||||||
|
"GET /api/users 200 120ms\n"
|
||||||
|
"POST /api/login 404 Not Found\n"
|
||||||
|
"GET /api/orders 500 Internal Server Error\n"
|
||||||
|
"PUT /api/users/42 200 45ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_event(line: str, llm) -> ApiEvent:
|
||||||
|
"""Parse a single log line into an ApiEvent using the LLM's structured output.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
line: str
|
||||||
|
Raw log line.
|
||||||
|
llm: ChatOpenAI
|
||||||
|
LLM instance configured with structured output.
|
||||||
|
"""
|
||||||
|
# Prepare a prompt that instructs the LLM to output a JSON matching the ApiEvent schema.
|
||||||
|
prompt = (
|
||||||
|
"Parse the following raw log line into a JSON object that matches one of the following schemas:\n"
|
||||||
|
"1. Ok event: {\n \"kind\": \"ok\", \"status\": 200, \"path\": string, \"duration_ms\": int\n}\n"
|
||||||
|
"2. Error event: {\n \"kind\": \"error\", \"status\": int, \"path\": string, \"error_message\": string\n}\n"
|
||||||
|
"Return only the JSON object, nothing else.\n"
|
||||||
|
f"Log line: {line.strip()}"
|
||||||
|
)
|
||||||
|
response = llm.invoke(prompt)
|
||||||
|
# The LLM is configured with structured output, so response is a dict
|
||||||
|
try:
|
||||||
|
event = ApiEvent(**response)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValueError(f"LLM output could not be parsed into ApiEvent: {e}")
|
||||||
|
return event
|
||||||
|
|
||||||
|
def parse_log(log_text: str, llm) -> List[ApiEvent]:
|
||||||
|
"""Parse a multiline log into a list of ApiEvent objects."""
|
||||||
|
events: List[ApiEvent] = []
|
||||||
|
for line in log_text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
event = parse_event(line, llm)
|
||||||
|
events.append(event)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Failed to parse line: {line}\nError: {exc}")
|
||||||
|
return events
|
||||||
|
|
||||||
|
def print_table(events: List[ApiEvent]):
|
||||||
|
"""Print a simple table of the parsed events."""
|
||||||
|
header = f"{'Kind':<6} | {'Path':<20} | {'Status':<6} | {'Details'}"
|
||||||
|
print(header)
|
||||||
|
print('-' * len(header))
|
||||||
|
for ev in events:
|
||||||
|
if ev.kind == "ok":
|
||||||
|
details = f"duration_ms={ev.duration_ms}"
|
||||||
|
else:
|
||||||
|
details = f"error_message={ev.error_message}"
|
||||||
|
print(f"{ev.kind:<6} | {ev.path:<20} | {ev.status:<6} | {details}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Parse raw API logs into structured events.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--log",
|
||||||
|
type=str,
|
||||||
|
help="Raw log text. If omitted, a default example is used.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
log_text = args.log or DEFAULT_LOG
|
||||||
|
|
||||||
|
# Load OpenAI API key from environment
|
||||||
|
openai_api_key = os.getenv("OPENAI_API_KEY")
|
||||||
|
if not openai_api_key:
|
||||||
|
raise RuntimeError("OPENAI_API_KEY environment variable not set.")
|
||||||
|
|
||||||
|
# Configure LLM with structured output
|
||||||
|
llm = ChatOpenAI(api_key=openai_api_key, temperature=0.0).with_structured_output(ApiEvent)
|
||||||
|
|
||||||
|
events = parse_log(log_text, llm)
|
||||||
|
|
||||||
|
print("\nParsed events:\n")
|
||||||
|
for ev in events:
|
||||||
|
print(ev.model_dump())
|
||||||
|
|
||||||
|
print("\nTable:\n")
|
||||||
|
print_table(events)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user