Skip to content

Commit 1f07d7c

Browse files
feat(evaluators): 17516 request payload contract (#271)
## Summary - Add an optional `tools` argument to `@control` for static definitions or a dynamic provider. Tool definitions are included only on LLM steps; omitting `tools` preserves existing decorator behavior. - Build canonical Galileo Core records from the complete Agent Control `Step` tree. `Trace.children` map to spans, `Session.children` map to traces, and tool/retriever children map to nested spans. - Use `Step.children` as the only source of execution hierarchy. Reject hierarchy fields in `Step.context` while preserving ordinary metadata. - Have Luna send the legacy `inputs` together with a canonical `record` when a supported runtime `Step` is available. Selector-selected values are passed into the shared records factory; the existing `ScorerInvokeRecord` request model and legacy request shape remain unchanged. - Normalize record fields with `galileo-core` models and cover serialization, request-model validation, and Core-model revalidation in tests. ## Scope - User-facing/API changes: optional keyword-only `tools` on `@control`, accepting static definitions or a `(args, kwargs)` provider; SDK documentation updated. Existing calls without tools remain compatible. - Internal changes: recursive Galileo record conversion from `Step.children`; Luna structured-record dual-write alongside legacy inputs; field-level normalization and request validation; regression tests. - Out of scope: calling Orbit's `execute_scorer`, `tool_calls`, Agent/Workflow/Control nested spans, framework-specific integrations, persistence, and changes to Orbit or Luna's public request/response contracts. The scorer version remains caller-supplied; the client does not infer one. ## Risk and Rollout - Risk level: moderate. Luna requests continue to include legacy inputs, and no database schema changes are included. - Rollback plan: revert this PR. No data migration is required. ## Testing - [x] Added or updated tests for `@control` tools, `Step.children` record trees, context hierarchy rejection, Luna request serialization, and Galileo Core serialization/revalidation. - [x] Galileo package test suite: 187 passed using the existing package virtual environment. - [x] Ruff, mypy, and `git diff --check` passed for the changed package files. - [ ] Repo-wide `make check` could not start because the configured Artifactory package index returned 403 while resolving `google-re2`. - [ ] Fresh CI is pending for the updated PR head. The previous run was against the pre-fix head and reported five Galileo test failures plus a generated TypeScript client check failure. - [ ] Manually verified behavior; validation is through automated tests and code review. ## Checklist - [x] Issue/spec: SAO-17516 (referenced in the PR title). - [x] Updated SDK documentation for the decorator API and direct Step construction. - [x] Follow-up noted: wiring Luna to Orbit's `execute_scorer` path and adding `tool_calls` are separate work.
1 parent 095edf0 commit 1f07d7c

14 files changed

Lines changed: 1609 additions & 549 deletions

File tree

‎evaluators/contrib/galileo/src/agent_control_evaluator_galileo/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@
3232
)
3333
from agent_control_evaluator_galileo.records import (
3434
GalileoRecord,
35+
GalileoRecordNormalizer,
3536
RecordFactoryError,
3637
UnsupportedStepTypeError,
3738
build_galileo_record,
3839
build_record,
39-
record_from_scorer_invoke_record,
4040
record_from_step,
4141
)
4242

@@ -51,10 +51,10 @@
5151
"LunaOperator",
5252
"LUNA_AVAILABLE",
5353
"GalileoRecord",
54+
"GalileoRecordNormalizer",
5455
"RecordFactoryError",
5556
"UnsupportedStepTypeError",
5657
"build_galileo_record",
5758
"build_record",
58-
"record_from_scorer_invoke_record",
5959
"record_from_step",
6060
]

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

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
from hmac import new as hmac_new
1212
from json import dumps
1313
from time import time
14-
from typing import Literal, cast, get_args
14+
from typing import Any, Literal, get_args
1515
from urllib.parse import urlsplit
1616

1717
import httpx
1818
from agent_control_models import JSONObject, JSONValue, Step
19-
from pydantic import BaseModel, Field, PrivateAttr, model_validator
19+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
2020

21+
from ..records import UnsupportedStepTypeError, record_from_step
2122
from .config import ScorerInvokeConfig
2223

2324
logger = logging.getLogger(__name__)
@@ -57,6 +58,7 @@
5758
"session",
5859
]
5960
SUPPORTED_SCORER_INVOKE_RECORD_TYPES = frozenset(get_args(ScorerInvokeRecordType))
61+
_MISSING_SELECTED_DATA = object()
6062

6163

6264
def _b64url(data: bytes) -> str:
@@ -219,6 +221,8 @@ class ScorerInvokeRecord(BaseModel):
219221
represented here. Orbit hydrates those fields from trusted server context.
220222
"""
221223

224+
model_config = ConfigDict(extra="allow")
225+
222226
type: ScorerInvokeRecordType
223227
name: str | None = None
224228
input: JSONValue = None
@@ -272,11 +276,13 @@ def to_dict(self) -> JSONObject:
272276
return request
273277

274278

275-
def _orbit_record_from_step(
279+
def _scorer_invoke_record_from_step(
276280
step: Step | None,
277281
*,
278282
selected_input: JSONValue,
279283
selected_output: JSONValue,
284+
selected_data: Any = _MISSING_SELECTED_DATA,
285+
payload_field: str = "input",
280286
) -> ScorerInvokeRecord | None:
281287
"""Translate a generic Agent Control step into Orbit's record contract.
282288
@@ -297,21 +303,30 @@ def _orbit_record_from_step(
297303
Returns:
298304
An Orbit-compatible record, or ``None`` for absent/unsupported steps.
299305
"""
300-
if step is None or step.type not in SUPPORTED_SCORER_INVOKE_RECORD_TYPES:
306+
if step is None:
301307
return None
302-
303-
# The membership check above narrows the runtime value to Orbit's known
304-
# discriminator set, but static type checkers cannot infer that relationship.
305-
record_type = cast(ScorerInvokeRecordType, step.type)
306-
return ScorerInvokeRecord(
307-
type=record_type,
308-
name=step.name,
309-
input=selected_input if selected_input is not None else step.input,
310-
output=selected_output if selected_output is not None else step.output,
311-
context=step.context,
312-
tools=step.tools,
313-
dataset_output=step.ground_truth,
314-
)
308+
try:
309+
if selected_data is _MISSING_SELECTED_DATA:
310+
galileo_core_record = record_from_step(
311+
step,
312+
selected_input=selected_input,
313+
selected_output=selected_output,
314+
)
315+
else:
316+
galileo_core_record = record_from_step(
317+
step,
318+
selected_data=selected_data,
319+
payload_field=payload_field,
320+
)
321+
except UnsupportedStepTypeError:
322+
return None
323+
# record_from_step() returns Galileo Core models (LlmSpan, Trace, etc.). Dumping
324+
# and revalidating coerces the model into the Luna request shape while preserving
325+
# extra canonical fields that Orbit accepts but ScorerInvokeRecord does not declare.
326+
canonical_record = galileo_core_record.model_dump(mode="json", exclude_none=True)
327+
if canonical_record.get("type") not in SUPPORTED_SCORER_INVOKE_RECORD_TYPES:
328+
return None
329+
return ScorerInvokeRecord.model_validate(canonical_record)
315330

316331

317332
class ScorerInvokeResponse(BaseModel):
@@ -487,6 +502,8 @@ async def invoke(
487502
input: JSONValue = None,
488503
output: JSONValue = None,
489504
step: Step | None = None,
505+
selected_data: Any = _MISSING_SELECTED_DATA,
506+
selected_data_payload_field: str = "input",
490507
execution_context: GalileoExecutionContext | None = None,
491508
config: ScorerInvokeConfig | JSONObject | None = None,
492509
timeout: float = DEFAULT_TIMEOUT_SECS,
@@ -502,6 +519,9 @@ async def invoke(
502519
input: Optional user/system prompt text.
503520
output: Optional model response text.
504521
step: Optional complete runtime step used for structured dual-write.
522+
selected_data: Raw selector-selected value used to construct the record,
523+
preserving its structure independently from serialized legacy inputs.
524+
selected_data_payload_field: Record input/output side for scalar selector values.
505525
execution_context: Optional authenticated Galileo scorer context.
506526
config: Optional Orbit-supported scorer invocation configuration.
507527
timeout: Request timeout in seconds.
@@ -531,6 +551,17 @@ async def invoke(
531551
invoke_config,
532552
http_timeout_seconds=timeout,
533553
)
554+
record = (
555+
_scorer_invoke_record_from_step(
556+
step,
557+
selected_input=input,
558+
selected_output=output,
559+
selected_data=selected_data,
560+
payload_field=selected_data_payload_field,
561+
)
562+
if step is not None and scorer_version_id is not None
563+
else None
564+
)
534565
request_body = ScorerInvokeRequest(
535566
scorer_id=scorer_id,
536567
scorer_version_id=scorer_version_id,
@@ -541,11 +572,7 @@ async def invoke(
541572
ground_truth=step.ground_truth if step is not None else None,
542573
tools=step.tools if step is not None else None,
543574
),
544-
record=_orbit_record_from_step(
545-
step,
546-
selected_input=input,
547-
selected_output=output,
548-
),
575+
record=record,
549576
execution_context=execution_context,
550577
config=invoke_config,
551578
).to_dict()

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ class LunaEvaluatorConfig(EvaluatorConfig):
5656
5757
Attributes:
5858
scorer_id: Required scorer identifier for Luna scorer invocation.
59-
scorer_version_id: Deprecated optional compatibility identifier. Orbit
60-
currently invokes the scorer's current default version.
59+
scorer_version_id: Optional for legacy requests; required when the
60+
contextual evaluator sends a structured record.
6161
scorer_label: Optional display/metadata label.
6262
threshold: Local threshold used by the evaluator for comparison.
6363
operator: Local comparison operator. Numeric operators use threshold as a number.
@@ -75,8 +75,8 @@ class LunaEvaluatorConfig(EvaluatorConfig):
7575
default=None,
7676
min_length=1,
7777
description=(
78-
"Deprecated optional compatibility identifier. Orbit currently invokes "
79-
"the scorer's current default version."
78+
"Optional for legacy requests and required when contextual evaluation "
79+
"sends a structured record."
8080
),
8181
)
8282
scorer_label: str | None = Field(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ async def _evaluate(
281281
scorer_kwargs = self._scorer_kwargs()
282282
if step is not None:
283283
scorer_kwargs["step"] = step
284+
scorer_kwargs["selected_data"] = data
285+
scorer_kwargs["selected_data_payload_field"] = self.config.payload_field
284286
if execution_context is not None:
285287
scorer_kwargs["execution_context"] = execution_context
286288
response = await self._get_client().invoke(

‎evaluators/contrib/galileo/src/agent_control_evaluator_galileo/records/__init__.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
UnsupportedStepTypeError,
77
build_galileo_record,
88
build_record,
9-
record_from_scorer_invoke_record,
109
record_from_step,
1110
)
1211
from .normalization import GalileoRecordNormalizer
@@ -18,6 +17,5 @@
1817
"UnsupportedStepTypeError",
1918
"build_galileo_record",
2019
"build_record",
21-
"record_from_scorer_invoke_record",
2220
"record_from_step",
2321
]

0 commit comments

Comments
 (0)