22
33import sys
44import time
5+ import json
56import argparse
67import requests
78import re
@@ -63,10 +64,11 @@ def classify_severity(line: str) -> str:
6364 return "UNKNOWN"
6465
6566
66- def explain_log_line (line : str , model : str , context : str = "" ) -> str :
67+ def explain_log_line (line : str , model : str , context : str = "" ) -> None :
68+ """Stream an explanation of a log line token by token from Ollama."""
6769 line = line .strip ()
6870 if not line :
69- return ""
71+ return
7072
7173 context_clause = f" familiar with { context } applications" if context else ""
7274 system_prompt = SYSTEM_PROMPT_TEMPLATE .format (context_clause = context_clause )
@@ -75,21 +77,33 @@ def explain_log_line(line: str, model: str, context: str = "") -> str:
7577 "model" : model ,
7678 "messages" : [
7779 {"role" : "system" , "content" : system_prompt },
78- {"role" : "user" , "content" : f"Explain this application log line in plain English, Keep explanations concise — 1-2 sentences maximum. Focus on what it means and whether action is needed.:\n \n { line } " }
80+ {"role" : "user" , "content" : (
81+ "Explain this application log line in plain English. "
82+ "Keep it concise — 1-2 sentences maximum. "
83+ "Focus on what it means and whether action is needed.:\n \n " + line
84+ )},
7985 ],
80- "stream" : False ,
86+ "stream" : True ,
8187 }
8288
8389 try :
84- response = requests .post (OLLAMA_URL , json = payload , timeout = 30 )
85- response .raise_for_status ()
86- return response .json ().get ("message" , {}).get ("content" , "" ).strip ()
90+ print (f" { DIM } ↳{ RESET } " , end = "" , flush = True )
91+ with requests .post (OLLAMA_URL , json = payload , timeout = 30 , stream = True ) as response :
92+ response .raise_for_status ()
93+ for chunk in response .iter_lines ():
94+ if chunk :
95+ data = json .loads (chunk )
96+ token = data .get ("message" , {}).get ("content" , "" )
97+ print (token , end = "" , flush = True )
98+ if data .get ("done" ):
99+ break
100+ print () # newline after stream ends
87101 except requests .exceptions .ConnectionError :
88- return "[ERROR] Cannot connect to Ollama. Is it running? Try: ollama serve"
102+ print ( f "[ERROR] Cannot connect to Ollama. Is it running? Try: ollama serve")
89103 except requests .exceptions .Timeout :
90- return "[ERROR] Ollama timed out. Model may still be loading."
104+ print ( f "[ERROR] Ollama timed out. Model may still be loading.")
91105 except Exception as e :
92- return f"[ERROR] { str (e )} "
106+ print ( f"[ERROR] { str (e )} " )
93107
94108
95109class PatternDetector :
@@ -185,7 +199,7 @@ def tail_file(filepath: str, model: str, context: str, min_severity: str): # pr
185199 detector = PatternDetector (window_seconds = 60 , threshold = 5 )
186200 summarizer = IncidentSummarizer (window_seconds = 120 , spike_threshold = 10 )
187201
188- print (f"\n { BOLD } 🔍 Log Explainer{ RESET } — SRE Incident Response Tool" )
202+ print (f"\n { BOLD } Log Explainer{ RESET } — SRE Incident Response Tool" )
189203 print (f"{ DIM } File: { filepath } { RESET } " )
190204 print (f"{ DIM } Model: { model } { RESET } " )
191205 print (f"{ DIM } Filter: { min_severity } +{ RESET } " )
@@ -238,17 +252,14 @@ def tail_file(filepath: str, model: str, context: str, min_severity: str): # pr
238252 if repeat_count :
239253 print (f" { SEVERITY_COLORS ['WARN' ]} ⚠ Pattern repeated { repeat_count } x in 60s — possible recurring issue{ RESET } " )
240254
241- # Explain the line
242- explanation = explain_log_line (line , model , context )
243- if explanation :
244- print (f" { DIM } ↳{ RESET } { explanation } " )
255+ # Stream explanation token by token
256+ explain_log_line (line , model , context )
245257
246258 # Incident summary on error spikes
247259 if summarizer .should_summarize ():
248260 print ()
249- print (f"{ SEVERITY_COLORS ['CRITICAL' ]} { BOLD } 🚨 INCIDENT SPIKE DETECTED — generating summary...{ RESET } " )
250- summary = explain_log_line (summarizer .get_summary_prompt (), model , context )
251- print (f"{ BOLD } { summary } { RESET } " )
261+ print (f"{ SEVERITY_COLORS ['CRITICAL' ]} { BOLD } INCIDENT SPIKE DETECTED — generating summary...{ RESET } " )
262+ explain_log_line (summarizer .get_summary_prompt (), model , context )
252263 print_separator ()
253264
254265 print ()
@@ -287,4 +298,4 @@ def main(): # pragma: no cover
287298
288299
289300if __name__ == "__main__" :
290- main ()
301+ main ()
0 commit comments