lyboai 0.1.0__tar.gz

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.
lyboai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: lyboai
3
+ Version: 0.1.0
4
+ Summary: LyboAI Mobile Edge Runtime — Python SDK for on-device AI agent creation, evaluation, and training workflows
5
+ Author: LyboAI
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://lyboai.app/developers
8
+ Project-URL: Documentation, https://lyboai.app/developers/docs
9
+ Project-URL: Repository, https://github.com/rajandua20/lyboai-mobile-edge-runtime
10
+ Keywords: on-device-ai,edge-ai,llm,slm,agents,offline-ai,private-ai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
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
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+
24
+ # lyboai (Python SDK)
25
+
26
+ Python is the **builder's language** for the LyboAI ecosystem: create agents,
27
+ package and sign capability packs, run evaluations, tune routing, and export
28
+ training datasets. Agents execute in the Rust runtime — locally via the
29
+ `lybo` binary (this SDK drives it over the JSON-lines protocol, identical to
30
+ the on-device C ABI) and on phones via the mobile bindings.
31
+
32
+ ```bash
33
+ pip install -e sdk/python # from the repo, or publish to your index
34
+ cargo build -p lybo-cli # the runtime the SDK drives
35
+ ```
36
+
37
+ ## Create an agent
38
+
39
+ ```python
40
+ from lyboai import Client
41
+
42
+ with Client(data_dir="/tmp/agent", binary="./target/debug/lybo") as lybo:
43
+ lybo.install_knowledge(doc_id="handbook", title="Team Handbook",
44
+ body="Support tickets are answered within 24 hours.")
45
+ lybo.register_skill(skill_id="app.ask", name="Ask the handbook",
46
+ triggers=["what does the handbook say"],
47
+ keywords=["handbook"], pattern="retrieval")
48
+
49
+ session = lybo.start_session()
50
+ out = lybo.run_to_completion(session, "what does the handbook say about tickets")
51
+ print(out["output"]["text"], out["output"]["sources"])
52
+ ```
53
+
54
+ ## Train routing from labelled examples
55
+
56
+ ```python
57
+ from lyboai import train_triggers
58
+
59
+ learned = train_triggers({
60
+ "faq.ask": ["what does the guide say about refunds", "search the faq"],
61
+ "task.plan": ["plan my week", "break this goal into steps"],
62
+ })
63
+ # → per-skill {"triggers": [...], "keywords": [...]} via class-discriminative
64
+ # tf-idf; feeds both the keyword tier and the on-device semantic centroids.
65
+ ```
66
+
67
+ ## Build, sign, evaluate, ship a pack
68
+
69
+ ```python
70
+ from lyboai import PackBuilder, sign_pack, evaluate
71
+
72
+ src = (PackBuilder("acme.faq", "FAQ", "1.0.0", publisher="acme")
73
+ .add_knowledge("faq", "Store FAQ", "Refunds take five business days.")
74
+ .add_skill("faq.ask", "Ask FAQ", pattern="retrieval",
75
+ triggers=learned["faq.ask"]["triggers"],
76
+ keywords=learned["faq.ask"]["keywords"],
77
+ knowledge_scopes=["acme.faq"])
78
+ .add_eval_case("faq.suite", "routes", "what does the guide say about refunds",
79
+ expect_skill="faq.ask", expect_contains=["refund"])
80
+ .save("pack.json"))
81
+ sign_pack("pack.json", SECRET_KEY_HEX, "dist/acme-faq.lybopack")
82
+
83
+ with Client(data_dir="/tmp/qa", trust=[f"acme:{PUBLIC_KEY_HEX}"]) as lybo:
84
+ lybo.install_pack_file("dist/acme-faq.lybopack")
85
+ report = evaluate(lybo, suite) # routing + outcome + content checks
86
+ assert report.ok, report.summary() # gate your CI on this
87
+ ```
88
+
89
+ ## Export training data (privacy-preserving)
90
+
91
+ ```python
92
+ from lyboai import export_routing_dataset
93
+ export_routing_dataset(lybo, "routing.jsonl")
94
+ # skill ids, confidence, outcomes, latency, model ids — never raw user content.
95
+ ```
96
+
97
+ Run the SDK's own tests: `LYBO_BIN=./target/debug/lybo python3 -m unittest discover sdk/python/tests`
lyboai-0.1.0/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # lyboai (Python SDK)
2
+
3
+ Python is the **builder's language** for the LyboAI ecosystem: create agents,
4
+ package and sign capability packs, run evaluations, tune routing, and export
5
+ training datasets. Agents execute in the Rust runtime — locally via the
6
+ `lybo` binary (this SDK drives it over the JSON-lines protocol, identical to
7
+ the on-device C ABI) and on phones via the mobile bindings.
8
+
9
+ ```bash
10
+ pip install -e sdk/python # from the repo, or publish to your index
11
+ cargo build -p lybo-cli # the runtime the SDK drives
12
+ ```
13
+
14
+ ## Create an agent
15
+
16
+ ```python
17
+ from lyboai import Client
18
+
19
+ with Client(data_dir="/tmp/agent", binary="./target/debug/lybo") as lybo:
20
+ lybo.install_knowledge(doc_id="handbook", title="Team Handbook",
21
+ body="Support tickets are answered within 24 hours.")
22
+ lybo.register_skill(skill_id="app.ask", name="Ask the handbook",
23
+ triggers=["what does the handbook say"],
24
+ keywords=["handbook"], pattern="retrieval")
25
+
26
+ session = lybo.start_session()
27
+ out = lybo.run_to_completion(session, "what does the handbook say about tickets")
28
+ print(out["output"]["text"], out["output"]["sources"])
29
+ ```
30
+
31
+ ## Train routing from labelled examples
32
+
33
+ ```python
34
+ from lyboai import train_triggers
35
+
36
+ learned = train_triggers({
37
+ "faq.ask": ["what does the guide say about refunds", "search the faq"],
38
+ "task.plan": ["plan my week", "break this goal into steps"],
39
+ })
40
+ # → per-skill {"triggers": [...], "keywords": [...]} via class-discriminative
41
+ # tf-idf; feeds both the keyword tier and the on-device semantic centroids.
42
+ ```
43
+
44
+ ## Build, sign, evaluate, ship a pack
45
+
46
+ ```python
47
+ from lyboai import PackBuilder, sign_pack, evaluate
48
+
49
+ src = (PackBuilder("acme.faq", "FAQ", "1.0.0", publisher="acme")
50
+ .add_knowledge("faq", "Store FAQ", "Refunds take five business days.")
51
+ .add_skill("faq.ask", "Ask FAQ", pattern="retrieval",
52
+ triggers=learned["faq.ask"]["triggers"],
53
+ keywords=learned["faq.ask"]["keywords"],
54
+ knowledge_scopes=["acme.faq"])
55
+ .add_eval_case("faq.suite", "routes", "what does the guide say about refunds",
56
+ expect_skill="faq.ask", expect_contains=["refund"])
57
+ .save("pack.json"))
58
+ sign_pack("pack.json", SECRET_KEY_HEX, "dist/acme-faq.lybopack")
59
+
60
+ with Client(data_dir="/tmp/qa", trust=[f"acme:{PUBLIC_KEY_HEX}"]) as lybo:
61
+ lybo.install_pack_file("dist/acme-faq.lybopack")
62
+ report = evaluate(lybo, suite) # routing + outcome + content checks
63
+ assert report.ok, report.summary() # gate your CI on this
64
+ ```
65
+
66
+ ## Export training data (privacy-preserving)
67
+
68
+ ```python
69
+ from lyboai import export_routing_dataset
70
+ export_routing_dataset(lybo, "routing.jsonl")
71
+ # skill ids, confidence, outcomes, latency, model ids — never raw user content.
72
+ ```
73
+
74
+ Run the SDK's own tests: `LYBO_BIN=./target/debug/lybo python3 -m unittest discover sdk/python/tests`
@@ -0,0 +1,40 @@
1
+ """LyboAI Mobile Edge Runtime — Python SDK.
2
+
3
+ Python is the *builder's* language in the LyboAI ecosystem: you use it to
4
+ create agents (skills, workflows, knowledge), package and sign capability
5
+ packs, run evaluations, and tune routing from labelled examples. The agents
6
+ themselves run inside the Rust runtime — on desktops via the `lybo` binary
7
+ (which this SDK drives over the JSON-lines protocol) and on phones via the
8
+ mobile bindings.
9
+
10
+ Quick start::
11
+
12
+ from lyboai import Client
13
+
14
+ with Client(data_dir="/tmp/agent") as lybo:
15
+ lybo.install_knowledge(doc_id="h", title="Handbook",
16
+ body="Tickets are answered within 24 hours.",
17
+ scope="app")
18
+ lybo.register_skill(skill_id="app.ask", name="Ask",
19
+ triggers=["what does the handbook say"],
20
+ keywords=["handbook"], pattern="retrieval")
21
+ session = lybo.start_session()
22
+ out = lybo.submit(session, "what does the handbook say about tickets")
23
+ print(out["output"]["text"])
24
+ """
25
+
26
+ from .client import Client, LyboError
27
+ from .packs import PackBuilder, sign_pack
28
+ from .training import evaluate, export_routing_dataset, train_triggers
29
+
30
+ __all__ = [
31
+ "Client",
32
+ "LyboError",
33
+ "PackBuilder",
34
+ "sign_pack",
35
+ "evaluate",
36
+ "export_routing_dataset",
37
+ "train_triggers",
38
+ ]
39
+
40
+ __version__ = "0.1.0"
@@ -0,0 +1,195 @@
1
+ """Protocol client: drives a local `lybo serve` runtime over JSON lines.
2
+
3
+ The protocol is identical to the C ABI used on devices, so anything built and
4
+ validated here behaves the same inside a mobile app.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ from typing import Any, Iterable, Optional
13
+
14
+
15
+ class LyboError(RuntimeError):
16
+ """Raised when the runtime returns ok=false."""
17
+
18
+
19
+ class Client:
20
+ """Synchronous client bound to a spawned `lybo serve` process."""
21
+
22
+ def __init__(
23
+ self,
24
+ data_dir: str,
25
+ binary: Optional[str] = None,
26
+ trust: Optional[Iterable[str]] = None, # "publisher:pubkeyhex"
27
+ packs_dir: Optional[str] = None,
28
+ model_cmd: Optional[str] = None,
29
+ semantic: bool = True,
30
+ ) -> None:
31
+ binary = binary or shutil.which("lybo") or "lybo"
32
+ args = [binary, "serve", "--data-dir", data_dir]
33
+ if semantic:
34
+ args.append("--semantic")
35
+ for t in trust or []:
36
+ args += ["--trust", t]
37
+ if packs_dir:
38
+ args += ["--packs", packs_dir]
39
+ if model_cmd:
40
+ args += ["--model-cmd", model_cmd]
41
+ self._proc = subprocess.Popen(
42
+ args,
43
+ stdin=subprocess.PIPE,
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.DEVNULL,
46
+ text=True,
47
+ )
48
+
49
+ # -- protocol core -----------------------------------------------------
50
+
51
+ def request(self, cmd: str, **params: Any) -> Any:
52
+ """Send one protocol request; return `data` or raise LyboError."""
53
+ payload = {"cmd": cmd, **{k: v for k, v in params.items() if v is not None}}
54
+ assert self._proc.stdin and self._proc.stdout
55
+ self._proc.stdin.write(json.dumps(payload) + "\n")
56
+ self._proc.stdin.flush()
57
+ line = self._proc.stdout.readline()
58
+ if not line:
59
+ raise LyboError("runtime closed the connection")
60
+ resp = json.loads(line)
61
+ if not resp.get("ok"):
62
+ raise LyboError(resp.get("error", {}).get("message", "unknown error"))
63
+ return resp.get("data")
64
+
65
+ # -- sessions ------------------------------------------------------------
66
+
67
+ def start_session(self, thread_id: Optional[str] = None) -> str:
68
+ return self.request("start_session", thread_id=thread_id)["session_id"]
69
+
70
+ def submit(self, session_id: str, text: str) -> dict:
71
+ return self.request("submit_input", session_id=session_id, text=text)
72
+
73
+ def resolve_approval(self, approval_id: str, approved: bool) -> dict:
74
+ return self.request("resolve_approval", approval_id=approval_id, approved=approved)
75
+
76
+ def run_to_completion(
77
+ self,
78
+ session_id: str,
79
+ text: str,
80
+ answers: Optional[list] = None,
81
+ approve: bool = True,
82
+ max_hops: int = 8,
83
+ ) -> dict:
84
+ """Submit input and automatically satisfy question/approval pauses —
85
+ the workhorse for evaluation and scripted testing."""
86
+ answers = list(answers or [])
87
+ out = self.submit(session_id, text)
88
+ hops = 0
89
+ while out.get("state") in ("awaiting_input", "awaiting_approval") and hops < max_hops:
90
+ hops += 1
91
+ if out["state"] == "awaiting_input":
92
+ answer = answers.pop(0) if answers else "unknown"
93
+ out = self.submit(session_id, str(answer))
94
+ else:
95
+ out = self.resolve_approval(out["approval_id"], approve)
96
+ return out
97
+
98
+ # -- events / approvals ----------------------------------------------------
99
+
100
+ def poll_events(self, max_events: int = 200) -> list:
101
+ return self.request("poll_events", max=max_events)
102
+
103
+ def pending_approvals(self) -> list:
104
+ return self.request("pending_approvals")
105
+
106
+ # -- registration ------------------------------------------------------------
107
+
108
+ def register_skill(
109
+ self,
110
+ skill_id: str,
111
+ name: str,
112
+ triggers: list,
113
+ keywords: list,
114
+ pattern: Optional[str] = None,
115
+ workflow_id: Optional[str] = None,
116
+ description: str = "",
117
+ allowed_tools: Optional[list] = None,
118
+ confirmation: str = "sensitive",
119
+ knowledge_scopes: Optional[list] = None,
120
+ output_schema: Optional[dict] = None,
121
+ ) -> None:
122
+ execution = (
123
+ {"kind": "workflow", "workflow_id": workflow_id}
124
+ if workflow_id
125
+ else {"kind": "pattern", "pattern": pattern or "simple_chat"}
126
+ )
127
+ self.request(
128
+ "register_skill",
129
+ skill={
130
+ "skill_id": skill_id,
131
+ "name": name,
132
+ "description": description,
133
+ "pack_id": "app",
134
+ "triggers": triggers,
135
+ "keywords": keywords,
136
+ "execution": execution,
137
+ "allowed_tools": allowed_tools or [],
138
+ "confirmation": confirmation,
139
+ "knowledge_scopes": knowledge_scopes or [],
140
+ "prompt_template": None,
141
+ "output_schema": output_schema,
142
+ },
143
+ )
144
+
145
+ def register_workflow(self, workflow: dict) -> None:
146
+ self.request("register_workflow", workflow=workflow)
147
+
148
+ def install_knowledge(self, doc_id: str, title: str, body: str, scope: str = "app",
149
+ source: Optional[str] = None) -> int:
150
+ return self.request(
151
+ "install_knowledge",
152
+ document={"doc_id": doc_id, "title": title, "body": body,
153
+ "scope": scope, "source": source},
154
+ )["chunks"]
155
+
156
+ # -- packs -----------------------------------------------------------------
157
+
158
+ def install_pack(self, pack: dict, bundled_unsigned_ok: bool = False) -> dict:
159
+ return self.request("install_pack", pack=pack, bundled_unsigned_ok=bundled_unsigned_ok)
160
+
161
+ def install_pack_file(self, path: str) -> dict:
162
+ with open(path, "r", encoding="utf-8") as f:
163
+ return self.install_pack(json.load(f))
164
+
165
+ def list_packs(self) -> list:
166
+ return self.request("list_packs")
167
+
168
+ # -- observability ------------------------------------------------------------
169
+
170
+ def recent_traces(self, limit: int = 20) -> list:
171
+ return self.request("recent_traces", limit=limit)
172
+
173
+ def verify_trace_chain(self) -> bool:
174
+ return bool(self.request("verify_trace_chain")["valid"])
175
+
176
+ # -- lifecycle ------------------------------------------------------------------
177
+
178
+ def close(self) -> None:
179
+ try:
180
+ if self._proc.stdin:
181
+ self._proc.stdin.write("exit\n")
182
+ self._proc.stdin.flush()
183
+ except Exception:
184
+ pass
185
+ self._proc.terminate()
186
+ try:
187
+ self._proc.wait(timeout=5)
188
+ except subprocess.TimeoutExpired:
189
+ self._proc.kill()
190
+
191
+ def __enter__(self) -> "Client":
192
+ return self
193
+
194
+ def __exit__(self, *exc: Any) -> None:
195
+ self.close()
@@ -0,0 +1,149 @@
1
+ """Pack authoring: build capability-pack documents in Python and sign them
2
+ with the `lybo` CLI (the secret key never touches this SDK's memory model —
3
+ signing shells out so keys can stay in your vault/CI environment)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import subprocess
9
+ from typing import Any, Optional
10
+
11
+
12
+ class PackBuilder:
13
+ """Fluent builder for `.lybopack` source documents."""
14
+
15
+ def __init__(
16
+ self,
17
+ pack_id: str,
18
+ name: str,
19
+ version: str,
20
+ publisher: str,
21
+ description: str = "",
22
+ min_runtime_version: str = "0.1.0",
23
+ permissions: Optional[list] = None,
24
+ observability: str = "local_only",
25
+ ) -> None:
26
+ self.doc: dict = {
27
+ "manifest": {
28
+ "pack_id": pack_id,
29
+ "name": name,
30
+ "version": version,
31
+ "publisher": publisher,
32
+ "publisher_key": "",
33
+ "description": description,
34
+ "domain": "",
35
+ "min_runtime_version": min_runtime_version,
36
+ "platforms": ["ios", "android"],
37
+ "regions": [],
38
+ "permissions": permissions or [],
39
+ "model_requirements": [],
40
+ "skills": [],
41
+ "workflows": [],
42
+ "knowledge_docs": 0,
43
+ "observability": observability,
44
+ "content_sha256": "",
45
+ "rollback_to": None,
46
+ },
47
+ "content": {
48
+ "skills": [],
49
+ "workflows": [],
50
+ "knowledge": [],
51
+ "prompts": [],
52
+ "policies": [],
53
+ "eval_suites": [],
54
+ "ui": None,
55
+ },
56
+ "signature": None,
57
+ }
58
+
59
+ # -- content -----------------------------------------------------------
60
+
61
+ def add_skill(
62
+ self,
63
+ skill_id: str,
64
+ name: str,
65
+ triggers: list,
66
+ keywords: list,
67
+ pattern: Optional[str] = None,
68
+ workflow_id: Optional[str] = None,
69
+ description: str = "",
70
+ allowed_tools: Optional[list] = None,
71
+ confirmation: str = "sensitive",
72
+ knowledge_scopes: Optional[list] = None,
73
+ output_schema: Optional[dict] = None,
74
+ ) -> "PackBuilder":
75
+ execution: dict[str, Any] = (
76
+ {"kind": "workflow", "workflow_id": workflow_id}
77
+ if workflow_id
78
+ else {"kind": "pattern", "pattern": pattern or "simple_chat"}
79
+ )
80
+ self.doc["content"]["skills"].append(
81
+ {
82
+ "skill_id": skill_id,
83
+ "name": name,
84
+ "description": description,
85
+ "pack_id": "",
86
+ "triggers": triggers,
87
+ "keywords": keywords,
88
+ "execution": execution,
89
+ "allowed_tools": allowed_tools or [],
90
+ "confirmation": confirmation,
91
+ "knowledge_scopes": knowledge_scopes or [],
92
+ "prompt_template": None,
93
+ "output_schema": output_schema,
94
+ }
95
+ )
96
+ self.doc["manifest"]["skills"].append(skill_id)
97
+ return self
98
+
99
+ def add_workflow(self, workflow: dict) -> "PackBuilder":
100
+ workflow.setdefault("pack_id", "")
101
+ self.doc["content"]["workflows"].append(workflow)
102
+ self.doc["manifest"]["workflows"].append(workflow["workflow_id"])
103
+ return self
104
+
105
+ def add_knowledge(self, doc_id: str, title: str, body: str,
106
+ source: Optional[str] = None) -> "PackBuilder":
107
+ self.doc["content"]["knowledge"].append(
108
+ {"doc_id": doc_id, "title": title, "body": body, "scope": "",
109
+ "source": source, "region": None, "review_by_ms": None, "expires_ms": None}
110
+ )
111
+ self.doc["manifest"]["knowledge_docs"] = len(self.doc["content"]["knowledge"])
112
+ return self
113
+
114
+ def add_eval_case(self, suite_id: str, case_id: str, input_text: str,
115
+ expect_skill: Optional[str] = None,
116
+ expect_contains: Optional[list] = None,
117
+ answers: Optional[list] = None) -> "PackBuilder":
118
+ suites = self.doc["content"]["eval_suites"]
119
+ suite = next((s for s in suites if s["suite_id"] == suite_id), None)
120
+ if suite is None:
121
+ suite = {"suite_id": suite_id,
122
+ "pack_id": self.doc["manifest"]["pack_id"], "cases": []}
123
+ suites.append(suite)
124
+ case = {"case_id": case_id, "input": input_text,
125
+ "expect_skill": expect_skill,
126
+ "expect_contains": expect_contains or [], "expect_schema": None}
127
+ if answers:
128
+ case["answers"] = answers # consumed by lyboai.evaluate, not the core
129
+ suite["cases"].append(case)
130
+ return self
131
+
132
+ # -- output ------------------------------------------------------------
133
+
134
+ def save(self, path: str) -> str:
135
+ with open(path, "w", encoding="utf-8") as f:
136
+ json.dump(self.doc, f, indent=2, ensure_ascii=False)
137
+ return path
138
+
139
+
140
+ def sign_pack(source_json: str, secret_key_hex: str, out_path: str,
141
+ lybo_binary: str = "lybo") -> str:
142
+ """Sign a pack source with the CLI; returns the output path."""
143
+ result = subprocess.run(
144
+ [lybo_binary, "sign", source_json, "--key", secret_key_hex, "-o", out_path],
145
+ capture_output=True, text=True,
146
+ )
147
+ if result.returncode != 0:
148
+ raise RuntimeError(f"lybo sign failed: {result.stderr.strip()}")
149
+ return out_path
@@ -0,0 +1,168 @@
1
+ """Training & evaluation utilities.
2
+
3
+ “Training” for on-device agents means three concrete, privacy-preserving
4
+ loops, all supported here:
5
+
6
+ 1. **Evaluation-driven iteration** — run labelled cases through the real
7
+ runtime (routing, workflows, approvals) and measure outcomes before you
8
+ ship a pack.
9
+ 2. **Trigger/keyword tuning** — learn discriminative keywords per skill from
10
+ labelled utterances, improving both the keyword tier and the semantic
11
+ centroid tier of the routing cascade (centroids are built from these
12
+ phrases on-device).
13
+ 3. **Dataset export** — turn runtime traces into training corpora (e.g. for
14
+ distilling a tiny intent classifier). Traces carry routing metadata, not
15
+ raw user content; your labelled corpus comes from your own eval sets.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ import re
23
+ from collections import Counter, defaultdict
24
+ from dataclasses import dataclass, field
25
+ from typing import Dict, List, Optional
26
+
27
+ from .client import Client
28
+
29
+ _STOP = {
30
+ "the", "a", "an", "and", "or", "to", "of", "in", "on", "for", "with",
31
+ "is", "are", "was", "be", "this", "that", "it", "my", "me", "i", "you",
32
+ "please", "can", "could", "would", "do", "does", "about", "at", "by",
33
+ }
34
+
35
+
36
+ def _tokens(text: str) -> List[str]:
37
+ return [t for t in re.split(r"[^a-z0-9]+", text.lower()) if len(t) > 1 and t not in _STOP]
38
+
39
+
40
+ # ── 1. Evaluation ─────────────────────────────────────────────────────────
41
+
42
+ @dataclass
43
+ class CaseResult:
44
+ case_id: str
45
+ passed: bool
46
+ failures: List[str] = field(default_factory=list)
47
+ routed_skill: Optional[str] = None
48
+ outcome_state: Optional[str] = None
49
+
50
+
51
+ @dataclass
52
+ class EvalReport:
53
+ total: int
54
+ passed: int
55
+ results: List[CaseResult]
56
+
57
+ @property
58
+ def ok(self) -> bool:
59
+ return self.passed == self.total
60
+
61
+ def summary(self) -> str:
62
+ lines = [f"{self.passed}/{self.total} cases passed"]
63
+ for r in self.results:
64
+ mark = "✓" if r.passed else "✗"
65
+ lines.append(f" {mark} {r.case_id}" + ("" if r.passed else f" — {'; '.join(r.failures)}"))
66
+ return "\n".join(lines)
67
+
68
+
69
+ def evaluate(client: Client, suite: dict, approve: bool = True) -> EvalReport:
70
+ """Run an eval suite (same shape packs ship) against a live runtime.
71
+
72
+ Checks routing (via the run's trace), completion state, and output
73
+ substrings. `answers` on a case feed `ask_user` pauses in order.
74
+ """
75
+ results: List[CaseResult] = []
76
+ for case in suite.get("cases", []):
77
+ session = client.start_session()
78
+ out = client.run_to_completion(
79
+ session, case["input"], answers=case.get("answers"), approve=approve
80
+ )
81
+ failures: List[str] = []
82
+
83
+ traces = client.recent_traces(1)
84
+ routed = traces[0].get("skill_id") if traces else None
85
+ expect_skill = case.get("expect_skill")
86
+ if expect_skill and routed != expect_skill:
87
+ failures.append(f"routed to {routed!r}, expected {expect_skill!r}")
88
+
89
+ state = out.get("state")
90
+ if state != "completed":
91
+ failures.append(f"finished in state {state!r}")
92
+ else:
93
+ text = json.dumps(out.get("output", {})).lower()
94
+ for needle in case.get("expect_contains") or []:
95
+ if needle.lower() not in text:
96
+ failures.append(f"output missing {needle!r}")
97
+
98
+ results.append(CaseResult(case["case_id"], not failures, failures, routed, state))
99
+ return EvalReport(len(results), sum(r.passed for r in results), results)
100
+
101
+
102
+ # ── 2. Trigger/keyword training ───────────────────────────────────────────
103
+
104
+ def train_triggers(examples: Dict[str, List[str]], top_keywords: int = 6) -> Dict[str, dict]:
105
+ """Learn routing triggers from labelled utterances.
106
+
107
+ `examples` maps skill_id → list of user phrases. Returns per-skill
108
+ `{"triggers": [...], "keywords": [...]}` where keywords are chosen by
109
+ class-discriminative tf-idf (frequent inside the class, rare outside).
110
+ Feed the result into skill definitions / PackBuilder; both the keyword
111
+ tier and the on-device semantic centroid tier consume them.
112
+ """
113
+ df: Counter = Counter()
114
+ class_tf: Dict[str, Counter] = defaultdict(Counter)
115
+ n_classes = max(len(examples), 1)
116
+
117
+ for skill_id, phrases in examples.items():
118
+ seen_in_class = set()
119
+ for phrase in phrases:
120
+ for tok in _tokens(phrase):
121
+ class_tf[skill_id][tok] += 1
122
+ seen_in_class.add(tok)
123
+ for tok in seen_in_class:
124
+ df[tok] += 1
125
+
126
+ out: Dict[str, dict] = {}
127
+ for skill_id, phrases in examples.items():
128
+ scored = {
129
+ tok: tf * (math.log((1 + n_classes) / (1 + df[tok])) + 1.0)
130
+ for tok, tf in class_tf[skill_id].items()
131
+ }
132
+ keywords = [t for t, _ in sorted(scored.items(), key=lambda kv: -kv[1])[:top_keywords]]
133
+ # Triggers: the most representative full phrases (shortest first for
134
+ # tiny-model friendliness), capped at 6.
135
+ triggers = sorted(set(phrases), key=len)[:6]
136
+ out[skill_id] = {"triggers": triggers, "keywords": keywords}
137
+ return out
138
+
139
+
140
+ # ── 3. Dataset export ─────────────────────────────────────────────────────
141
+
142
+ def export_routing_dataset(client: Client, path: str, limit: int = 200) -> int:
143
+ """Export routing/outcome metadata from local traces as JSONL.
144
+
145
+ Privacy note: traces do not contain raw user content by design — this
146
+ export carries skill/workflow ids, confidence, outcomes, latency, and
147
+ model ids, suitable for routing analytics and classifier-distillation
148
+ pipelines that join against your *own* labelled eval corpora.
149
+ """
150
+ rows = 0
151
+ with open(path, "w", encoding="utf-8") as f:
152
+ for trace in client.recent_traces(limit):
153
+ intent_meta = next(
154
+ (e.get("meta") for e in trace.get("events", [])
155
+ if e.get("kind") == "intent_resolved"), {},
156
+ ) or {}
157
+ f.write(json.dumps({
158
+ "trace_id": trace.get("trace_id"),
159
+ "skill_id": trace.get("skill_id"),
160
+ "workflow_id": trace.get("workflow_id"),
161
+ "routing_confidence": intent_meta.get("confidence"),
162
+ "outcome": trace.get("outcome"),
163
+ "model_ids": trace.get("model_ids"),
164
+ "latency_ms": (trace.get("completed_ms") or 0) - trace.get("started_ms", 0),
165
+ "steps": len(trace.get("events", [])),
166
+ }) + "\n")
167
+ rows += 1
168
+ return rows
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: lyboai
3
+ Version: 0.1.0
4
+ Summary: LyboAI Mobile Edge Runtime — Python SDK for on-device AI agent creation, evaluation, and training workflows
5
+ Author: LyboAI
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://lyboai.app/developers
8
+ Project-URL: Documentation, https://lyboai.app/developers/docs
9
+ Project-URL: Repository, https://github.com/rajandua20/lyboai-mobile-edge-runtime
10
+ Keywords: on-device-ai,edge-ai,llm,slm,agents,offline-ai,private-ai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
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
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+
24
+ # lyboai (Python SDK)
25
+
26
+ Python is the **builder's language** for the LyboAI ecosystem: create agents,
27
+ package and sign capability packs, run evaluations, tune routing, and export
28
+ training datasets. Agents execute in the Rust runtime — locally via the
29
+ `lybo` binary (this SDK drives it over the JSON-lines protocol, identical to
30
+ the on-device C ABI) and on phones via the mobile bindings.
31
+
32
+ ```bash
33
+ pip install -e sdk/python # from the repo, or publish to your index
34
+ cargo build -p lybo-cli # the runtime the SDK drives
35
+ ```
36
+
37
+ ## Create an agent
38
+
39
+ ```python
40
+ from lyboai import Client
41
+
42
+ with Client(data_dir="/tmp/agent", binary="./target/debug/lybo") as lybo:
43
+ lybo.install_knowledge(doc_id="handbook", title="Team Handbook",
44
+ body="Support tickets are answered within 24 hours.")
45
+ lybo.register_skill(skill_id="app.ask", name="Ask the handbook",
46
+ triggers=["what does the handbook say"],
47
+ keywords=["handbook"], pattern="retrieval")
48
+
49
+ session = lybo.start_session()
50
+ out = lybo.run_to_completion(session, "what does the handbook say about tickets")
51
+ print(out["output"]["text"], out["output"]["sources"])
52
+ ```
53
+
54
+ ## Train routing from labelled examples
55
+
56
+ ```python
57
+ from lyboai import train_triggers
58
+
59
+ learned = train_triggers({
60
+ "faq.ask": ["what does the guide say about refunds", "search the faq"],
61
+ "task.plan": ["plan my week", "break this goal into steps"],
62
+ })
63
+ # → per-skill {"triggers": [...], "keywords": [...]} via class-discriminative
64
+ # tf-idf; feeds both the keyword tier and the on-device semantic centroids.
65
+ ```
66
+
67
+ ## Build, sign, evaluate, ship a pack
68
+
69
+ ```python
70
+ from lyboai import PackBuilder, sign_pack, evaluate
71
+
72
+ src = (PackBuilder("acme.faq", "FAQ", "1.0.0", publisher="acme")
73
+ .add_knowledge("faq", "Store FAQ", "Refunds take five business days.")
74
+ .add_skill("faq.ask", "Ask FAQ", pattern="retrieval",
75
+ triggers=learned["faq.ask"]["triggers"],
76
+ keywords=learned["faq.ask"]["keywords"],
77
+ knowledge_scopes=["acme.faq"])
78
+ .add_eval_case("faq.suite", "routes", "what does the guide say about refunds",
79
+ expect_skill="faq.ask", expect_contains=["refund"])
80
+ .save("pack.json"))
81
+ sign_pack("pack.json", SECRET_KEY_HEX, "dist/acme-faq.lybopack")
82
+
83
+ with Client(data_dir="/tmp/qa", trust=[f"acme:{PUBLIC_KEY_HEX}"]) as lybo:
84
+ lybo.install_pack_file("dist/acme-faq.lybopack")
85
+ report = evaluate(lybo, suite) # routing + outcome + content checks
86
+ assert report.ok, report.summary() # gate your CI on this
87
+ ```
88
+
89
+ ## Export training data (privacy-preserving)
90
+
91
+ ```python
92
+ from lyboai import export_routing_dataset
93
+ export_routing_dataset(lybo, "routing.jsonl")
94
+ # skill ids, confidence, outcomes, latency, model ids — never raw user content.
95
+ ```
96
+
97
+ Run the SDK's own tests: `LYBO_BIN=./target/debug/lybo python3 -m unittest discover sdk/python/tests`
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ lyboai/__init__.py
4
+ lyboai/client.py
5
+ lyboai/packs.py
6
+ lyboai/training.py
7
+ lyboai.egg-info/PKG-INFO
8
+ lyboai.egg-info/SOURCES.txt
9
+ lyboai.egg-info/dependency_links.txt
10
+ lyboai.egg-info/top_level.txt
11
+ tests/test_sdk.py
@@ -0,0 +1 @@
1
+ lyboai
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lyboai"
7
+ version = "0.1.0"
8
+ description = "LyboAI Mobile Edge Runtime — Python SDK for on-device AI agent creation, evaluation, and training workflows"
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "LyboAI" }]
13
+ keywords = ["on-device-ai", "edge-ai", "llm", "slm", "agents", "offline-ai", "private-ai"]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Operating System :: OS Independent",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://lyboai.app/developers"
30
+ Documentation = "https://lyboai.app/developers/docs"
31
+ Repository = "https://github.com/rajandua20/lyboai-mobile-edge-runtime"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["lyboai*"]
35
+ exclude = ["tests*"]
lyboai-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,147 @@
1
+ """Python SDK integration tests against the real Rust runtime.
2
+
3
+ Run: LYBO_BIN=/path/to/lybo python3 -m unittest discover sdk/python/tests
4
+ """
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
13
+
14
+ from lyboai import Client, PackBuilder, evaluate, export_routing_dataset, sign_pack, train_triggers
15
+
16
+ BIN = os.environ.get("LYBO_BIN", "lybo")
17
+ REPO = os.environ.get("LYBO_REPO", os.path.join(os.path.dirname(__file__), "..", "..", ".."))
18
+
19
+
20
+ def new_client(**kw):
21
+ return Client(data_dir=tempfile.mkdtemp(prefix="lybo-py-"), binary=BIN, **kw)
22
+
23
+
24
+ class TestClient(unittest.TestCase):
25
+ def test_chat_events_and_traces(self):
26
+ with new_client() as lybo:
27
+ s = lybo.start_session()
28
+ out = lybo.submit(s, "hello there")
29
+ self.assertEqual(out["state"], "completed")
30
+ kinds = {e["type"] for e in lybo.poll_events(300)}
31
+ self.assertIn("agent.started", kinds)
32
+ self.assertIn("agent.completed", kinds)
33
+ self.assertEqual(len(lybo.recent_traces(5)), 1)
34
+ self.assertTrue(lybo.verify_trace_chain())
35
+
36
+ def test_agent_creation_rag_and_workflow(self):
37
+ with new_client() as lybo:
38
+ lybo.install_knowledge(
39
+ doc_id="handbook", title="Team Handbook",
40
+ body="Support tickets must be answered within 24 hours. "
41
+ "Escalations go to the duty manager.",
42
+ )
43
+ lybo.register_skill(
44
+ skill_id="app.ask", name="Ask handbook",
45
+ triggers=["what does the handbook say"],
46
+ keywords=["handbook", "policy"], pattern="retrieval",
47
+ )
48
+ lybo.register_workflow({
49
+ "workflow_id": "wf.note", "version": "1", "pack_id": "app",
50
+ "description": "", "initial_state": "confirm",
51
+ "max_steps": 16, "auto_checkpoint": True,
52
+ "states": {
53
+ "confirm": {
54
+ "step": {"kind": "confirm",
55
+ "message_template": "Save \"{{input}}\"?"},
56
+ "transitions": [
57
+ {"when": {"kind": "approved"}, "to": "save"},
58
+ {"when": {"kind": "rejected"}, "to": "cancelled"}],
59
+ },
60
+ "save": {
61
+ "step": {"kind": "tool_call", "tool_id": "create_note",
62
+ "args_template": {"title": "Note", "body": "{{input}}"},
63
+ "output_field": "note"},
64
+ "transitions": [{"when": {"kind": "tool_ok"}, "to": "done"}],
65
+ },
66
+ "done": {"step": {"kind": "end",
67
+ "output_template": {"status": "saved", "note": "$note"}},
68
+ "transitions": []},
69
+ "cancelled": {"step": {"kind": "end",
70
+ "output_template": {"status": "cancelled"}},
71
+ "transitions": []},
72
+ },
73
+ })
74
+ lybo.register_skill(
75
+ skill_id="app.note", name="Save note",
76
+ triggers=["save a note"], keywords=["note", "save"],
77
+ workflow_id="wf.note",
78
+ )
79
+
80
+ s = lybo.start_session()
81
+ out = lybo.submit(s, "what does the handbook say about tickets")
82
+ self.assertEqual(out["state"], "completed")
83
+ self.assertIn("Team Handbook", out["output"]["sources"])
84
+
85
+ out = lybo.run_to_completion(s, "save a note: call the supplier tomorrow")
86
+ self.assertEqual(out["state"], "completed")
87
+ self.assertEqual(out["output"]["status"], "saved")
88
+
89
+ def test_training_utilities(self):
90
+ learned = train_triggers({
91
+ "research.ask": ["what does the manual say about pumps",
92
+ "search the documents for safety rules",
93
+ "look up the maintenance guide"],
94
+ "task.plan": ["plan my week", "break this goal into steps",
95
+ "make a plan for the launch"],
96
+ })
97
+ self.assertIn("manual", learned["research.ask"]["keywords"] +
98
+ learned["research.ask"]["triggers"][0].split())
99
+ self.assertIn("plan", learned["task.plan"]["keywords"])
100
+ self.assertNotIn("plan", learned["research.ask"]["keywords"])
101
+
102
+ def test_pack_build_sign_install_evaluate_export(self):
103
+ keys = json.loads(subprocess.run(
104
+ [BIN, "keygen"], capture_output=True, text=True).stdout)
105
+ learned = train_triggers({
106
+ "faq.ask": ["what does the guide say about refunds",
107
+ "search the faq for shipping times"],
108
+ })["faq.ask"]
109
+
110
+ with tempfile.TemporaryDirectory() as tmp:
111
+ src = os.path.join(tmp, "pack.json")
112
+ out_pack = os.path.join(tmp, "faq.lybopack")
113
+ (PackBuilder("py.faq", "FAQ", "1.0.0", "py-tests",
114
+ description="Python-built pack")
115
+ .add_knowledge("faq", "Store FAQ",
116
+ "Refunds are processed within five business days. "
117
+ "Shipping takes two days domestically.")
118
+ .add_skill("faq.ask", "Ask FAQ",
119
+ triggers=learned["triggers"], keywords=learned["keywords"],
120
+ pattern="retrieval",
121
+ knowledge_scopes=["py.faq"])
122
+ .add_eval_case("faq.suite", "routes",
123
+ "what does the guide say about refunds",
124
+ expect_skill="faq.ask", expect_contains=["refund"])
125
+ .save(src))
126
+ sign_pack(src, keys["secret_key_hex"], out_pack, lybo_binary=BIN)
127
+
128
+ with new_client(trust=[f"py-tests:{keys['public_key_hex']}"]) as lybo:
129
+ installed = lybo.install_pack_file(out_pack)
130
+ self.assertEqual(installed["pack_id"], "py.faq")
131
+
132
+ with open(src, encoding="utf-8") as f:
133
+ suite = json.load(f)["content"]["eval_suites"][0]
134
+ report = evaluate(lybo, suite)
135
+ self.assertTrue(report.ok, report.summary())
136
+
137
+ ds = os.path.join(tmp, "routing.jsonl")
138
+ rows = export_routing_dataset(lybo, ds)
139
+ self.assertGreaterEqual(rows, 1)
140
+ with open(ds, encoding="utf-8") as fh:
141
+ first = json.loads(fh.readline())
142
+ self.assertEqual(first["skill_id"], "faq.ask")
143
+ self.assertNotIn("input", first) # no raw content in exports
144
+
145
+
146
+ if __name__ == "__main__":
147
+ unittest.main(verbosity=2)