logdetective 0.5.8__py3-none-any.whl → 0.5.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
logdetective/constants.py CHANGED
@@ -1,3 +1,8 @@
1
+ """This file contains various constants to be used as a fallback
2
+ in case other values are not specified. Prompt templates should be modified
3
+ in prompts.yaml instead.
4
+ """
5
+
1
6
  # pylint: disable=line-too-long
2
7
  DEFAULT_ADVISOR = "fedora-copr/Mistral-7B-Instruct-v0.2-GGUF"
3
8
 
@@ -19,7 +24,7 @@ Analysis:
19
24
 
20
25
  """
21
26
 
22
- SUMMARIZE_PROMPT_TEMPLATE = """
27
+ SUMMARIZATION_PROMPT_TEMPLATE = """
23
28
  Does following log contain error or issue?
24
29
 
25
30
  Log:
@@ -6,7 +6,7 @@ import drain3
6
6
  from drain3.template_miner_config import TemplateMinerConfig
7
7
  from llama_cpp import Llama, LlamaGrammar
8
8
 
9
- from logdetective.constants import SUMMARIZE_PROMPT_TEMPLATE
9
+ from logdetective.constants import SUMMARIZATION_PROMPT_TEMPLATE
10
10
  from logdetective.utils import get_chunks
11
11
 
12
12
  LOG = logging.getLogger("logdetective")
@@ -17,12 +17,18 @@ class LLMExtractor:
17
17
  A class that extracts relevant information from logs using a language model.
18
18
  """
19
19
 
20
- def __init__(self, model: Llama, n_lines: int = 2):
20
+ def __init__(
21
+ self,
22
+ model: Llama,
23
+ n_lines: int = 2,
24
+ prompt: str = SUMMARIZATION_PROMPT_TEMPLATE,
25
+ ):
21
26
  self.model = model
22
27
  self.n_lines = n_lines
23
28
  self.grammar = LlamaGrammar.from_string(
24
29
  'root ::= ("Yes" | "No")', verbose=False
25
30
  )
31
+ self.prompt = prompt
26
32
 
27
33
  def __call__(
28
34
  self, log: str, n_lines: int = 2, neighbors: bool = False
@@ -41,7 +47,7 @@ class LLMExtractor:
41
47
 
42
48
  for i in range(0, len(log_lines), self.n_lines):
43
49
  block = "\n".join(log_lines[i: i + self.n_lines])
44
- prompt = SUMMARIZE_PROMPT_TEMPLATE.format(log)
50
+ prompt = self.prompt.format(log)
45
51
  out = self.model(prompt, max_tokens=7, grammar=self.grammar)
46
52
  out = f"{out['choices'][0]['text']}\n"
47
53
  results.append((block, out))
@@ -9,6 +9,7 @@ from logdetective.utils import (
9
9
  retrieve_log_content,
10
10
  format_snippets,
11
11
  compute_certainty,
12
+ load_prompts,
12
13
  )
13
14
  from logdetective.extractors import LLMExtractor, DrainExtractor
14
15
 
@@ -65,10 +66,13 @@ def setup_args():
65
66
  )
66
67
  parser.add_argument("-v", "--verbose", action="count", default=0)
67
68
  parser.add_argument("-q", "--quiet", action="store_true")
69
+ parser.add_argument(
70
+ "--prompts", type=str, default="", help="Path to prompt configuration file."
71
+ )
68
72
  return parser.parse_args()
69
73
 
70
74
 
71
- def main(): # pylint: disable=too-many-statements
75
+ def main(): # pylint: disable=too-many-statements,too-many-locals
72
76
  """Main execution function."""
73
77
  args = setup_args()
74
78
 
@@ -83,6 +87,9 @@ def main(): # pylint: disable=too-many-statements
83
87
  if args.quiet:
84
88
  log_level = 0
85
89
 
90
+ # Get prompts configuration
91
+ prompts_configuration = load_prompts(args.prompts)
92
+
86
93
  logging.basicConfig(stream=sys.stdout)
87
94
  LOG.setLevel(log_level)
88
95
 
@@ -103,7 +110,11 @@ def main(): # pylint: disable=too-many-statements
103
110
  )
104
111
  else:
105
112
  summarizer_model = initialize_model(args.summarizer, verbose=args.verbose > 2)
106
- extractor = LLMExtractor(summarizer_model, args.verbose > 1)
113
+ extractor = LLMExtractor(
114
+ summarizer_model,
115
+ args.verbose > 1,
116
+ prompts_configuration.summarization_prompt_template,
117
+ )
107
118
 
108
119
  LOG.info("Getting summary")
109
120
 
@@ -127,7 +138,12 @@ def main(): # pylint: disable=too-many-statements
127
138
  stream = True
128
139
  if args.no_stream:
129
140
  stream = False
130
- response = process_log(log_summary, model, stream)
141
+ response = process_log(
142
+ log_summary,
143
+ model,
144
+ stream,
145
+ prompt_template=prompts_configuration.prompt_template,
146
+ )
131
147
  probs = []
132
148
  print("Explanation:")
133
149
  # We need to extract top token probability from the response
logdetective/models.py ADDED
@@ -0,0 +1,33 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+ from logdetective.constants import (
5
+ PROMPT_TEMPLATE,
6
+ PROMPT_TEMPLATE_STAGED,
7
+ SUMMARIZATION_PROMPT_TEMPLATE,
8
+ SNIPPET_PROMPT_TEMPLATE,
9
+ )
10
+
11
+
12
+ class PromptConfig(BaseModel):
13
+ """Configuration for basic log detective prompts."""
14
+
15
+ prompt_template: str = PROMPT_TEMPLATE
16
+ summarization_prompt_template: str = SUMMARIZATION_PROMPT_TEMPLATE
17
+ snippet_prompt_template: str = SNIPPET_PROMPT_TEMPLATE
18
+ prompt_template_staged: str = PROMPT_TEMPLATE_STAGED
19
+
20
+ def __init__(self, data: Optional[dict] = None):
21
+ super().__init__()
22
+ if data is None:
23
+ return
24
+ self.prompt_template = data.get("prompt_template", PROMPT_TEMPLATE)
25
+ self.summarization_prompt_template = data.get(
26
+ "summarization_prompt_template", SUMMARIZATION_PROMPT_TEMPLATE
27
+ )
28
+ self.snippet_prompt_template = data.get(
29
+ "snippet_prompt_template", SNIPPET_PROMPT_TEMPLATE
30
+ )
31
+ self.prompt_template_staged = data.get(
32
+ "prompt_template_staged", PROMPT_TEMPLATE_STAGED
33
+ )
@@ -0,0 +1,50 @@
1
+ prompt_template: |
2
+ Given following log snippets, and nothing else, explain what failure, if any, occured during build of this package.
3
+
4
+ Analysis of the snippets must be in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
5
+ Snippets themselves must not be altered in any way whatsoever.
6
+
7
+ Snippets are delimited with '================'.
8
+
9
+ Finally, drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
10
+
11
+ Snippets:
12
+
13
+ {}
14
+
15
+ Analysis:
16
+
17
+
18
+ summarization_prompt_template: |
19
+ Does following log contain error or issue?
20
+
21
+ Log:
22
+
23
+ {}
24
+
25
+ Answer:
26
+
27
+
28
+ snippet_prompt_template: |
29
+ Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
30
+
31
+ Snippet:
32
+
33
+ {}
34
+
35
+ Analysis:
36
+
37
+ prompt_template_staged: |
38
+ Given following log snippets, their explanation, and nothing else, explain what failure, if any, occured during build of this package.
39
+
40
+ Snippets are in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
41
+
42
+ Snippets are delimited with '================'.
43
+
44
+ Drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
45
+
46
+ Snippets:
47
+
48
+ {}
49
+
50
+ Analysis:
@@ -21,17 +21,13 @@ import gitlab.v4.objects
21
21
  import jinja2
22
22
  import requests
23
23
 
24
- from logdetective.constants import (
25
- PROMPT_TEMPLATE,
26
- SNIPPET_PROMPT_TEMPLATE,
27
- PROMPT_TEMPLATE_STAGED,
28
- )
29
24
  from logdetective.extractors import DrainExtractor
30
25
  from logdetective.utils import (
31
26
  validate_url,
32
27
  compute_certainty,
33
28
  format_snippets,
34
29
  format_analyzed_snippets,
30
+ load_prompts,
35
31
  )
36
32
  from logdetective.server.utils import load_server_config, get_log
37
33
  from logdetective.server.metric import track_request
@@ -51,8 +47,10 @@ LLM_CPP_SERVER_TIMEOUT = os.environ.get("LLAMA_CPP_SERVER_TIMEOUT", 600)
51
47
  LOG_SOURCE_REQUEST_TIMEOUT = os.environ.get("LOG_SOURCE_REQUEST_TIMEOUT", 60)
52
48
  API_TOKEN = os.environ.get("LOGDETECTIVE_TOKEN", None)
53
49
  SERVER_CONFIG_PATH = os.environ.get("LOGDETECTIVE_SERVER_CONF", None)
50
+ SERVER_PROMPT_PATH = os.environ.get("LOGDETECTIVE_PROMPTS", None)
54
51
 
55
52
  SERVER_CONFIG = load_server_config(SERVER_CONFIG_PATH)
53
+ PROMPT_CONFIG = load_prompts(SERVER_PROMPT_PATH)
56
54
 
57
55
  MR_REGEX = re.compile(r"refs/merge-requests/(\d+)/.*$")
58
56
  FAILURE_LOG_REGEX = re.compile(r"(\w*\.log)")
@@ -298,7 +296,7 @@ async def analyze_log(build_log: BuildLog):
298
296
  log_summary = mine_logs(log_text)
299
297
  log_summary = format_snippets(log_summary)
300
298
  response = await submit_text(
301
- PROMPT_TEMPLATE.format(log_summary),
299
+ PROMPT_CONFIG.prompt_template.format(log_summary),
302
300
  api_endpoint=SERVER_CONFIG.inference.api_endpoint,
303
301
  )
304
302
  certainty = 0
@@ -338,7 +336,7 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
338
336
  analyzed_snippets = await asyncio.gather(
339
337
  *[
340
338
  submit_text(
341
- SNIPPET_PROMPT_TEMPLATE.format(s),
339
+ PROMPT_CONFIG.snippet_prompt_template.format(s),
342
340
  api_endpoint=SERVER_CONFIG.inference.api_endpoint,
343
341
  )
344
342
  for s in log_summary
@@ -349,7 +347,7 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
349
347
  AnalyzedSnippet(line_number=e[0][0], text=e[0][1], explanation=e[1])
350
348
  for e in zip(log_summary, analyzed_snippets)
351
349
  ]
352
- final_prompt = PROMPT_TEMPLATE_STAGED.format(
350
+ final_prompt = PROMPT_CONFIG.prompt_template_staged.format(
353
351
  format_analyzed_snippets(analyzed_snippets)
354
352
  )
355
353
 
@@ -395,7 +393,7 @@ async def analyze_log_stream(build_log: BuildLog):
395
393
  headers["Authorization"] = f"Bearer {SERVER_CONFIG.inference.api_token}"
396
394
 
397
395
  stream = await submit_text_chat_completions(
398
- PROMPT_TEMPLATE.format(log_summary), stream=True, headers=headers
396
+ PROMPT_CONFIG.prompt_template.format(log_summary), stream=True, headers=headers
399
397
  )
400
398
 
401
399
  return StreamingResponse(stream)
logdetective/utils.py CHANGED
@@ -4,10 +4,13 @@ from typing import Iterator, List, Dict, Tuple, Generator
4
4
  from urllib.parse import urlparse
5
5
  import numpy as np
6
6
  import requests
7
+ import yaml
7
8
 
8
9
  from llama_cpp import Llama, CreateCompletionResponse, CreateCompletionStreamResponse
9
- from logdetective.constants import PROMPT_TEMPLATE, SNIPPET_DELIMITER
10
+ from logdetective.constants import SNIPPET_DELIMITER
10
11
  from logdetective.server.models import AnalyzedSnippet
12
+ from logdetective.models import PromptConfig
13
+
11
14
 
12
15
  LOG = logging.getLogger("logdetective")
13
16
 
@@ -110,7 +113,7 @@ def compute_certainty(probs: List[Dict]) -> float:
110
113
 
111
114
 
112
115
  def process_log(
113
- log: str, model: Llama, stream: bool
116
+ log: str, model: Llama, stream: bool, prompt_template: str
114
117
  ) -> CreateCompletionResponse | Iterator[CreateCompletionStreamResponse]:
115
118
  """Processes a given log using the provided language model and returns its summary.
116
119
 
@@ -122,7 +125,7 @@ def process_log(
122
125
  str: The summary of the given log generated by the language model.
123
126
  """
124
127
  response = model(
125
- prompt=PROMPT_TEMPLATE.format(log), stream=stream, max_tokens=0, logprobs=1
128
+ prompt=prompt_template.format(log), stream=stream, max_tokens=0, logprobs=1
126
129
  )
127
130
 
128
131
  return response
@@ -199,3 +202,15 @@ def validate_url(url: str) -> bool:
199
202
  if not (result.path or result.netloc):
200
203
  return False
201
204
  return True
205
+
206
+
207
+ def load_prompts(path: str | None) -> PromptConfig:
208
+ """Load prompts from given yaml file if there is one.
209
+ Alternatively use defaults."""
210
+ if path:
211
+ try:
212
+ with open(path, "r") as file:
213
+ return PromptConfig(yaml.safe_load(file))
214
+ except FileNotFoundError:
215
+ print("Prompt configuration file not found, reverting to defaults.")
216
+ return PromptConfig()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: logdetective
3
- Version: 0.5.8
3
+ Version: 0.5.9
4
4
  Summary: Log using LLM AI to search for build/test failures and provide ideas for fixing these.
5
5
  License: Apache-2.0
6
6
  Author: Jiri Podivin
@@ -29,9 +29,9 @@ Requires-Dist: matplotlib (>=3.8.4,<4.0.0) ; extra == "server" or extra == "serv
29
29
  Requires-Dist: numpy (>=1.26.0)
30
30
  Requires-Dist: psycopg2 (>=2.9.9,<3.0.0) ; extra == "server"
31
31
  Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0) ; extra == "server-testing"
32
- Requires-Dist: pydantic (>=2.8.2,<3.0.0) ; extra == "server" or extra == "server-testing"
32
+ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
33
33
  Requires-Dist: python-gitlab (>=4.4.0)
34
- Requires-Dist: pyyaml (>=6.0.1,<7.0.0) ; extra == "server" or extra == "server-testing"
34
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
35
35
  Requires-Dist: requests (>0.2.31)
36
36
  Requires-Dist: sqlalchemy (>=2.0.36,<3.0.0) ; extra == "server" or extra == "server-testing"
37
37
  Project-URL: homepage, https://github.com/fedora-copr/logdetective
@@ -363,6 +363,21 @@ http GET "localhost:8080/metrics/analyze/requests?days=5" > /tmp/plot_days.svg
363
363
  http GET "localhost:8080/metrics/analyze/requests?weeks=5" > /tmp/plot_weeks.svg
364
364
  ```
365
365
 
366
+ System Prompts
367
+ --------------
368
+
369
+ Prompt templates used by Log Detective are stored in the `prompts.yml` file.
370
+ It is possible to modify the file in place, or provide your own.
371
+ In CLI you can override prompt templates location using `--prompts` option,
372
+ while in the container service deployment the `LOGDETECTIVE_PROMPTS` environment variable
373
+ is used instead.
374
+
375
+ Prompts need to have a form compatible with python [format string syntax](https://docs.python.org/3/library/string.html#format-string-syntax)
376
+ with spaces, or replacement fields marked with curly braces, `{}` left for insertion of snippets.
377
+
378
+ Number of replacement fields in new prompts, must be the same as in originals.
379
+ Although their position may be different.
380
+
366
381
  License
367
382
  -------
368
383
 
@@ -1,8 +1,10 @@
1
1
  logdetective/__init__.py,sha256=VqRngDcuFT7JWms8Qc_MsOvajoXVOKPr-S1kqY3Pqhc,59
2
- logdetective/constants.py,sha256=SPSs1Bq6zPms3RsFTmsADwgrnFTn4fefNHzrB-M3RAE,1383
2
+ logdetective/constants.py,sha256=eiS6eYhEgl_Rlyi_B9j00DDp9A-UDhuFz3ACWtKf_SU,1558
3
3
  logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
4
- logdetective/extractors.py,sha256=cjxndfJaQur54GXksIQXL7YTxkOng8I8UnQZMN2t5_w,3388
5
- logdetective/logdetective.py,sha256=KN0KASW63VAnrjVeXK5AO0ob-vSexutTyeg1fd4uj70,4884
4
+ logdetective/extractors.py,sha256=7ahzWbTtU9MveG1Q7wU9LO8OJgs85X-cHmWltUhCe9M,3491
5
+ logdetective/logdetective.py,sha256=1EFrml_gHdyKEZX4iXBxhGgmU7R7_S26-Fr0WUDaA7E,5316
6
+ logdetective/models.py,sha256=nrGBmMRu8i6UhFflQKAp81Y3Sd_Aaoor0i_yqSJoLT0,1115
7
+ logdetective/prompts.yml,sha256=OBOWDErlbigbLrStcCY5HKPReNb0g-SNlCnD4QawF7k,1268
6
8
  logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
9
  logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
10
  logdetective/server/database/base.py,sha256=oMJUvbWeapIUP-8Cf_DR9ptFg8CsYeaBAIjOVEzx8SM,1668
@@ -10,12 +12,12 @@ logdetective/server/database/models.py,sha256=arIahOCT-hTmh904DXrWSkH7rlo13Ppu-O
10
12
  logdetective/server/metric.py,sha256=VYMifrfIhcqgyu6YYN0c1nt8fC1iJ2_LCB7Bh2AheoE,2679
11
13
  logdetective/server/models.py,sha256=cf1ngu_-19rP_i49s5cEwIzh6SfL_ZpVy4EykCpfWck,8076
12
14
  logdetective/server/plot.py,sha256=3o-CNHjel04ekpwSB4ckV7dbiF663cfPkimQ0aP9U_8,7073
13
- logdetective/server/server.py,sha256=ALVD9cwG4d8OQXfOPbRtt4y0nlh2C-8jP8pQeaufC3g,24533
15
+ logdetective/server/server.py,sha256=VGfBgbjUcyBd8hop-ea-O_Mo-FoGLDyP-elAWzRu51g,24605
14
16
  logdetective/server/templates/gitlab_comment.md.j2,sha256=kheTkhQ-LfuFkr8av-Mw2a-9VYEUbDTLwaa-CKI6OkI,1622
15
17
  logdetective/server/utils.py,sha256=OFvhttjv3yp8kfim5_s4mNG8ly21qyILxE0o3DcVVKg,1340
16
- logdetective/utils.py,sha256=eudens1_T6iTtYhyzoYCpwuWgFHUMDSt6eWnrAB-mAI,6188
17
- logdetective-0.5.8.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
18
- logdetective-0.5.8.dist-info/METADATA,sha256=dUCiCPfW8ILyshanWpb_zHdm9q3LIBYKZUWyfQWqsCA,14115
19
- logdetective-0.5.8.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
20
- logdetective-0.5.8.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
21
- logdetective-0.5.8.dist-info/RECORD,,
18
+ logdetective/utils.py,sha256=yTEjfTTaCS8lreKRkwKzLo6Po8cOYzInjSEx4CwpyqA,6665
19
+ logdetective-0.5.9.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
20
+ logdetective-0.5.9.dist-info/METADATA,sha256=YZbrICuAKXVD4LEEH6orwX-fuX3i3hpSsKuNa1nosoI,14737
21
+ logdetective-0.5.9.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
22
+ logdetective-0.5.9.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
23
+ logdetective-0.5.9.dist-info/RECORD,,