logdetective 0.2.7__tar.gz → 0.2.8__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.8}/PKG-INFO +14 -3
- {logdetective-0.2.7 → logdetective-0.2.8}/README.md +10 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/server.py +77 -27
- {logdetective-0.2.7 → logdetective-0.2.8}/pyproject.toml +3 -3
- {logdetective-0.2.7 → logdetective-0.2.8}/LICENSE +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/__init__.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/constants.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/drain3.ini +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/extractors.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/logdetective/logdetective.py +0 -0
- {logdetective-0.2.7 → logdetective-0.2.8}/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.8
|
|
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
|
-------
|
|
@@ -1,12 +1,13 @@
|
|
|
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
|
|
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
13
|
from logdetective.constants import PROMPT_TEMPLATE, SNIPPET_PROMPT_TEMPLATE
|
|
@@ -44,13 +45,42 @@ class StagedResponse(Response):
|
|
|
44
45
|
|
|
45
46
|
LOG = logging.getLogger("logdetective")
|
|
46
47
|
|
|
47
|
-
app = FastAPI()
|
|
48
48
|
|
|
49
49
|
LLM_CPP_HOST = os.environ.get("LLAMA_CPP_HOST", "localhost")
|
|
50
50
|
LLM_CPP_SERVER_ADDRESS = f"http://{LLM_CPP_HOST}"
|
|
51
51
|
LLM_CPP_SERVER_PORT = os.environ.get("LLAMA_CPP_SERVER_PORT", 8000)
|
|
52
52
|
LLM_CPP_SERVER_TIMEOUT = os.environ.get("LLAMA_CPP_SERVER_TIMEOUT", 600)
|
|
53
53
|
LOG_SOURCE_REQUEST_TIMEOUT = os.environ.get("LOG_SOURCE_REQUEST_TIMEOUT", 60)
|
|
54
|
+
API_TOKEN = os.environ.get("LOGDETECTIVE_TOKEN", None)
|
|
55
|
+
|
|
56
|
+
def requires_token_when_set(authentication: Annotated[str | None, Header()] = None):
|
|
57
|
+
"""
|
|
58
|
+
FastAPI Depend function that expects a header named Authentication
|
|
59
|
+
|
|
60
|
+
If LOGDETECTIVE_TOKEN env var is set, validate the client-supplied token
|
|
61
|
+
otherwise ignore it
|
|
62
|
+
"""
|
|
63
|
+
if not API_TOKEN:
|
|
64
|
+
LOG.info("LOGDETECTIVE_TOKEN env var not set, authentication disabled")
|
|
65
|
+
# no token required, means local dev environment
|
|
66
|
+
return
|
|
67
|
+
token = None
|
|
68
|
+
if authentication:
|
|
69
|
+
try:
|
|
70
|
+
token = authentication.split(" ", 1)[1]
|
|
71
|
+
except (ValueError, IndexError):
|
|
72
|
+
LOG.warning(
|
|
73
|
+
"Authentication header has invalid structure (%s), it should be 'Bearer TOKEN'",
|
|
74
|
+
authentication)
|
|
75
|
+
# eat the exception and raise 401 below
|
|
76
|
+
token = None
|
|
77
|
+
if token == API_TOKEN:
|
|
78
|
+
return
|
|
79
|
+
LOG.info("LOGDETECTIVE_TOKEN env var is set (%s), clien token = %s",
|
|
80
|
+
API_TOKEN, token)
|
|
81
|
+
raise HTTPException(status_code=401, detail=f"Token {token} not valid.")
|
|
82
|
+
|
|
83
|
+
app = FastAPI(dependencies=[Depends(requires_token_when_set)])
|
|
54
84
|
|
|
55
85
|
|
|
56
86
|
def process_url(url: str) -> str:
|
|
@@ -91,7 +121,8 @@ def mine_logs(log: str) -> List[str]:
|
|
|
91
121
|
|
|
92
122
|
return log_summary
|
|
93
123
|
|
|
94
|
-
def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1
|
|
124
|
+
async def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1, stream: bool = False,
|
|
125
|
+
model: str = "default-model"):
|
|
95
126
|
"""Submit prompt to LLM.
|
|
96
127
|
max_tokens: number of tokens to be produces, 0 indicates run until encountering EOS
|
|
97
128
|
log_probs: number of token choices to produce log probs for
|
|
@@ -100,7 +131,9 @@ def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1):
|
|
|
100
131
|
data = {
|
|
101
132
|
"prompt": text,
|
|
102
133
|
"max_tokens": str(max_tokens),
|
|
103
|
-
"logprobs": str(log_probs)
|
|
134
|
+
"logprobs": str(log_probs),
|
|
135
|
+
"stream": stream,
|
|
136
|
+
"model": model}
|
|
104
137
|
|
|
105
138
|
try:
|
|
106
139
|
# Expects llama-cpp server to run on LLM_CPP_SERVER_ADDRESS:LLM_CPP_SERVER_PORT
|
|
@@ -108,24 +141,27 @@ def submit_text(text: str, max_tokens: int = 0, log_probs: int = 1):
|
|
|
108
141
|
f"{LLM_CPP_SERVER_ADDRESS}:{LLM_CPP_SERVER_PORT}/v1/completions",
|
|
109
142
|
headers={"Content-Type":"application/json"},
|
|
110
143
|
data=json.dumps(data),
|
|
111
|
-
timeout=int(LLM_CPP_SERVER_TIMEOUT)
|
|
144
|
+
timeout=int(LLM_CPP_SERVER_TIMEOUT),
|
|
145
|
+
stream=stream)
|
|
112
146
|
except requests.RequestException as ex:
|
|
113
147
|
raise HTTPException(
|
|
114
148
|
status_code=400,
|
|
115
149
|
detail=f"Llama-cpp query failed: {ex}") from ex
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
if not stream:
|
|
151
|
+
if not response.ok:
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=400,
|
|
154
|
+
detail="Something went wrong while getting a response from the llama server: "
|
|
155
|
+
f"[{response.status_code}] {response.text}")
|
|
156
|
+
try:
|
|
157
|
+
response = json.loads(response.text)
|
|
158
|
+
except UnicodeDecodeError as ex:
|
|
159
|
+
LOG.error("Error encountered while parsing llama server response: %s", ex)
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=400,
|
|
162
|
+
detail=f"Couldn't parse the response.\nError: {ex}\nData: {response.text}") from ex
|
|
163
|
+
else:
|
|
164
|
+
return response
|
|
129
165
|
|
|
130
166
|
return CreateCompletionResponse(response)
|
|
131
167
|
|
|
@@ -140,7 +176,8 @@ async def analyze_log(build_log: BuildLog):
|
|
|
140
176
|
"""
|
|
141
177
|
log_text = process_url(build_log.url)
|
|
142
178
|
log_summary = mine_logs(log_text)
|
|
143
|
-
response = submit_text(PROMPT_TEMPLATE.format(log_summary))
|
|
179
|
+
response = await submit_text(PROMPT_TEMPLATE.format(log_summary))
|
|
180
|
+
certainty = 0
|
|
144
181
|
|
|
145
182
|
if "logprobs" in response["choices"][0]:
|
|
146
183
|
try:
|
|
@@ -167,13 +204,11 @@ async def analyze_log_staged(build_log: BuildLog):
|
|
|
167
204
|
log_text = process_url(build_log.url)
|
|
168
205
|
log_summary = mine_logs(log_text)
|
|
169
206
|
|
|
170
|
-
|
|
207
|
+
# Process snippets asynchronously
|
|
208
|
+
analyzed_snippets = await asyncio.gather(
|
|
209
|
+
*[submit_text(SNIPPET_PROMPT_TEMPLATE.format(s)) for s in log_summary])
|
|
171
210
|
|
|
172
|
-
|
|
173
|
-
response = submit_text(SNIPPET_PROMPT_TEMPLATE.format(snippet))
|
|
174
|
-
analyzed_snippets.append(response)
|
|
175
|
-
|
|
176
|
-
final_analysis = submit_text(
|
|
211
|
+
final_analysis = await submit_text(
|
|
177
212
|
PROMPT_TEMPLATE.format([e["choices"][0]["text"] for e in analyzed_snippets]))
|
|
178
213
|
|
|
179
214
|
certainty = 0
|
|
@@ -190,3 +225,18 @@ async def analyze_log_staged(build_log: BuildLog):
|
|
|
190
225
|
|
|
191
226
|
return StagedResponse(
|
|
192
227
|
explanation=final_analysis, snippets=analyzed_snippets, response_certainty=certainty)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.post("/analyze/stream", response_class=StreamingResponse)
|
|
231
|
+
async def analyze_log_stream(build_log: BuildLog):
|
|
232
|
+
"""Stream response endpoint for Logdetective.
|
|
233
|
+
Request must be in form {"url":"<YOUR_URL_HERE>"}.
|
|
234
|
+
URL must be valid for the request to be passed to the LLM server.
|
|
235
|
+
Meaning that it must contain appropriate scheme, path and netloc,
|
|
236
|
+
while lacking result, params or query fields.
|
|
237
|
+
"""
|
|
238
|
+
log_text = process_url(build_log.url)
|
|
239
|
+
log_summary = mine_logs(log_text)
|
|
240
|
+
stream = await submit_text(PROMPT_TEMPLATE.format(log_summary), stream=True)
|
|
241
|
+
|
|
242
|
+
return StreamingResponse(stream)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "logdetective"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.8"
|
|
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
|
|
File without changes
|