nebelus 0.1.1__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.
- nebelus-0.1.1/.github/workflows/release.yml +34 -0
- nebelus-0.1.1/PKG-INFO +117 -0
- nebelus-0.1.1/README.md +99 -0
- nebelus-0.1.1/action/action.yml +39 -0
- nebelus-0.1.1/examples/agents-apply.yml +20 -0
- nebelus-0.1.1/examples/triage_workflow.py +34 -0
- nebelus-0.1.1/pyproject.toml +26 -0
- nebelus-0.1.1/src/nebelus/__init__.py +22 -0
- nebelus-0.1.1/src/nebelus/_transport.py +103 -0
- nebelus-0.1.1/src/nebelus/cli.py +85 -0
- nebelus-0.1.1/src/nebelus/client.py +234 -0
- nebelus-0.1.1/src/nebelus/export.py +59 -0
- nebelus-0.1.1/src/nebelus/langgraph.py +126 -0
- nebelus-0.1.1/src/nebelus/models.py +75 -0
- nebelus-0.1.1/tests/test_client.py +335 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Publishes the `nebelus` package to PyPI via Trusted Publishing (OIDC) —
|
|
2
|
+
# no API token exists anywhere. Requires the PyPI-side "pending publisher"
|
|
3
|
+
# to be configured for: owner Nebelus, repo nebelus-python, workflow
|
|
4
|
+
# release.yml, environment pypi.
|
|
5
|
+
name: release
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: pip install -e ".[dev]"
|
|
18
|
+
- run: ruff check src tests
|
|
19
|
+
- run: pytest tests -q
|
|
20
|
+
publish:
|
|
21
|
+
needs: test
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment: pypi
|
|
24
|
+
permissions:
|
|
25
|
+
id-token: write # OIDC for PyPI Trusted Publishing
|
|
26
|
+
contents: read
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: pip install build
|
|
33
|
+
- run: python -m build
|
|
34
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
nebelus-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nebelus
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Nebelus Agents API — build, edit, and ship governed AI agents from code
|
|
5
|
+
Author-email: Nebelus <support@nebelus.ai>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pydantic>=2.5
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: langgraph>=0.2; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
15
|
+
Provides-Extra: langgraph
|
|
16
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Nebelus Agents API — Python SDK
|
|
20
|
+
|
|
21
|
+
Build, edit, and ship governed AI agents from code. Every agent you create here is the
|
|
22
|
+
same artifact your team sees in the Nebelus portal — one construction service, every
|
|
23
|
+
surface, your organization's governance applied identically.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from nebelus import Nebelus, AgentManifest
|
|
27
|
+
|
|
28
|
+
nb = Nebelus() # NEBELUS_API_KEY + NEBELUS_BASE_URL (default https://api.nebelus.ai)
|
|
29
|
+
|
|
30
|
+
# Discover what this organization can build — machine-readable.
|
|
31
|
+
info = nb.describe()
|
|
32
|
+
|
|
33
|
+
manifest = AgentManifest(
|
|
34
|
+
name="Return-policy concierge",
|
|
35
|
+
model_id="claude-haiku-4-5",
|
|
36
|
+
system_message="Answer from the return policy. Escalate anything ambiguous.",
|
|
37
|
+
)
|
|
38
|
+
agent = nb.apply(manifest) # create-or-update, key-wise merge — never clobbers portal edits
|
|
39
|
+
print(nb.agents.probe(agent.id, "Can I return a jacket after 20 days?").reply)
|
|
40
|
+
# Deploying is a human act unless your org opted in to programmatic deployment:
|
|
41
|
+
# nb.agents.deploy(agent.id) # needs the api.construction.deploy scope + the org opt-in
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Keys: Nebelus portal → Settings → API keys (administrator-managed). Scopes:
|
|
45
|
+
`api.construction.read`, `api.construction.write`, and `api.construction.deploy`.
|
|
46
|
+
Your Build Envelope (if your organization uses one) applies to code exactly as it applies
|
|
47
|
+
to every other surface.
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
Everything above is also a command (`pip install nebelus` puts `nebelus` on your PATH):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
nebelus describe # everything your org can build, machine-readable
|
|
55
|
+
nebelus catalog --view tools --query crm
|
|
56
|
+
nebelus apply agent.py # a file defining `manifest = AgentManifest(...)`
|
|
57
|
+
nebelus diff agent.py # what apply would change ("in sync" when nothing)
|
|
58
|
+
nebelus validate <agent-id> # pre-flight findings before you probe or deploy
|
|
59
|
+
nebelus probe <agent-id> "Hi there" # run the draft through the real runtime
|
|
60
|
+
nebelus export <agent-id> > agent.py # a live agent as a maintainable Python manifest
|
|
61
|
+
nebelus deploy <agent-id> # needs the deploy scope + the org's opt-in
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Two-way with the portal
|
|
65
|
+
|
|
66
|
+
`nebelus export` (or `nebelus.export_to_code`) turns any live agent — including one a
|
|
67
|
+
colleague built visually — into a Python manifest you own in git. `apply` takes it back.
|
|
68
|
+
The merge contract makes this safe in both directions: a manifest only manages the fields
|
|
69
|
+
it declares, so portal edits to everything else survive every apply, and `diff` never
|
|
70
|
+
reports server-side normalization as drift.
|
|
71
|
+
|
|
72
|
+
## Coming from LangGraph
|
|
73
|
+
|
|
74
|
+
`from_langgraph` translates a `StateGraph`'s topology into a Nebelus workflow — nodes,
|
|
75
|
+
edges, and conditional-edge targets map mechanically (both sides share the
|
|
76
|
+
`__start__`/`__end__` sentinels). What a node *does* and how a router *decides* live in
|
|
77
|
+
your Python, so you declare those explicitly; anything with no declarative equivalent
|
|
78
|
+
stays in your code and attaches to the agent as an MCP server or custom API endpoint.
|
|
79
|
+
Incomplete translations return named diagnostics instead of a manifest — nothing is
|
|
80
|
+
guessed:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from nebelus import Nebelus, from_langgraph
|
|
84
|
+
|
|
85
|
+
t = from_langgraph(
|
|
86
|
+
my_state_graph,
|
|
87
|
+
node_map={"triage": {"type": "agent", "config": {"system_prompt": "...", "model_id": "claude-haiku-4-5"}}},
|
|
88
|
+
router_map={"triage": {"conditions": [{"expression": "...", "target": "billing"}],
|
|
89
|
+
"default_target": "general"}},
|
|
90
|
+
manifest_id="triage-v1", name="Triage", model_id="claude-haiku-4-5",
|
|
91
|
+
)
|
|
92
|
+
if t.complete:
|
|
93
|
+
Nebelus().apply(t.manifest)
|
|
94
|
+
else:
|
|
95
|
+
print("\n".join(t.diagnostics)) # names every unmapped node and undeclared router
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Install the source-graph dependency with `pip install "nebelus[langgraph]"`.
|
|
99
|
+
|
|
100
|
+
## GitHub Action
|
|
101
|
+
|
|
102
|
+
Keep manifests in git and let CI hold them in sync — PRs show the diff, merges apply it
|
|
103
|
+
(see `examples/agents-apply.yml`):
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
- uses: Nebelus/nebelus-python/action@main
|
|
107
|
+
with:
|
|
108
|
+
manifests: agents/*.py
|
|
109
|
+
api-key: ${{ secrets.NEBELUS_API_KEY }}
|
|
110
|
+
mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Rate limits
|
|
114
|
+
|
|
115
|
+
The API rate-limits per key (generous defaults; probes are tighter since each spends real
|
|
116
|
+
model money). The SDK waits out short `Retry-After` pauses automatically and raises
|
|
117
|
+
`nebelus.RateLimited` (with `.retry_after`) when the pause is too long to hold in-process.
|
nebelus-0.1.1/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Nebelus Agents API — Python SDK
|
|
2
|
+
|
|
3
|
+
Build, edit, and ship governed AI agents from code. Every agent you create here is the
|
|
4
|
+
same artifact your team sees in the Nebelus portal — one construction service, every
|
|
5
|
+
surface, your organization's governance applied identically.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from nebelus import Nebelus, AgentManifest
|
|
9
|
+
|
|
10
|
+
nb = Nebelus() # NEBELUS_API_KEY + NEBELUS_BASE_URL (default https://api.nebelus.ai)
|
|
11
|
+
|
|
12
|
+
# Discover what this organization can build — machine-readable.
|
|
13
|
+
info = nb.describe()
|
|
14
|
+
|
|
15
|
+
manifest = AgentManifest(
|
|
16
|
+
name="Return-policy concierge",
|
|
17
|
+
model_id="claude-haiku-4-5",
|
|
18
|
+
system_message="Answer from the return policy. Escalate anything ambiguous.",
|
|
19
|
+
)
|
|
20
|
+
agent = nb.apply(manifest) # create-or-update, key-wise merge — never clobbers portal edits
|
|
21
|
+
print(nb.agents.probe(agent.id, "Can I return a jacket after 20 days?").reply)
|
|
22
|
+
# Deploying is a human act unless your org opted in to programmatic deployment:
|
|
23
|
+
# nb.agents.deploy(agent.id) # needs the api.construction.deploy scope + the org opt-in
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Keys: Nebelus portal → Settings → API keys (administrator-managed). Scopes:
|
|
27
|
+
`api.construction.read`, `api.construction.write`, and `api.construction.deploy`.
|
|
28
|
+
Your Build Envelope (if your organization uses one) applies to code exactly as it applies
|
|
29
|
+
to every other surface.
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
Everything above is also a command (`pip install nebelus` puts `nebelus` on your PATH):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
nebelus describe # everything your org can build, machine-readable
|
|
37
|
+
nebelus catalog --view tools --query crm
|
|
38
|
+
nebelus apply agent.py # a file defining `manifest = AgentManifest(...)`
|
|
39
|
+
nebelus diff agent.py # what apply would change ("in sync" when nothing)
|
|
40
|
+
nebelus validate <agent-id> # pre-flight findings before you probe or deploy
|
|
41
|
+
nebelus probe <agent-id> "Hi there" # run the draft through the real runtime
|
|
42
|
+
nebelus export <agent-id> > agent.py # a live agent as a maintainable Python manifest
|
|
43
|
+
nebelus deploy <agent-id> # needs the deploy scope + the org's opt-in
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Two-way with the portal
|
|
47
|
+
|
|
48
|
+
`nebelus export` (or `nebelus.export_to_code`) turns any live agent — including one a
|
|
49
|
+
colleague built visually — into a Python manifest you own in git. `apply` takes it back.
|
|
50
|
+
The merge contract makes this safe in both directions: a manifest only manages the fields
|
|
51
|
+
it declares, so portal edits to everything else survive every apply, and `diff` never
|
|
52
|
+
reports server-side normalization as drift.
|
|
53
|
+
|
|
54
|
+
## Coming from LangGraph
|
|
55
|
+
|
|
56
|
+
`from_langgraph` translates a `StateGraph`'s topology into a Nebelus workflow — nodes,
|
|
57
|
+
edges, and conditional-edge targets map mechanically (both sides share the
|
|
58
|
+
`__start__`/`__end__` sentinels). What a node *does* and how a router *decides* live in
|
|
59
|
+
your Python, so you declare those explicitly; anything with no declarative equivalent
|
|
60
|
+
stays in your code and attaches to the agent as an MCP server or custom API endpoint.
|
|
61
|
+
Incomplete translations return named diagnostics instead of a manifest — nothing is
|
|
62
|
+
guessed:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from nebelus import Nebelus, from_langgraph
|
|
66
|
+
|
|
67
|
+
t = from_langgraph(
|
|
68
|
+
my_state_graph,
|
|
69
|
+
node_map={"triage": {"type": "agent", "config": {"system_prompt": "...", "model_id": "claude-haiku-4-5"}}},
|
|
70
|
+
router_map={"triage": {"conditions": [{"expression": "...", "target": "billing"}],
|
|
71
|
+
"default_target": "general"}},
|
|
72
|
+
manifest_id="triage-v1", name="Triage", model_id="claude-haiku-4-5",
|
|
73
|
+
)
|
|
74
|
+
if t.complete:
|
|
75
|
+
Nebelus().apply(t.manifest)
|
|
76
|
+
else:
|
|
77
|
+
print("\n".join(t.diagnostics)) # names every unmapped node and undeclared router
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Install the source-graph dependency with `pip install "nebelus[langgraph]"`.
|
|
81
|
+
|
|
82
|
+
## GitHub Action
|
|
83
|
+
|
|
84
|
+
Keep manifests in git and let CI hold them in sync — PRs show the diff, merges apply it
|
|
85
|
+
(see `examples/agents-apply.yml`):
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- uses: Nebelus/nebelus-python/action@main
|
|
89
|
+
with:
|
|
90
|
+
manifests: agents/*.py
|
|
91
|
+
api-key: ${{ secrets.NEBELUS_API_KEY }}
|
|
92
|
+
mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Rate limits
|
|
96
|
+
|
|
97
|
+
The API rate-limits per key (generous defaults; probes are tighter since each spends real
|
|
98
|
+
model money). The SDK waits out short `Retry-After` pauses automatically and raises
|
|
99
|
+
`nebelus.RateLimited` (with `.retry_after`) when the pause is too long to hold in-process.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: "Nebelus Agents Apply"
|
|
2
|
+
description: "Diff or apply Nebelus agent manifests (.py defining `manifest = AgentManifest(...)`) from your repo."
|
|
3
|
+
branding:
|
|
4
|
+
icon: upload-cloud
|
|
5
|
+
color: orange
|
|
6
|
+
inputs:
|
|
7
|
+
manifests:
|
|
8
|
+
description: "Whitespace-separated manifest file paths"
|
|
9
|
+
required: true
|
|
10
|
+
api-key:
|
|
11
|
+
description: "Nebelus API key (store as a repo secret)"
|
|
12
|
+
required: true
|
|
13
|
+
base-url:
|
|
14
|
+
description: "API base URL"
|
|
15
|
+
required: false
|
|
16
|
+
default: "https://api.nebelus.ai"
|
|
17
|
+
mode:
|
|
18
|
+
description: "'diff' (report only — use on pull_request) or 'apply' (use on push to main)"
|
|
19
|
+
required: false
|
|
20
|
+
default: "diff"
|
|
21
|
+
runs:
|
|
22
|
+
using: composite
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
- shell: bash
|
|
28
|
+
run: pip install --quiet nebelus
|
|
29
|
+
- shell: bash
|
|
30
|
+
env:
|
|
31
|
+
NEBELUS_API_KEY: ${{ inputs.api-key }}
|
|
32
|
+
NEBELUS_BASE_URL: ${{ inputs.base-url }}
|
|
33
|
+
run: |
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
for f in ${{ inputs.manifests }}; do
|
|
36
|
+
echo "::group::${{ inputs.mode }} $f"
|
|
37
|
+
nebelus "${{ inputs.mode }}" "$f"
|
|
38
|
+
echo "::endgroup::"
|
|
39
|
+
done
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copy to .github/workflows/ in the repo that holds your agent manifests.
|
|
2
|
+
# PRs show the diff; merges to main apply it. Deploys stay a human act unless
|
|
3
|
+
# your org opted in and the key carries the deploy scope.
|
|
4
|
+
name: Agents
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
paths: ["agents/**.py"]
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
paths: ["agents/**.py"]
|
|
11
|
+
jobs:
|
|
12
|
+
agents:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: Nebelus/nebelus-python/action@main
|
|
17
|
+
with:
|
|
18
|
+
manifests: agents/*.py
|
|
19
|
+
api-key: ${{ secrets.NEBELUS_API_KEY }}
|
|
20
|
+
mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""A complete workflow agent as code — apply with `nebelus apply examples/triage_workflow.py`."""
|
|
2
|
+
|
|
3
|
+
from nebelus import AgentManifest
|
|
4
|
+
|
|
5
|
+
manifest = AgentManifest(
|
|
6
|
+
manifest_id="triage-example-v1",
|
|
7
|
+
name="Support triage",
|
|
8
|
+
model_id="claude-haiku-4-5",
|
|
9
|
+
pattern_type="workflow",
|
|
10
|
+
description="Routes billing questions to a specialist, everything else to a generalist.",
|
|
11
|
+
pattern_config={
|
|
12
|
+
"nodes": [
|
|
13
|
+
{"name": "triage", "type": "agent",
|
|
14
|
+
"config": {"system_prompt": "Classify the request as 'billing' or 'general'. Reply with exactly one word.",
|
|
15
|
+
"model_id": "claude-haiku-4-5"}},
|
|
16
|
+
{"name": "router", "type": "condition",
|
|
17
|
+
"config": {"conditions": [{"expression": "'billing' in str(state.get('triage_out','')).lower()",
|
|
18
|
+
"target": "billing"}],
|
|
19
|
+
"default_target": "general"}},
|
|
20
|
+
{"name": "billing", "type": "agent",
|
|
21
|
+
"config": {"system_prompt": "You are the billing specialist. Be precise.",
|
|
22
|
+
"model_id": "claude-haiku-4-5"}},
|
|
23
|
+
{"name": "general", "type": "agent",
|
|
24
|
+
"config": {"system_prompt": "You are the general assistant. Be brief.",
|
|
25
|
+
"model_id": "claude-haiku-4-5"}},
|
|
26
|
+
],
|
|
27
|
+
"edges": [
|
|
28
|
+
{"from": "__start__", "to": "triage"},
|
|
29
|
+
{"from": "triage", "to": "router"},
|
|
30
|
+
{"from": "billing", "to": "__end__"},
|
|
31
|
+
{"from": "general", "to": "__end__"},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nebelus"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Nebelus Agents API — build, edit, and ship governed AI agents from code"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Nebelus", email = "support@nebelus.ai" }]
|
|
13
|
+
dependencies = ["httpx>=0.27", "pydantic>=2.5"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
nebelus = "nebelus.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest>=8", "respx>=0.21", "ruff>=0.6", "langgraph>=0.2"]
|
|
20
|
+
langgraph = ["langgraph>=0.2"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/nebelus"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
line-length = 120
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Nebelus Agents API — official Python SDK."""
|
|
2
|
+
|
|
3
|
+
from ._transport import NebelusAPIError, NotFound, RateLimited
|
|
4
|
+
from .client import Nebelus
|
|
5
|
+
from .export import export_to_code
|
|
6
|
+
from .langgraph import Translation, from_langgraph
|
|
7
|
+
from .models import Agent, AgentManifest, ProbeResult, ValidationResult
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.1"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Agent",
|
|
12
|
+
"AgentManifest",
|
|
13
|
+
"Nebelus",
|
|
14
|
+
"NebelusAPIError",
|
|
15
|
+
"NotFound",
|
|
16
|
+
"ProbeResult",
|
|
17
|
+
"RateLimited",
|
|
18
|
+
"Translation",
|
|
19
|
+
"ValidationResult",
|
|
20
|
+
"export_to_code",
|
|
21
|
+
"from_langgraph",
|
|
22
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""HTTP transport: auth, errors-as-exceptions with the API's machine payload intact."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.nebelus.ai"
|
|
12
|
+
API_PREFIX = "/api/v1/construction"
|
|
13
|
+
|
|
14
|
+
# 429s are retried automatically when the server's Retry-After is short enough
|
|
15
|
+
# to wait out in-process; longer waits surface as RateLimited for the caller.
|
|
16
|
+
MAX_RETRY_AFTER_SECONDS = 30.0
|
|
17
|
+
MAX_RATE_LIMIT_RETRIES = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NebelusAPIError(Exception):
|
|
21
|
+
"""Any non-2xx from the API. Carries the FULL machine-readable payload:
|
|
22
|
+
`detail` (human reason), and when present `envelope` ({blocked, requested,
|
|
23
|
+
allowed_hint}), `blocked` (opt-in gates), etc."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, status_code: int, payload: Any):
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.payload = payload if isinstance(payload, dict) else {"detail": str(payload)}
|
|
28
|
+
super().__init__(f"[{status_code}] {self.payload.get('detail') or self.payload}")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def detail(self) -> str:
|
|
32
|
+
return str(self.payload.get("detail", ""))
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def envelope(self) -> dict | None:
|
|
36
|
+
"""The Build Envelope refusal payload, when the refusal came from one."""
|
|
37
|
+
return self.payload.get("envelope")
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def blocked(self) -> str | None:
|
|
41
|
+
return self.payload.get("blocked")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotFound(NebelusAPIError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimited(NebelusAPIError):
|
|
49
|
+
"""429 that could not be waited out in-process. `retry_after` is the
|
|
50
|
+
server's requested pause in seconds, when it sent one."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, status_code: int, payload: Any, retry_after: float | None = None):
|
|
53
|
+
super().__init__(status_code, payload)
|
|
54
|
+
self.retry_after = retry_after
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Transport:
|
|
58
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 60.0):
|
|
59
|
+
self.api_key = api_key or os.environ.get("NEBELUS_API_KEY") or ""
|
|
60
|
+
if not self.api_key:
|
|
61
|
+
raise ValueError("No API key. Pass api_key= or set NEBELUS_API_KEY.")
|
|
62
|
+
self.base_url = (base_url or os.environ.get("NEBELUS_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
63
|
+
self._client = httpx.Client(
|
|
64
|
+
base_url=f"{self.base_url}{API_PREFIX}",
|
|
65
|
+
headers={"Authorization": f"Bearer {self.api_key}", "User-Agent": "nebelus-python/0.1.1"},
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def request(self, method: str, path: str, *, json: Any = None, params: Any = None) -> Any:
|
|
70
|
+
for attempt in range(MAX_RATE_LIMIT_RETRIES + 1):
|
|
71
|
+
r = self._client.request(method, path, json=json, params=params)
|
|
72
|
+
if r.status_code != 429:
|
|
73
|
+
break
|
|
74
|
+
retry_after = _retry_after_seconds(r)
|
|
75
|
+
if attempt == MAX_RATE_LIMIT_RETRIES or retry_after is None or retry_after > MAX_RETRY_AFTER_SECONDS:
|
|
76
|
+
raise RateLimited(429, _body_of(r), retry_after=retry_after)
|
|
77
|
+
time.sleep(retry_after)
|
|
78
|
+
body = _body_of(r)
|
|
79
|
+
if r.status_code == 404:
|
|
80
|
+
raise NotFound(r.status_code, body)
|
|
81
|
+
if r.status_code >= 400:
|
|
82
|
+
raise NebelusAPIError(r.status_code, body)
|
|
83
|
+
return body
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
self._client.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _body_of(r: httpx.Response) -> Any:
|
|
90
|
+
try:
|
|
91
|
+
return r.json()
|
|
92
|
+
except Exception: # noqa: BLE001
|
|
93
|
+
return r.text
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _retry_after_seconds(r: httpx.Response) -> float | None:
|
|
97
|
+
raw = r.headers.get("Retry-After")
|
|
98
|
+
if raw is None:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
return max(0.0, float(raw))
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""`nebelus` — the CLI over the SDK. Same auth (NEBELUS_API_KEY / NEBELUS_BASE_URL)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .client import Nebelus, NebelusAPIError
|
|
12
|
+
from .export import export_to_code
|
|
13
|
+
from .models import AgentManifest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_manifest(path: str) -> AgentManifest:
|
|
17
|
+
p = Path(path)
|
|
18
|
+
if p.suffix == ".json":
|
|
19
|
+
return AgentManifest.model_validate(json.loads(p.read_text()))
|
|
20
|
+
spec = importlib.util.spec_from_file_location("nebelus_manifest", p)
|
|
21
|
+
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
|
|
22
|
+
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
23
|
+
manifest = getattr(mod, "manifest", None)
|
|
24
|
+
if not isinstance(manifest, AgentManifest):
|
|
25
|
+
raise SystemExit(f"{path} must define `manifest = AgentManifest(...)`")
|
|
26
|
+
return manifest
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv: list[str] | None = None) -> int:
|
|
30
|
+
ap = argparse.ArgumentParser(prog="nebelus", description="Nebelus Agents API CLI")
|
|
31
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
32
|
+
sub.add_parser("describe", help="everything buildable in this org, machine-readable")
|
|
33
|
+
c = sub.add_parser("catalog", help="org catalog")
|
|
34
|
+
c.add_argument("--view", default="models")
|
|
35
|
+
c.add_argument("--query")
|
|
36
|
+
a = sub.add_parser("apply", help="create-or-update from a manifest (.py or .json)")
|
|
37
|
+
a.add_argument("path")
|
|
38
|
+
d = sub.add_parser("diff", help="what apply would change")
|
|
39
|
+
d.add_argument("path")
|
|
40
|
+
p = sub.add_parser("probe", help="run the draft for real")
|
|
41
|
+
p.add_argument("agent_id")
|
|
42
|
+
p.add_argument("message")
|
|
43
|
+
dep = sub.add_parser("deploy", help="deploy (needs org opt-in + deploy scope)")
|
|
44
|
+
dep.add_argument("agent_id")
|
|
45
|
+
e = sub.add_parser("export", help="export a live agent as a Python manifest")
|
|
46
|
+
e.add_argument("agent_id")
|
|
47
|
+
v = sub.add_parser("validate", help="pre-flight findings")
|
|
48
|
+
v.add_argument("agent_id")
|
|
49
|
+
args = ap.parse_args(argv)
|
|
50
|
+
|
|
51
|
+
nb = Nebelus()
|
|
52
|
+
try:
|
|
53
|
+
if args.cmd == "describe":
|
|
54
|
+
print(json.dumps(nb.describe(), indent=2, default=str))
|
|
55
|
+
elif args.cmd == "catalog":
|
|
56
|
+
print(json.dumps(nb.catalog(view=args.view, query=args.query), indent=2, default=str))
|
|
57
|
+
elif args.cmd == "apply":
|
|
58
|
+
agent = nb.apply(_load_manifest(args.path))
|
|
59
|
+
print(f"{agent.id} {agent.status}")
|
|
60
|
+
elif args.cmd == "diff":
|
|
61
|
+
changes = nb.diff(_load_manifest(args.path))
|
|
62
|
+
print(json.dumps(changes, indent=2, default=str) if changes else "in sync")
|
|
63
|
+
elif args.cmd == "probe":
|
|
64
|
+
r = nb.agents.probe(args.agent_id, args.message)
|
|
65
|
+
print(r.reply or r.model_dump())
|
|
66
|
+
elif args.cmd == "deploy":
|
|
67
|
+
print(json.dumps(nb.agents.deploy(args.agent_id)))
|
|
68
|
+
elif args.cmd == "validate":
|
|
69
|
+
print(json.dumps(nb.agents.validate(args.agent_id).model_dump(), indent=2))
|
|
70
|
+
elif args.cmd == "export":
|
|
71
|
+
print(export_to_code(nb.agents.get(args.agent_id)))
|
|
72
|
+
except NebelusAPIError as exc:
|
|
73
|
+
print(f"error [{exc.status_code}]: {exc.detail}", file=sys.stderr)
|
|
74
|
+
if exc.envelope:
|
|
75
|
+
print(f"envelope: {json.dumps(exc.envelope)}", file=sys.stderr)
|
|
76
|
+
if exc.blocked:
|
|
77
|
+
print(f"blocked: {exc.blocked}", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
finally:
|
|
80
|
+
nb.close()
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|