logdetective 0.2.7__tar.gz → 0.2.9__tar.gz
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-0.2.7 → logdetective-0.2.9}/PKG-INFO +14 -3
- {logdetective-0.2.7 → logdetective-0.2.9}/README.md +10 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/constants.py +20 -3
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/server.py +91 -31
- {logdetective-0.2.7 → logdetective-0.2.9}/pyproject.toml +3 -3
- {logdetective-0.2.7 → logdetective-0.2.9}/LICENSE +0 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/__init__.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/drain3.ini +0 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/extractors.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/logdetective.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.9}/logdetective/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 0.2.
|
|
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 (
|
|
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 (
|
|
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
|
-------
|
|
@@ -165,6 +165,11 @@ Requests can then be made with post requests, for example:
|
|
|
165
165
|
|
|
166
166
|
curl --header "Content-Type: application/json" --request POST --data '{"url":"<YOUR_URL_HERE>"}' http://localhost:8080/analyze
|
|
167
167
|
|
|
168
|
+
For more accurate responses, you can use `/analyze/staged` endpoint. This will submit snippets to model for individual analysis first.
|
|
169
|
+
Afterwards the model outputs are used to construct final prompt. This will take substantially longer, compared to plain `/analyze`
|
|
170
|
+
|
|
171
|
+
curl --header "Content-Type: application/json" --request POST --data '{"url":"<YOUR_URL_HERE>"}' http://localhost:8080/analyze/staged
|
|
172
|
+
|
|
168
173
|
We also have a Containerfile and composefile to run the logdetective server and llama server in containers.
|
|
169
174
|
|
|
170
175
|
Before doing `podman-compose up`, make sure to set `MODELS_PATH` environment variable and point to a directory with your local model files:
|
|
@@ -176,6 +181,11 @@ $ ll $MODELS_PATH
|
|
|
176
181
|
|
|
177
182
|
If the variable is not set, `./models` is mounted inside by default.
|
|
178
183
|
|
|
184
|
+
Model can be downloaded from [our Hugging Space](https://huggingface.co/fedora-copr) by:
|
|
185
|
+
```
|
|
186
|
+
$ 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
|
|
187
|
+
```
|
|
188
|
+
|
|
179
189
|
|
|
180
190
|
License
|
|
181
191
|
-------
|
|
@@ -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 = '================'
|
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
[tool.poetry]
|
|
2
2
|
name = "logdetective"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.9"
|
|
4
4
|
description = "Log using LLM AI to search for build/test failures and provide ideas for fixing these."
|
|
5
5
|
authors = ["Jiri Podivin <jpodivin@gmail.com>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -27,8 +27,8 @@ issues = "https://github.com/fedora-copr/logdetective/issues"
|
|
|
27
27
|
|
|
28
28
|
[tool.poetry.dependencies]
|
|
29
29
|
python = "^3.11"
|
|
30
|
-
requests = "
|
|
31
|
-
llama-cpp-python = "
|
|
30
|
+
requests = ">0.2.31"
|
|
31
|
+
llama-cpp-python = ">0.2.56,!=0.2.86"
|
|
32
32
|
drain3 = "^0.9.11"
|
|
33
33
|
huggingface-hub = ">0.23.2"
|
|
34
34
|
numpy = "^1.26.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|