Skip to content

Commit 5300544

Browse files
committed
feature flag and request runner's only when step and execution_context present
1 parent b5bcd87 commit 5300544

5 files changed

Lines changed: 108 additions & 28 deletions

File tree

‎evaluators/contrib/galileo/src/agent_control_evaluator_galileo/llm/client.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -522,11 +522,12 @@ async def invoke(
522522
scorer_label: Optional display/metadata label.
523523
input: Optional user/system prompt text.
524524
output: Optional model response text.
525-
step: Optional complete runtime step used for structured dual-write.
525+
step: Required runtime step; invocation is rejected without it.
526526
selected_data: Raw selector-selected value used to construct the record,
527527
preserving its structure independently from serialized legacy inputs.
528528
selected_data_payload_field: Record input/output side for scalar selector values.
529-
execution_context: Optional authenticated Galileo scorer context.
529+
execution_context: Required authenticated Galileo scorer context; Orbit uses
530+
it to fetch LLM credentials.
530531
config: Optional scorer invocation configuration.
531532
timeout: Request timeout in seconds.
532533
headers: Additional request headers.
@@ -535,12 +536,16 @@ async def invoke(
535536
Parsed scorer invocation response.
536537
537538
Raises:
538-
ValueError: If neither input nor output is provided, or the timeout
539-
ordering is invalid.
539+
ValueError: If step or execution_context are absent, neither input nor
540+
output is provided, or the timeout ordering is invalid.
540541
RuntimeError: If the API response is not a JSON object.
541542
httpx.HTTPStatusError: If the invoke endpoint returns an error status code.
542543
httpx.RequestError: If the request fails before a response is received.
543544
"""
545+
if step is None:
546+
raise ValueError("LLM scorer invocation requires a runtime step.")
547+
if execution_context is None:
548+
raise ValueError("LLM scorer invocation requires an authenticated execution context.")
544549
if not (_has_value(input) or _has_value(output)):
545550
raise ValueError("At least one of input or output must be provided.")
546551

@@ -557,7 +562,7 @@ async def invoke(
557562
selected_data=selected_data,
558563
payload_field=selected_data_payload_field,
559564
)
560-
if step is not None and scorer_version_id is not None
565+
if scorer_version_id is not None
561566
else None
562567
)
563568
request_body = ScorerInvokeRequest(
@@ -567,8 +572,8 @@ async def invoke(
567572
inputs=ScorerInvokeInputs(
568573
query="" if input is None else input,
569574
response="" if output is None else output,
570-
ground_truth=step.ground_truth if step is not None else None,
571-
tools=step.tools if step is not None else None,
575+
ground_truth=step.ground_truth,
576+
tools=step.tools,
572577
),
573578
record=record,
574579
execution_context=execution_context,

‎evaluators/contrib/galileo/src/agent_control_evaluator_galileo/llm/config.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import os
56
from typing import Literal
67

78
from agent_control_evaluators import EvaluatorConfig
@@ -12,6 +13,17 @@
1213
LlmPayloadField = Literal["input", "output"]
1314

1415
_NUMERIC_OPERATORS = frozenset({"gt", "gte", "lt", "lte"})
16+
LLM_INVOKE_RUNTIME_FLAG_ENV = "GALILEO_FEATURE_FLAG_LLM_INVOKE_RUNTIME"
17+
18+
19+
def llm_invoke_runtime_enabled() -> bool:
20+
"""Return whether the LLM scorer invoke runtime is enabled.
21+
22+
The LLM evaluator requires Orbit support for ``execution_context`` to fetch
23+
LLM credentials. Set ``GALILEO_FEATURE_FLAG_LLM_INVOKE_RUNTIME=enabled`` once
24+
the Orbit-side support is confirmed ready.
25+
"""
26+
return os.getenv(LLM_INVOKE_RUNTIME_FLAG_ENV) == "enabled"
1527

1628

1729
class ScorerInvokeConfig(BaseModel):

‎evaluators/contrib/galileo/src/agent_control_evaluator_galileo/llm/evaluator.py‎

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from agent_control_models import EvaluatorResult, JSONObject, JSONValue, Step
1414

1515
from .client import GalileoExecutionContext, GalileoLLMClient, ScorerInvokeResponse
16-
from .config import LlmEvaluatorConfig, coerce_number
16+
from .config import LlmEvaluatorConfig, coerce_number, llm_invoke_runtime_enabled
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -117,8 +117,13 @@ class LlmEvaluator(Evaluator[LlmEvaluatorConfig]):
117117

118118
@classmethod
119119
def is_available(cls) -> bool:
120-
"""Check whether required runtime dependencies are available."""
121-
return LLM_AVAILABLE
120+
"""Return True only when the LLM invoke runtime feature flag is enabled.
121+
122+
The LLM evaluator requires Orbit-side support for ``execution_context`` to
123+
fetch LLM credentials. It is unavailable until
124+
``GALILEO_FEATURE_FLAG_LLM_INVOKE_RUNTIME=enabled`` is set.
125+
"""
126+
return LLM_AVAILABLE and llm_invoke_runtime_enabled()
122127

123128
def __init__(self, config: LlmEvaluatorConfig) -> None:
124129
"""Initialize the direct LLM-as-judge evaluator.
@@ -267,6 +272,28 @@ async def _evaluate(
267272
extensions: JSONObject | None = None,
268273
) -> EvaluatorResult:
269274
"""Run an LLM scorer evaluation with optional structured runtime context."""
275+
if step is None:
276+
return EvaluatorResult(
277+
matched=False,
278+
confidence=0.0,
279+
message="LLM scorer requires a runtime step; evaluation skipped.",
280+
metadata=self._base_metadata(),
281+
error="LLM scorer requires a runtime step; evaluation skipped.",
282+
)
283+
284+
try:
285+
execution_context = self._execution_context_from_extensions(extensions)
286+
except ValueError as exc:
287+
return self._handle_error(exc)
288+
if execution_context is None:
289+
return EvaluatorResult(
290+
matched=False,
291+
confidence=0.0,
292+
message="LLM scorer requires authenticated execution context; evaluation skipped.",
293+
metadata=self._base_metadata(),
294+
error="LLM scorer requires authenticated execution context; evaluation skipped.",
295+
)
296+
270297
input_text, output_text = self._prepare_payload(data)
271298
if not (_has_text(input_text) or _has_text(output_text)):
272299
return EvaluatorResult(
@@ -277,14 +304,11 @@ async def _evaluate(
277304
)
278305

279306
try:
280-
execution_context = self._execution_context_from_extensions(extensions)
281307
scorer_kwargs = self._scorer_kwargs()
282-
if step is not None:
283-
scorer_kwargs["step"] = step
284-
scorer_kwargs["selected_data"] = data
285-
scorer_kwargs["selected_data_payload_field"] = self.config.payload_field
286-
if execution_context is not None:
287-
scorer_kwargs["execution_context"] = execution_context
308+
scorer_kwargs["step"] = step
309+
scorer_kwargs["selected_data"] = data
310+
scorer_kwargs["selected_data_payload_field"] = self.config.payload_field
311+
scorer_kwargs["execution_context"] = execution_context
288312
response = await self._get_client().invoke(
289313
**scorer_kwargs,
290314
input=input_text if _has_text(input_text) else None,

‎evaluators/contrib/galileo/tests/test_llm_coverage_gaps.py‎

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,29 @@
1717
LLM_ENV = {
1818
"GALILEO_API_SECRET_KEY": "test-secret",
1919
"GALILEO_LUNA_INVOKE_URL": "http://luna-invoke:8090",
20+
"GALILEO_FEATURE_FLAG_LLM_INVOKE_RUNTIME": "enabled",
2021
}
2122

23+
_EXTENSIONS = {
24+
"namespace_key": "org-1",
25+
"caller_id": "user-1",
26+
"target_type": "log_stream",
27+
"target_id": "run-1",
28+
"metadata": {},
29+
}
30+
31+
32+
def _make_invoke_kwargs() -> dict[str, object]:
33+
"""Minimal required kwargs for GalileoLLMClient.invoke()."""
34+
from agent_control_models import Step
35+
36+
from agent_control_evaluator_galileo.llm.client import GalileoExecutionContext
37+
38+
return {
39+
"step": Step(type="llm", name="test", input="prompt"),
40+
"execution_context": GalileoExecutionContext(organization_id="org-1"),
41+
}
42+
2243

2344
def _decode_jwt_payload(token: str) -> dict[str, object]:
2445
payload_segment = token.split(".")[1]
@@ -289,12 +310,15 @@ async def test_evaluator_handles_non_success_status(monkeypatch):
289310
"""A non-success status from the scorer must surface as an error result."""
290311
for key, value in LLM_ENV.items():
291312
monkeypatch.setenv(key, value)
313+
from agent_control_models import Step
314+
292315
from agent_control_evaluator_galileo.llm import LlmEvaluator, ScorerInvokeResponse
293316
from agent_control_evaluator_galileo.llm.client import GalileoLLMClient
294317

295318
evaluator = LlmEvaluator.from_dict(
296319
{"scorer_id": "scorer-123", "threshold": 0.5, "operator": "gte"}
297320
)
321+
step = Step(type="llm", name="test", input="prompt")
298322

299323
with patch.object(GalileoLLMClient, "invoke", new_callable=AsyncMock) as mock_invoke:
300324
mock_invoke.return_value = ScorerInvokeResponse(
@@ -304,7 +328,7 @@ async def test_evaluator_handles_non_success_status(monkeypatch):
304328
error_message="upstream timeout",
305329
)
306330

307-
result = await evaluator.evaluate("hello")
331+
result = await evaluator.evaluate_with_extensions("hello", step, _EXTENSIONS)
308332

309333
assert result.matched is False
310334
assert result.error is not None
@@ -316,13 +340,16 @@ async def test_evaluator_skips_empty_data(monkeypatch):
316340
"""Evaluator skips invocation when there is no text to score."""
317341
for key, value in LLM_ENV.items():
318342
monkeypatch.setenv(key, value)
343+
from agent_control_models import Step
344+
319345
from agent_control_evaluator_galileo.llm import LlmEvaluator
320346
from agent_control_evaluator_galileo.llm.client import GalileoLLMClient
321347

322348
evaluator = LlmEvaluator.from_dict({"scorer_id": "scorer-123", "threshold": 0.5})
349+
step = Step(type="llm", name="test", input="prompt")
323350

324351
with patch.object(GalileoLLMClient, "invoke", new_callable=AsyncMock) as mock_invoke:
325-
result = await evaluator.evaluate(None)
352+
result = await evaluator.evaluate_with_extensions(None, step, _EXTENSIONS)
326353

327354
assert result.matched is False
328355
assert result.error is None
@@ -335,10 +362,13 @@ async def test_evaluator_returns_error_result_on_http_error(monkeypatch):
335362
"""HTTP errors must surface as a non-matched EvaluatorResult with metadata."""
336363
for key, value in LLM_ENV.items():
337364
monkeypatch.setenv(key, value)
365+
from agent_control_models import Step
366+
338367
from agent_control_evaluator_galileo.llm import LlmEvaluator
339368
from agent_control_evaluator_galileo.llm.client import GalileoLLMClient
340369

341370
evaluator = LlmEvaluator.from_dict({"scorer_id": "scorer-123", "threshold": 0.5})
371+
step = Step(type="llm", name="test", input="prompt")
342372

343373
fake_request = httpx.Request("POST", "http://luna-invoke:8090/api/v1/scorers/invoke")
344374
fake_response = httpx.Response(500, text="internal server error", request=fake_request)
@@ -349,7 +379,7 @@ async def test_evaluator_returns_error_result_on_http_error(monkeypatch):
349379
new_callable=AsyncMock,
350380
side_effect=httpx.HTTPStatusError("500", request=fake_request, response=fake_response),
351381
):
352-
result = await evaluator.evaluate("hello")
382+
result = await evaluator.evaluate_with_extensions("hello", step, _EXTENSIONS)
353383

354384
assert result.matched is False
355385
assert result.error is not None
@@ -755,7 +785,7 @@ async def test_invoke_raises_when_response_is_not_a_json_object(monkeypatch):
755785

756786
try:
757787
with pytest.raises(RuntimeError, match="not a JSON object"):
758-
await client.invoke(scorer_id="scorer-123", input="hello")
788+
await client.invoke(scorer_id="scorer-123", input="hello", **_make_invoke_kwargs())
759789
finally:
760790
await client.close()
761791

@@ -785,7 +815,7 @@ async def test_invoke_propagates_http_status_error(monkeypatch):
785815

786816
try:
787817
with pytest.raises(httpx.HTTPStatusError):
788-
await client.invoke(scorer_id="scorer-123", input="hello")
818+
await client.invoke(scorer_id="scorer-123", input="hello", **_make_invoke_kwargs())
789819
finally:
790820
await client.close()
791821

@@ -806,7 +836,7 @@ async def test_invoke_propagates_request_error(monkeypatch):
806836

807837
try:
808838
with pytest.raises(httpx.RequestError):
809-
await client.invoke(scorer_id="scorer-123", input="hello")
839+
await client.invoke(scorer_id="scorer-123", input="hello", **_make_invoke_kwargs())
810840
finally:
811841
await client.close()
812842

@@ -846,6 +876,7 @@ def handler(request: httpx.Request) -> httpx.Response:
846876
scorer_id="scorer-123",
847877
input="hello",
848878
headers={"Galileo-API-Key": "should-be-stripped", "X-Custom": "keep-me"},
879+
**_make_invoke_kwargs(),
849880
)
850881
finally:
851882
await client.close()
@@ -873,7 +904,7 @@ def handler(request: httpx.Request) -> httpx.Response:
873904
client._client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
874905

875906
try:
876-
await client.invoke(scorer_id="scorer-123", input="hello")
907+
await client.invoke(scorer_id="scorer-123", input="hello", **_make_invoke_kwargs())
877908
finally:
878909
await client.close()
879910

@@ -951,7 +982,7 @@ def test_coerce_number_parses_numeric_string(self):
951982

952983
@pytest.mark.asyncio
953984
async def test_evaluator_with_context_uses_step(monkeypatch):
954-
"""evaluate_with_context passes the step to _evaluate."""
985+
"""evaluate_with_extensions passes the step and execution_context to invoke."""
955986
for key, value in LLM_ENV.items():
956987
monkeypatch.setenv(key, value)
957988
from agent_control_models import Step
@@ -964,29 +995,33 @@ async def test_evaluator_with_context_uses_step(monkeypatch):
964995

965996
with patch.object(GalileoLLMClient, "invoke", new_callable=AsyncMock) as mock_invoke:
966997
mock_invoke.return_value = ScorerInvokeResponse(score=0.8, status="success")
967-
result = await evaluator.evaluate_with_context("selected input", step)
998+
result = await evaluator.evaluate_with_extensions("selected input", step, _EXTENSIONS)
968999

9691000
assert result.matched is True
9701001
assert mock_invoke.call_args.kwargs["step"] == step
1002+
assert mock_invoke.call_args.kwargs["execution_context"] is not None
9711003

9721004

9731005
@pytest.mark.asyncio
9741006
async def test_evaluator_scorer_label_echoed_in_metadata(monkeypatch):
9751007
"""scorer_label echoed from the response is included in result metadata."""
9761008
for key, value in LLM_ENV.items():
9771009
monkeypatch.setenv(key, value)
1010+
from agent_control_models import Step
1011+
9781012
from agent_control_evaluator_galileo.llm import LlmEvaluator
9791013
from agent_control_evaluator_galileo.llm.client import GalileoLLMClient, ScorerInvokeResponse
9801014

9811015
evaluator = LlmEvaluator.from_dict(
9821016
{"scorer_id": "scorer-123", "threshold": 0.5, "scorer_label": "my-label"}
9831017
)
1018+
step = Step(type="llm", name="test", input="prompt")
9841019

9851020
with patch.object(GalileoLLMClient, "invoke", new_callable=AsyncMock) as mock_invoke:
9861021
mock_invoke.return_value = ScorerInvokeResponse(
9871022
scorer_label="echoed-label", score=0.8, status="success"
9881023
)
989-
result = await evaluator.evaluate("hello")
1024+
result = await evaluator.evaluate_with_extensions("hello", step, _EXTENSIONS)
9901025

9911026
assert result.metadata.get("scorer_label") == "echoed-label"
9921027

@@ -996,6 +1031,8 @@ async def test_evaluator_scorer_version_id_in_metadata(monkeypatch):
9961031
"""scorer_version_id is included in metadata when configured."""
9971032
for key, value in LLM_ENV.items():
9981033
monkeypatch.setenv(key, value)
1034+
from agent_control_models import Step
1035+
9991036
from agent_control_evaluator_galileo.llm import LlmEvaluator
10001037
from agent_control_evaluator_galileo.llm.client import GalileoLLMClient, ScorerInvokeResponse
10011038

@@ -1006,10 +1043,11 @@ async def test_evaluator_scorer_version_id_in_metadata(monkeypatch):
10061043
"threshold": 0.5,
10071044
}
10081045
)
1046+
step = Step(type="llm", name="test", input="prompt")
10091047

10101048
with patch.object(GalileoLLMClient, "invoke", new_callable=AsyncMock) as mock_invoke:
10111049
mock_invoke.return_value = ScorerInvokeResponse(score=0.8, status="success")
1012-
result = await evaluator.evaluate("hello")
1050+
result = await evaluator.evaluate_with_extensions("hello", step, _EXTENSIONS)
10131051

10141052
assert result.metadata.get("requested_scorer_version_id") == "ver-456"
10151053
assert mock_invoke.call_args.kwargs["scorer_version_id"] == "ver-456"

‎evaluators/contrib/galileo/tests/test_llm_evaluator.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
LLM_ENV = {
1212
"GALILEO_API_SECRET_KEY": "test-secret",
1313
"GALILEO_LUNA_INVOKE_URL": "http://luna-invoke:8090",
14+
"GALILEO_FEATURE_FLAG_LLM_INVOKE_RUNTIME": "enabled",
1415
}
1516

1617

0 commit comments

Comments
 (0)