fix(needs_fixes): 1 исправлений, 0 отстояно — main.py

This commit is contained in:
gurevich committed 2026-06-30 19:06:20 +00:00
1 parent 7b1aad6b16
commit 3042cc37ca
1 file changed
+88 -83
+88 -83
View File
@@ -1,31 +1,39 @@
"""
# main.py
# Structured log parsing with LangChain and Pydantic v2
# No deepagents dependency – uses standard LangChain tooling
# Author: Student
# Date: 2026-06-30
import os import os
import asyncio
import argparse import argparse
from typing import List, Union, Annotated import textwrap
from typing import List, Annotated, Union
from pydantic import BaseModel, Field, Literal
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langchain.tools import tool from langchain_core.output_parsers import PydanticOutputParser
from deepagents import create_deep_agent from langchain_core.prompts import ChatPromptTemplate
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from pydantic import BaseModel, Field, Literal
# ---------- Pydantic models ----------
# ----------------- Pydantic models -----------------
class HttpOkEvent(BaseModel): class HttpOkEvent(BaseModel):
kind: Literal["ok"] = Field(..., description="Event kind: ok") kind: Literal["ok"] = Field("ok", description="Event kind: ok")
status: Literal[200] = Field(..., description="HTTP status code") status: Literal[200] = Field(200, description="HTTP status code")
path: str = Field(..., description="Request path") path: str = Field(..., description="Requested path")
duration_ms: int = Field(..., description="Duration in milliseconds") duration_ms: int = Field(..., description="Duration in milliseconds")
class HttpErrorEvent(BaseModel): class HttpErrorEvent(BaseModel):
kind: Literal["error"] = Field(..., description="Event kind: error") kind: Literal["error"] = Field("error", description="Event kind: error")
status: int = Field(..., description="HTTP status code (4xx/5xx)") status: int = Field(..., description="HTTP error status code (4xx/5xx)")
path: str = Field(..., description="Request path") path: str = Field(..., description="Requested path")
error_message: str = Field(..., description="Error message") error_message: str = Field(..., description="Error description")
ApiEvent = Annotated[Union[HttpOkEvent, HttpErrorEvent], Field(discriminator="kind")] ApiEvent = Annotated[Union[HttpOkEvent, HttpErrorEvent], Field(discriminator="kind")]
# ----------------- LLM ----------------- # ---------- LLM & Parser ----------
llm = ChatOpenAI( llm = ChatOpenAI(
model="openai/gpt-oss-20b:free", model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1", base_url="https://openrouter.ai/api/v1",
@@ -33,84 +41,81 @@ llm = ChatOpenAI(
temperature=0.0, temperature=0.0,
) )
# ----------------- Backend ----------------- parser = PydanticOutputParser(pydantic_object=ApiEvent)
backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"), prompt = ChatPromptTemplate.from_messages([
FilesystemBackend(), ("system", "You are a log parser that outputs structured events as JSON.")
]) ])
# ----------------- Tools ----------------- # ---------- Helper functions ----------
@tool
def parse_block(block: str) -> ApiEvent:
"""Parse a single log block into a typed ApiEvent using LLM structured output."""
# Use the LLM to produce structured output
result = llm.with_structured_output(ApiEvent).invoke({"messages": [HumanMessage(content=block)]})
# The LLM returns an ApiEvent instance directly
return result
@tool def parse_block(block: str) -> ApiEvent:
def parse_log(log_text: str) -> str: """Parse a single log block using the LLM and structured output parser."""
"""Parse the entire log text and return a formatted table of events.""" # Construct prompt for the block
# Split into blocks (empty lines or '---' separators) messages = [HumanMessage(content=block.strip())]
raw_blocks = [b.strip() for b in log_text.split("\n") if b.strip()] # Ask LLM to output JSON matching the ApiEvent schema
response = llm.invoke(messages)
# Parse the response JSON into the Pydantic model
return parser.parse(response.content)
def split_blocks(text: str) -> List[str]:
"""Split raw log text into individual event blocks.
Supports both line‑by‑line and '---' separators.
"""
if "---" in text:
return [b.strip() for b in text.split("---") if b.strip()]
return [line.strip() for line in text.splitlines() if line.strip()]
# ---------- CLI ----------
DEFAULT_LOG = textwrap.dedent("""
200 /api/users 120ms
404 /api/unknown 30ms
500 /api/orders 250ms
200 /api/products 80ms
403 /api/admin 15ms
""")
def main():
parser_cli = argparse.ArgumentParser(description="Parse raw log into structured events.")
parser_cli.add_argument("--log", type=str, help="Path to log file or raw log string.")
args = parser_cli.parse_args()
if args.log:
if os.path.isfile(args.log):
raw = open(args.log, "r", encoding="utf-8").read()
else:
raw = args.log
else:
raw = DEFAULT_LOG
blocks = split_blocks(raw)
events: List[ApiEvent] = [] events: List[ApiEvent] = []
for block in raw_blocks: for block in blocks:
try: try:
event = parse_block(block) event = parse_block(block)
events.append(event) events.append(event)
except Exception as e: except Exception as e:
# If parsing fails, skip the block but keep a note print(f"Failed to parse block: {block!r}\nError: {e}")
events.append(
HttpErrorEvent( # Output structured events
kind="error", print("\nParsed events:\n")
status=0,
path="<unknown>",
error_message=f"Failed to parse block: {e}",
)
)
# Build table
header = f"{'kind':<6} | {'path':<20} | {'status':<6} | {'detail':<30}"
lines = [header, "-" * len(header)]
for ev in events: for ev in events:
if isinstance(ev, HttpOkEvent): print(ev.model_dump())
detail = f"duration {ev.duration_ms}ms"
# Pretty table
print("\nTable:\n")
header = f"{'kind':<6} | {'path':<15} | {'status':<6} | details"
print(header)
print('-' * len(header))
for ev in events:
if ev.kind == "ok":
print(f"{ev.kind:<6} | {ev.path:<15} | {ev.status:<6} | duration {ev.duration_ms}ms")
else: else:
detail = ev.error_message print(f"{ev.kind:<6} | {ev.path:<15} | {ev.status:<6} | error: {ev.error_message}")
line = f"{ev.kind:<6} | {ev.path:<20} | {ev.status:<6} | {detail:<30}"
lines.append(line)
return "\n".join(lines)
# ----------------- Agent -----------------
agent = create_deep_agent(
model=llm,
tools=[parse_block, parse_log],
backend=backend,
system_prompt="You are a log parsing agent. Use the provided tools to parse the log and return a formatted table.",
)
# ----------------- CLI -----------------
async def run_agent(log_text: str):
result = await agent.ainvoke(
{"messages": [HumanMessage(content=log_text)]},
{"configurable": {"thread_id": "log-session"}},
)
# The tool output is in the last message
print(result["messages"][-1].content)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Parse raw log into structured events.") main()
parser.add_argument("--file", type=str, help="Path to a log file. If omitted, uses default sample.")
args = parser.parse_args()
if args.file:
with open(args.file, "r", encoding="utf-8") as f:
log = f.read()
else:
# Default sample log
log = """
2023-10-01 12:00:00 INFO /api/users 200 123ms
2023-10-01 12:00:01 ERROR /api/orders 404 Not Found
2023-10-01 12:00:02 INFO /api/products 200 98ms
2023-10-01 12:00:03 ERROR /api/payments 500 Internal Server Error
""" """
asyncio.run(run_agent(log))