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.
@@ -0,0 +1,21 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+
10
+ # Environment
11
+ .env
12
+
13
+ # IDE
14
+ .vscode/
15
+ .idea/
16
+
17
+ # Docker
18
+ docker-compose.override.yml
19
+
20
+ # OS
21
+ .DS_Store
@@ -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
+ ```
@@ -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,5 @@
1
+ from .client import NemoFlowClient, AsyncNemoFlowClient
2
+ from .guard import guard, nemoflow_guard
3
+
4
+ __all__ = ["NemoFlowClient", "AsyncNemoFlowClient", "guard", "nemoflow_guard"]
5
+ __version__ = "0.1.0"
@@ -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"