logdetective 0.2.7__py3-none-any.whl → 0.2.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
@@ -32,9 +32,7 @@ Answer:
32
32
  """
33
33
 
34
34
  SNIPPET_PROMPT_TEMPLATE = """
35
- Analyse following RPM build log snippet.
36
- Analysis of the snippets must be in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
37
- Snippets themselves must not be altered in any way whatsoever.
35
+ Analyse following RPM build log snippet. Decribe contents accurately, without speculation or suggestions for resolution.
38
36
 
39
37
  Snippet:
40
38
 
@@ -43,3 +41,22 @@ Snippet:
43
41
  Analysis:
44
42
 
45
43
  """
44
+
45
+ PROMPT_TEMPLATE_STAGED = """
46
+ Given following log snippets, their explanation, and nothing else, explain what failure, if any, occured during build of this package.
47
+
48
+ Snippets are in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
49
+
50
+ Snippets are delimited with '================'.
51
+
52
+ Drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
53
+
54
+ Snippets:
55
+
56
+ {}
57
+
58
+ Analysis:
59
+
60
+ """
61
+
62
+ SNIPPET_DELIMITER = '================'
logdetective/server.py CHANGED
@@ -1,15 +1,18 @@
1
+ import asyncio
1
2
  import json
2
3
  import logging
3
4
  import os
4
- from typing import List
5
+ from typing import List, Annotated, Dict
5
6
 
6
7
  from llama_cpp import CreateCompletionResponse
7
- from fastapi import FastAPI, HTTPException
8
+ from fastapi import FastAPI, HTTPException, Depends, Header
9
+ from fastapi.responses import StreamingResponse
8
10
  from pydantic import BaseModel
9
-
10
11
  import requests
11
12
 
12
- from logdetective.constants import PROMPT_TEMPLATE, SNIPPET_PROMPT_TEMPLATE
13
+ from logdetective.constants import (
14
+ PROMPT_TEMPLATE, SNIPPET_PROMPT_TEMPLATE,
15
+ PROMPT_TEMPLATE_STAGED, SNIPPET_DELIMITER)
13
16
  from logdetective.extractors import DrainExtractor
14
17
  from logdetective.utils import validate_url, compute_certainty
15
18
 
@@ -37,20 +40,49 @@ class StagedResponse(Response):
37
40
  explanation: CreateCompletionResponse
38
41
  https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.llama_types.CreateCompletionResponse
39
42
  response_certainty: float
40
- snippets: list of CreateCompletionResponse
43
+ snippets:
44
+ list of dictionaries { 'snippet' : '<original_text>, 'comment': CreateCompletionResponse }
41
45
  """
42
- snippets: List[CreateCompletionResponse]
43
-
46
+ snippets: List[Dict[str, str | CreateCompletionResponse]]
44
47
 
45
48
  LOG = logging.getLogger("logdetective")
46
49
 
47
- app = FastAPI()
48
50
 
49
51
  LLM_CPP_HOST = os.environ.get("LLAMA_CPP_HOST", "localhost")
50
52
  LLM_CPP_SERVER_ADDRESS = f"http://{LLM_CPP_HOST}"
51
53
  LLM_CPP_SERVER_PORT = os.environ.get("LLAMA_CPP_SERVER_PORT", 8000)
52
54
  LLM_CPP_SERVER_TIMEOUT = os.environ.get("LLAMA_CPP_SERVER_TIMEOUT", 600)
53
55
  LOG_SOURCE_REQUEST_TIMEOUT = os.environ.get("LOG_SOURCE_REQUEST_TIMEOUT", 60)
56
+ API_TOKEN = os.environ.get("LOGDETECTIVE_TOKEN", None)
57
+
58
+ def requires_token_when_set(authentication: Annotated[str | None, Header()] = None):
59
+ """
60
+ FastAPI Depend function that expects a header named Authentication
61
+
62
+ If LOGDETECTIVE_TOKEN env var is set, validate the client-supplied token
63
+ otherwise ignore it
64
+ """
65
+ if not API_TOKEN:
66
+ LOG.info("LOGDETECTIVE_TOKEN env var not set, authentication disabled")
67
+ # no token required, means local dev environment
68
+ return
69
+ token = None
70
+ if authentication:
71
+ try:
72
+ token = authentication.split(" ", 1)[1]
73
+ except (ValueError, IndexError):
74
+ LOG.warning(
75
+ "Authentication header has invalid structure (%s), it should be 'Bearer TOKEN'",
76
+ authentication)
77
+ # eat the exception and raise 401 below
78
+ token = None
79
+ if token == API_TOKEN:
80
+ return
81
+ LOG.info("LOGDETECTIVE_TOKEN env var is set (%s), clien token = %s",
82
+ API_TOKEN, token)
83
+ raise HTTPException(status_code=401, detail=f"Token {token} not valid.")
84
+
85
+ app = FastAPI(dependencies=[Depends(requires_token_when_set)])
54
86
 
55
87
 
56
88
  def process_url(url: str) -> str:
@@ -91,7 +123,8 @@ def mine_logs(log: str) -> List[str]:
91
123
 
92
124
  return log_summary
93
125
 
94
- def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1):
126
+ async def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1, stream: bool = False,
127
+ model: str = "default-model"):
95
128
  """Submit prompt to LLM.
96
129
  max_tokens: number of tokens to be produces, 0 indicates run until encountering EOS
97
130
  log_probs: number of token choices to produce log probs for
@@ -100,7 +133,9 @@ def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1):
100
133
  data = {
101
134
  "prompt": text,
102
135
  "max_tokens": str(max_tokens),
103
- "logprobs": str(log_probs)}
136
+ "logprobs": str(log_probs),
137
+ "stream": stream,
138
+ "model": model}
104
139
 
105
140
  try:
106
141
  # Expects llama-cpp server to run on LLM_CPP_SERVER_ADDRESS:LLM_CPP_SERVER_PORT
@@ -108,24 +143,27 @@ def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1):
108
143
  f"{LLM_CPP_SERVER_ADDRESS}:{LLM_CPP_SERVER_PORT}/v1/completions",
109
144
  headers={"Content-Type":"application/json"},
110
145
  data=json.dumps(data),
111
- timeout=int(LLM_CPP_SERVER_TIMEOUT))
146
+ timeout=int(LLM_CPP_SERVER_TIMEOUT),
147
+ stream=stream)
112
148
  except requests.RequestException as ex:
113
149
  raise HTTPException(
114
150
  status_code=400,
115
151
  detail=f"Llama-cpp query failed: {ex}") from ex
116
-
117
- if not response.ok:
118
- raise HTTPException(
119
- status_code=400,
120
- detail="Something went wrong while getting a response from the llama server: "
121
- f"[{response.status_code}] {response.text}")
122
- try:
123
- response = json.loads(response.text)
124
- except UnicodeDecodeError as ex:
125
- LOG.error("Error encountered while parsing llama server response: %s", ex)
126
- raise HTTPException(
127
- status_code=400,
128
- detail=f"Couldn't parse the response.\nError: {ex}\nData: {response.text}") from ex
152
+ if not stream:
153
+ if not response.ok:
154
+ raise HTTPException(
155
+ status_code=400,
156
+ detail="Something went wrong while getting a response from the llama server: "
157
+ f"[{response.status_code}] {response.text}")
158
+ try:
159
+ response = json.loads(response.text)
160
+ except UnicodeDecodeError as ex:
161
+ LOG.error("Error encountered while parsing llama server response: %s", ex)
162
+ raise HTTPException(
163
+ status_code=400,
164
+ detail=f"Couldn't parse the response.\nError: {ex}\nData: {response.text}") from ex
165
+ else:
166
+ return response
129
167
 
130
168
  return CreateCompletionResponse(response)
131
169
 
@@ -140,7 +178,8 @@ async def analyze_log(build_log: BuildLog):
140
178
  """
141
179
  log_text = process_url(build_log.url)
142
180
  log_summary = mine_logs(log_text)
143
- response = submit_text(PROMPT_TEMPLATE.format(log_summary))
181
+ response = await submit_text(PROMPT_TEMPLATE.format(log_summary))
182
+ certainty = 0
144
183
 
145
184
  if "logprobs" in response["choices"][0]:
146
185
  try:
@@ -167,16 +206,22 @@ async def analyze_log_staged(build_log: BuildLog):
167
206
  log_text = process_url(build_log.url)
168
207
  log_summary = mine_logs(log_text)
169
208
 
170
- analyzed_snippets = []
209
+ # Process snippets asynchronously
210
+ analyzed_snippets = await asyncio.gather(
211
+ *[submit_text(SNIPPET_PROMPT_TEMPLATE.format(s)) for s in log_summary])
171
212
 
172
- for snippet in log_summary:
173
- response = submit_text(SNIPPET_PROMPT_TEMPLATE.format(snippet))
174
- analyzed_snippets.append(response)
213
+ analyzed_snippets = [
214
+ {"snippet":e[0], "comment":e[1]} for e in zip(log_summary, analyzed_snippets)]
175
215
 
176
- final_analysis = submit_text(
177
- PROMPT_TEMPLATE.format([e["choices"][0]["text"] for e in analyzed_snippets]))
216
+ final_prompt = PROMPT_TEMPLATE_STAGED.format(
217
+ f"\n{SNIPPET_DELIMITER}\n".join([
218
+ f"[{e["snippet"]}] : [{e["comment"]["choices"][0]["text"]}]"
219
+ for e in analyzed_snippets]))
220
+
221
+ final_analysis = await submit_text(final_prompt)
178
222
 
179
223
  certainty = 0
224
+
180
225
  if "logprobs" in final_analysis["choices"][0]:
181
226
  try:
182
227
  certainty = compute_certainty(
@@ -190,3 +235,18 @@ async def analyze_log_staged(build_log: BuildLog):
190
235
 
191
236
  return StagedResponse(
192
237
  explanation=final_analysis, snippets=analyzed_snippets, response_certainty=certainty)
238
+
239
+
240
+ @app.post("/analyze/stream", response_class=StreamingResponse)
241
+ async def analyze_log_stream(build_log: BuildLog):
242
+ """Stream response endpoint for Logdetective.
243
+ Request must be in form {"url":"<YOUR_URL_HERE>"}.
244
+ URL must be valid for the request to be passed to the LLM server.
245
+ Meaning that it must contain appropriate scheme, path and netloc,
246
+ while lacking result, params or query fields.
247
+ """
248
+ log_text = process_url(build_log.url)
249
+ log_summary = mine_logs(log_text)
250
+ stream = await submit_text(PROMPT_TEMPLATE.format(log_summary), stream=True)
251
+
252
+ return StreamingResponse(stream)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: logdetective
3
- Version: 0.2.7
3
+ Version: 0.2.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
@@ -14,15 +14,16 @@ Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
17
18
  Classifier: Topic :: Internet :: Log Analysis
18
19
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
20
  Classifier: Topic :: Software Development :: Debuggers
20
21
  Provides-Extra: server
21
22
  Requires-Dist: drain3 (>=0.9.11,<0.10.0)
22
23
  Requires-Dist: huggingface-hub (>0.23.2)
23
- Requires-Dist: llama-cpp-python (>=0.2.56,<0.3.0,!=0.2.86)
24
+ Requires-Dist: llama-cpp-python (>0.2.56,!=0.2.86)
24
25
  Requires-Dist: numpy (>=1.26.0,<2.0.0)
25
- Requires-Dist: requests (>=2.31.0,<3.0.0)
26
+ Requires-Dist: requests (>0.2.31)
26
27
  Project-URL: homepage, https://github.com/fedora-copr/logdetective
27
28
  Project-URL: issues, https://github.com/fedora-copr/logdetective/issues
28
29
  Description-Content-Type: text/markdown
@@ -194,6 +195,11 @@ Requests can then be made with post requests, for example:
194
195
 
195
196
  curl --header "Content-Type: application/json" --request POST --data '{"url":"<YOUR_URL_HERE>"}' http://localhost:8080/analyze
196
197
 
198
+ For more accurate responses, you can use `/analyze/staged` endpoint. This will submit snippets to model for individual analysis first.
199
+ Afterwards the model outputs are used to construct final prompt. This will take substantially longer, compared to plain `/analyze`
200
+
201
+ curl --header "Content-Type: application/json" --request POST --data '{"url":"<YOUR_URL_HERE>"}' http://localhost:8080/analyze/staged
202
+
197
203
  We also have a Containerfile and composefile to run the logdetective server and llama server in containers.
198
204
 
199
205
  Before doing `podman-compose up`, make sure to set `MODELS_PATH` environment variable and point to a directory with your local model files:
@@ -205,6 +211,11 @@ $ ll $MODELS_PATH
205
211
 
206
212
  If the variable is not set, `./models` is mounted inside by default.
207
213
 
214
+ Model can be downloaded from [our Hugging Space](https://huggingface.co/fedora-copr) by:
215
+ ```
216
+ $ curl -L -o models/mistral-7b-instruct-v0.2.Q4_K_S.gguf https://huggingface.co/fedora-copr/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/ggml-model-Q4_K_S.gguf
217
+ ```
218
+
208
219
 
209
220
  License
210
221
  -------
@@ -0,0 +1,12 @@
1
+ logdetective/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ logdetective/constants.py,sha256=6XekuU7sbkY1Pmu4NJajgFbJ0no8PQ3DxQm8NeLKtjE,1383
3
+ logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
4
+ logdetective/extractors.py,sha256=eRizRiKhC3MPTHXS5nlRKcEudEaqct7G28V1bZYGkqI,3103
5
+ logdetective/logdetective.py,sha256=f7ASCJg_Yt6VBFieXBYgQYdenfXjC60ZdLHhzQHideI,4372
6
+ logdetective/server.py,sha256=DGW5Lk4u7zwmIk0ZeZj1w37zjvHEDSv1KEibc-yTwzY,9614
7
+ logdetective/utils.py,sha256=nTbaDVEfbHVQPTZe58T04HHZ6JWUJ1PonRRnzGX8hY0,4794
8
+ logdetective-0.2.9.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
9
+ logdetective-0.2.9.dist-info/METADATA,sha256=seYd175pEZg4VhJkQBMo0_M_Kt0VAy0Y-8d7_UblnSQ,9797
10
+ logdetective-0.2.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
+ logdetective-0.2.9.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
12
+ logdetective-0.2.9.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,12 +0,0 @@
1
- logdetective/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- logdetective/constants.py,sha256=1Ls2VJXb7NwSgi_HmTOA1c52K16SZIeDYBXlvBJ07zU,991
3
- logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
4
- logdetective/extractors.py,sha256=eRizRiKhC3MPTHXS5nlRKcEudEaqct7G28V1bZYGkqI,3103
5
- logdetective/logdetective.py,sha256=f7ASCJg_Yt6VBFieXBYgQYdenfXjC60ZdLHhzQHideI,4372
6
- logdetective/server.py,sha256=m0NPtk9tAUzyu9O8jIAfgEzynZ-WCHqVvCJkHOm08Ks,7073
7
- logdetective/utils.py,sha256=nTbaDVEfbHVQPTZe58T04HHZ6JWUJ1PonRRnzGX8hY0,4794
8
- logdetective-0.2.7.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
9
- logdetective-0.2.7.dist-info/METADATA,sha256=3iqnKnVJy6aTaAqP77btyqSGqCpjT8_PQqpWaNwLKHg,9100
10
- logdetective-0.2.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
- logdetective-0.2.7.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
12
- logdetective-0.2.7.dist-info/RECORD,,