logdetective 1.2.0__py3-none-any.whl → 1.4.0__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 +15 -0
- logdetective/logdetective.py +5 -1
- logdetective/models.py +14 -0
- logdetective/prompts.yml +43 -0
- logdetective/server/config.py +13 -1
- logdetective/server/emoji.py +3 -1
- logdetective/server/llm.py +40 -91
- logdetective/server/models.py +10 -3
- logdetective/server/server.py +15 -4
- logdetective/utils.py +32 -0
- {logdetective-1.2.0.dist-info → logdetective-1.4.0.dist-info}/METADATA +2 -1
- {logdetective-1.2.0.dist-info → logdetective-1.4.0.dist-info}/RECORD +15 -15
- {logdetective-1.2.0.dist-info → logdetective-1.4.0.dist-info}/LICENSE +0 -0
- {logdetective-1.2.0.dist-info → logdetective-1.4.0.dist-info}/WHEEL +0 -0
- {logdetective-1.2.0.dist-info → logdetective-1.4.0.dist-info}/entry_points.txt +0 -0
logdetective/constants.py
CHANGED
|
@@ -69,6 +69,17 @@ Analysis:
|
|
|
69
69
|
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
|
+
DEFAULT_SYSTEM_PROMPT = """
|
|
73
|
+
You are a highly capable large language model based expert system specialized in
|
|
74
|
+
packaging and delivery of software using RPM (RPM Package Manager). Your purpose is to diagnose
|
|
75
|
+
RPM build failures, identifying root causes and proposing solutions if possible.
|
|
76
|
+
You are truthful, concise, and helpful.
|
|
77
|
+
|
|
78
|
+
You never speculate about package being built or fabricate information.
|
|
79
|
+
If you do not know the answer, you acknowledge the fact and end your response.
|
|
80
|
+
Your responses must be as short as possible.
|
|
81
|
+
"""
|
|
82
|
+
|
|
72
83
|
SNIPPET_DELIMITER = "================"
|
|
73
84
|
|
|
74
85
|
DEFAULT_TEMPERATURE = 0.8
|
|
@@ -76,3 +87,7 @@ DEFAULT_TEMPERATURE = 0.8
|
|
|
76
87
|
# Tuning for LLM-as-a-Service
|
|
77
88
|
LLM_DEFAULT_MAX_QUEUE_SIZE = 50
|
|
78
89
|
LLM_DEFAULT_REQUESTS_PER_MINUTE = 60
|
|
90
|
+
|
|
91
|
+
# Roles for chat API
|
|
92
|
+
SYSTEM_ROLE_DEFAULT = "developer"
|
|
93
|
+
USER_ROLE_DEFAULT = "user"
|
logdetective/logdetective.py
CHANGED
|
@@ -149,6 +149,10 @@ async def run(): # pylint: disable=too-many-statements,too-many-locals
|
|
|
149
149
|
log_summary = format_snippets(log_summary)
|
|
150
150
|
LOG.info("Log summary: \n %s", log_summary)
|
|
151
151
|
|
|
152
|
+
prompt = (
|
|
153
|
+
f"{prompts_configuration.default_system_prompt}\n"
|
|
154
|
+
f"{prompts_configuration.prompt_template}")
|
|
155
|
+
|
|
152
156
|
stream = True
|
|
153
157
|
if args.no_stream:
|
|
154
158
|
stream = False
|
|
@@ -156,7 +160,7 @@ async def run(): # pylint: disable=too-many-statements,too-many-locals
|
|
|
156
160
|
log_summary,
|
|
157
161
|
model,
|
|
158
162
|
stream,
|
|
159
|
-
prompt_template=
|
|
163
|
+
prompt_template=prompt,
|
|
160
164
|
temperature=args.temperature,
|
|
161
165
|
)
|
|
162
166
|
probs = []
|
logdetective/models.py
CHANGED
|
@@ -6,6 +6,7 @@ from logdetective.constants import (
|
|
|
6
6
|
PROMPT_TEMPLATE_STAGED,
|
|
7
7
|
SUMMARIZATION_PROMPT_TEMPLATE,
|
|
8
8
|
SNIPPET_PROMPT_TEMPLATE,
|
|
9
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
|
|
@@ -17,6 +18,10 @@ class PromptConfig(BaseModel):
|
|
|
17
18
|
snippet_prompt_template: str = SNIPPET_PROMPT_TEMPLATE
|
|
18
19
|
prompt_template_staged: str = PROMPT_TEMPLATE_STAGED
|
|
19
20
|
|
|
21
|
+
default_system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
22
|
+
snippet_system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
23
|
+
staged_system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
24
|
+
|
|
20
25
|
def __init__(self, data: Optional[dict] = None):
|
|
21
26
|
super().__init__()
|
|
22
27
|
if data is None:
|
|
@@ -31,3 +36,12 @@ class PromptConfig(BaseModel):
|
|
|
31
36
|
self.prompt_template_staged = data.get(
|
|
32
37
|
"prompt_template_staged", PROMPT_TEMPLATE_STAGED
|
|
33
38
|
)
|
|
39
|
+
self.default_system_prompt = data.get(
|
|
40
|
+
"default_system_prompt", DEFAULT_SYSTEM_PROMPT
|
|
41
|
+
)
|
|
42
|
+
self.snippet_system_prompt = data.get(
|
|
43
|
+
"snippet_system_prompt", DEFAULT_SYSTEM_PROMPT
|
|
44
|
+
)
|
|
45
|
+
self.staged_system_prompt = data.get(
|
|
46
|
+
"staged_system_prompt", DEFAULT_SYSTEM_PROMPT
|
|
47
|
+
)
|
logdetective/prompts.yml
CHANGED
|
@@ -59,3 +59,46 @@ prompt_template_staged: |
|
|
|
59
59
|
{}
|
|
60
60
|
|
|
61
61
|
Analysis:
|
|
62
|
+
|
|
63
|
+
# System prompts
|
|
64
|
+
# System prompts are meant to serve as general guide for model behavior,
|
|
65
|
+
# describing role and purpose it is meant to serve.
|
|
66
|
+
# Sample system prompts in this file are intentionally the same,
|
|
67
|
+
# however, in some circumstances it may be beneficial have different
|
|
68
|
+
# system prompts for each sub case. For example when a specialized model is deployed
|
|
69
|
+
# to analyze snippets.
|
|
70
|
+
|
|
71
|
+
# Default prompt is used by the CLI tool and also for final analysis
|
|
72
|
+
# with /analyze and /analyze/stream API endpoints
|
|
73
|
+
default_system_prompt: |
|
|
74
|
+
You are a highly capable large language model based expert system specialized in
|
|
75
|
+
packaging and delivery of software using RPM (RPM Package Manager). Your purpose is to diagnose
|
|
76
|
+
RPM build failures, identifying root causes and proposing solutions if possible.
|
|
77
|
+
You are truthful, concise, and helpful.
|
|
78
|
+
|
|
79
|
+
You never speculate about package being built or fabricate information.
|
|
80
|
+
If you do not know the answer, you acknowledge the fact and end your response.
|
|
81
|
+
Your responses must be as short as possible.
|
|
82
|
+
|
|
83
|
+
# Snippet system prompt is used for analysis of individual snippets
|
|
84
|
+
snippet_system_prompt: |
|
|
85
|
+
You are a highly capable large language model based expert system specialized in
|
|
86
|
+
packaging and delivery of software using RPM (RPM Package Manager). Your purpose is to diagnose
|
|
87
|
+
RPM build failures, identifying root causes and proposing solutions if possible.
|
|
88
|
+
You are truthful, concise, and helpful.
|
|
89
|
+
|
|
90
|
+
You never speculate about package being built or fabricate information.
|
|
91
|
+
If you do not know the answer, you acknowledge the fact and end your response.
|
|
92
|
+
Your responses must be as short as possible.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Staged system prompt is used by /analyze/staged API endpoint
|
|
96
|
+
staged_system_prompt: |
|
|
97
|
+
You are a highly capable large language model based expert system specialized in
|
|
98
|
+
packaging and delivery of software using RPM (RPM Package Manager). Your purpose is to diagnose
|
|
99
|
+
RPM build failures, identifying root causes and proposing solutions if possible.
|
|
100
|
+
You are truthful, concise, and helpful.
|
|
101
|
+
|
|
102
|
+
You never speculate about package being built or fabricate information.
|
|
103
|
+
If you do not know the answer, you acknowledge the fact and end your response.
|
|
104
|
+
Your responses must be as short as possible.
|
logdetective/server/config.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
3
|
import yaml
|
|
4
|
+
from openai import AsyncOpenAI
|
|
5
|
+
|
|
4
6
|
from logdetective.utils import load_prompts
|
|
5
|
-
from logdetective.server.models import Config
|
|
7
|
+
from logdetective.server.models import Config, InferenceConfig
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def load_server_config(path: str | None) -> Config:
|
|
@@ -49,6 +51,14 @@ def get_log(config: Config):
|
|
|
49
51
|
return log
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
def get_openai_api_client(ineference_config: InferenceConfig):
|
|
55
|
+
"""Set up AsyncOpenAI client with default configuration.
|
|
56
|
+
"""
|
|
57
|
+
return AsyncOpenAI(
|
|
58
|
+
api_key=ineference_config.api_token,
|
|
59
|
+
base_url=ineference_config.url)
|
|
60
|
+
|
|
61
|
+
|
|
52
62
|
SERVER_CONFIG_PATH = os.environ.get("LOGDETECTIVE_SERVER_CONF", None)
|
|
53
63
|
SERVER_PROMPT_PATH = os.environ.get("LOGDETECTIVE_PROMPTS", None)
|
|
54
64
|
|
|
@@ -56,3 +66,5 @@ SERVER_CONFIG = load_server_config(SERVER_CONFIG_PATH)
|
|
|
56
66
|
PROMPT_CONFIG = load_prompts(SERVER_PROMPT_PATH)
|
|
57
67
|
|
|
58
68
|
LOG = get_log(SERVER_CONFIG)
|
|
69
|
+
|
|
70
|
+
CLIENT = get_openai_api_client(SERVER_CONFIG.inference)
|
logdetective/server/emoji.py
CHANGED
|
@@ -44,7 +44,7 @@ async def _handle_gitlab_operation(func: Callable, *args):
|
|
|
44
44
|
"""
|
|
45
45
|
try:
|
|
46
46
|
return await asyncio.to_thread(func, *args)
|
|
47
|
-
except gitlab.GitlabError as e:
|
|
47
|
+
except (gitlab.GitlabError, gitlab.GitlabGetError) as e:
|
|
48
48
|
log_msg = f"Error during GitLab operation {func}{args}: {e}"
|
|
49
49
|
if "Not Found" in str(e):
|
|
50
50
|
LOG.error(log_msg)
|
|
@@ -64,6 +64,8 @@ async def collect_emojis_in_comments( # pylint: disable=too-many-locals
|
|
|
64
64
|
mrs = {}
|
|
65
65
|
for comment in comments:
|
|
66
66
|
mr_job_db = GitlabMergeRequestJobs.get_by_id(comment.merge_request_job_id)
|
|
67
|
+
if not mr_job_db:
|
|
68
|
+
continue
|
|
67
69
|
if mr_job_db.id not in projects:
|
|
68
70
|
projects[mr_job_db.id] = project = await _handle_gitlab_operation(
|
|
69
71
|
gitlab_conn.projects.get, mr_job_db.project_id
|
logdetective/server/llm.py
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import asyncio
|
|
3
|
-
import json
|
|
4
3
|
import random
|
|
5
|
-
from typing import List, Tuple,
|
|
4
|
+
from typing import List, Tuple, Union, Dict
|
|
6
5
|
|
|
7
6
|
import backoff
|
|
8
|
-
from aiohttp import StreamReader
|
|
9
7
|
from fastapi import HTTPException
|
|
10
8
|
|
|
11
9
|
import aiohttp
|
|
10
|
+
from openai import AsyncStream
|
|
11
|
+
from openai.types.chat import ChatCompletionChunk
|
|
12
12
|
|
|
13
13
|
from logdetective.constants import SNIPPET_DELIMITER
|
|
14
14
|
from logdetective.extractors import DrainExtractor
|
|
15
15
|
from logdetective.utils import (
|
|
16
16
|
compute_certainty,
|
|
17
|
+
prompt_to_messages,
|
|
17
18
|
)
|
|
18
|
-
from logdetective.server.config import LOG, SERVER_CONFIG, PROMPT_CONFIG
|
|
19
|
+
from logdetective.server.config import LOG, SERVER_CONFIG, PROMPT_CONFIG, CLIENT
|
|
19
20
|
from logdetective.server.models import (
|
|
20
21
|
AnalyzedSnippet,
|
|
21
22
|
InferenceConfig,
|
|
@@ -54,59 +55,6 @@ def mine_logs(log: str) -> List[Tuple[int, str]]:
|
|
|
54
55
|
return log_summary
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
async def submit_to_llm_endpoint(
|
|
58
|
-
url_path: str,
|
|
59
|
-
data: Dict[str, Any],
|
|
60
|
-
headers: Dict[str, str],
|
|
61
|
-
stream: bool,
|
|
62
|
-
inference_cfg: InferenceConfig = SERVER_CONFIG.inference,
|
|
63
|
-
) -> Any:
|
|
64
|
-
"""Send request to an API endpoint. Verifying successful request unless
|
|
65
|
-
the using the stream response.
|
|
66
|
-
|
|
67
|
-
url_path: The endpoint path to query. (e.g. "/v1/chat/completions"). It should
|
|
68
|
-
not include the scheme and netloc of the URL, which is stored in the
|
|
69
|
-
InferenceConfig.
|
|
70
|
-
data:
|
|
71
|
-
headers:
|
|
72
|
-
stream:
|
|
73
|
-
inference_cfg: An InferenceConfig object containing the URL, max_tokens
|
|
74
|
-
and other relevant configuration for talking to an inference server.
|
|
75
|
-
"""
|
|
76
|
-
async with inference_cfg.get_limiter():
|
|
77
|
-
LOG.debug("async request %s headers=%s data=%s", url_path, headers, data)
|
|
78
|
-
session = inference_cfg.get_http_session()
|
|
79
|
-
|
|
80
|
-
if inference_cfg.api_token:
|
|
81
|
-
headers["Authorization"] = f"Bearer {inference_cfg.api_token}"
|
|
82
|
-
|
|
83
|
-
response = await session.post(
|
|
84
|
-
url_path,
|
|
85
|
-
headers=headers,
|
|
86
|
-
# we need to use the `json=` parameter here and let aiohttp
|
|
87
|
-
# handle the json-encoding
|
|
88
|
-
json=data,
|
|
89
|
-
timeout=int(LLM_CPP_SERVER_TIMEOUT),
|
|
90
|
-
# Docs says chunked takes int, but:
|
|
91
|
-
# DeprecationWarning: Chunk size is deprecated #1615
|
|
92
|
-
# So let's make sure we either put True or None here
|
|
93
|
-
chunked=True if stream else None,
|
|
94
|
-
raise_for_status=True,
|
|
95
|
-
)
|
|
96
|
-
if stream:
|
|
97
|
-
return response
|
|
98
|
-
try:
|
|
99
|
-
return json.loads(await response.text())
|
|
100
|
-
except UnicodeDecodeError as ex:
|
|
101
|
-
LOG.error(
|
|
102
|
-
"Error encountered while parsing llama server response: %s", ex
|
|
103
|
-
)
|
|
104
|
-
raise HTTPException(
|
|
105
|
-
status_code=400,
|
|
106
|
-
detail=f"Couldn't parse the response.\nError: {ex}\nData: {response.text}",
|
|
107
|
-
) from ex
|
|
108
|
-
|
|
109
|
-
|
|
110
58
|
def should_we_giveup(exc: aiohttp.ClientResponseError) -> bool:
|
|
111
59
|
"""
|
|
112
60
|
From backoff's docs:
|
|
@@ -138,10 +86,10 @@ def we_give_up(details: backoff._typing.Details):
|
|
|
138
86
|
on_giveup=we_give_up,
|
|
139
87
|
)
|
|
140
88
|
async def submit_text(
|
|
141
|
-
|
|
89
|
+
messages: List[Dict[str, str]],
|
|
142
90
|
inference_cfg: InferenceConfig,
|
|
143
91
|
stream: bool = False,
|
|
144
|
-
) -> Union[Explanation,
|
|
92
|
+
) -> Union[Explanation, AsyncStream[ChatCompletionChunk]]:
|
|
145
93
|
"""Submit prompt to LLM.
|
|
146
94
|
inference_cfg: The configuration section from the config.json representing
|
|
147
95
|
the relevant inference server for this request.
|
|
@@ -149,40 +97,31 @@ async def submit_text(
|
|
|
149
97
|
"""
|
|
150
98
|
LOG.info("Analyzing the text")
|
|
151
99
|
|
|
152
|
-
headers = {"Content-Type": "application/json"}
|
|
153
|
-
|
|
154
|
-
if SERVER_CONFIG.inference.api_token:
|
|
155
|
-
headers["Authorization"] = f"Bearer {SERVER_CONFIG.inference.api_token}"
|
|
156
|
-
|
|
157
100
|
LOG.info("Submitting to /v1/chat/completions endpoint")
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"stream": stream,
|
|
169
|
-
"model": inference_cfg.model,
|
|
170
|
-
"temperature": inference_cfg.temperature,
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
response = await submit_to_llm_endpoint(
|
|
174
|
-
"/v1/chat/completions",
|
|
175
|
-
data,
|
|
176
|
-
headers,
|
|
177
|
-
inference_cfg=inference_cfg,
|
|
178
|
-
stream=stream,
|
|
179
|
-
)
|
|
102
|
+
async with inference_cfg.get_limiter():
|
|
103
|
+
response = await CLIENT.chat.completions.create(
|
|
104
|
+
messages=messages,
|
|
105
|
+
max_tokens=inference_cfg.max_tokens,
|
|
106
|
+
logprobs=inference_cfg.log_probs,
|
|
107
|
+
stream=stream,
|
|
108
|
+
model=inference_cfg.model,
|
|
109
|
+
temperature=inference_cfg.temperature,
|
|
110
|
+
)
|
|
180
111
|
|
|
181
|
-
if
|
|
112
|
+
if isinstance(response, AsyncStream):
|
|
182
113
|
return response
|
|
114
|
+
if not response.choices[0].message.content:
|
|
115
|
+
LOG.error("No response content recieved from %s", inference_cfg.url)
|
|
116
|
+
raise RuntimeError()
|
|
117
|
+
if response.choices[0].logprobs and response.choices[0].logprobs.content:
|
|
118
|
+
logprobs = [e.to_dict() for e in response.choices[0].logprobs.content]
|
|
119
|
+
else:
|
|
120
|
+
logprobs = None
|
|
121
|
+
|
|
183
122
|
return Explanation(
|
|
184
|
-
text=response
|
|
185
|
-
logprobs=
|
|
123
|
+
text=response.choices[0].message.content,
|
|
124
|
+
logprobs=logprobs,
|
|
186
125
|
)
|
|
187
126
|
|
|
188
127
|
|
|
@@ -193,7 +132,12 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
|
|
|
193
132
|
# Process snippets asynchronously
|
|
194
133
|
awaitables = [
|
|
195
134
|
submit_text(
|
|
196
|
-
|
|
135
|
+
prompt_to_messages(
|
|
136
|
+
PROMPT_CONFIG.snippet_prompt_template.format(s),
|
|
137
|
+
PROMPT_CONFIG.snippet_system_prompt,
|
|
138
|
+
SERVER_CONFIG.inference.system_role,
|
|
139
|
+
SERVER_CONFIG.inference.user_role,
|
|
140
|
+
),
|
|
197
141
|
inference_cfg=SERVER_CONFIG.snippet_inference,
|
|
198
142
|
)
|
|
199
143
|
for s in log_summary
|
|
@@ -207,9 +151,14 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
|
|
|
207
151
|
final_prompt = PROMPT_CONFIG.prompt_template_staged.format(
|
|
208
152
|
format_analyzed_snippets(analyzed_snippets)
|
|
209
153
|
)
|
|
210
|
-
|
|
211
|
-
final_analysis = await submit_text(
|
|
154
|
+
messages = prompt_to_messages(
|
|
212
155
|
final_prompt,
|
|
156
|
+
PROMPT_CONFIG.staged_system_prompt,
|
|
157
|
+
SERVER_CONFIG.inference.system_role,
|
|
158
|
+
SERVER_CONFIG.inference.user_role,
|
|
159
|
+
)
|
|
160
|
+
final_analysis = await submit_text(
|
|
161
|
+
messages,
|
|
213
162
|
inference_cfg=SERVER_CONFIG.inference,
|
|
214
163
|
)
|
|
215
164
|
|
logdetective/server/models.py
CHANGED
|
@@ -20,6 +20,8 @@ from logdetective.constants import (
|
|
|
20
20
|
DEFAULT_TEMPERATURE,
|
|
21
21
|
LLM_DEFAULT_MAX_QUEUE_SIZE,
|
|
22
22
|
LLM_DEFAULT_REQUESTS_PER_MINUTE,
|
|
23
|
+
SYSTEM_ROLE_DEFAULT,
|
|
24
|
+
USER_ROLE_DEFAULT,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -136,11 +138,15 @@ class InferenceConfig(BaseModel): # pylint: disable=too-many-instance-attribute
|
|
|
136
138
|
max_tokens: int = -1
|
|
137
139
|
log_probs: bool = True
|
|
138
140
|
url: str = ""
|
|
139
|
-
|
|
141
|
+
# OpenAI client library requires a string to be specified for API token
|
|
142
|
+
# even if it is not checked on the server side
|
|
143
|
+
api_token: str = "None"
|
|
140
144
|
model: str = ""
|
|
141
145
|
temperature: NonNegativeFloat = DEFAULT_TEMPERATURE
|
|
142
146
|
max_queue_size: int = LLM_DEFAULT_MAX_QUEUE_SIZE
|
|
143
147
|
http_timeout: float = 5.0
|
|
148
|
+
user_role: str = USER_ROLE_DEFAULT
|
|
149
|
+
system_role: str = SYSTEM_ROLE_DEFAULT
|
|
144
150
|
_http_session: aiohttp.ClientSession = None
|
|
145
151
|
_limiter: AsyncLimiter = AsyncLimiter(LLM_DEFAULT_REQUESTS_PER_MINUTE)
|
|
146
152
|
|
|
@@ -153,11 +159,12 @@ class InferenceConfig(BaseModel): # pylint: disable=too-many-instance-attribute
|
|
|
153
159
|
self.log_probs = data.get("log_probs", True)
|
|
154
160
|
self.url = data.get("url", "")
|
|
155
161
|
self.http_timeout = data.get("http_timeout", 5.0)
|
|
156
|
-
self.api_token = data.get("api_token", "")
|
|
162
|
+
self.api_token = data.get("api_token", "None")
|
|
157
163
|
self.model = data.get("model", "default-model")
|
|
158
164
|
self.temperature = data.get("temperature", DEFAULT_TEMPERATURE)
|
|
159
165
|
self.max_queue_size = data.get("max_queue_size", LLM_DEFAULT_MAX_QUEUE_SIZE)
|
|
160
|
-
|
|
166
|
+
self.user_role = data.get("user_role", USER_ROLE_DEFAULT)
|
|
167
|
+
self.system_role = data.get("system_role", SYSTEM_ROLE_DEFAULT)
|
|
161
168
|
self._requests_per_minute = data.get(
|
|
162
169
|
"requests_per_minute", LLM_DEFAULT_REQUESTS_PER_MINUTE
|
|
163
170
|
)
|
logdetective/server/server.py
CHANGED
|
@@ -20,6 +20,7 @@ import logdetective.server.database.base
|
|
|
20
20
|
from logdetective.utils import (
|
|
21
21
|
compute_certainty,
|
|
22
22
|
format_snippets,
|
|
23
|
+
prompt_to_messages,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
from logdetective.server.config import SERVER_CONFIG, PROMPT_CONFIG, LOG
|
|
@@ -135,9 +136,14 @@ async def analyze_log(
|
|
|
135
136
|
log_text = await remote_log.process_url()
|
|
136
137
|
log_summary = mine_logs(log_text)
|
|
137
138
|
log_summary = format_snippets(log_summary)
|
|
138
|
-
|
|
139
|
-
response = await submit_text(
|
|
139
|
+
messages = prompt_to_messages(
|
|
140
140
|
PROMPT_CONFIG.prompt_template.format(log_summary),
|
|
141
|
+
PROMPT_CONFIG.default_system_prompt,
|
|
142
|
+
SERVER_CONFIG.inference.system_role,
|
|
143
|
+
SERVER_CONFIG.inference.user_role,
|
|
144
|
+
)
|
|
145
|
+
response = await submit_text(
|
|
146
|
+
messages,
|
|
141
147
|
inference_cfg=SERVER_CONFIG.inference,
|
|
142
148
|
)
|
|
143
149
|
certainty = 0
|
|
@@ -204,10 +210,15 @@ async def analyze_log_stream(
|
|
|
204
210
|
log_text = await remote_log.process_url()
|
|
205
211
|
log_summary = mine_logs(log_text)
|
|
206
212
|
log_summary = format_snippets(log_summary)
|
|
207
|
-
|
|
213
|
+
messages = prompt_to_messages(
|
|
214
|
+
PROMPT_CONFIG.prompt_template.format(log_summary),
|
|
215
|
+
PROMPT_CONFIG.default_system_prompt,
|
|
216
|
+
SERVER_CONFIG.inference.system_role,
|
|
217
|
+
SERVER_CONFIG.inference.user_role,
|
|
218
|
+
)
|
|
208
219
|
try:
|
|
209
220
|
stream = submit_text(
|
|
210
|
-
|
|
221
|
+
messages,
|
|
211
222
|
inference_cfg=SERVER_CONFIG.inference,
|
|
212
223
|
stream=True,
|
|
213
224
|
)
|
logdetective/utils.py
CHANGED
|
@@ -195,3 +195,35 @@ def load_prompts(path: str | None) -> PromptConfig:
|
|
|
195
195
|
except FileNotFoundError:
|
|
196
196
|
print("Prompt configuration file not found, reverting to defaults.")
|
|
197
197
|
return PromptConfig()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def prompt_to_messages(
|
|
201
|
+
user_message: str, system_prompt: str | None = None,
|
|
202
|
+
system_role: str = "developer", user_role: str = "user") -> List[Dict[str, str]]:
|
|
203
|
+
"""Turn prompt into list of message dictionaries.
|
|
204
|
+
If `system_role` and `user_role` are the same, only a single message is created,
|
|
205
|
+
as concatenation of `user_message` and `system_prompt`. This is useful for models which
|
|
206
|
+
do not have separate system role, such as mistral.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
if system_role == user_role:
|
|
210
|
+
messages = [
|
|
211
|
+
{
|
|
212
|
+
"role": system_role,
|
|
213
|
+
"content": f"{system_prompt}\n{user_message}"
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
else:
|
|
217
|
+
|
|
218
|
+
messages = [
|
|
219
|
+
{
|
|
220
|
+
"role": system_role,
|
|
221
|
+
"content": system_prompt
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"role": user_role,
|
|
225
|
+
"content": user_message,
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
return messages
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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
|
|
@@ -30,6 +30,7 @@ Requires-Dist: huggingface-hub (>0.23.2)
|
|
|
30
30
|
Requires-Dist: llama-cpp-python (>0.2.56,!=0.2.86)
|
|
31
31
|
Requires-Dist: matplotlib (>=3.8.4,<4.0.0) ; extra == "server" or extra == "server-testing"
|
|
32
32
|
Requires-Dist: numpy (>=1.26.0)
|
|
33
|
+
Requires-Dist: openai (>=1.82.1,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
33
34
|
Requires-Dist: psycopg2 (>=2.9.9,<3.0.0) ; extra == "server"
|
|
34
35
|
Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0) ; extra == "server-testing"
|
|
35
36
|
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
logdetective/__init__.py,sha256=VqRngDcuFT7JWms8Qc_MsOvajoXVOKPr-S1kqY3Pqhc,59
|
|
2
|
-
logdetective/constants.py,sha256=
|
|
2
|
+
logdetective/constants.py,sha256=yH2vpYENfJpoYUC6KzzTSRDFOiMmjtRK-GU9vfv8o3o,2560
|
|
3
3
|
logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
|
|
4
4
|
logdetective/extractors.py,sha256=7ahzWbTtU9MveG1Q7wU9LO8OJgs85X-cHmWltUhCe9M,3491
|
|
5
|
-
logdetective/logdetective.py,sha256=
|
|
6
|
-
logdetective/models.py,sha256=
|
|
5
|
+
logdetective/logdetective.py,sha256=YwjKSgW2iW4BAxWTOgUbrVbk2Lbv76vOLFTTtuPg3hg,5928
|
|
6
|
+
logdetective/models.py,sha256=0DHCtGUqZzPcduSU4Z5AzuNn3g9XBh6UhLBcA9mDG6M,1653
|
|
7
7
|
logdetective/prompts-summary-first.yml,sha256=3Zfp4NNOfaFYq5xBlBjeQa5PdjYfS4v17OtJqQ-DRpU,821
|
|
8
8
|
logdetective/prompts-summary-only.yml,sha256=8U9AMJV8ePW-0CoXOXlQoO92DAJDeutIT8ntSkkm6W0,470
|
|
9
|
-
logdetective/prompts.yml,sha256=
|
|
9
|
+
logdetective/prompts.yml,sha256=2WGox--VRINjsxaJc7fql-v1sIVYWYZLEjJV_vvBm9o,3969
|
|
10
10
|
logdetective/remote_log.py,sha256=1oeMIdDE_ob_2QrlXYTAA_m-36pNEicXbZwrCyzNgwo,2256
|
|
11
11
|
logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
logdetective/server/compressors.py,sha256=qzrT-BPSksXY6F2L6ger04GGrgdBsGOfK2YuCFRs0Q4,5427
|
|
13
|
-
logdetective/server/config.py,sha256=
|
|
13
|
+
logdetective/server/config.py,sha256=yD6pRTZze9bqoCxfdxpupXrvb18auAf95mq4BoowAbs,2113
|
|
14
14
|
logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
logdetective/server/database/base.py,sha256=1mcjEbhwLl4RalvT3oy6XVctjJoWIW3H9aI_sMWJBK8,1728
|
|
16
16
|
logdetective/server/database/models/__init__.py,sha256=xy2hkygyw6_87zPKkG20i7g7_LXTGR__PUeojhbvv94,496
|
|
17
17
|
logdetective/server/database/models/merge_request_jobs.py,sha256=hw88wV1-3x7i53sX7ZotKClc6OsH1njPpbRSZofnqr4,18670
|
|
18
18
|
logdetective/server/database/models/metrics.py,sha256=yl9fS4IPVFWDeFvPAxO6zOVu6oLF319ApvVLAgnD5yU,13928
|
|
19
|
-
logdetective/server/emoji.py,sha256=
|
|
19
|
+
logdetective/server/emoji.py,sha256=W1nJLU1UnTG8FGttOs6gC7x3TcjxiBuviXklD9f2Mu8,4370
|
|
20
20
|
logdetective/server/gitlab.py,sha256=wQSlvdWn6XEi1oP6HhI75bIhm6bgdpWr3zu2WXF0_oE,16473
|
|
21
|
-
logdetective/server/llm.py,sha256=
|
|
21
|
+
logdetective/server/llm.py,sha256=q9LdoAmsx9MpBjnjLyJ9GBU27jKViTaWbVXyMsmsCI0,5721
|
|
22
22
|
logdetective/server/metric.py,sha256=B3ew_qSmtEMj6xl-FoOtS4F_bkplp-shhtfHF1cG_Io,4010
|
|
23
|
-
logdetective/server/models.py,sha256=
|
|
23
|
+
logdetective/server/models.py,sha256=I45uLnq_zqn_r0FdOdop9zQPbsOWOY_M39NBBOXP134,15738
|
|
24
24
|
logdetective/server/plot.py,sha256=eZs4r9gua-nW3yymSMIz1leL9mb4QKlh6FJZSeOfZ5M,14872
|
|
25
|
-
logdetective/server/server.py,sha256
|
|
25
|
+
logdetective/server/server.py,sha256=V-lSG2cCTxoGwvUc8mEmLQQWS4g_W_dER2o118RufAk,18792
|
|
26
26
|
logdetective/server/templates/gitlab_full_comment.md.j2,sha256=DQZ2WVFedpuXI6znbHIW4wpF9BmFS8FaUkowh8AnGhE,1627
|
|
27
27
|
logdetective/server/templates/gitlab_short_comment.md.j2,sha256=fzScpayv2vpRLczP_0O0YxtA8rsKvR6gSv4ntNdWb98,1443
|
|
28
|
-
logdetective/utils.py,sha256=
|
|
29
|
-
logdetective-1.
|
|
30
|
-
logdetective-1.
|
|
31
|
-
logdetective-1.
|
|
32
|
-
logdetective-1.
|
|
33
|
-
logdetective-1.
|
|
28
|
+
logdetective/utils.py,sha256=4VDghJs6mTz8PjkYaV794LUWWyEqBRddXKkENJzR5n4,7025
|
|
29
|
+
logdetective-1.4.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
30
|
+
logdetective-1.4.0.dist-info/METADATA,sha256=UlBDdFwH4kswo5NqhvEWfz7dGcex7aXfFOUt1JI5n4Y,17709
|
|
31
|
+
logdetective-1.4.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
32
|
+
logdetective-1.4.0.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
|
|
33
|
+
logdetective-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|