laya 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
laya/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Laya: Fast, non-autoregressive System 1 decision engine with calibrated probabilities."""
2
+
3
+ from .agent import Agent, RLAgent, load
4
+ from .email import clean_email_body, email_questions, email_state
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "Agent",
9
+ "RLAgent",
10
+ "load",
11
+ "clean_email_body",
12
+ "email_questions",
13
+ "email_state",
14
+ "__version__",
15
+ ]
laya/agent.py ADDED
@@ -0,0 +1,200 @@
1
+ """High-level inference runtime for laya System 1 decision models."""
2
+ import json
3
+ import os
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+ import numpy as np
7
+ import torch
8
+
9
+ from .common import (
10
+ QTYPES,
11
+ amp_dtype,
12
+ build_model,
13
+ build_sequence,
14
+ collate_items,
15
+ confidence_from_probs,
16
+ render_options,
17
+ temp_bucket,
18
+ )
19
+
20
+
21
+ def _fix_tokenizer_config(path: str):
22
+ """Ensure tokenizer_config.json can be loaded across all transformers versions."""
23
+ cfg_file = os.path.join(path, "tokenizer", "tokenizer_config.json")
24
+ if not os.path.exists(cfg_file):
25
+ return
26
+ try:
27
+ with open(cfg_file) as f:
28
+ tcfg = json.load(f)
29
+ if tcfg.get("tokenizer_class") in (None, "TokenizersBackend"):
30
+ tcfg["tokenizer_class"] = "PreTrainedTokenizerFast"
31
+ tcfg.pop("backend", None)
32
+ tcfg.pop("is_local", None)
33
+ with open(cfg_file, "w") as f:
34
+ json.dump(tcfg, f, indent=2)
35
+ except Exception:
36
+ pass
37
+
38
+
39
+ class Agent:
40
+ """System 1 decision model runtime: fast, non-autoregressive, calibrated decisions."""
41
+
42
+ def __init__(
43
+ self,
44
+ model_id_or_path: str = "convaiinnovations/rl-agent",
45
+ device: Optional[str] = None,
46
+ token: Optional[str] = None,
47
+ ):
48
+ from safetensors.torch import load_file
49
+ from transformers import AutoTokenizer
50
+
51
+ model_dir = model_id_or_path
52
+ if not os.path.exists(model_dir):
53
+ from huggingface_hub import snapshot_download
54
+
55
+ model_dir = snapshot_download(model_id_or_path, token=token or os.environ.get("HF_TOKEN"))
56
+
57
+ _fix_tokenizer_config(model_dir)
58
+
59
+ cfg_path = os.path.join(model_dir, "rl_agent_config.json")
60
+ with open(cfg_path) as f:
61
+ self.cfg = json.load(f)
62
+
63
+ if device is None:
64
+ if torch.cuda.is_available():
65
+ self.device = torch.device("cuda")
66
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
67
+ self.device = torch.device("mps")
68
+ else:
69
+ self.device = torch.device("cpu")
70
+ else:
71
+ self.device = torch.device(device)
72
+
73
+ tok_dir = os.path.join(model_dir, "tokenizer")
74
+ self.tok = AutoTokenizer.from_pretrained(tok_dir if os.path.exists(tok_dir) else self.cfg.get("encoder"))
75
+
76
+ enc_dir = os.path.join(model_dir, "encoder")
77
+ self.model = build_model(self.cfg, encoder_dir=enc_dir if os.path.exists(enc_dir) else None)
78
+
79
+ weights_path = os.path.join(model_dir, "model.safetensors")
80
+ self.model.load_state_dict(load_file(weights_path), strict=True)
81
+ self.model.to(self.device).eval()
82
+
83
+ self.temperature = self.cfg.get("temperature", [1.0, 1.0, 1.0])
84
+ self.temperature_by_options = self.cfg.get("temperature_by_options", {})
85
+ self.dtype = amp_dtype(self.cfg.get("amp_dtype", "fp16"))
86
+
87
+ if self.device.type == "cuda" and torch.cuda.get_device_capability(self.device)[0] < 8:
88
+ self.dtype = torch.float16
89
+ elif self.device.type in ("cpu", "mps"):
90
+ self.dtype = torch.float32
91
+
92
+ @staticmethod
93
+ def _to_internal(qdef: Dict) -> Dict:
94
+ t = qdef["type"]
95
+ crit = qdef.get("criteria")
96
+ if t == "choice" and isinstance(crit, list):
97
+ crit = {c: None for c in crit}
98
+ ins = qdef["instructions"]
99
+ if not isinstance(ins, str):
100
+ ins = json.dumps(ins)
101
+ return {"t": t, "ins": ins, "crit": crit}
102
+
103
+ @torch.no_grad()
104
+ def system_one(self, state: Union[str, dict, list], questions: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
105
+ """Evaluate typed questions across state in a single, parallel forward pass.
106
+
107
+ Args:
108
+ state: Text string, JSON dict, or conversation turn list.
109
+ questions: Dictionary mapping question_id -> question definition.
110
+ - choice: {"type": "choice", "instructions": "...", "criteria": {"optA": "...", ...}}
111
+ - score: {"type": "score", "instructions": "...", "criteria": ["lvl0", "lvl1", ...]}
112
+ - noul: {"type": "noul", "instructions": "..."}
113
+
114
+ Returns:
115
+ Dictionary with answers, probabilities, calibrated confidence, and token usage.
116
+ """
117
+ ids = list(questions.keys())
118
+ items = []
119
+ max_len = self.cfg.get("max_len", 512)
120
+ head_max_len = self.cfg.get("head_max_len", 192)
121
+
122
+ for qid in ids:
123
+ q = self._to_internal(questions[qid])
124
+ seq, markers = build_sequence(self.tok, state, q, max_len, head_max_len)
125
+ if len(markers) != len(render_options(q)):
126
+ raise ValueError("question %r options exceed head_max_len=%d" % (qid, head_max_len))
127
+ items.append({"ids": seq, "markers": markers, "qtype": QTYPES[q["t"]]})
128
+
129
+ b = collate_items([items], self.tok.pad_token_id)
130
+ use_amp = self.device.type == "cuda"
131
+
132
+ with torch.autocast(device_type=self.device.type, dtype=self.dtype, enabled=use_amp):
133
+ logits, act = self.model(
134
+ b["input_ids"].to(self.device),
135
+ b["attention_mask"].to(self.device),
136
+ b["marker_pos"].to(self.device),
137
+ b["marker_mask"].to(self.device),
138
+ b["qtype"].to(self.device),
139
+ )
140
+
141
+ logits = logits.float().cpu().numpy()
142
+ act = torch.softmax(act.float(), -1).cpu().numpy()
143
+
144
+ answers = {}
145
+ n_tokens = int(b["attention_mask"].sum())
146
+
147
+ for r, qid in enumerate(ids):
148
+ q = self._to_internal(questions[qid])
149
+ k = len(items[r]["markers"])
150
+ qt = QTYPES[q["t"]]
151
+ t_scale = self.temperature_by_options.get(temp_bucket(qt, k), self.temperature[qt])
152
+ z = logits[r, :k] / max(1e-3, float(t_scale))
153
+ p = np.exp(z - z.max())
154
+ p = p / p.sum()
155
+
156
+ conf_score = round(confidence_from_probs(p, k), 4)
157
+ ext = {"act_probability": round(float(act[r, 0]), 4)}
158
+
159
+ if q["t"] == "choice":
160
+ keys = list(q["crit"].keys())
161
+ answers[qid] = {
162
+ "type": "choice",
163
+ "choice": keys[int(p.argmax())],
164
+ "probabilities": {kk: round(float(v), 4) for kk, v in zip(keys, p)},
165
+ "confidence": conf_score,
166
+ "action": ext,
167
+ }
168
+ elif q["t"] == "score":
169
+ exp_score = float((np.arange(k) * p).sum())
170
+ answers[qid] = {
171
+ "type": "score",
172
+ "score": round(exp_score, 4),
173
+ "legend": {str(i): c for i, c in enumerate(q["crit"])},
174
+ "probabilities": {str(i): round(float(v), 4) for i, v in enumerate(p)},
175
+ "confidence": conf_score,
176
+ "action": ext,
177
+ }
178
+ else:
179
+ answers[qid] = {
180
+ "type": "noul",
181
+ "noul": round(float(p[1]), 4),
182
+ "confidence": round(max(float(p[1]), 1.0 - float(p[1])), 4),
183
+ "action": ext,
184
+ }
185
+
186
+ return {
187
+ "model": "laya-rl-agent",
188
+ "answers": answers,
189
+ "usage": {"input_tokens": n_tokens, "output_tokens": 0},
190
+ }
191
+
192
+ predict = system_one
193
+
194
+
195
+ RLAgent = Agent
196
+
197
+
198
+ def load(model_id_or_path: str = "convaiinnovations/rl-agent", device: Optional[str] = None, token: Optional[str] = None) -> Agent:
199
+ """Helper function to load a Laya agent model."""
200
+ return Agent(model_id_or_path, device=device, token=token)
laya/common.py ADDED
@@ -0,0 +1,167 @@
1
+ """Core model architecture, token sequence construction, and confidence estimation for laya."""
2
+ import json
3
+ import math
4
+ import os
5
+ from typing import Dict, List, Optional, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+ QTYPES = {"choice": 0, "score": 1, "noul": 2}
12
+ QTYPE_NAMES = {v: k for k, v in QTYPES.items()}
13
+
14
+
15
+ def serialize_state(state: Union[str, dict, list]) -> str:
16
+ if isinstance(state, str):
17
+ return state
18
+ return json.dumps(state, ensure_ascii=False)
19
+
20
+
21
+ def render_options(q: Dict) -> List[str]:
22
+ """Render option texts in label-index order. Noul is always [false, true]."""
23
+ t, crit = q["t"], q.get("crit")
24
+ if t == "choice":
25
+ return [k if not v else "%s: %s" % (k, v) for k, v in crit.items()]
26
+ if t == "score":
27
+ return ["level %d: %s" % (i, c) for i, c in enumerate(crit)]
28
+ crit = crit or {}
29
+ return [
30
+ "false: " + (crit.get("false") or "no, the statement does not hold"),
31
+ "true: " + (crit.get("true") or "yes, the statement holds"),
32
+ ]
33
+
34
+
35
+ def build_sequence(
36
+ tok,
37
+ state: Union[str, dict, list],
38
+ q: Dict,
39
+ max_len: int = 512,
40
+ head_max_len: int = 192,
41
+ option_order: Optional[List[int]] = None,
42
+ truncate_left: bool = False,
43
+ ):
44
+ """Format: [CLS] <type> instructions [SEP] [MASK] opt0 [MASK] opt1 ... [SEP] state [SEP]."""
45
+ mask_tok = tok.mask_token
46
+ opts = render_options(q)
47
+ order = option_order if option_order is not None else list(range(len(opts)))
48
+ ins = str(q["ins"]).replace(mask_tok, " ")
49
+ head_ids = tok("%s question: %s" % (q["t"], ins), add_special_tokens=False)["input_ids"]
50
+ opt_ids = []
51
+ for i in order:
52
+ opt_ids.append(
53
+ [tok.mask_token_id]
54
+ + tok(" " + opts[i].replace(mask_tok, " "), add_special_tokens=False)["input_ids"][:48]
55
+ )
56
+ opt_budget = head_max_len - sum(len(o) for o in opt_ids)
57
+ if opt_budget < 16:
58
+ per = max(4, (head_max_len - 16) // max(1, len(opt_ids)))
59
+ opt_ids = [o[:per] for o in opt_ids]
60
+ opt_budget = head_max_len - sum(len(o) for o in opt_ids)
61
+ head_ids = head_ids[: max(8, opt_budget)]
62
+ ids = [tok.cls_token_id] + head_ids + [tok.sep_token_id]
63
+ markers = []
64
+ for o in opt_ids:
65
+ markers.append(len(ids))
66
+ ids.extend(o)
67
+ ids.append(tok.sep_token_id)
68
+ room = max(0, max_len - len(ids) - 1)
69
+ st = tok(serialize_state(state).replace(mask_tok, " "), add_special_tokens=False)["input_ids"]
70
+ st = st[-room:] if truncate_left else st[:room]
71
+ ids = ids + st + [tok.sep_token_id]
72
+ return ids[:max_len], [m for m in markers if m < max_len]
73
+
74
+
75
+ class DecisionModel(nn.Module):
76
+ """Bidirectional transformer encoder backbone + typed decision head."""
77
+
78
+ def __init__(self, encoder: nn.Module, head_layers: int = 2, n_act: int = 2, dropout: float = 0.1):
79
+ super().__init__()
80
+ self.encoder = encoder
81
+ d = encoder.config.hidden_size
82
+ nhead = max(1, d // 64)
83
+ layer = nn.TransformerEncoderLayer(d, nhead, 4 * d, dropout, batch_first=True, norm_first=True)
84
+ self.head = nn.TransformerEncoder(layer, head_layers, enable_nested_tensor=False) if head_layers > 0 else None
85
+ self.type_emb = nn.Embedding(3, d)
86
+ self.scorer = nn.Sequential(nn.LayerNorm(d), nn.Linear(d, d), nn.GELU(), nn.Linear(d, 1))
87
+ self.act_head = nn.Sequential(nn.Linear(d + 4, 256), nn.GELU(), nn.Linear(256, n_act))
88
+ self.register_buffer("temperature", torch.ones(3))
89
+ self.head_checkpointing = False
90
+
91
+ def forward(self, input_ids, attention_mask, marker_pos, marker_mask, qtype, detach_encoder: bool = False):
92
+ h = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
93
+ if detach_encoder:
94
+ h = h.detach()
95
+ h = h + self.type_emb(qtype)[:, None, :]
96
+ if self.head is not None:
97
+ pad = ~attention_mask.bool()
98
+ for layer in self.head.layers:
99
+ h = layer(h, src_key_padding_mask=pad)
100
+ idx = marker_pos.clamp(min=0)[:, :, None].expand(-1, -1, h.size(-1))
101
+ m = torch.gather(h, 1, idx)
102
+ logits = self.scorer(m).squeeze(-1).float()
103
+ logits = logits.masked_fill(~marker_mask, -1e4)
104
+
105
+ p = torch.softmax(logits.detach(), -1)
106
+ k = marker_mask.sum(-1).clamp(min=2).float()
107
+ ent = -(p * torch.log(p.clamp_min(1e-9))).sum(-1) / torch.log(k)
108
+ top2 = p.topk(2, -1).values
109
+ feats = torch.stack([top2[:, 0], top2[:, 0] - top2[:, 1], ent, k / 255.0], -1)
110
+ pooled = h[:, 0].float()
111
+ act_logits = self.act_head(torch.cat([pooled, feats], -1))
112
+ return logits, act_logits
113
+
114
+
115
+ def build_model(cfg: Dict, encoder_dir: Optional[str] = None) -> DecisionModel:
116
+ from transformers import AutoConfig, AutoModel
117
+
118
+ if encoder_dir and os.path.exists(encoder_dir):
119
+ ecfg = AutoConfig.from_pretrained(encoder_dir)
120
+ enc = AutoModel.from_config(ecfg, attn_implementation="sdpa")
121
+ else:
122
+ enc = AutoModel.from_pretrained(cfg["encoder"], attn_implementation="sdpa")
123
+ return DecisionModel(enc, cfg.get("head_layers", 2), len(cfg.get("act_costs", {})) + 1)
124
+
125
+
126
+ def confidence_from_probs(p: np.ndarray, k: int) -> float:
127
+ """Normalized Shannon entropy confidence: 1 - H(p) / log(k)."""
128
+ if k < 2:
129
+ return 1.0
130
+ p = p[:k]
131
+ ent = -(p * np.log(np.clip(p, 1e-12, 1.0))).sum()
132
+ return float(np.clip(1.0 - ent / math.log(k), 0.0, 1.0))
133
+
134
+
135
+ def temp_bucket(qtype: int, k: int) -> str:
136
+ size = "2" if k <= 2 else "3-5" if k <= 5 else "6-10" if k <= 10 else "11+"
137
+ return "%s:%s" % (QTYPE_NAMES[int(qtype)], size)
138
+
139
+
140
+ def amp_dtype(name: Optional[str]) -> torch.dtype:
141
+ return torch.bfloat16 if name == "bf16" else torch.float16
142
+
143
+
144
+ def collate_items(batch, pad_id: int):
145
+ items = [it for group in batch for it in group]
146
+ if not items:
147
+ return None
148
+ n, L = len(items), max(len(it["ids"]) for it in items)
149
+ kmax = max(len(it["markers"]) for it in items)
150
+ ids = torch.full((n, L), pad_id, dtype=torch.long)
151
+ att = torch.zeros((n, L), dtype=torch.long)
152
+ mpos = torch.zeros((n, kmax), dtype=torch.long)
153
+ mmask = torch.zeros((n, kmax), dtype=torch.bool)
154
+ for i, it in enumerate(items):
155
+ ids[i, : len(it["ids"])] = torch.tensor(it["ids"])
156
+ att[i, : len(it["ids"])] = 1
157
+ k = len(it["markers"])
158
+ mpos[i, :k] = torch.tensor(it["markers"])
159
+ mmask[i, :k] = True
160
+ return {
161
+ "input_ids": ids,
162
+ "attention_mask": att,
163
+ "marker_pos": mpos,
164
+ "marker_mask": mmask,
165
+ "qtype": torch.tensor([it["qtype"] for it in items]),
166
+ "meta": [{k: it[k] for k in it if k not in ("ids", "markers")} for it in items],
167
+ }
laya/email.py ADDED
@@ -0,0 +1,90 @@
1
+ """Email utilities for cleaning and structuring email inputs in laya."""
2
+ import re
3
+ from typing import Dict, Optional
4
+
5
+ _QUOTE_HEADERS = [
6
+ re.compile(r"^\s*On .{0,300}wrote:\s*$", re.I),
7
+ re.compile(r"^\s*-{2,}\s*(Original|Forwarded) Message\s*-{2,}", re.I),
8
+ re.compile(r"^\s*_{8,}\s*$"),
9
+ re.compile(r"^\s*From:\s.+$", re.I),
10
+ ]
11
+ _SIGNATURE_MARKERS = [
12
+ re.compile(r"^\s*--\s*$"),
13
+ re.compile(r"^\s*(best|kind|warm|many thanks|thanks|thank you|regards|cheers|sincerely)[\w ,!.]*$", re.I),
14
+ re.compile(r"^\s*sent from my (iphone|android|mobile|ipad)", re.I),
15
+ ]
16
+ _DISCLAIMER = re.compile(
17
+ r"(confidential|intended (solely )?for the (use of the )?(named )?(addressee|recipient)|"
18
+ r"if you (have )?received this (e-?mail|message) in error)",
19
+ re.I,
20
+ )
21
+
22
+
23
+ def clean_email_body(body: str, max_chars: int = 3000) -> str:
24
+ """Remove quoted email history, signatures and disclaimers to keep input focused."""
25
+ text = (body or "").replace("\r\n", "\n").replace("\r", "\n").replace("\\n", "\n")
26
+ lines = []
27
+ for line in text.split("\n"):
28
+ if any(p.match(line) for p in _QUOTE_HEADERS) and lines:
29
+ break
30
+ if line.lstrip().startswith(">"):
31
+ continue
32
+ lines.append(line.rstrip())
33
+ cut = len(lines)
34
+ for i in range(max(1, min(int(len(lines) * 0.6), len(lines) - 8)), len(lines)):
35
+ if len(lines[i].strip()) <= 40 and any(p.match(lines[i]) for p in _SIGNATURE_MARKERS):
36
+ cut = i
37
+ break
38
+ lines = lines[:cut]
39
+ paragraphs = [p for p in re.split(r"\n\s*\n", "\n".join(lines)) if not _DISCLAIMER.search(p)]
40
+ text = re.sub(r"[ \t]+", " ", "\n\n".join(p.strip() for p in paragraphs if p.strip()))
41
+ return text[:max_chars]
42
+
43
+
44
+ def email_state(subject: str, body: str, sender: Optional[str] = None, clean: bool = True, **extra) -> Dict:
45
+ """Construct a clean state dictionary for email classification."""
46
+ state = {
47
+ "subject": (subject or "").strip(),
48
+ "body": clean_email_body(body) if clean else (body or ""),
49
+ }
50
+ if sender:
51
+ state["from"] = sender
52
+ state.update({k: v for k, v in extra.items() if v is not None})
53
+ return state
54
+
55
+
56
+ def email_questions(categories: Optional[Dict[str, str]] = None) -> Dict:
57
+ """Standard pre-built questions for email triage."""
58
+ categories = categories or {
59
+ "billing": "invoices, payments, refunds",
60
+ "technical": "bugs, outages, integrations",
61
+ "sales": "pricing, demos, new purchases",
62
+ "security": "phishing, scams, account compromise",
63
+ "hr": "hiring, leave, payroll",
64
+ "other": "none of the above",
65
+ }
66
+ return {
67
+ "category": {
68
+ "type": "choice",
69
+ "instructions": "Which team should handle the email in `body`?",
70
+ "criteria": categories,
71
+ },
72
+ "is_spam": {
73
+ "type": "noul",
74
+ "instructions": "Is this email unsolicited spam or bulk marketing?",
75
+ },
76
+ "is_phishing": {
77
+ "type": "noul",
78
+ "instructions": "Is this email a phishing or scam attempt to steal money, credentials, or personal data?",
79
+ "criteria": {"true": "phishing, scam, or fraud", "false": "a legitimate email"},
80
+ },
81
+ "urgency": {
82
+ "type": "score",
83
+ "instructions": "How urgent is the request in `body`?",
84
+ "criteria": ["no time pressure", "needs attention soon", "blocking issue or hard deadline"],
85
+ },
86
+ "needs_reply": {
87
+ "type": "noul",
88
+ "instructions": "Does the sender expect a reply?",
89
+ },
90
+ }
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: laya
3
+ Version: 0.1.0
4
+ Summary: Fast, non-autoregressive System 1 decision engine with calibrated probabilities
5
+ Author: Convai Innovations
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://huggingface.co/convaiinnovations/rl-agent
8
+ Project-URL: Demo, https://huggingface.co/spaces/convaiinnovations/rl-agent-demo
9
+ Keywords: decision-model,rlcd,calibration,system-one,routing,guardrails,moderation,triage
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: torch>=2.0.0
23
+ Requires-Dist: transformers>=4.45.0
24
+ Requires-Dist: safetensors>=0.4.0
25
+ Requires-Dist: huggingface_hub>=0.20.0
26
+ Requires-Dist: numpy>=1.20.0
27
+ Dynamic: requires-python
28
+
29
+ # Laya (लय)
30
+
31
+ Fast, non-autoregressive System 1 decision engine with mathematically calibrated probabilities.
32
+
33
+ Laya lets you evaluate typed questions (`choice`, `score`, `noul`) over any state (text, email, ticket, or JSON document) in **a single forward pass (~33–38 ms on GPU)**. It produces structured decision outputs and calibrated confidence scores without text generation, token streaming, or hallucinations.
34
+
35
+ Compatible with [RL Agent models on Hugging Face](https://huggingface.co/convaiinnovations/rl-agent).
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install laya
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ import laya
51
+
52
+ # 1. Load the model from Hugging Face Hub (auto-downloads weights)
53
+ agent = laya.load("convaiinnovations/rl-agent")
54
+
55
+ # 2. Provide any state (string or dictionary)
56
+ state = {
57
+ "from": "user@acme.com",
58
+ "subject": "Duplicate charge on invoice #4411",
59
+ "body": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan."
60
+ }
61
+
62
+ # 3. Define your typed questions
63
+ questions = {
64
+ # choice: categorical selection with probabilities & confidence
65
+ "department": {
66
+ "type": "choice",
67
+ "instructions": "Which department should handle this email?",
68
+ "criteria": {
69
+ "billing": "invoices, payments, refunds",
70
+ "technical": "bugs, outages, system errors",
71
+ "sales": "pricing, new contracts",
72
+ "other": "everything else"
73
+ }
74
+ },
75
+ # score: placement on an ordinal rubric
76
+ "urgency": {
77
+ "type": "score",
78
+ "instructions": "How urgent is this request?",
79
+ "criteria": ["not urgent", "soon", "critical deadline or blocking issue"]
80
+ },
81
+ # noul: calibrated boolean probability P(true)
82
+ "churn_risk": {
83
+ "type": "noul",
84
+ "instructions": "Does the user threaten to cancel or leave?"
85
+ },
86
+ "is_phishing": {
87
+ "type": "noul",
88
+ "instructions": "Is this email a phishing or scam attempt?"
89
+ }
90
+ }
91
+
92
+ # 4. Run all questions in ONE single forward pass (~35 ms on GPU)
93
+ result = agent.predict(state, questions)
94
+ answers = result["answers"]
95
+
96
+ print("Department :", answers["department"]["choice"])
97
+ # -> billing (confidence: 0.94)
98
+
99
+ print("Urgency :", answers["urgency"]["score"])
100
+ # -> 1.84 / 2.0
101
+
102
+ print("Churn Risk :", answers["churn_risk"]["noul"])
103
+ # -> 0.892 (89.2% probability)
104
+
105
+ print("Phishing :", answers["is_phishing"]["noul"])
106
+ # -> 0.008 (0.8% probability)
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Automated Confidence Gating
112
+
113
+ Because Laya's probabilities are trained with strictly proper scoring rules (RLCD), confidence scores are statistically meaningful:
114
+
115
+ ```python
116
+ dept = answers["department"]["choice"]
117
+ conf = answers["department"]["confidence"]
118
+
119
+ if conf >= 0.85:
120
+ # High confidence: automated action without human in the loop
121
+ route_automatically(dept)
122
+ else:
123
+ # Low confidence: escalate to human triage
124
+ escalate_to_human_agent(dept, reason=f"Low confidence ({conf:.2f})")
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Decision Primitives
130
+
131
+ | Primitive | Output | Use Cases |
132
+ |---|---|---|
133
+ | **`choice`** | Top label, probabilities per option, confidence | Department routing, intent classification, topic categorization |
134
+ | **`score`** | Expected level on ordinal rubric, distribution, confidence | Frustration level, ticket urgency, harm severity |
135
+ | **`noul`** | Calibrated probability $P(\text{true}) \in [0.0, 1.0]$ | Phishing detection, spam filtering, jailbreak detection, churn risk |
136
+
137
+ ---
138
+
139
+ ## Live Demo & Resources
140
+
141
+ * **Hugging Face Model:** [convaiinnovations/rl-agent](https://huggingface.co/convaiinnovations/rl-agent)
142
+ * **Interactive Web Demo:** [convaiinnovations/rl-agent-demo](https://huggingface.co/spaces/convaiinnovations/rl-agent-demo)
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ Apache 2.0. Developed by Convai Innovations.
@@ -0,0 +1,8 @@
1
+ laya/__init__.py,sha256=2iGbmVceeH3D51eGy_ejlyJJXSG_5wf4ViCF_4H_k1M,362
2
+ laya/agent.py,sha256=G-cZCL_YVjzjWT4XfpBtIm4isdGCXmlJhqSggh7Uuz8,7656
3
+ laya/common.py,sha256=7kt__IyYpWnSowY38AR72qRrSNsjbpmtsT0vhS6MuXA,6706
4
+ laya/email.py,sha256=VROPYET4mL3v29iqugjOW00-Ygg8YWle3wb0bXHEjSw,3606
5
+ laya-0.1.0.dist-info/METADATA,sha256=KRzGX_cEB_HZLAiNCQs4c7CxWNGEqRloqVOn5VR_C-g,4963
6
+ laya-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ laya-0.1.0.dist-info/top_level.txt,sha256=8JpyeuR1X7myxlm4zABQAq_B0Fd6kxcnv-eC2a0P-2U,5
8
+ laya-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ laya