result-companion 0.0.1__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.
- result_companion/__init__.py +8 -0
- result_companion/core/analizers/__init__.py +0 -0
- result_companion/core/analizers/common.py +58 -0
- result_companion/core/analizers/factory_common.py +104 -0
- result_companion/core/analizers/local/__init__.py +0 -0
- result_companion/core/analizers/local/ollama_exceptions.py +10 -0
- result_companion/core/analizers/local/ollama_install.py +279 -0
- result_companion/core/analizers/local/ollama_runner.py +124 -0
- result_companion/core/analizers/local/ollama_server_manager.py +185 -0
- result_companion/core/analizers/models.py +17 -0
- result_companion/core/analizers/remote/__init__.py +0 -0
- result_companion/core/analizers/remote/custom_endpoint.py +0 -0
- result_companion/core/analizers/remote/openai.py +0 -0
- result_companion/core/chunking/chunking.py +113 -0
- result_companion/core/chunking/utils.py +114 -0
- result_companion/core/configs/default_config.yaml +85 -0
- result_companion/core/html/__init__.py +0 -0
- result_companion/core/html/html_creator.py +179 -0
- result_companion/core/html/llm_injector.py +20 -0
- result_companion/core/parsers/__init__.py +0 -0
- result_companion/core/parsers/config.py +256 -0
- result_companion/core/parsers/result_parser.py +101 -0
- result_companion/core/results/__init__.py +0 -0
- result_companion/core/results/visitors.py +34 -0
- result_companion/core/utils/__init__.py +0 -0
- result_companion/core/utils/log_levels.py +23 -0
- result_companion/core/utils/logging_config.py +115 -0
- result_companion/core/utils/progress.py +61 -0
- result_companion/entrypoints/__init__.py +0 -0
- result_companion/entrypoints/cli/__init__.py +0 -0
- result_companion/entrypoints/cli/cli_app.py +266 -0
- result_companion/entrypoints/run_rc.py +171 -0
- result_companion-0.0.1.dist-info/METADATA +216 -0
- result_companion-0.0.1.dist-info/RECORD +37 -0
- result_companion-0.0.1.dist-info/WHEEL +4 -0
- result_companion-0.0.1.dist-info/entry_points.txt +3 -0
- result_companion-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional, Type, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from result_companion.core.analizers.local.ollama_exceptions import (
|
|
10
|
+
OllamaNotInstalled,
|
|
11
|
+
OllamaServerNotRunning,
|
|
12
|
+
)
|
|
13
|
+
from result_companion.core.utils.logging_config import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OllamaServerManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages the lifecycle of an Ollama server.
|
|
19
|
+
|
|
20
|
+
Can be used as a context manager to ensure proper server initialization and cleanup:
|
|
21
|
+
|
|
22
|
+
with OllamaServerManager() as server:
|
|
23
|
+
# Code that requires the Ollama server to be running
|
|
24
|
+
...
|
|
25
|
+
# Server will be automatically cleaned up when exiting the with block
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
server_url: str = "http://localhost:11434",
|
|
31
|
+
start_timeout: int = 30,
|
|
32
|
+
wait_for_start: int = 1,
|
|
33
|
+
start_cmd: list = ["ollama", "serve"],
|
|
34
|
+
):
|
|
35
|
+
self.server_url = server_url
|
|
36
|
+
self.start_timeout = start_timeout
|
|
37
|
+
self._process: Optional[subprocess.Popen] = None
|
|
38
|
+
self.wait_for_start = wait_for_start
|
|
39
|
+
self.start_cmd = start_cmd
|
|
40
|
+
atexit.register(self.cleanup)
|
|
41
|
+
self._server_started_by_manager = False
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
"""
|
|
45
|
+
Context manager entry point. Ensures the server is running before proceeding.
|
|
46
|
+
"""
|
|
47
|
+
if not self.is_running(skip_logs=True):
|
|
48
|
+
self.start()
|
|
49
|
+
self._server_started_by_manager = True
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
53
|
+
"""
|
|
54
|
+
Context manager exit point. Cleans up the server if it was started by this manager.
|
|
55
|
+
"""
|
|
56
|
+
if self._server_started_by_manager:
|
|
57
|
+
self.cleanup()
|
|
58
|
+
return False # Propagate any exceptions
|
|
59
|
+
|
|
60
|
+
def is_running(self, skip_logs: bool = False) -> bool:
|
|
61
|
+
"""Checks if the Ollama server is running."""
|
|
62
|
+
if not skip_logs:
|
|
63
|
+
logger.debug(
|
|
64
|
+
f"Checking if Ollama server is running at {self.server_url}..."
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
response = requests.get(self.server_url, timeout=5)
|
|
68
|
+
return response.status_code == 200 and "Ollama is running" in response.text
|
|
69
|
+
except requests.exceptions.RequestException:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def _check_process_alive(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Check if the managed process is still alive and raise an exception if it died.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
OllamaServerNotRunning: If the process has terminated unexpectedly.
|
|
78
|
+
"""
|
|
79
|
+
if self._process is None:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if self._process.poll() is not None:
|
|
83
|
+
try:
|
|
84
|
+
_, stderr = self._process.communicate(timeout=1)
|
|
85
|
+
error_msg = (
|
|
86
|
+
stderr.decode().strip()
|
|
87
|
+
if stderr
|
|
88
|
+
else "Process terminated unexpectedly"
|
|
89
|
+
)
|
|
90
|
+
except subprocess.TimeoutExpired:
|
|
91
|
+
error_msg = "Process terminated unexpectedly"
|
|
92
|
+
except Exception as e:
|
|
93
|
+
error_msg = (
|
|
94
|
+
f"Process terminated unexpectedly (error reading output: {e})"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
raise OllamaServerNotRunning(f"Ollama server process died: {error_msg}")
|
|
98
|
+
|
|
99
|
+
def start(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Starts the Ollama server if it is not running.
|
|
102
|
+
Raises:
|
|
103
|
+
OllamaNotInstalled: If the 'ollama' command is not found.
|
|
104
|
+
OllamaServerNotRunning: If the server fails to start within the timeout.
|
|
105
|
+
"""
|
|
106
|
+
if self.is_running(skip_logs=True):
|
|
107
|
+
logger.debug("Ollama server is already running.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
logger.info("Ollama server is not running. Attempting to start it...")
|
|
111
|
+
try:
|
|
112
|
+
self._process = subprocess.Popen(
|
|
113
|
+
self.start_cmd,
|
|
114
|
+
stdout=subprocess.PIPE,
|
|
115
|
+
stderr=subprocess.PIPE,
|
|
116
|
+
preexec_fn=os.setsid if os.name != "nt" else None, # Unix only
|
|
117
|
+
)
|
|
118
|
+
except FileNotFoundError:
|
|
119
|
+
raise OllamaNotInstalled(
|
|
120
|
+
"Ollama command not found. Ensure it is installed and in your PATH."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
logger.info(f"Launched 'ollama serve' process with PID: {self._process.pid}")
|
|
124
|
+
|
|
125
|
+
# Check if process died immediately after launch
|
|
126
|
+
time.sleep(0.1) # Brief pause to let process initialize
|
|
127
|
+
self._check_process_alive()
|
|
128
|
+
|
|
129
|
+
start_time = time.time()
|
|
130
|
+
while time.time() - start_time < self.start_timeout:
|
|
131
|
+
if self.is_running(skip_logs=True):
|
|
132
|
+
logger.info("Ollama server started successfully.")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Check if process died during startup wait
|
|
136
|
+
self._check_process_alive()
|
|
137
|
+
|
|
138
|
+
time.sleep(self.wait_for_start)
|
|
139
|
+
|
|
140
|
+
# If the server did not start, clean up and raise an error.
|
|
141
|
+
self.cleanup()
|
|
142
|
+
raise OllamaServerNotRunning(
|
|
143
|
+
f"Failed to start Ollama server within {self.start_timeout}s timeout."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def cleanup(self) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Gracefully terminates the Ollama server, or kills it if necessary.
|
|
149
|
+
"""
|
|
150
|
+
if self._process is not None:
|
|
151
|
+
logger.debug(
|
|
152
|
+
f"Cleaning up Ollama server process with PID: {self._process.pid}"
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
self._process.terminate()
|
|
156
|
+
self._process.wait(timeout=5)
|
|
157
|
+
logger.debug("Ollama server terminated gracefully.")
|
|
158
|
+
except subprocess.TimeoutExpired:
|
|
159
|
+
self._process.kill()
|
|
160
|
+
logger.debug("Ollama server killed forcefully.")
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.warning(f"Error during Ollama server cleanup: {exc}")
|
|
163
|
+
self._process = None
|
|
164
|
+
self._server_started_by_manager = False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
T = TypeVar("T", bound="OllamaServerManager")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def resolve_server_manager(server_manager: Union[Optional[T], Type[T]], **kwargs) -> T:
|
|
171
|
+
"""
|
|
172
|
+
Resolve a server manager parameter that can be either a class or an instance.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
server_manager: Either an OllamaServerManager instance, a subclass of OllamaServerManager,
|
|
176
|
+
or None (in which case a default OllamaServerManager will be created).
|
|
177
|
+
**kwargs: Additional arguments to pass to the constructor if a new instance is created.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
An instance of OllamaServerManager or one of its subclasses.
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(server_manager, type):
|
|
183
|
+
return server_manager(**kwargs)
|
|
184
|
+
|
|
185
|
+
return server_manager or OllamaServerManager(**kwargs)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Callable, Tuple
|
|
2
|
+
|
|
3
|
+
from langchain_anthropic import ChatAnthropic
|
|
4
|
+
from langchain_aws import BedrockLLM
|
|
5
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
6
|
+
from langchain_ollama.llms import OllamaLLM
|
|
7
|
+
from langchain_openai import AzureChatOpenAI, ChatOpenAI
|
|
8
|
+
|
|
9
|
+
MODELS = Tuple[
|
|
10
|
+
OllamaLLM
|
|
11
|
+
| AzureChatOpenAI
|
|
12
|
+
| BedrockLLM
|
|
13
|
+
| ChatGoogleGenerativeAI
|
|
14
|
+
| ChatOpenAI
|
|
15
|
+
| ChatAnthropic,
|
|
16
|
+
Callable,
|
|
17
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
from langchain.prompts import PromptTemplate
|
|
5
|
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
|
6
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
7
|
+
from langchain_core.runnables import RunnableSerializable
|
|
8
|
+
|
|
9
|
+
from result_companion.core.analizers.models import MODELS
|
|
10
|
+
from result_companion.core.chunking.utils import Chunking
|
|
11
|
+
from result_companion.core.utils.logging_config import get_progress_logger
|
|
12
|
+
|
|
13
|
+
logger = get_progress_logger("Chunking")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_sumarization_chain(
|
|
17
|
+
prompt: PromptTemplate, model: MODELS
|
|
18
|
+
) -> RunnableSerializable:
|
|
19
|
+
return prompt | model | StrOutputParser()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def split_text_into_chunks_using_text_splitter(
|
|
23
|
+
text: str, chunk_size: int, overlap: int
|
|
24
|
+
) -> list:
|
|
25
|
+
splitter = RecursiveCharacterTextSplitter(
|
|
26
|
+
chunk_size=chunk_size,
|
|
27
|
+
chunk_overlap=overlap,
|
|
28
|
+
length_function=len,
|
|
29
|
+
is_separator_regex=False,
|
|
30
|
+
)
|
|
31
|
+
return splitter.split_text(text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def accumulate_llm_results_for_summarizaton_chain(
|
|
35
|
+
test_case: dict,
|
|
36
|
+
chunk_analysis_prompt: str,
|
|
37
|
+
final_synthesis_prompt: str,
|
|
38
|
+
chunking_strategy: Chunking,
|
|
39
|
+
llm: MODELS,
|
|
40
|
+
chunk_concurrency: int = 1,
|
|
41
|
+
) -> Tuple[str, str, list]:
|
|
42
|
+
chunks = split_text_into_chunks_using_text_splitter(
|
|
43
|
+
str(test_case), chunking_strategy.chunk_size, chunking_strategy.chunk_size // 10
|
|
44
|
+
)
|
|
45
|
+
return await summarize_test_case(
|
|
46
|
+
test_case,
|
|
47
|
+
chunks,
|
|
48
|
+
llm,
|
|
49
|
+
chunk_analysis_prompt,
|
|
50
|
+
final_synthesis_prompt,
|
|
51
|
+
chunk_concurrency,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def summarize_test_case(
|
|
56
|
+
test_case: dict,
|
|
57
|
+
chunks: list,
|
|
58
|
+
llm: MODELS,
|
|
59
|
+
chunk_analysis_prompt: str,
|
|
60
|
+
final_synthesis_prompt: str,
|
|
61
|
+
chunk_concurrency: int = 1,
|
|
62
|
+
) -> Tuple[str, str, list]:
|
|
63
|
+
"""Summarizes large test case by analyzing chunks and synthesizing results.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
test_case: Test case dictionary with name and data.
|
|
67
|
+
chunks: List of text chunks to analyze.
|
|
68
|
+
llm: Language model instance.
|
|
69
|
+
chunk_analysis_prompt: Template for analyzing chunks.
|
|
70
|
+
final_synthesis_prompt: Template for final synthesis.
|
|
71
|
+
chunk_concurrency: Chunks to process concurrently.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (final_analysis, test_name, chunks).
|
|
75
|
+
"""
|
|
76
|
+
logger.info(f"### For test case {test_case['name']}, {len(chunks)=}")
|
|
77
|
+
|
|
78
|
+
summarization_prompt = PromptTemplate(
|
|
79
|
+
input_variables=["text"],
|
|
80
|
+
template=chunk_analysis_prompt,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
summarization_chain = build_sumarization_chain(summarization_prompt, llm)
|
|
84
|
+
semaphore = asyncio.Semaphore(chunk_concurrency)
|
|
85
|
+
test_name = test_case["name"]
|
|
86
|
+
total_chunks = len(chunks)
|
|
87
|
+
|
|
88
|
+
async def process_with_limit(chunk: str, chunk_idx: int) -> str:
|
|
89
|
+
async with semaphore:
|
|
90
|
+
logger.debug(
|
|
91
|
+
f"[{test_name}] Processing chunk {chunk_idx + 1}/{total_chunks}, length {len(chunk)}"
|
|
92
|
+
)
|
|
93
|
+
return await summarization_chain.ainvoke({"text": chunk})
|
|
94
|
+
|
|
95
|
+
chunk_tasks = [process_with_limit(chunk, i) for i, chunk in enumerate(chunks)]
|
|
96
|
+
summaries = await asyncio.gather(*chunk_tasks)
|
|
97
|
+
|
|
98
|
+
aggregated_summary = "\n\n---\n\n".join(
|
|
99
|
+
[
|
|
100
|
+
f"### Chunk {i+1}/{total_chunks}\n{summary}"
|
|
101
|
+
for i, summary in enumerate(summaries)
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
final_prompt = PromptTemplate(
|
|
106
|
+
input_variables=["summary"],
|
|
107
|
+
template=final_synthesis_prompt,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
final_analysis_chain = build_sumarization_chain(final_prompt, llm)
|
|
111
|
+
final_result = await final_analysis_chain.ainvoke({"summary": aggregated_summary})
|
|
112
|
+
|
|
113
|
+
return final_result, test_case["name"], chunks
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import tiktoken
|
|
5
|
+
|
|
6
|
+
from result_companion.core.parsers.config import TokenizerModel
|
|
7
|
+
from result_companion.core.utils.logging_config import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Chunking:
|
|
12
|
+
chunk_size: int
|
|
13
|
+
number_of_chunks: int
|
|
14
|
+
raw_text_len: int
|
|
15
|
+
tokens_from_raw_text: int
|
|
16
|
+
tokenized_chunks: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def azure_openai_tokenizer(text: str) -> int:
|
|
20
|
+
"""Tokenizer for Azure OpenAI models using tiktoken."""
|
|
21
|
+
# TODO: check if not starting something on import
|
|
22
|
+
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
|
|
23
|
+
return len(encoding.encode(text))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ollama_tokenizer(text: str) -> int:
|
|
27
|
+
"""Placeholder tokenizer for Ollama (custom implementation required)."""
|
|
28
|
+
return len(text) // 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bedrock_tokenizer(text: str) -> int:
|
|
32
|
+
"""Placeholder tokenizer for Bedrock LLM (custom implementation required)."""
|
|
33
|
+
return len(text) // 5
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def google_tokenizer(text: str) -> int:
|
|
37
|
+
"""Tokenizer for Google Generative AI models.
|
|
38
|
+
|
|
39
|
+
Google's tokenization is approximately 4 characters per token on average,
|
|
40
|
+
but we use the tiktoken cl100k_base encoding which is close to Google's tokenization.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
# Use cl100k_base encoding which is similar to Google's tokenization
|
|
44
|
+
encoding = tiktoken.get_encoding("cl100k_base")
|
|
45
|
+
return len(encoding.encode(text))
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.warning(f"Failed to use cl100k_base encoding for Google tokenizer: {e}")
|
|
48
|
+
# Fallback to approximate tokenization (4 chars per token)
|
|
49
|
+
return len(text) // 4
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def anthropic_tokenizer(text: str) -> int:
|
|
53
|
+
"""Tokenizer for Anthropic Claude models.
|
|
54
|
+
|
|
55
|
+
Uses cl100k_base encoding as approximation for Claude tokenization.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
encoding = tiktoken.get_encoding("cl100k_base")
|
|
59
|
+
return len(encoding.encode(text))
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(
|
|
62
|
+
f"Failed to use cl100k_base encoding for Anthropic tokenizer: {e}"
|
|
63
|
+
)
|
|
64
|
+
return len(text) // 4
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
tokenizer_mappings = {
|
|
68
|
+
"azure_openai_tokenizer": azure_openai_tokenizer,
|
|
69
|
+
"ollama_tokenizer": ollama_tokenizer,
|
|
70
|
+
"bedrock_tokenizer": bedrock_tokenizer,
|
|
71
|
+
"google_tokenizer": google_tokenizer,
|
|
72
|
+
"openai_tokenizer": azure_openai_tokenizer, # Same tokenization as Azure
|
|
73
|
+
"anthropic_tokenizer": anthropic_tokenizer,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def calculate_overall_chunk_size(
|
|
78
|
+
raw_text: str, actual_tokens_from_text: int, max_tokens_acceptable: int
|
|
79
|
+
) -> Chunking:
|
|
80
|
+
raw_text_len = len(raw_text)
|
|
81
|
+
N_tokenized_chunks = math.ceil(actual_tokens_from_text / max_tokens_acceptable)
|
|
82
|
+
if max_tokens_acceptable > actual_tokens_from_text:
|
|
83
|
+
return Chunking(
|
|
84
|
+
chunk_size=0,
|
|
85
|
+
number_of_chunks=0,
|
|
86
|
+
raw_text_len=raw_text_len,
|
|
87
|
+
tokens_from_raw_text=actual_tokens_from_text,
|
|
88
|
+
tokenized_chunks=N_tokenized_chunks,
|
|
89
|
+
)
|
|
90
|
+
chunk_size = raw_text_len / N_tokenized_chunks
|
|
91
|
+
logger.info(
|
|
92
|
+
f"Chunk size: {chunk_size}, Number of chunks: {N_tokenized_chunks}, Raw text length: {raw_text_len}"
|
|
93
|
+
)
|
|
94
|
+
return Chunking(
|
|
95
|
+
chunk_size=chunk_size,
|
|
96
|
+
number_of_chunks=N_tokenized_chunks,
|
|
97
|
+
raw_text_len=raw_text_len,
|
|
98
|
+
tokens_from_raw_text=actual_tokens_from_text,
|
|
99
|
+
tokenized_chunks=N_tokenized_chunks,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def calculate_chunk_size(
|
|
104
|
+
test_case: dict, system_prompt: str, tokenizer_from_config: TokenizerModel
|
|
105
|
+
) -> Chunking:
|
|
106
|
+
LLM_fed_text = str(test_case) + system_prompt
|
|
107
|
+
tokenizer = tokenizer_mappings[tokenizer_from_config.tokenizer]
|
|
108
|
+
max_content_tokens = tokenizer_from_config.max_content_tokens
|
|
109
|
+
text_to_tokens = tokenizer(LLM_fed_text)
|
|
110
|
+
return calculate_overall_chunk_size(
|
|
111
|
+
actual_tokens_from_text=text_to_tokens,
|
|
112
|
+
max_tokens_acceptable=max_content_tokens,
|
|
113
|
+
raw_text=LLM_fed_text,
|
|
114
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
version: 1.0
|
|
2
|
+
|
|
3
|
+
test_filter:
|
|
4
|
+
include_tags: []
|
|
5
|
+
exclude_tags: []
|
|
6
|
+
include_passing: false
|
|
7
|
+
|
|
8
|
+
llm_config:
|
|
9
|
+
question_prompt: |
|
|
10
|
+
You analyze Robot Framework test failures in JSON format.
|
|
11
|
+
Structure: test → keywords (recursive) → name, args, messages, status.
|
|
12
|
+
|
|
13
|
+
ANALYSIS PRIORITY:
|
|
14
|
+
1. Find keywords with status="FAIL" - start there
|
|
15
|
+
2. Check preceding PASS keywords for setup issues
|
|
16
|
+
3. Look for: tracebacks, exceptions, assertion errors, timeouts
|
|
17
|
+
4. Cascading failures: find the FIRST failure, not symptoms
|
|
18
|
+
|
|
19
|
+
RESPOND EXACTLY IN THIS FORMAT (be terse):
|
|
20
|
+
|
|
21
|
+
**Flow**
|
|
22
|
+
- [Only keywords leading to failure. Max 5 bullets]
|
|
23
|
+
|
|
24
|
+
**Root Cause**
|
|
25
|
+
[Keyword name in quotes. Error type. Why it failed. Max 2-3 sentences]
|
|
26
|
+
Confidence: HIGH/MEDIUM/LOW
|
|
27
|
+
|
|
28
|
+
**Fix**
|
|
29
|
+
- [Actionable fix for test code or environment. Max 1-2 bullets]
|
|
30
|
+
|
|
31
|
+
NO PREAMBLE. NO REASONING. DIRECT ANSWER ONLY.
|
|
32
|
+
|
|
33
|
+
chunking:
|
|
34
|
+
chunk_analysis_prompt: |
|
|
35
|
+
EXTRACT failure-relevant info from this Robot Framework test chunk.
|
|
36
|
+
|
|
37
|
+
OUTPUT FORMAT (use exactly):
|
|
38
|
+
ERRORS: [any exceptions, tracebacks, assertion failures - quote exact text]
|
|
39
|
+
FAIL_KEYWORDS: [keyword names with status=FAIL, in order]
|
|
40
|
+
SUSPECT_KEYWORDS: [PASS keywords with warnings/errors in messages]
|
|
41
|
+
NOTES: [one line only - anything else relevant, or "none"]
|
|
42
|
+
|
|
43
|
+
If chunk has no errors/failures: respond only "CLEAN: no issues"
|
|
44
|
+
|
|
45
|
+
{text}
|
|
46
|
+
|
|
47
|
+
final_synthesis_prompt: |
|
|
48
|
+
NO PREAMBLE. NO REASONING. DIRECT OUTPUT ONLY.
|
|
49
|
+
|
|
50
|
+
Synthesize chunk summaries into final analysis.
|
|
51
|
+
Ignore CLEAN chunks. Deduplicate repeated errors.
|
|
52
|
+
Find the FIRST failure in execution order - that's the root cause.
|
|
53
|
+
|
|
54
|
+
**Flow**
|
|
55
|
+
- [Only failure-path keywords from all chunks. Max 5 bullets]
|
|
56
|
+
|
|
57
|
+
**Root Cause**
|
|
58
|
+
[First failing keyword in quotes. Error type. Why. Max 2-3 sentences]
|
|
59
|
+
Confidence: HIGH/MEDIUM/LOW
|
|
60
|
+
|
|
61
|
+
**Fix**
|
|
62
|
+
- [Actionable fix. Max 1-2 bullets]
|
|
63
|
+
|
|
64
|
+
{summary}
|
|
65
|
+
|
|
66
|
+
prompt_template: |
|
|
67
|
+
Question: {question}
|
|
68
|
+
|
|
69
|
+
Answer the question based on the following context: {context}
|
|
70
|
+
|
|
71
|
+
llm_factory:
|
|
72
|
+
model_type: "OllamaLLM" # AzureChatOpenAI OllamaLLM BedrockLLM
|
|
73
|
+
parameters:
|
|
74
|
+
model: "deepseek-r1:1.5b"
|
|
75
|
+
strategy:
|
|
76
|
+
parameters:
|
|
77
|
+
model_name: "deepseek-r1"
|
|
78
|
+
|
|
79
|
+
tokenizer:
|
|
80
|
+
tokenizer: ollama_tokenizer
|
|
81
|
+
max_content_tokens: 140000
|
|
82
|
+
|
|
83
|
+
concurrency:
|
|
84
|
+
test_case: 1
|
|
85
|
+
chunk: 1
|
|
File without changes
|