Skip to content

feat(python): expose the custom N-target classifier to Python - #365

Open
rbehal wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
gumloop:feat/libsy-custom-classifier-python-binding
Open

feat(python): expose the custom N-target classifier to Python#365
rbehal wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
gumloop:feat/libsy-custom-classifier-python-binding

Conversation

@rbehal

@rbehal rbehal commented Aug 11, 2026

Copy link
Copy Markdown

What

Binds the Rust core's LlmClassifierConfig::Custom (CustomClassifierConfig + the TargetSelector policy) to Python as libsy.custom_classifier(judge_target, targets, *, default_target, config), alongside the existing llm_task_classifier / stage_router factories. Includes the CustomClassifierConfig pyclass, type stubs, factory re-exports, and binding tests mirroring test_libsy_minimal_bindings.py.

Why

The custom schema-driven classifier already exists in the Rust core and is reachable from the server's TOML mode = "custom", but not from the Python bindings, which currently expose only two-tier routing. Python-embedded hosts that route across more than two targets (our case: a five-lane model pool behind an existing product feature) currently have to cascade binary classifiers to approximate one N-way decision.

Notes

  • The judge's verdict selects a target label through the config's JSON Pointer; a judge failure or unusable verdict falls open to default_target, matching the Rust core's semantics (covered in the tests).
  • No behavior change to any existing binding; the new function follows target_cores + ClientEntry conventions.

We're integrating Switchyard as the routing foundation inside Gumloop's agent platform (decision-only, host-executed model calls) — happy to iterate on API shape if you'd prefer different ergonomics.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added a configurable custom classifier for routing requests across multiple targets.
    • Supports structured judge responses, named targets, default-target fallback, session affinity, message-hash fallback, recent-turn windows, and token limits.
    • Exposed the classifier and configuration through the Python package.
  • Bug Fixes

    • Added validation for invalid default targets and configurations.
    • Added fallback handling for judge failures and invalid classification results.

Binds LlmClassifierConfig::Custom (CustomClassifierConfig + the TargetSelector
policy) as libsy.custom_classifier(judge_target, targets, *, default_target,
config): schema-driven routing across two or more labeled targets, with the
judge's schema-validated verdict selecting a label through a JSON Pointer and
unusable verdicts falling open to default_target.

The capability already exists in the Rust core and the server's TOML mode;
this makes it reachable from Python-embedded hosts that route across more
than two tiers.

Signed-off-by: rbehal <rahulbehal01@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@rbehal
rbehal requested a review from a team as a code owner August 11, 2026 19:29
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The pull request adds Python bindings for schema-driven custom classifier routing. It introduces configuration and factory APIs, exports them through the Python package, wires labeled targets and fallback behavior, and adds routing and validation tests.

Changes

Custom classifier routing

Layer / File(s) Summary
Classifier configuration contract
crates/switchyard-py/src/libsy_bindings.rs, switchyard_rust/libsy.py, switchyard/libsy/__init__.py
Adds CustomClassifierConfig with prompt, response schema, selector, affinity, fallback, window, and token settings. Exports the configuration through the Python package.
Classifier construction and module wiring
crates/switchyard-py/src/libsy_bindings.rs, switchyard_rust/libsy.py, switchyard/libsy/algorithms.py
Adds the custom_classifier factory. The factory converts the judge and labeled targets, applies the default target, validates construction errors, and registers the Python API.
Routing and validation tests
tests/test_libsy_custom_classifier.py
Tests routing to each configured target, judge prompt and schema handling, fallback behavior, default-target validation, and minimum target-count validation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

I’m a rabbit with routes in a row,
Sending each verdict where it should go.
If the judge fails the call,
The default catches all.
Schemas guide the carrots I sow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes exposing the custom N-target classifier through Python bindings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@switchyard_rust/libsy.py`:
- Around line 68-80: Document the public API symbols CustomClassifierConfig, its
__init__ constructor, and custom_classifier with concise triple-quoted
docstrings. Describe JSON Pointer-based selection, the default_target fallback
behavior, relevant invariants, and error behavior without changing the
implementation.

In `@tests/test_libsy_custom_classifier.py`:
- Around line 77-87: Annotate the build function with the Algorithm return type,
using the existing Algorithm symbol available in the module, while leaving its
custom_classifier construction unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5c1a8c76-06f4-448a-9c9e-385809728cff

📥 Commits

Reviewing files that changed from the base of the PR and between 22b78d9 and c02e2b1.

📒 Files selected for processing (5)
  • crates/switchyard-py/src/libsy_bindings.rs
  • switchyard/libsy/__init__.py
  • switchyard/libsy/algorithms.py
  • switchyard_rust/libsy.py
  • tests/test_libsy_custom_classifier.py

Comment thread switchyard_rust/libsy.py
Comment on lines +68 to +80
@final
class CustomClassifierConfig:
def __init__(
self,
prompt: str,
response_schema: Mapping[str, object],
selector: str,
*,
session_affinity: bool = False,
message_hash_fallback: bool = False,
recent_turn_window: int | None = None,
max_output_tokens: int = 4096,
) -> None: ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add docstrings for the new public API.

Add concise triple-quoted docstrings to CustomClassifierConfig, its constructor, and
custom_classifier. Document JSON Pointer selection and default_target fallback behavior.

As per coding guidelines: “Add concise triple-quoted docstrings for public functions, classes,
methods, and API entry points; document behavior, important invariants, and relevant error behavior.”

Also applies to: 111-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@switchyard_rust/libsy.py` around lines 68 - 80, Document the public API
symbols CustomClassifierConfig, its __init__ constructor, and custom_classifier
with concise triple-quoted docstrings. Describe JSON Pointer-based selection,
the default_target fallback behavior, relevant invariants, and error behavior
without changing the implementation.

Source: Coding guidelines

Comment on lines +77 to +87
def build(judge_client: Any, clients: dict[str, EchoClient]):
return algorithms.custom_classifier(
LlmTarget("judge", judge_client),
[(lane, LlmTarget(lane, clients[lane])) for lane in LANES],
default_target="grok",
config=CustomClassifierConfig(
"Pick the best lane.",
SCHEMA,
"/model",
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a return type to build.

Line 77 has no return annotation. Strict mypy can report no-untyped-def for this helper.
Annotate the function with the returned Algorithm type.

As per coding guidelines: “Use Python 3.12+ syntax, including X | Y union types, and maintain
comprehensive type hints; code must pass strict mypy.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_libsy_custom_classifier.py` around lines 77 - 87, Annotate the
build function with the Algorithm return type, using the existing Algorithm
symbol available in the module, while leaving its custom_classifier construction
unchanged.

Source: Coding guidelines

rbehal added a commit to gumloop/gumloop-switchyard that referenced this pull request Aug 11, 2026
Binds LlmClassifierConfig::Custom (CustomClassifierConfig + TargetSelector)
as custom_classifier(judge_target, targets, *, default_target, config):
schema-driven routing across two or more labeled targets; unusable verdicts
and judge failures fall open to default_target. Filed upstream as
NVIDIA-NeMo#365.

Co-authored-by: Cursor <cursoragent@cursor.com>
rbehal added a commit to gumloop/gumloop-switchyard that referenced this pull request Aug 11, 2026
Binds LlmClassifierConfig::Custom (CustomClassifierConfig + TargetSelector)
as custom_classifier(judge_target, targets, *, default_target, config):
schema-driven routing across two or more labeled targets; unusable verdicts
and judge failures fall open to default_target. Filed upstream as
NVIDIA-NeMo#365.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant