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 +97 -0
- lyboai-0.1.0/README.md +74 -0
- lyboai-0.1.0/lyboai/__init__.py +40 -0
- lyboai-0.1.0/lyboai/client.py +195 -0
- lyboai-0.1.0/lyboai/packs.py +149 -0
- lyboai-0.1.0/lyboai/training.py +168 -0
- lyboai-0.1.0/lyboai.egg-info/PKG-INFO +97 -0
- lyboai-0.1.0/lyboai.egg-info/SOURCES.txt +11 -0
- lyboai-0.1.0/lyboai.egg-info/dependency_links.txt +1 -0
- lyboai-0.1.0/lyboai.egg-info/top_level.txt +1 -0
- lyboai-0.1.0/pyproject.toml +35 -0
- lyboai-0.1.0/setup.cfg +4 -0
- lyboai-0.1.0/tests/test_sdk.py +147 -0
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 @@
|
|
|
1
|
+
|
|
@@ -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,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)
|