logdetective 0.2.7__py3-none-any.whl → 0.2.8__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/server.py CHANGED
@@ -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
- 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
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
- analyzed_snippets = []
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
- for snippet in log_summary:
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
  Metadata-Version: 2.1
2
2
  Name: logdetective
3
- Version: 0.2.7
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 (>=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
  -------
@@ -3,10 +3,10 @@ logdetective/constants.py,sha256=1Ls2VJXb7NwSgi_HmTOA1c52K16SZIeDYBXlvBJ07zU,991
3
3
  logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
4
4
  logdetective/extractors.py,sha256=eRizRiKhC3MPTHXS5nlRKcEudEaqct7G28V1bZYGkqI,3103
5
5
  logdetective/logdetective.py,sha256=f7ASCJg_Yt6VBFieXBYgQYdenfXjC60ZdLHhzQHideI,4372
6
- logdetective/server.py,sha256=m0NPtk9tAUzyu9O8jIAfgEzynZ-WCHqVvCJkHOm08Ks,7073
6
+ logdetective/server.py,sha256=UDH7LEFZ-rnIrrnZDZTcYluom1XnsvYG1b5Fc2xiivs,9226
7
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,,
8
+ logdetective-0.2.8.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
9
+ logdetective-0.2.8.dist-info/METADATA,sha256=gFcfcgLCOQXlQu4eWbHzGzaP8z7-yMwQ36-jFJsSg7c,9797
10
+ logdetective-0.2.8.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
+ logdetective-0.2.8.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
12
+ logdetective-0.2.8.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