Skip to content

Commit a5700f2

Browse files
authored
Refresh Tests (#352)
* Refresh Testing Framework
1 parent d4b862d commit a5700f2

File tree

117 files changed

+19433
-8378
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+19433
-8378
lines changed

.github/workflows/image_smoke.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
# Upload security results to GitHub Security tab
9393
- name: Upload Trivy Results to GitHub Security
9494
if: matrix.build.name == 'aio'
95-
uses: github/codeql-action/upload-sarif@v3
95+
uses: github/codeql-action/upload-sarif@v4
9696
with:
9797
sarif_file: trivy-results-aio.sarif
9898
category: trivy-aio

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ ignore=CVS,.venv
5252
# ignore-list. The regex matches against paths and can be in Posix or Windows
5353
# format. Because '\\' represents the directory delimiter on Windows systems,
5454
# it can't be used as an escape character.
55-
ignore-paths=.*[/\\]wip[/\\].*,src/client/mcp,docs/themes/relearn,docs/public,docs/static/demoware,src/server/agents/chatbot.py
55+
ignore-paths=.*[/\\]wip[/\\].*,src/client/mcp,docs/themes/relearn,docs/public,docs/static/demoware,src/server/agents
5656

5757
# Files or directories matching the regular expression patterns are skipped.
5858
# The regex matches against base names, not paths. The default value ignores

pytest.ini

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@
44
; spell-checker: disable
55

66
[pytest]
7-
pythonpath = src
7+
pythonpath = src tests
8+
addopts = --disable-warnings --import-mode=importlib
89
filterwarnings =
910
ignore::DeprecationWarning
10-
asyncio_default_fixture_loop_scope = function
11+
asyncio_default_fixture_loop_scope = function
12+
13+
; Test markers for selective test execution
14+
; Usage examples:
15+
; pytest -m "unit" # Run only unit tests
16+
; pytest -m "integration" # Run only integration tests
17+
; pytest -m "not slow" # Skip slow tests
18+
; pytest -m "not db" # Skip tests requiring database
19+
; pytest -m "unit and not slow" # Fast unit tests only
20+
markers =
21+
unit: Unit tests (mocked dependencies, fast execution)
22+
integration: Integration tests (real components, may require external services)
23+
slow: Slow tests (deselect with '-m "not slow"')
24+
db: Tests requiring Oracle database container (deselect with '-m "not db"')
25+
db_container: Alias for db marker - tests requiring database container

src/client/content/chatbot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from streamlit import session_state as state
1717

1818
from client.content.config.tabs.models import get_models
19-
from client.utils import st_common, api_call, client, vs_options
19+
from client.utils import st_common, api_call, client, vs_options, tool_options
2020
from client.utils.st_footer import render_chat_footer
2121
from common import logging_config
2222

@@ -82,7 +82,7 @@ def setup_sidebar():
8282
st.stop()
8383

8484
state.enable_client = True
85-
st_common.tools_sidebar()
85+
tool_options.tools_sidebar()
8686
st_common.history_sidebar()
8787
st_common.ll_sidebar()
8888
vs_options.vector_search_sidebar()

src/client/content/testbed.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from client.content.config.tabs.models import get_models
1919

20-
from client.utils import st_common, api_call, vs_options
20+
from client.utils import st_common, api_call, vs_options, tool_options
2121

2222
from common import logging_config
2323

@@ -496,7 +496,7 @@ def render_evaluation_ui(available_ll_models: list) -> None:
496496

497497
st.subheader("Q&A Evaluation", divider="red")
498498
st.info("Use the sidebar settings for chatbot evaluation parameters", icon="⬅️")
499-
st_common.tools_sidebar()
499+
tool_options.tools_sidebar()
500500
st_common.ll_sidebar()
501501
vs_options.vector_search_sidebar()
502502
st.write("Choose a model to judge the correctness of the chatbot answer, then start evaluation.")

src/client/utils/api_call.py

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,6 @@ def sanitize_sensitive_data(data):
4242
return data
4343

4444

45-
def _handle_json_response(response, method: str):
46-
"""Parse JSON response and handle parsing errors."""
47-
try:
48-
data = response.json()
49-
logger.debug("%s Data: %s", method, data)
50-
return response
51-
except (json.JSONDecodeError, ValueError) as json_ex:
52-
error_msg = f"Server returned invalid JSON response. Status: {response.status_code}"
53-
logger.error("Response text: %s", response.text[:500])
54-
error_msg += f". Response preview: {response.text[:200]}"
55-
raise ApiError(error_msg) from json_ex
56-
57-
5845
def _handle_http_error(ex: requests.exceptions.HTTPError):
5946
"""Extract error message from HTTP error response."""
6047
try:
@@ -66,6 +53,12 @@ def _handle_http_error(ex: requests.exceptions.HTTPError):
6653
return failure
6754

6855

56+
def _error_response(message: str) -> None:
57+
"""Display error to user and raise ApiError."""
58+
st.error(f"API Error: {message}")
59+
raise ApiError(message)
60+
61+
6962
def send_request(
7063
method: str,
7164
endpoint: str,
@@ -75,68 +68,65 @@ def send_request(
7568
retries: int = 3,
7669
backoff_factor: float = 2.0,
7770
) -> dict:
78-
"""Send API requests with retry logic."""
71+
"""Send API requests with retry logic. Returns JSON response or error dict."""
72+
method_map = {"GET": requests.get, "POST": requests.post, "PATCH": requests.patch, "DELETE": requests.delete}
73+
if method not in method_map:
74+
return _error_response(f"Unsupported HTTP method: {method}")
75+
7976
url = urljoin(f"{state.server['url']}:{state.server['port']}/", endpoint)
8077
payload = payload or {}
81-
token = state.server["key"]
82-
headers = {"Authorization": f"Bearer {token}"}
83-
# Send client in header if it exists
78+
headers = {"Authorization": f"Bearer {state.server['key']}"}
8479
if getattr(state, "client_settings", {}).get("client"):
8580
headers["Client"] = state.client_settings["client"]
8681

87-
method_map = {"GET": requests.get, "POST": requests.post, "PATCH": requests.patch, "DELETE": requests.delete}
88-
89-
if method not in method_map:
90-
raise ApiError(f"Unsupported HTTP method: {method}")
91-
92-
args = {
82+
args = {k: v for k, v in {
9383
"url": url,
9484
"headers": headers,
9585
"timeout": timeout,
9686
"params": params,
9787
"files": payload.get("files") if method == "POST" else None,
9888
"json": payload.get("json") if method in ["POST", "PATCH"] else None,
99-
}
100-
args = {k: v for k, v in args.items() if v is not None}
101-
# Avoid logging out binary data in files
89+
}.items() if v is not None}
90+
10291
log_args = sanitize_sensitive_data(args.copy())
10392
try:
10493
if log_args.get("files"):
10594
log_args["files"] = [(field_name, (f[0], "<binary_data>", f[2])) for field_name, f in log_args["files"]]
10695
except (ValueError, IndexError):
10796
pass
10897
logger.info("%s Request: %s", method, log_args)
98+
99+
result = None
109100
for attempt in range(retries + 1):
110101
try:
111102
response = method_map[method](**args)
112103
logger.info("%s Response: %s", method, response)
113104
response.raise_for_status()
114-
return _handle_json_response(response, method)
105+
result = response.json()
106+
logger.debug("%s Data: %s", method, result)
107+
break
115108

116109
except requests.exceptions.HTTPError as ex:
117110
logger.error("HTTP Error: %s", ex)
118-
raise ApiError(_handle_http_error(ex)) from ex
111+
_error_response(_handle_http_error(ex))
119112

120113
except requests.exceptions.ConnectionError as ex:
121114
logger.error("Attempt %d: Connection Error: %s", attempt + 1, ex)
122115
if attempt < retries:
123-
sleep_time = backoff_factor * (2**attempt)
124-
logger.info("Retrying in %.1f seconds...", sleep_time)
125-
time.sleep(sleep_time)
116+
time.sleep(backoff_factor * (2**attempt))
126117
continue
127-
raise ApiError(f"Connection failed after {retries + 1} attempts: {str(ex)}") from ex
118+
_error_response(f"Connection failed after {retries + 1} attempts")
128119

129-
except requests.exceptions.RequestException as ex:
130-
logger.error("Request Error: %s", ex)
131-
raise ApiError(f"Request failed: {str(ex)}") from ex
120+
except (requests.exceptions.RequestException, json.JSONDecodeError, ValueError) as ex:
121+
logger.error("Request/JSON Error: %s", ex)
122+
_error_response(f"Request failed: {str(ex)}")
132123

133-
raise ApiError("An unexpected error occurred.")
124+
return result if result is not None else _error_response("An unexpected error occurred.")
134125

135126

136-
def get(endpoint: str, params: Optional[dict] = None, retries: int = 3, backoff_factor: float = 2.0) -> json:
127+
def get(endpoint: str, params: Optional[dict] = None, retries: int = 3, backoff_factor: float = 2.0) -> dict:
137128
"""GET Requests"""
138-
response = send_request("GET", endpoint, params=params, retries=retries, backoff_factor=backoff_factor)
139-
return response.json()
129+
return send_request("GET", endpoint, params=params, retries=retries, backoff_factor=backoff_factor)
140130

141131

142132
def post(
@@ -146,9 +136,9 @@ def post(
146136
timeout: int = 60,
147137
retries: int = 5,
148138
backoff_factor: float = 1.5,
149-
) -> json:
139+
) -> dict:
150140
"""POST Requests"""
151-
response = send_request(
141+
return send_request(
152142
"POST",
153143
endpoint,
154144
params=params,
@@ -157,7 +147,6 @@ def post(
157147
retries=retries,
158148
backoff_factor=backoff_factor,
159149
)
160-
return response.json()
161150

162151

163152
def patch(
@@ -168,9 +157,9 @@ def patch(
168157
retries: int = 5,
169158
backoff_factor: float = 1.5,
170159
toast=True,
171-
) -> None:
160+
) -> dict:
172161
"""PATCH Requests"""
173-
response = send_request(
162+
result = send_request(
174163
"PATCH",
175164
endpoint,
176165
params=params,
@@ -182,13 +171,13 @@ def patch(
182171
if toast:
183172
st.toast("Update Successful.", icon="✅")
184173
time.sleep(1)
185-
return response.json()
174+
return result
186175

187176

188-
def delete(endpoint: str, timeout: int = 60, retries: int = 5, backoff_factor: float = 1.5, toast=True) -> None:
177+
def delete(endpoint: str, timeout: int = 60, retries: int = 5, backoff_factor: float = 1.5, toast=True) -> dict:
189178
"""DELETE Requests"""
190-
response = send_request("DELETE", endpoint, timeout=timeout, retries=retries, backoff_factor=backoff_factor)
191-
success = response.json()["message"]
179+
result = send_request("DELETE", endpoint, timeout=timeout, retries=retries, backoff_factor=backoff_factor)
192180
if toast:
193-
st.toast(success, icon="✅")
181+
st.toast(result.get("message", "Deleted."), icon="✅")
194182
time.sleep(1)
183+
return result

src/client/utils/st_common.py

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -232,61 +232,3 @@ def ll_sidebar() -> None:
232232
key="selected_ll_model_presence_penalty",
233233
on_change=update_client_settings("ll_model"),
234234
)
235-
236-
237-
#####################################################
238-
# Tools Options
239-
#####################################################
240-
def tools_sidebar() -> None:
241-
"""Tools Sidebar Settings"""
242-
243-
# Setup Tool Box
244-
state.tool_box = {
245-
"LLM Only": {"description": "Do not use tools", "enabled": True},
246-
"Vector Search": {"description": "Use AI with Unstructured Data", "enabled": True},
247-
"NL2SQL": {"description": "Use AI with Structured Data", "enabled": True},
248-
}
249-
250-
def _update_set_tool():
251-
"""Update user settings as to which tool is being used"""
252-
state.client_settings["tools_enabled"] = [state.selected_tool]
253-
254-
def _disable_tool(tool: str, reason: str = None) -> None:
255-
"""Disable a tool in the tool box"""
256-
if reason:
257-
logger.debug("%s Disabled (%s)", tool, reason)
258-
st.warning(f"{reason}. Disabling {tool}.", icon="⚠️")
259-
state.tool_box[tool]["enabled"] = False
260-
261-
if not is_db_configured():
262-
logger.debug("Vector Search/NL2SQL Disabled (Database not configured)")
263-
st.warning("Database is not configured. Disabling Vector Search and NL2SQL tools.", icon="⚠️")
264-
_disable_tool("Vector Search")
265-
_disable_tool("NL2SQL")
266-
else:
267-
# Check to enable Vector Store
268-
embed_models_enabled = enabled_models_lookup("embed")
269-
db_alias = state.client_settings.get("database", {}).get("alias")
270-
database_lookup = state_configs_lookup("database_configs", "name")
271-
if not embed_models_enabled:
272-
_disable_tool("Vector Search", "No embedding models are configured and/or enabled.")
273-
elif not database_lookup[db_alias].get("vector_stores"):
274-
_disable_tool("Vector Search", "Database has no vector stores.")
275-
else:
276-
# Check if any vector stores use an enabled embedding model
277-
vector_stores = database_lookup[db_alias].get("vector_stores", [])
278-
usable_vector_stores = [vs for vs in vector_stores if vs.get("model") in embed_models_enabled]
279-
if not usable_vector_stores:
280-
_disable_tool("Vector Search", "No vector stores match the enabled embedding models")
281-
282-
tool_box = [key for key, val in state.tool_box.items() if val["enabled"]]
283-
current_tool = state.client_settings["tools_enabled"][0]
284-
tool_index = tool_box.index(current_tool) if current_tool in tool_box else 0
285-
st.sidebar.selectbox(
286-
"Tool Selection",
287-
tool_box,
288-
index=tool_index,
289-
label_visibility="collapsed",
290-
on_change=_update_set_tool,
291-
key="selected_tool",
292-
)

src/client/utils/tool_options.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Copyright (c) 2024, 2025, Oracle and/or its affiliates.
3+
Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
4+
"""
5+
# spell-checker:ignore selectbox
6+
7+
import streamlit as st
8+
from streamlit import session_state as state
9+
10+
from client.utils import st_common
11+
from common import logging_config
12+
13+
logger = logging_config.logging.getLogger("client.utils.st_common")
14+
15+
16+
def tools_sidebar() -> None:
17+
"""Tools Sidebar Settings"""
18+
19+
# Setup Tool Box
20+
state.tool_box = {
21+
"LLM Only": {"description": "Do not use tools", "enabled": True},
22+
"Vector Search": {"description": "Use AI with Unstructured Data", "enabled": True},
23+
"NL2SQL": {"description": "Use AI with Structured Data", "enabled": True},
24+
}
25+
26+
def _update_set_tool():
27+
"""Update user settings as to which tool is being used"""
28+
state.client_settings["tools_enabled"] = [state.selected_tool]
29+
30+
def _disable_tool(tool: str, reason: str = None) -> None:
31+
"""Disable a tool in the tool box"""
32+
if reason:
33+
logger.debug("%s Disabled (%s)", tool, reason)
34+
st.warning(f"{reason}. Disabling {tool}.", icon="⚠️")
35+
state.tool_box[tool]["enabled"] = False
36+
37+
if not st_common.is_db_configured():
38+
logger.debug("Vector Search/NL2SQL Disabled (Database not configured)")
39+
st.warning("Database is not configured. Disabling Vector Search and NL2SQL tools.", icon="⚠️")
40+
_disable_tool("Vector Search")
41+
_disable_tool("NL2SQL")
42+
else:
43+
# Check to enable Vector Store
44+
embed_models_enabled = st_common.enabled_models_lookup("embed")
45+
db_alias = state.client_settings.get("database", {}).get("alias")
46+
database_lookup = st_common.state_configs_lookup("database_configs", "name")
47+
if not embed_models_enabled:
48+
_disable_tool("Vector Search", "No embedding models are configured and/or enabled.")
49+
elif not database_lookup[db_alias].get("vector_stores"):
50+
_disable_tool("Vector Search", "Database has no vector stores.")
51+
else:
52+
# Check if any vector stores use an enabled embedding model
53+
vector_stores = database_lookup[db_alias].get("vector_stores", [])
54+
usable_vector_stores = [vs for vs in vector_stores if vs.get("model") in embed_models_enabled]
55+
if not usable_vector_stores:
56+
_disable_tool("Vector Search", "No vector stores match the enabled embedding models")
57+
58+
tool_box = [key for key, val in state.tool_box.items() if val["enabled"]]
59+
current_tool = state.client_settings["tools_enabled"][0]
60+
if current_tool not in tool_box:
61+
state.client_settings["tools_enabled"] = ["LLM Only"]
62+
tool_index = tool_box.index(current_tool) if current_tool in tool_box else 0
63+
st.sidebar.selectbox(
64+
"Tool Selection",
65+
tool_box,
66+
index=tool_index,
67+
label_visibility="collapsed",
68+
on_change=_update_set_tool,
69+
key="selected_tool",
70+
)

0 commit comments

Comments
 (0)