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

This commit is contained in:
2026-06-30 19:06:20 +00:00
parent 7b1aad6b16
commit 3042cc37ca
+90 -85
View File
@@ -1,31 +1,39 @@
import os """
import asyncio # main.py
import argparse # Structured log parsing with LangChain and Pydantic v2
from typing import List, Union, Annotated # No deepagents dependency uses standard LangChain tooling
# Author: Student
# Date: 2026-06-30
import os
import argparse
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 linebyline 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))