发布

  • [OPIK-5333][BE]: agentic tools online scoring; (#6649)

    frostbyte_neo 发布于 2026-05-15 10:15:43 +00:00

    • [OPIK-5333] [BE] feat: add agentic-tools online scoring config flags

    Adds OnlineScoringConfig.agenticToolsEnabled (default true) and
    OnlineScoringConfig.agenticToolsThresholdTokens (default 50000), wired
    through config.yml as ONLINE_SCORING_AGENTIC_TOOLS_ENABLED and
    ONLINE_SCORING_AGENTIC_TOOLS_THRESHOLD_TOKENS env vars.

    These gate the size-based routing decision in subsequent commits — when
    the rendered context for a trace or thread exceeds the threshold, the
    scorer flips into the read/jq/search agentic-tools path instead of
    rendering the full content inline.

    • [OPIK-5333] [BE] feat: ReadTool output safety cap + thread-redirect error message

    ReadTool: add OUTPUT_SAFETY_CHARS (40 K, ~20 K tokens) per-call cap with
    guardOutput() that walks FULL → MEDIUM → SKELETON until the payload fits.
    Drives off the LAST REQUESTED tier rather than current.tier() so the loop
    terminates cleanly when GenericCompressor collapses MEDIUM and SKELETON
    to the same returned tier.

    ToolArgs: replace the bare 'type=thread is not supported' rejection with
    a redirect that tells the model what to do instead (use read(type=trace,
    id=...) on the trace ids in the thread skeleton, or jq for path-targeted
    lookups). Misrouted calls now cost one wasted round, not many.

    • [OPIK-5333] [BE] feat: estimateTraceContextTokens + supportsToolCalling helpers

    Adds two utility methods on OnlineScoringEngine that the size-based
    agentic-tools routing depends on:

    • estimateTraceContextTokens(trace, spans, traceCompressor) — char-based
      token estimate for the {trace, spans} composite. Used to decide whether
      the inline-rendered prompt would risk overflowing the model's window
      and the scorer should flip into the read/jq/search agentic path.

    • supportsToolCalling(provider) — allowlist of LLM providers known to
      support tool calling. Providers outside the list fall back to the
      inline path even when context exceeds the size threshold.

    • [OPIK-5333] [BE] feat: route huge contexts through agentic-tools path

    Trace-level LLM-as-judge online scoring now flips into the read/jq/search
    agentic path when the rendered context is estimated to exceed the configured
    threshold (default 50 K tokens) — until now the agentic path was reserved
    for the test-suite assertion case (experimentId != null). Both gates are
    unified in LlmAsJudgeToolsMode.shouldUseTools(message, estimatedTokens, config).

    Below the threshold, behaviour is unchanged: the inline path renders trace
    input/output verbatim and uses provider-native structured output.

    Above the threshold:

    • User-mapped variables (input/output/metadata) are capped at MAX_PROMPT_FIELD_CHARS
      so a huge field doesn't pre-load context.
    • Drill-down hint points the model at tier=MEDIUM (full structure with
      per-string truncation + jq pointers) instead of tier=FULL which could
      overflow the window. ReadTool's per-call output cap silently downgrades
      a tier=FULL request anyway, but this avoids a wasted round.
    • Provider tool-calling support gates the path: a model whose provider
      doesn't support tools falls back to inline with a warning rather than
      requesting tool specs the API will reject.

    Diagnostic logs at the gate decision so operators can correlate scoring
    failures with mode flips:

    • 'Trace context exceeds N tokens; switching to agentic-tools mode for traceId X'
    • 'Trace context exceeds N tokens but provider for model Y does not support
      tool calling; falling back to inline path'

    Threshold and master switch are env-configurable via
    ONLINE_SCORING_AGENTIC_TOOLS_THRESHOLD_TOKENS and
    ONLINE_SCORING_AGENTIC_TOOLS_ENABLED (added in earlier commit).

    • [OPIK-5333] [BE] feat: cumulative tool-output budget + defensive messages copy

    Adds CUMULATIVE_TOOL_OUTPUT_BUDGET_CHARS (150 K, ~75 K tokens) cap on the
    total size of tool results across all rounds of handleToolCalls. Once
    exhausted, further tool calls return a budget-exhausted sentinel so the
    judge has to compose its final answer from data already gathered, rather
    than overflowing the model's context window with one more huge result.

    Pairs with ReadTool's per-call OUTPUT_SAFETY_CHARS (40 K): up to ~7 max-cap
    reads fit before the cumulative budget kicks in.

    Also defensive-copies the messages list when building per-round followUp
    and finalRequest. ChatRequestBuilder.messages stores by reference; a later
    iteration mutating the list would retroactively change what an async chat
    client sees in earlier requests. Snapshot per round.

    • [OPIK-5333] [BE] feat: inject built-in spans variable into trace-level Python evaluators

    Adds OnlineScoringEngine.toReplacements(variables, trace, spans) overload
    that auto-injects the built-in 'spans' variable holding the trace's spans
    serialized as a compact JSON array (sorted by start_time). User-defined
    spans mappings win over the auto-injection — built-in only fires when
    the variable name is otherwise unclaimed.

    OnlineScoringUserDefinedMetricPythonScorer pre-fetches spans before
    evaluating so metrics that reference {{spans}} can compute aggregates
    across the full execution trace (error counts, span-type stats, etc.).
    Metrics that don't reference 'spans' pay only the cost of the fetch +
    serialization.

    • [OPIK-5333] [BE] test: ReadTool guardOutput regression coverage

    Two cases for the per-call output cap (OUTPUT_SAFETY_CHARS):

    • oversizedFullSpanDowngradesToMedium — a single huge string forces the
      walk from FULL → MEDIUM and asserts the downgraded tier in the response.

    • guardOutputTerminatesEvenWhenCompressorCollapsesTiers — regression for
      an infinite-loop bug. GenericCompressor.pickTier collapses MEDIUM,
      SKELETON, and SUMMARY requests all to MEDIUM. If guardOutput drove its
      walk off current.tier() (which is always MEDIUM after the first
      downgrade), it would keep asking for SKELETON, getting MEDIUM back, and
      looping forever. Many fields × moderate size each is the data shape
      that surfaces it.

    • [OPIK-5333] [BE] chore: spotless formatting

    • [OPIK-5333] [BE] fix: review feedback + CI config validation

    • OnlineScoringConfig: drop @Builder.Default on agenticToolsEnabled and
      agenticToolsThresholdTokens, use plain field initializers. The @Builder
      default only applies when using the builder; Dropwizard YAML
      deserialization uses the no-args constructor, where field initializers
      apply directly. This was causing 26 CI failures: config-test.yml has no
      onlineScoring.agenticToolsThresholdTokens, the field stayed at Java
      default 0, and the @Min(1) validator rejected it on every test that
      loaded the config.
    • config-test.yml: add explicit agenticToolsEnabled and agenticToolsThresholdTokens
      values so the test config matches main config.yml's shape.
    • OnlineScoringLlmAsJudgeScorer.evaluate: pre-fetch spans up-front so
      estimateTraceContextTokens accounts for big spans on small traces (PR
      review #1, High). The big-spans-on-small-trace shape would have skipped
      the gate without this. Cost: one DB query per scoring run, negligible
      versus the LLM call.
    • Log lines: quote interpolated values as '{}' to match the
      .agents/skills/opik-backend/SKILL.md convention (PR review #2 + #4).
    • [OPIK-5333] [BE] fix: update test for SpanService constructor arg

    Adds the missing SpanService mock + constructor argument to
    OnlineScoringUserDefinedMetricPythonScorerTest. The test was missed when
    the scorer's constructor gained a SpanService parameter (commit f55e8cd5fc,
    spans-injection feature) — local 'mvn test -Dtest=ReadToolTest' didn't
    catch it because it wasn't in scope, but 'mvn install' / CI 'mvn package'
    fails testCompile on the mismatched arity.

    Also stubs spanService.getByTraceIds(any()) to return Flux.empty() in the
    two ScoringTests that exercise score() — without the stub, the new
    prepareData call to spanService NPEs on the unmocked Flux.

    This is the actual root cause of the 25 CI failures: testCompile failed,
    no surefire reports were produced, every test job reported failure with
    'Could not find any files for **/target/surefire-reports/TEST-*.xml'.

    • [OPIK-5333] [BE] fix: align test with defensive messages copy

    The existing handleToolCallsAccumulatesResultsAndFinalizesWithStructuredRequestShape
    test asserted that the round-1 captured ChatRequest's messages had size 4 —
    which only held because handleToolCalls used to share the mutable list by
    reference (the original comment in the test acknowledged this:
    'ArgumentCaptor captures the request by reference, so by assertion time
    the captured round-1 messages list reflects the final 4-element state').

    Commit d0aa21aff6 (cumulative tool-output budget + defensive messages copy)
    intentionally fixed that reference-sharing — every round now passes a
    fresh ArrayList to ChatRequestBuilder. The captured round-1 request is
    correctly a 3-element snapshot now (original UserMessage + AiMessage with
    tool calls + ToolExecutionResult). The forcing UserMessage that pushed the
    old assertion to 4 is only present in the final structured re-issue.

    Updated round-1 size assertion to 3 and rewrote the comment to explain
    the snapshot semantics. Final-request assertions unchanged.

    • [OPIK-5333] [BE] refactor: make spans fetch opt-in for Python metrics

    Only call spanService.getByTraceIds when the user's metric explicitly
    maps a spans argument. Most trace-level Python metrics use only
    input/output/metadata, so skipping the fetch for them removes a DB
    query the evaluator never reads.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: address spans serialization, log redaction, branch coverage

    Pass the trace's spans to the Python evaluator as a typed JSON array
    instead of a JSON string. Widens PythonEvaluatorRequest.data to
    Map<String, Object> so json.loads(argv[2]) in the runner yields
    spans as a list of dicts that maps cleanly onto the metric's
    score(..., spans) signature.

    Replace the verbatim INFO dump of the rendered evaluator input with a
    shape-only summary (keys + per-argument sizes), mirroring the LLM
    scorer's summarizeRequest redaction. The rendered values are user
    trace content (input/output/metadata/spans) and were landing in clear
    text downstream of the user-facing log sinks.

    Cover both branches of the arguments.containsKey("spans") gate with
    explicit tests: one verifies spanService.getByTraceIds is skipped when
    absent, the other asserts the data sent to the evaluator carries spans
    as a List<Span>.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] chore: clean up imports, field placement, and verbose comments

    Replace fully-qualified type references with proper imports, lift static
    fields back to the top of their classes, fix a missing EOF newline, and
    trim a few comment blocks that had grown past their useful weight (the
    duplicated @Builder.Default rationale and the OUTPUT_SAFETY_CHARS
    calibration breakdown). Drops one unnecessary @SuppressWarnings on a
    String captor.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: redact span scorer log and unblock CI

    Move summarizeEvaluatorInput to OnlineScoringEngine and use it from the
    span scorer too — the trace scorer was already redacted, the span side
    was still dumping the full rendered map. Widen the integration test
    matcher in AutomationRuleEvaluatorsResourceTest to accept the new
    'Sending traceId ... to Python evaluator: arguments=[...]' summary
    format alongside the LLM scorer's summary.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] chore: drop unused size-aware shouldUseTools overload

    The 3-arg shouldUseTools(message, tokens, config) was never wired up.
    The scorer combines size and provider locally because its diagnostic
    logs need both booleans separately. Trim the doc-claimed single source
    of truth framing to match what is actually centralized.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] refactor: move agenticToolsEnabled to serviceToggles

    The boolean lives with the rest of the feature flags now
    (TOGGLE_AGENTIC_TOOLS_ENABLED, defaults to true) instead of on the
    onlineScoring config bucket. The numeric threshold stays on
    OnlineScoringConfig since it is a tunable, not a toggle. The LLM scorer
    injects ServiceTogglesConfig and reads the gate from there.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] chore: default TOGGLE_AGENTIC_TOOLS_ENABLED to false

    The agentic-tools online-scoring path is dark-launched off so existing
    deployments stay on the inline behavior until the toggle is flipped per
    environment.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: stamp projectName onto traces from the update path

    TraceDAO.findByIds (used by OnlineScoringSampler.onTracesUpdated) sets
    projectId but leaves projectName null because the ClickHouse traces
    table doesn't carry the name. Downstream, FeedbackScoreService.
    processScoreBatch groups by projectName and re-resolves projectId from
    it via retrieveByNamesOrCreate, so a null name routes every score
    through WorkspaceUtils.getProjectName(null) which returns
    "Default Project". The end result: scores from any trace completed via
    PATCH (the SDK's trace.end() flow) land in Default Project instead of
    the trace's actual project.

    Resolve the missing names once per batch via ProjectService.findByIds
    and stamp them onto the traces before publishing the scoring event.
    The create-path was already correct (TraceService.create explicitly
    sets projectName before posting TracesCreated) and is untouched.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] refactor: reuse findIdToNameByIds + cover enrichment path

    Swap the local stampProjectNames helper to use the existing
    ProjectService.findIdToNameByIds (which already returns the projectId ->
    name map we were building by hand), warn-log when a lookup misses
    instead of silently dropping the name, and add unit coverage for the
    three branches: enriched, miss with passthrough, and the CreatedPath
    shortcut where every trace already carries a name.

    The warn-log path avoids fail-fast on transient lookup misses — losing
    the score entirely is worse than landing on the existing Default
    Project fallback that the score service applies downstream.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] test: cover the LLM-as-judge agentic-tools routing gate

    Extract the inline gate decision in OnlineScoringLlmAsJudgeScorer.evaluate
    into a package-private shouldUseAgenticTools(message, tokens, model) so it
    can be unit-tested independently of the rest of the scoring flow. Behavior
    is unchanged; the diagnostic warn/info logs move with the gate so operators
    still see the same routing decisions.

    Add a parameterized truth-table covering the gate's four inputs
    (experimentId / toggle / tokens vs threshold / provider tool support).
    Nine rows: the three experimentId-true cases (always tools regardless of
    other inputs), the size-based path with each precondition flipped, and
    a no-preconditions baseline.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: thread spans from evaluate into handleToolCalls

    The size-estimate fetch in evaluate(...) was discarded; handleToolCalls
    re-fetched spans inside its body, doubling the DB round-trip. Pass the
    already-fetched list in instead. Drops the now-redundant spanService
    stubs from three handleToolCalls tests (Mockito strict mode flagged
    them once the production code stopped calling getByTraceIds).

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] refactor: make LLM-as-judge online scoring fully reactive

    Reviewer feedback on #6649 flagged that PR #6511 (OPIK-5745) reintroduced
    the blocking pattern that OPIK-6308 had eliminated from the online
    evaluator path. The size-based agentic-tools branch was making it worse
    by routing more traffic through the offending fetchSpans .block().

    Pushes the scoring chain to be reactive end-to-end:

    • ToolExecutor.execute returns Mono; ReadTool's I/O composes via
      flatMap with no .block() helpers.
    • handleToolCalls + the per-round tool dispatch become a recursive Mono
      chain with concatMap to preserve message ordering.
    • aiProxyService.scoreTrace (sync LangChain4j) wrapped in fromCallable +
      subscribeOn(boundedElastic) as a per-call boundary, replacing the
      whole-evaluate wrapper.
    • prepareEvaluation scheduled on Schedulers.parallel() since it's
      CPU-bound; spans pre-fetch in the Python scorer also moved to reactive.
    • toolCallLoop side effects deferred via Mono.defer so list mutation
      only fires at subscription time.
    • MDC threaded through handleToolCalls/toolCallLoop/executeToolOrBudget-
      Exhausted so internal slf4j logs keep their workspace/trace/rule tags.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: address remaining andrescrz review comments

    Five comments on #6649:

    • ReadTool.guardOutput: replace while loop with a bounded for loop capped
      at CompressionTier.values().length so a future tier change can't turn
      the recompress walk into an infinite loop, even if downgradeTierOrSame's
      contract drifts. Demote the per-iteration "downgrading" log from info
      to debug — a many-rounds-per-evaluation agent could trigger 2-3 per
      read call; the terminal warn covers the case that actually matters.
    • ToolArgs.parseType: switch the thread-rejection error from string
      concatenation to a Java text block.
    • OnlineScoringEngine: swap jakarta.validation.constraints.NotNull for
      lombok.NonNull (Jakarta annotation does nothing on plain Java methods;
      Lombok generates a real runtime null check). Add @NonNull on missing
      arguments of the new public methods (toReplacements/spans variant,
      summarizeEvaluatorInput, supportsToolCalling).
    • OnlineScoringEngine.estimateTraceContextTokens: make the 4-chars/token
      ratio a parameter sourced from a new onlineScoring.agenticToolsCharsPer-
      Token config entry (env var ONLINE_SCORING_AGENTIC_TOOLS_CHARS_PER_TOKEN,
      default 4). Lets operators tune for code/JSON-heavy workloads where the
      natural-language ratio under-estimates.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] chore: minor cleanups in guardOutput + ToolArgs
    • ReadTool.guardOutput: cache CompressionTier.values().length in a local
      so the bound + trailing log don't each pay an Enum.values() array alloc.
    • ToolArgs.parseType: add a one-line comment explaining the \ line
      continuations in the text block so a reader doesn't wonder why we
      aren't letting the block break naturally.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: address LifeXplorer review nits

    Three comments on #6649:

    • OnlineScoringEngine: the supportsToolCalling Javadoc was orphaned —
      two /** */ blocks landed adjacent so the first attached to nothing
      and the actual method ended up undocumented. Move the supportsTool-
      Calling Javadoc to immediately precede its method.
    • OnlineScoringConfig.agenticToolsThresholdTokens: bump @Min(1) →
      @Min(1000). With MAX_PROMPT_FIELD_CHARS = 4000 (~1K tokens per
      template variable), any threshold below ~1K tokens would flip the
      whole online-scoring fleet onto the agentic-tools path. Dropwizard
      now fails fast on a typo'd env var instead of silently degrading.
    • OnlineScoringSamplerTest: lift com.comet.opik.domain.ProjectService
      out of the inline FQN reference and into the imports section,
      matching surrounding style.
    • [OPIK-5333] [BE] fix: add agenticToolsCharsPerToken to config-test.yml

    config-test.yml lists every onlineScoring field explicitly (matching the
    all-fields-set pattern rather than relying on Java field initializers
    to survive Dropwizard's YAML deserialization). I missed adding the new
    agenticToolsCharsPerToken entry, which made OnlineScoringEngineTest fail
    at startup with "must be greater than or equal to 1" — Bean Validation
    running against the unset field at value 0.

    Production config.yml is unaffected — it has the env-var-default
    ${ONLINE_SCORING_AGENTIC_TOOLS_CHARS_PER_TOKEN:-4} so operators who
    don't override the env var still get 4.

    • [OPIK-5333] [BE] fix: redact raw exception message in ToolRegistry error envelope

    ToolRegistry.execute was echoing e.getMessage() back to the LLM through
    the {"error": ...} envelope when a tool implementation threw. That can
    carry ClickHouse query fragments, internal paths, or any other detail
    the failing tool surfaced in its exception text — info the model has no
    business seeing.

    Mirror ReadTool's exception-path pattern: generate a UUID correlation
    id, log the full exception under that id at warn, and return the id +
    tool name to the model. An operator can grep the warn log when the
    model reports a ref:.

    Tests updated accordingly: assert tool name + ref: marker are present,
    assert the raw exception message ("kaboom") is NOT present.

    Flagged by baz-reviewer on the current refactor. Behavior preexisted
    from PR #6511 (OPIK-5745); the reactive refactor preserved the leak and
    this commit closes it.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: address remaining PR review nits
    • Gate experimentIdPath on provider tool-call support so non-tool-calling
      providers (e.g. OLLAMA) fall back to inline scoring with experiments.
    • Build fullJson once in prepareEvaluation and pre-seed it into the
      tool-call cache, avoiding a duplicate traceCompressor.buildFullJson on
      the agentic-tools path.
    • Log toolsEnabled reflecting the actual useTools decision (which factors
      in size threshold + provider support) rather than just shouldUseTools.
    • Quote the evaluator-input summary placeholder in Python scorer logs
      for readability and grep-ability.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: Preconditions + log regex update for Python scorers
    • Replace manual if/throw in estimateTokensFromJson with Guava
      Preconditions.checkArgument per andrescrz review nit.
    • Update AutomationRuleEvaluatorsResourceTest regex to match the new
      quoted 'arguments=[...]' format emitted by the Python scorers after
      the placeholder-quoting fix landed; restores Integration Group 13.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] chore: address andrescrz nits on tools/
    • Annotate public-method parameters with @NonNull on the ToolExecutor
      SPI (ctx), the three tool impls, ToolRegistry, and ToolArgs helpers
      (errorJson, cacheMiss, parseType, requireString). Documents the
      contract where it wasn't enforced.
    • Extract the thread-type-rejection text block in ToolArgs to a private
      constant (THREAD_TYPE_REJECTION_TEMPLATE).
    • Drop the (JsonNode) casts on ObjectMapper#valueToTree in ReadTool —
      the lambda return type pins the generic inference, no cast needed.
    • Replace the multi-part runtime string concat in ToolRegistry's
      exception fallback with String.formatted; tidy the warn-log format
      string into a single line.
    • Drop the isDebugEnabled() guard in GetTraceSpansTool — the placeholder
      args are all cheap (length / size lookups), so SLF4J's deferred
      substitution already handles the level check.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] docs: clarify OUTPUT_SAFETY_CHARS is best-effort, log overshoot

    Addresses the Baz Reviewer comment on guardOutput: when the smallest
    tier (SKELETON) is still over the cap, we return the over-cap payload
    rather than hard-truncating the JSON envelope, so the final response
    can technically exceed OUTPUT_SAFETY_CHARS. Hard-truncating would
    corrupt the envelope and break the agent's tool round, so the cap is
    deliberately a heuristic.

    • Document the best-effort contract on OUTPUT_SAFETY_CHARS and
      guardOutput's Javadoc.
    • Include the actual payload size in the terminal-branch WARN so
      operators can see how much we overshot and tighten the per-tier
      limits in TraceCompressor / GenericCompressor if needed.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] refactor: address remaining Baz Reviewer comments
    • Remove @NonNull from ToolExecutor.execute(ctx) per
      .agents/skills/opik-backend/SKILL.md ("don't put validation
      annotations on interface method parameters"). Enforcement still
      lives on the four implementing classes. The interface Javadoc now
      documents the contract explicitly.
    • Extract logAndPrepareEvaluatorInput on OnlineScoringEngine and use
      it from both Python scorers (trace + span). Removes the duplicated
      MDC scope, "Evaluating X 'id' sampled by rule" entry log, "Sending X
      'id' to Python evaluator: ''" exit log, and error-rethrow
      fallback. Callers now supply only what differs: entity label
      ("traceId" / "spanId"), id, rule name, and a Supplier that builds
      the rendered evaluator input.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

    • [OPIK-5333] [BE] fix: address two LifeXplorer review nits
    • Move the "Shape-only summary..." javadoc back to where it belongs, on
      summarizeEvaluatorInput. It got orphaned above logAndPrepareEvaluatorInput
      when that helper was inserted earlier; the two javadoc blocks were
      sitting back-to-back with the first one describing the wrong method.
    • Drop @NotNull on the new agenticToolsEnabled primitive boolean. Jakarta
      @NotNull has no runtime effect on primitives (they can't be null and
      default to false). Pre-existing booleans in ServiceTogglesConfig still
      carry @NotNull for consistency with the file's prior convention; not
      reworking those in this PR.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com


    Co-authored-by: aadereiko aliaksandr@comet.com
    Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com
    Co-authored-by: Sasha sasha@Sashas-MacBook-Pro.local

    下载附件