logdetective 1.2.0__tar.gz → 1.4.0__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.
Files changed (34) hide show
  1. {logdetective-1.2.0 → logdetective-1.4.0}/PKG-INFO +2 -1
  2. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/constants.py +15 -0
  3. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/logdetective.py +5 -1
  4. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/models.py +14 -0
  5. logdetective-1.4.0/logdetective/prompts.yml +104 -0
  6. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/config.py +13 -1
  7. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/emoji.py +3 -1
  8. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/llm.py +40 -91
  9. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/models.py +10 -3
  10. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/server.py +15 -4
  11. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/utils.py +32 -0
  12. {logdetective-1.2.0 → logdetective-1.4.0}/pyproject.toml +4 -3
  13. logdetective-1.2.0/logdetective/prompts.yml +0 -61
  14. {logdetective-1.2.0 → logdetective-1.4.0}/LICENSE +0 -0
  15. {logdetective-1.2.0 → logdetective-1.4.0}/README.md +0 -0
  16. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/__init__.py +0 -0
  17. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/drain3.ini +0 -0
  18. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/extractors.py +0 -0
  19. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/prompts-summary-first.yml +0 -0
  20. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/prompts-summary-only.yml +0 -0
  21. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/remote_log.py +0 -0
  22. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/__init__.py +0 -0
  23. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/compressors.py +0 -0
  24. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/database/__init__.py +0 -0
  25. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/database/base.py +0 -0
  26. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/database/models/__init__.py +0 -0
  27. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/database/models/merge_request_jobs.py +0 -0
  28. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/database/models/metrics.py +0 -0
  29. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/gitlab.py +0 -0
  30. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/metric.py +0 -0
  31. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/plot.py +0 -0
  32. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/templates/gitlab_full_comment.md.j2 +0 -0
  33. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective/server/templates/gitlab_short_comment.md.j2 +0 -0
  34. {logdetective-1.2.0 → logdetective-1.4.0}/logdetective.1.asciidoc +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: logdetective
3
- Version: 1.2.0
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)
@@ -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"
@@ -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=prompts_configuration.prompt_template,
163
+ prompt_template=prompt,
160
164
  temperature=args.temperature,
161
165
  )
162
166
  probs = []
@@ -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
+ )
@@ -0,0 +1,104 @@
1
+ # This file is intended for customization of prompts
2
+ # It is used only in server mode.
3
+ # On command line you have to load it using --prompts
4
+ # The defaults are stored in constants.py
5
+
6
+ prompt_template: |
7
+ Given following log snippets, and nothing else, explain what failure, if any, occurred during build of this package.
8
+
9
+ Analysis of the snippets must be in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
10
+ Snippets themselves must not be altered in any way whatsoever.
11
+
12
+ Snippets are delimited with '================'.
13
+
14
+ Finally, drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
15
+
16
+ Explanation of the issue, and recommended solution, should take handful of sentences.
17
+
18
+ Snippets:
19
+
20
+ {}
21
+
22
+ Analysis:
23
+
24
+
25
+ summarization_prompt_template: |
26
+ Does following log contain error or issue?
27
+
28
+ Log:
29
+
30
+ {}
31
+
32
+ Answer:
33
+
34
+
35
+ snippet_prompt_template: |
36
+ Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
37
+
38
+ Your analysis must be as concise as possible, while keeping relevant information intact.
39
+
40
+ Snippet:
41
+
42
+ {}
43
+
44
+ Analysis:
45
+
46
+ prompt_template_staged: |
47
+ Given following log snippets, their explanation, and nothing else, explain what failure, if any, occurred during build of this package.
48
+
49
+ Snippets are in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
50
+
51
+ Snippets are delimited with '================'.
52
+
53
+ Drawing on information from all snippets, provide a concise explanation of the issue and recommend a solution.
54
+
55
+ Explanation of the issue, and recommended solution, should take a handful of sentences.
56
+
57
+ Snippets:
58
+
59
+ {}
60
+
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.
@@ -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)
@@ -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
@@ -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, Dict, Any, Union
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
- text: str,
89
+ messages: List[Dict[str, str]],
142
90
  inference_cfg: InferenceConfig,
143
91
  stream: bool = False,
144
- ) -> Union[Explanation, StreamReader]:
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
- data = {
160
- "messages": [
161
- {
162
- "role": "user",
163
- "content": text,
164
- }
165
- ],
166
- "max_tokens": inference_cfg.max_tokens,
167
- "logprobs": inference_cfg.log_probs,
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 stream:
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["choices"][0]["message"]["content"],
185
- logprobs=response["choices"][0]["logprobs"]["content"],
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
- PROMPT_CONFIG.snippet_prompt_template.format(s),
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
 
@@ -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
- api_token: str = ""
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
  )
@@ -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
- PROMPT_CONFIG.prompt_template.format(log_summary),
221
+ messages,
211
222
  inference_cfg=SERVER_CONFIG.inference,
212
223
  stream=True,
213
224
  )
@@ -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
  [tool.poetry]
2
2
  name = "logdetective"
3
- version = "1.2.0"
3
+ version = "1.4.0"
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"
@@ -52,10 +52,11 @@ alembic = {version = "^1.13.3", optional = true }
52
52
  matplotlib = {version = "^3.8.4", optional = true }
53
53
  backoff = {version = "2.2.1", optional = true }
54
54
  sentry-sdk = {version = "^2.17.0", optional = true, extras = ["fastapi"]}
55
+ openai = {version = "^1.82.1", optional = true}
55
56
 
56
57
  [tool.poetry.extras]
57
- server = ["fastapi", "sqlalchemy", "psycopg2", "alembic", "matplotlib", "backoff", "aiolimiter", "sentry-sdk"]
58
- server-testing = ["fastapi", "sqlalchemy", "psycopg2-binary", "alembic", "matplotlib", "backoff", "pytest-asyncio", "sentry-sdk"]
58
+ server = ["fastapi", "sqlalchemy", "psycopg2", "alembic", "matplotlib", "backoff", "aiolimiter", "sentry-sdk", "openai"]
59
+ server-testing = ["fastapi", "sqlalchemy", "psycopg2-binary", "alembic", "matplotlib", "backoff", "pytest-asyncio", "sentry-sdk", "openai"]
59
60
 
60
61
  [build-system]
61
62
  requires = ["poetry-core"]
@@ -1,61 +0,0 @@
1
- # This file is intended for customization of prompts
2
- # It is used only in server mode.
3
- # On command line you have to load it using --prompts
4
- # The defaults are stored in constants.py
5
-
6
- prompt_template: |
7
- Given following log snippets, and nothing else, explain what failure, if any, occurred during build of this package.
8
-
9
- Analysis of the snippets must be in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
10
- Snippets themselves must not be altered in any way whatsoever.
11
-
12
- Snippets are delimited with '================'.
13
-
14
- Finally, drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
15
-
16
- Explanation of the issue, and recommended solution, should take handful of sentences.
17
-
18
- Snippets:
19
-
20
- {}
21
-
22
- Analysis:
23
-
24
-
25
- summarization_prompt_template: |
26
- Does following log contain error or issue?
27
-
28
- Log:
29
-
30
- {}
31
-
32
- Answer:
33
-
34
-
35
- snippet_prompt_template: |
36
- Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
37
-
38
- Your analysis must be as concise as possible, while keeping relevant information intact.
39
-
40
- Snippet:
41
-
42
- {}
43
-
44
- Analysis:
45
-
46
- prompt_template_staged: |
47
- Given following log snippets, their explanation, and nothing else, explain what failure, if any, occurred during build of this package.
48
-
49
- Snippets are in a format of [X] : [Y], where [X] is a log snippet, and [Y] is the explanation.
50
-
51
- Snippets are delimited with '================'.
52
-
53
- Drawing on information from all snippets, provide a concise explanation of the issue and recommend a solution.
54
-
55
- Explanation of the issue, and recommended solution, should take a handful of sentences.
56
-
57
- Snippets:
58
-
59
- {}
60
-
61
- Analysis:
File without changes
File without changes