<!-- OpenSmartRoute: osr-decorator-sdk. https://osr-web.kindrock-caaab1a2.centralindia.azurecontainerapps.io/docs/skills/osr-decorator-sdk -->

# OpenSmartRoute decorator SDK

`opensmartroute.sdk` stores *blueprints* (factories) in a `ComponentRegistry`; decorators are
transparent (they return the decorated object) and `osr.components` is the process-wide registry the
top-level decorators bind to. Use a private `ComponentRegistry()` in tests and multi-tenant apps.

## Declaring components

```python
import opensmartroute as osr

@osr.tool("weather", domains=["travel"], actions=["lookup"], cost=0.0, examples=["weather in paris"])
def weather(request, **kw):
    """Current weather for a city."""            # first docstring paragraph -> target.description
    return lookup(request.text)                  # handler(request, **kw) -> response

@osr.agent("planner", domains=["travel"], min_complexity=0.3, quality_prior=0.9, latency_ms=4000)
def planner(request, **kw): ...

@osr.skill("cite-sources", domains=["research"], primary=False)   # skill slot pre-processor
def cite(request):                                               # may return RouteRequest | str | None
    return "Always cite primary sources."

@osr.strategy(weight=0.8)                        # fn(request, signals, candidates) -> {id: 0..1 | StrategyScore}
def geo_affinity(request, signals, candidates):
    region = request.constraints.region
    return {t.id: 1.0 if region in t.constraints.regions else 0.4 for t in candidates}

@osr.strategy                                    # Strategy subclass; name inferred -> "recency"
class RecencyStrategy(osr.Strategy):
    def score(self, request, signals, candidates): ...
    def update(self, outcome): ...               # optional: learn from Outcome

@osr.signal(order=-1)                            # order < 0 runs before built-in extractors
def urgency(request, signals):
    return {"urgent": "asap" in request.text.lower()}   # Signals fields are set; other keys -> signals.extra

@osr.policy_rule                                 # fn(target, request, signals) -> rejection reason | None
def business_hours_only(target, request, signals):
    if target.kind is osr.TargetKind.HUMAN and request.context.get("after_hours"):
        return "human queues are closed"
    return None

@osr.middleware                                  # fn(request, next_) -> RouteDecision
def tag_tenant(request, next_):
    request.context.setdefault("tenant", "public")
    return next_(request)

@osr.telemetry                                   # Telemetry subclass only
class Collect(osr.enterprise.Telemetry):
    def on_decision(self, request, decision): ...
```

`@target(id, kind, *, name, description, domains, actions, languages, modalities, tags,
min_complexity, max_complexity, supports_tools, cost, latency_ms, quality_prior, examples,
instructions, primary, constraints, metadata, family, effort)` is the general form; `@llm`, `@persona`
(`primary=False`) and `@human` are the other shorthands. The docstring's first paragraph is the
`description` (what routing matches), the rest is `instructions` (what the model is told once selected).

## The short forms (reach for these first)

```python
osr.components.load("targets.yaml", "rules.yaml")     # a catalogue and its rules become components

@osr.handler("gpt-4o", "gpt-4o-mini")                 # code for targets the catalogue declares
def chat(request, **kw): ...                           # unknown id -> ConfigurationError (lists known ids)

osr.rule("pii-stays-home", prefer=["onprem"], pin=True, contains_pii=True)   # rules.yaml in code

@osr.rule(prefer=["human-desk"], weight=0.8, domains=["support"])   # + a predicate fn(request, signals) -> bool
def angry(request, signals): return "unacceptable" in request.text.lower()

@osr.guard                                             # fn(request) -> None | str/False (SecurityError) | RouteRequest
def no_empty(request): return "empty request" if not request.text.strip() else None

@osr.on_decision                                       # fn(request, decision); @on_outcome fn(outcome);
def log_it(request, decision): ...                     # @on_error fn(request, error)

@osr.signal(name="urgent")                             # a scalar lands in signals.extra["urgent"]
def urgent(request, signals): return "asap" in request.text.lower()

d = osr.route("Refund order #123")                     # the registry builds + caches its router (live())
osr.feedback(d, success=True, quality=0.9)             # Outcome from the decision, learned
res = osr.run("Summarise ..."); osr.feedback(res, quality=0.7)   # route + execute + learn; later judgement = correction
await osr.aroute("..."); await osr.arun("...")        # async forms (alive() = AsyncRouter over live())
```

Rule conditions are the `when` keys (`RULE_CONDITIONS`: domains, actions, all_actions, language,
min_complexity, max_complexity, contains_pii, needs_tools, event, workflow, context) - a typo raises
`ConfigurationError`. Declared rules become one `rules` strategy in front of whichever ensemble is used
(custom strategies or the defaults). `live()` is a `Router`, or an `EnterpriseRouter` once a guard,
middleware or hook is registered; it is rebuilt when the registry changes after first use.

With the CLI: `osr --components my_components serve` / `route` (repeatable, `module:setup` calls
`setup(registry)`; `OSR_SERVER_COMPONENTS` for the container) imports the module and applies its declarations
to the router built from the catalogue - `registry.apply(router)` in code; guards, middleware and hooks make
`serve` harden the router so they run.

## Assembling

```python
router = osr.components.router(registry)                    # Router; registered strategies replace defaults
app = osr.components.builder(registry).with_auto_learning().build()      # EnterpriseRouter
RouterBuilder(registry).with_defaults().with_components(reg, middleware=False)  # selective wiring
```

- `registry.strategies() / weights() / rules() / rules_strategy() / extractors() / policy() / middlewares() /
  telemetry_sinks() / targets() / target_registry(base)` materialise fresh instances (handlers bound).
- `add(*instances, weight=)` registers ready-made objects (targets, strategies, rules, ...); duplicates raise
  `ConfigurationError` (`register(..., replace=True)` to override); `unregister`, `clear`, `merge` manage the
  catalogue.
- Plugins: `registry.include("pkg.module")` (decorators run on import; `"pkg.module:setup"` calls
  `setup(registry)`), `registry.discover()` loads `opensmartroute.plugins` entry points.

## Settings (all tunables, no hardcoded numbers)

```python
from opensmartroute.settings import RoutingSettings, WeightSettings
osr.configure(routing=RoutingSettings(narrow_above=200), weights=WeightSettings(rules=3.0))
osr.Router(reg)                               # uses configured settings
osr.Router(reg, settings=osr.Settings())      # isolated library defaults
```

Groups: `routing` (softmax_temperature, slot_quality_floor, skill_set_margin, narrow_above,
narrow_to, cost/latency_norm_scale), `policy` (jailbreak_threshold, safety_tag, data_boundaries,
wildcard_language), `rules`, `capability`, `bandit`, `weights` (one per strategy name).
Environment overlay `OSR_<GROUP>_<FIELD>` (e.g. `OSR_WEIGHTS_CAPABILITY=1.5`); bad values raise
`ConfigurationError(details={"env": key})`. `osr settings [--json]` lists every key.

## Conventions when writing components

- Strategy names are snake_case nouns; decorated classes drop the `Strategy`/`Signal`/`Middleware`/
  `Telemetry` suffix automatically. Scores are clipped to `[0, 1]`; omit a candidate to abstain.
- Never read thresholds from literals - take a `settings: Settings | None = None` argument and call
  `opensmartroute.settings.resolve(settings)`.
- Brand-derived names come from `opensmartroute.branding` (`env_key`, `error_code`, `metadata_key`,
  `logger`, `user_agent`).
