nemoflow 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.
- nemoflow-0.1.0/.gitignore +21 -0
- nemoflow-0.1.0/PKG-INFO +149 -0
- nemoflow-0.1.0/README.md +125 -0
- nemoflow-0.1.0/nemoflow/__init__.py +5 -0
- nemoflow-0.1.0/nemoflow/client.py +219 -0
- nemoflow-0.1.0/nemoflow/guard.py +176 -0
- nemoflow-0.1.0/pyproject.toml +35 -0
nemoflow-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nemoflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reliability oracle for AI agents — pick the right tool from the start
|
|
5
|
+
Project-URL: Homepage, https://nemoflow.ai
|
|
6
|
+
Project-URL: Documentation, https://api.nemoflow.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/netvistamedia/nemoflow
|
|
8
|
+
Author-email: NemoFlow <hello@nemoflow.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,api,llm,reliability,tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT 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: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# NemoFlow Python SDK
|
|
26
|
+
|
|
27
|
+
Python client for the [NemoFlow API](https://api.nemoflow.ai) — the reliability oracle for AI agents.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install nemoflow
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start — one line of code
|
|
36
|
+
|
|
37
|
+
The `guard` function wraps any tool call with automatic reliability checking:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from nemoflow import NemoFlowClient, guard
|
|
41
|
+
|
|
42
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
43
|
+
|
|
44
|
+
# Wrap any tool call — assesses before, reports after, automatically
|
|
45
|
+
result = guard(client, "https://api.openai.com/v1/chat/completions",
|
|
46
|
+
lambda: openai.chat.completions.create(model="gpt-4", messages=[...]))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it. NemoFlow will:
|
|
50
|
+
1. Check the tool's reliability score before calling
|
|
51
|
+
2. Execute the tool call
|
|
52
|
+
3. Report success/failure back (building the data moat)
|
|
53
|
+
4. Classify errors automatically
|
|
54
|
+
|
|
55
|
+
## Auto-fallback
|
|
56
|
+
|
|
57
|
+
When a tool fails, automatically try alternatives:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
result = guard(
|
|
61
|
+
client,
|
|
62
|
+
"https://api.openai.com/v1/chat/completions",
|
|
63
|
+
lambda: openai.chat.completions.create(model="gpt-4", messages=msgs),
|
|
64
|
+
fallbacks=[
|
|
65
|
+
("https://api.anthropic.com/v1/messages",
|
|
66
|
+
lambda: anthropic.messages.create(model="claude-sonnet-4-20250514", messages=msgs)),
|
|
67
|
+
("https://api.groq.com/openai/v1/chat/completions",
|
|
68
|
+
lambda: groq.chat.completions.create(model="llama-3.3-70b", messages=msgs)),
|
|
69
|
+
],
|
|
70
|
+
min_score=50, # Skip tools scoring below 50
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Decorator
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from nemoflow import NemoFlowClient, nemoflow_guard
|
|
78
|
+
|
|
79
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
80
|
+
|
|
81
|
+
@nemoflow_guard(client, "https://api.stripe.com/v1/charges")
|
|
82
|
+
def charge_customer(amount, currency):
|
|
83
|
+
return stripe.Charge.create(amount=amount, currency=currency)
|
|
84
|
+
|
|
85
|
+
# Every call is now automatically assessed + reported
|
|
86
|
+
charge_customer(1000, "usd")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Journey tracking
|
|
90
|
+
|
|
91
|
+
Report fallback patterns to power hidden gem discovery:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# First attempt fails
|
|
95
|
+
client.report("https://api.sendgrid.com/v3/mail/send",
|
|
96
|
+
success=False, error_category="rate_limit",
|
|
97
|
+
session_id="session-123", attempt_number=1)
|
|
98
|
+
|
|
99
|
+
# Fallback succeeds
|
|
100
|
+
client.report("https://api.resend.com/emails",
|
|
101
|
+
success=True, latency_ms=180,
|
|
102
|
+
session_id="session-123", attempt_number=2,
|
|
103
|
+
previous_tool="https://api.sendgrid.com/v3/mail/send")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Discovery
|
|
107
|
+
|
|
108
|
+
Find hidden gems and fallback chains based on real agent behavior:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# Tools that shine as fallbacks
|
|
112
|
+
gems = client.discover_hidden_gems(category="email")
|
|
113
|
+
|
|
114
|
+
# What to try when SendGrid fails
|
|
115
|
+
chain = client.discover_fallback_chain("https://api.sendgrid.com/v3/mail/send")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Direct API usage
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from nemoflow import NemoFlowClient
|
|
122
|
+
|
|
123
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
124
|
+
|
|
125
|
+
# Assess
|
|
126
|
+
result = client.assess("https://api.openai.com/v1/chat/completions",
|
|
127
|
+
context="customer support chatbot")
|
|
128
|
+
print(result["reliability_score"]) # 89.0
|
|
129
|
+
print(result["predicted_failure_risk"]) # "low"
|
|
130
|
+
print(result["common_pitfalls"]) # ["timeout (8% of failures)"]
|
|
131
|
+
print(result["top_alternatives"]) # [{"tool": "...", "score": 90}]
|
|
132
|
+
|
|
133
|
+
# Report
|
|
134
|
+
client.report("https://api.openai.com/v1/chat/completions",
|
|
135
|
+
success=True, latency_ms=2500)
|
|
136
|
+
|
|
137
|
+
client.close()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Async support
|
|
141
|
+
|
|
142
|
+
`AsyncNemoFlowClient` has the same interface — all methods are `async`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from nemoflow import AsyncNemoFlowClient
|
|
146
|
+
|
|
147
|
+
async with AsyncNemoFlowClient(api_key="nf_live_...") as client:
|
|
148
|
+
result = await client.assess("https://api.openai.com/v1/chat/completions")
|
|
149
|
+
```
|
nemoflow-0.1.0/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# NemoFlow Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the [NemoFlow API](https://api.nemoflow.ai) — the reliability oracle for AI agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nemoflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start — one line of code
|
|
12
|
+
|
|
13
|
+
The `guard` function wraps any tool call with automatic reliability checking:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from nemoflow import NemoFlowClient, guard
|
|
17
|
+
|
|
18
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
19
|
+
|
|
20
|
+
# Wrap any tool call — assesses before, reports after, automatically
|
|
21
|
+
result = guard(client, "https://api.openai.com/v1/chat/completions",
|
|
22
|
+
lambda: openai.chat.completions.create(model="gpt-4", messages=[...]))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it. NemoFlow will:
|
|
26
|
+
1. Check the tool's reliability score before calling
|
|
27
|
+
2. Execute the tool call
|
|
28
|
+
3. Report success/failure back (building the data moat)
|
|
29
|
+
4. Classify errors automatically
|
|
30
|
+
|
|
31
|
+
## Auto-fallback
|
|
32
|
+
|
|
33
|
+
When a tool fails, automatically try alternatives:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
result = guard(
|
|
37
|
+
client,
|
|
38
|
+
"https://api.openai.com/v1/chat/completions",
|
|
39
|
+
lambda: openai.chat.completions.create(model="gpt-4", messages=msgs),
|
|
40
|
+
fallbacks=[
|
|
41
|
+
("https://api.anthropic.com/v1/messages",
|
|
42
|
+
lambda: anthropic.messages.create(model="claude-sonnet-4-20250514", messages=msgs)),
|
|
43
|
+
("https://api.groq.com/openai/v1/chat/completions",
|
|
44
|
+
lambda: groq.chat.completions.create(model="llama-3.3-70b", messages=msgs)),
|
|
45
|
+
],
|
|
46
|
+
min_score=50, # Skip tools scoring below 50
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Decorator
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from nemoflow import NemoFlowClient, nemoflow_guard
|
|
54
|
+
|
|
55
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
56
|
+
|
|
57
|
+
@nemoflow_guard(client, "https://api.stripe.com/v1/charges")
|
|
58
|
+
def charge_customer(amount, currency):
|
|
59
|
+
return stripe.Charge.create(amount=amount, currency=currency)
|
|
60
|
+
|
|
61
|
+
# Every call is now automatically assessed + reported
|
|
62
|
+
charge_customer(1000, "usd")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Journey tracking
|
|
66
|
+
|
|
67
|
+
Report fallback patterns to power hidden gem discovery:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# First attempt fails
|
|
71
|
+
client.report("https://api.sendgrid.com/v3/mail/send",
|
|
72
|
+
success=False, error_category="rate_limit",
|
|
73
|
+
session_id="session-123", attempt_number=1)
|
|
74
|
+
|
|
75
|
+
# Fallback succeeds
|
|
76
|
+
client.report("https://api.resend.com/emails",
|
|
77
|
+
success=True, latency_ms=180,
|
|
78
|
+
session_id="session-123", attempt_number=2,
|
|
79
|
+
previous_tool="https://api.sendgrid.com/v3/mail/send")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Discovery
|
|
83
|
+
|
|
84
|
+
Find hidden gems and fallback chains based on real agent behavior:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Tools that shine as fallbacks
|
|
88
|
+
gems = client.discover_hidden_gems(category="email")
|
|
89
|
+
|
|
90
|
+
# What to try when SendGrid fails
|
|
91
|
+
chain = client.discover_fallback_chain("https://api.sendgrid.com/v3/mail/send")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Direct API usage
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from nemoflow import NemoFlowClient
|
|
98
|
+
|
|
99
|
+
client = NemoFlowClient(api_key="nf_live_...")
|
|
100
|
+
|
|
101
|
+
# Assess
|
|
102
|
+
result = client.assess("https://api.openai.com/v1/chat/completions",
|
|
103
|
+
context="customer support chatbot")
|
|
104
|
+
print(result["reliability_score"]) # 89.0
|
|
105
|
+
print(result["predicted_failure_risk"]) # "low"
|
|
106
|
+
print(result["common_pitfalls"]) # ["timeout (8% of failures)"]
|
|
107
|
+
print(result["top_alternatives"]) # [{"tool": "...", "score": 90}]
|
|
108
|
+
|
|
109
|
+
# Report
|
|
110
|
+
client.report("https://api.openai.com/v1/chat/completions",
|
|
111
|
+
success=True, latency_ms=2500)
|
|
112
|
+
|
|
113
|
+
client.close()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Async support
|
|
117
|
+
|
|
118
|
+
`AsyncNemoFlowClient` has the same interface — all methods are `async`.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from nemoflow import AsyncNemoFlowClient
|
|
122
|
+
|
|
123
|
+
async with AsyncNemoFlowClient(api_key="nf_live_...") as client:
|
|
124
|
+
result = await client.assess("https://api.openai.com/v1/chat/completions")
|
|
125
|
+
```
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
_DEFAULT_BASE_URL = "https://api.nemoflow.ai"
|
|
8
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NemoFlowClient:
|
|
12
|
+
"""Synchronous client for the NemoFlow API."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
18
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._api_key = api_key
|
|
21
|
+
self._base_url = base_url.rstrip("/")
|
|
22
|
+
self._client = httpx.Client(
|
|
23
|
+
base_url=self._base_url,
|
|
24
|
+
headers={"X-Api-Key": self._api_key},
|
|
25
|
+
timeout=timeout,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# -- endpoints -----------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def assess(
|
|
31
|
+
self,
|
|
32
|
+
tool_identifier: str,
|
|
33
|
+
context: str = "",
|
|
34
|
+
sample_payload: Optional[dict[str, Any]] = None,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Assess a tool's reliability and get recommendations."""
|
|
37
|
+
body: dict[str, Any] = {
|
|
38
|
+
"tool_identifier": tool_identifier,
|
|
39
|
+
"context": context,
|
|
40
|
+
}
|
|
41
|
+
if sample_payload is not None:
|
|
42
|
+
body["sample_payload"] = sample_payload
|
|
43
|
+
|
|
44
|
+
resp = self._client.post("/v1/assess", json=body)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
return resp.json()
|
|
47
|
+
|
|
48
|
+
def report(
|
|
49
|
+
self,
|
|
50
|
+
tool_identifier: str,
|
|
51
|
+
success: bool,
|
|
52
|
+
error_category: Optional[str] = None,
|
|
53
|
+
latency_ms: Optional[int] = None,
|
|
54
|
+
context: str = "",
|
|
55
|
+
session_id: Optional[str] = None,
|
|
56
|
+
attempt_number: Optional[int] = None,
|
|
57
|
+
previous_tool: Optional[str] = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Report a tool execution outcome.
|
|
60
|
+
|
|
61
|
+
For journey tracking, include session_id, attempt_number, and
|
|
62
|
+
previous_tool when retrying after a failure. This data powers
|
|
63
|
+
the hidden gems and fallback chain features.
|
|
64
|
+
"""
|
|
65
|
+
body: dict[str, Any] = {
|
|
66
|
+
"tool_identifier": tool_identifier,
|
|
67
|
+
"success": success,
|
|
68
|
+
"context": context,
|
|
69
|
+
}
|
|
70
|
+
if error_category is not None:
|
|
71
|
+
body["error_category"] = error_category
|
|
72
|
+
if latency_ms is not None:
|
|
73
|
+
body["latency_ms"] = latency_ms
|
|
74
|
+
if session_id is not None:
|
|
75
|
+
body["session_id"] = session_id
|
|
76
|
+
if attempt_number is not None:
|
|
77
|
+
body["attempt_number"] = attempt_number
|
|
78
|
+
if previous_tool is not None:
|
|
79
|
+
body["previous_tool"] = previous_tool
|
|
80
|
+
|
|
81
|
+
resp = self._client.post("/v1/report", json=body)
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
return resp.json()
|
|
84
|
+
|
|
85
|
+
def discover_hidden_gems(
|
|
86
|
+
self, category: Optional[str] = None, limit: int = 10
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""Find hidden gem tools that shine as fallbacks."""
|
|
89
|
+
params: dict[str, Any] = {"limit": limit}
|
|
90
|
+
if category:
|
|
91
|
+
params["category"] = category
|
|
92
|
+
resp = self._client.get("/v1/discover/hidden-gems", params=params)
|
|
93
|
+
resp.raise_for_status()
|
|
94
|
+
return resp.json()
|
|
95
|
+
|
|
96
|
+
def discover_fallback_chain(
|
|
97
|
+
self, tool_identifier: str, limit: int = 5
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""Get the best fallback tools when this tool fails."""
|
|
100
|
+
resp = self._client.get(
|
|
101
|
+
"/v1/discover/fallback-chain",
|
|
102
|
+
params={"tool_identifier": tool_identifier, "limit": limit},
|
|
103
|
+
)
|
|
104
|
+
resp.raise_for_status()
|
|
105
|
+
return resp.json()
|
|
106
|
+
|
|
107
|
+
# -- lifecycle -----------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
self._client.close()
|
|
111
|
+
|
|
112
|
+
def __enter__(self) -> NemoFlowClient:
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __exit__(self, *exc: Any) -> None:
|
|
116
|
+
self.close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AsyncNemoFlowClient:
|
|
120
|
+
"""Asynchronous client for the NemoFlow API."""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
api_key: str,
|
|
125
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
126
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
127
|
+
) -> None:
|
|
128
|
+
self._api_key = api_key
|
|
129
|
+
self._base_url = base_url.rstrip("/")
|
|
130
|
+
self._client = httpx.AsyncClient(
|
|
131
|
+
base_url=self._base_url,
|
|
132
|
+
headers={"X-Api-Key": self._api_key},
|
|
133
|
+
timeout=timeout,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# -- endpoints -----------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
async def assess(
|
|
139
|
+
self,
|
|
140
|
+
tool_identifier: str,
|
|
141
|
+
context: str = "",
|
|
142
|
+
sample_payload: Optional[dict[str, Any]] = None,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Assess a tool's reliability and get recommendations."""
|
|
145
|
+
body: dict[str, Any] = {
|
|
146
|
+
"tool_identifier": tool_identifier,
|
|
147
|
+
"context": context,
|
|
148
|
+
}
|
|
149
|
+
if sample_payload is not None:
|
|
150
|
+
body["sample_payload"] = sample_payload
|
|
151
|
+
|
|
152
|
+
resp = await self._client.post("/v1/assess", json=body)
|
|
153
|
+
resp.raise_for_status()
|
|
154
|
+
return resp.json()
|
|
155
|
+
|
|
156
|
+
async def report(
|
|
157
|
+
self,
|
|
158
|
+
tool_identifier: str,
|
|
159
|
+
success: bool,
|
|
160
|
+
error_category: Optional[str] = None,
|
|
161
|
+
latency_ms: Optional[int] = None,
|
|
162
|
+
context: str = "",
|
|
163
|
+
session_id: Optional[str] = None,
|
|
164
|
+
attempt_number: Optional[int] = None,
|
|
165
|
+
previous_tool: Optional[str] = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Report a tool execution outcome."""
|
|
168
|
+
body: dict[str, Any] = {
|
|
169
|
+
"tool_identifier": tool_identifier,
|
|
170
|
+
"success": success,
|
|
171
|
+
"context": context,
|
|
172
|
+
}
|
|
173
|
+
if error_category is not None:
|
|
174
|
+
body["error_category"] = error_category
|
|
175
|
+
if latency_ms is not None:
|
|
176
|
+
body["latency_ms"] = latency_ms
|
|
177
|
+
if session_id is not None:
|
|
178
|
+
body["session_id"] = session_id
|
|
179
|
+
if attempt_number is not None:
|
|
180
|
+
body["attempt_number"] = attempt_number
|
|
181
|
+
if previous_tool is not None:
|
|
182
|
+
body["previous_tool"] = previous_tool
|
|
183
|
+
|
|
184
|
+
resp = await self._client.post("/v1/report", json=body)
|
|
185
|
+
resp.raise_for_status()
|
|
186
|
+
return resp.json()
|
|
187
|
+
|
|
188
|
+
async def discover_hidden_gems(
|
|
189
|
+
self, category: Optional[str] = None, limit: int = 10
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""Find hidden gem tools that shine as fallbacks."""
|
|
192
|
+
params: dict[str, Any] = {"limit": limit}
|
|
193
|
+
if category:
|
|
194
|
+
params["category"] = category
|
|
195
|
+
resp = await self._client.get("/v1/discover/hidden-gems", params=params)
|
|
196
|
+
resp.raise_for_status()
|
|
197
|
+
return resp.json()
|
|
198
|
+
|
|
199
|
+
async def discover_fallback_chain(
|
|
200
|
+
self, tool_identifier: str, limit: int = 5
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Get the best fallback tools when this tool fails."""
|
|
203
|
+
resp = await self._client.get(
|
|
204
|
+
"/v1/discover/fallback-chain",
|
|
205
|
+
params={"tool_identifier": tool_identifier, "limit": limit},
|
|
206
|
+
)
|
|
207
|
+
resp.raise_for_status()
|
|
208
|
+
return resp.json()
|
|
209
|
+
|
|
210
|
+
# -- lifecycle -----------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
async def close(self) -> None:
|
|
213
|
+
await self._client.aclose()
|
|
214
|
+
|
|
215
|
+
async def __aenter__(self) -> AsyncNemoFlowClient:
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
219
|
+
await self.close()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""NemoFlow Guard — one-line reliability wrapper for tool calls.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from nemoflow import NemoFlowClient, guard
|
|
5
|
+
|
|
6
|
+
client = NemoFlowClient("nf_live_...")
|
|
7
|
+
|
|
8
|
+
# Wrap any tool call — assess before, report after, auto-fallback
|
|
9
|
+
result = guard(client, "https://api.openai.com/v1/chat/completions",
|
|
10
|
+
lambda: openai.chat.completions.create(...))
|
|
11
|
+
|
|
12
|
+
# With fallbacks — tries alternatives if primary scores too low
|
|
13
|
+
result = guard(client, "https://api.openai.com/v1/chat/completions",
|
|
14
|
+
lambda: openai.chat.completions.create(...),
|
|
15
|
+
fallbacks=[
|
|
16
|
+
("https://api.anthropic.com/v1/messages",
|
|
17
|
+
lambda: anthropic.messages.create(...)),
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
# As a decorator
|
|
21
|
+
@nemoflow_guard(client, "https://api.stripe.com/v1/charges")
|
|
22
|
+
def charge_customer(amount, currency):
|
|
23
|
+
return stripe.Charge.create(amount=amount, currency=currency)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
from functools import wraps
|
|
31
|
+
from typing import Any, Callable, TypeVar
|
|
32
|
+
|
|
33
|
+
from nemoflow.client import NemoFlowClient
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def guard(
|
|
39
|
+
client: NemoFlowClient,
|
|
40
|
+
tool_identifier: str,
|
|
41
|
+
fn: Callable[[], T],
|
|
42
|
+
*,
|
|
43
|
+
context: str = "",
|
|
44
|
+
min_score: float = 0.0,
|
|
45
|
+
fallbacks: list[tuple[str, Callable[[], T]]] | None = None,
|
|
46
|
+
) -> T:
|
|
47
|
+
"""Execute a tool call with NemoFlow reliability guard.
|
|
48
|
+
|
|
49
|
+
1. Assesses the tool's reliability score
|
|
50
|
+
2. If score < min_score and fallbacks exist, tries the best-scoring fallback
|
|
51
|
+
3. Executes the tool call
|
|
52
|
+
4. Reports success/failure back to NemoFlow
|
|
53
|
+
5. On failure with fallbacks, automatically tries the next option
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
client: NemoFlowClient instance
|
|
57
|
+
tool_identifier: The tool's API identifier
|
|
58
|
+
fn: The actual tool call to execute (as a callable)
|
|
59
|
+
context: Workflow context for context-bucketed scoring
|
|
60
|
+
min_score: Minimum reliability score to proceed (0-100). Default 0 = always try.
|
|
61
|
+
fallbacks: List of (tool_identifier, callable) pairs to try on failure
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The result of the successful tool call
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
The exception from the last failed tool call if all options are exhausted
|
|
68
|
+
"""
|
|
69
|
+
session_id = uuid.uuid4().hex[:16]
|
|
70
|
+
all_tools = [(tool_identifier, fn)] + (fallbacks or [])
|
|
71
|
+
|
|
72
|
+
last_error = None
|
|
73
|
+
|
|
74
|
+
for attempt, (ident, call) in enumerate(all_tools, start=1):
|
|
75
|
+
# Assess
|
|
76
|
+
try:
|
|
77
|
+
assessment = client.assess(ident, context=context)
|
|
78
|
+
score = assessment.get("reliability_score", 100)
|
|
79
|
+
except Exception:
|
|
80
|
+
score = 100 # If assess fails, don't block the tool call
|
|
81
|
+
|
|
82
|
+
# Skip if score too low and we have more options
|
|
83
|
+
if score < min_score and attempt < len(all_tools):
|
|
84
|
+
client.report(
|
|
85
|
+
ident, success=False, error_category="skipped_low_score",
|
|
86
|
+
context=context, session_id=session_id,
|
|
87
|
+
attempt_number=attempt,
|
|
88
|
+
previous_tool=all_tools[attempt - 2][0] if attempt > 1 else None,
|
|
89
|
+
)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Execute
|
|
93
|
+
start = time.perf_counter()
|
|
94
|
+
try:
|
|
95
|
+
result = call()
|
|
96
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
97
|
+
|
|
98
|
+
# Report success
|
|
99
|
+
client.report(
|
|
100
|
+
ident, success=True, latency_ms=latency_ms,
|
|
101
|
+
context=context, session_id=session_id,
|
|
102
|
+
attempt_number=attempt,
|
|
103
|
+
previous_tool=all_tools[attempt - 2][0] if attempt > 1 else None,
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
latency_ms = int((time.perf_counter() - start) * 1000)
|
|
109
|
+
last_error = e
|
|
110
|
+
|
|
111
|
+
# Classify error
|
|
112
|
+
error_category = _classify_error(e)
|
|
113
|
+
|
|
114
|
+
# Report failure
|
|
115
|
+
client.report(
|
|
116
|
+
ident, success=False, error_category=error_category,
|
|
117
|
+
latency_ms=latency_ms, context=context,
|
|
118
|
+
session_id=session_id, attempt_number=attempt,
|
|
119
|
+
previous_tool=all_tools[attempt - 2][0] if attempt > 1 else None,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# If no more fallbacks, raise
|
|
123
|
+
if attempt >= len(all_tools):
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
# Should not reach here, but just in case
|
|
127
|
+
raise last_error # type: ignore
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def nemoflow_guard(
|
|
131
|
+
client: NemoFlowClient,
|
|
132
|
+
tool_identifier: str,
|
|
133
|
+
*,
|
|
134
|
+
context: str = "",
|
|
135
|
+
min_score: float = 0.0,
|
|
136
|
+
):
|
|
137
|
+
"""Decorator version of guard.
|
|
138
|
+
|
|
139
|
+
Usage:
|
|
140
|
+
@nemoflow_guard(client, "https://api.stripe.com/v1/charges")
|
|
141
|
+
def charge(amount, currency):
|
|
142
|
+
return stripe.Charge.create(amount=amount, currency=currency)
|
|
143
|
+
"""
|
|
144
|
+
def decorator(fn: Callable[..., T]) -> Callable[..., T]:
|
|
145
|
+
@wraps(fn)
|
|
146
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
147
|
+
return guard(
|
|
148
|
+
client, tool_identifier,
|
|
149
|
+
lambda: fn(*args, **kwargs),
|
|
150
|
+
context=context, min_score=min_score,
|
|
151
|
+
)
|
|
152
|
+
return wrapper
|
|
153
|
+
return decorator
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _classify_error(error: Exception) -> str:
|
|
157
|
+
"""Best-effort classification of an exception into NemoFlow error categories."""
|
|
158
|
+
name = type(error).__name__.lower()
|
|
159
|
+
message = str(error).lower()
|
|
160
|
+
|
|
161
|
+
if "timeout" in name or "timeout" in message or "timed out" in message:
|
|
162
|
+
return "timeout"
|
|
163
|
+
if "ratelimit" in name or "rate" in message and "limit" in message or "429" in message or "too many" in message:
|
|
164
|
+
return "rate_limit"
|
|
165
|
+
if "auth" in name or "unauthorized" in message or "403" in message or "401" in message:
|
|
166
|
+
return "auth_failure"
|
|
167
|
+
if "validation" in name or "invalid" in message or "422" in message:
|
|
168
|
+
return "validation_error"
|
|
169
|
+
if "notfound" in name or "not found" in message or "404" in message:
|
|
170
|
+
return "not_found"
|
|
171
|
+
if "permission" in name or "forbidden" in message:
|
|
172
|
+
return "permission_denied"
|
|
173
|
+
if "connect" in name or "connection" in message or "dns" in message:
|
|
174
|
+
return "connection_error"
|
|
175
|
+
|
|
176
|
+
return "server_error"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nemoflow"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reliability oracle for AI agents — pick the right tool from the start"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "NemoFlow", email = "hello@nemoflow.ai"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agents", "reliability", "tools", "llm", "api"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.24.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://nemoflow.ai"
|
|
34
|
+
Documentation = "https://api.nemoflow.ai/docs"
|
|
35
|
+
Repository = "https://github.com/netvistamedia/nemoflow"
|