trodo-python 2.20.0__py3-none-any.whl → 2.21.0__py3-none-any.whl
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.
- trodo/__init__.py +1 -1
- trodo/managers/prompt_manager.py +35 -2
- trodo/otel/wrap_agent.py +22 -0
- trodo/prompts/compile.py +16 -3
- {trodo_python-2.20.0.dist-info → trodo_python-2.21.0.dist-info}/METADATA +1 -1
- {trodo_python-2.20.0.dist-info → trodo_python-2.21.0.dist-info}/RECORD +8 -8
- {trodo_python-2.20.0.dist-info → trodo_python-2.21.0.dist-info}/WHEEL +0 -0
- {trodo_python-2.20.0.dist-info → trodo_python-2.21.0.dist-info}/top_level.txt +0 -0
trodo/__init__.py
CHANGED
trodo/managers/prompt_manager.py
CHANGED
|
@@ -38,6 +38,11 @@ __all__ = [
|
|
|
38
38
|
DEFAULT_TTL_SECONDS = 60.0
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
# 404 bodies that mean "your SELECTOR is wrong", not "the prompt is gone".
|
|
42
|
+
# Mirrors backend/models/prompt.js missReason -- the wire contract's error half.
|
|
43
|
+
_CONFIG_ERROR_CODES = frozenset({"version_not_found", "label_not_found", "no_versions"})
|
|
44
|
+
|
|
45
|
+
|
|
41
46
|
def _cache_key(name: str, version: Optional[Union[int, str]], label: Optional[str]) -> str:
|
|
42
47
|
# Resolution happens server-side on every fetch; the client only caches
|
|
43
48
|
# under whatever selector was asked for. So a label flip propagates within
|
|
@@ -197,7 +202,14 @@ class PromptManager:
|
|
|
197
202
|
Availability ladder — fresh cache -> stale cache -> ``fallback`` ->
|
|
198
203
|
raise. A prompt fetch is on your hot path, so a Trodo outage degrades
|
|
199
204
|
rather than takes your app down. Check ``prompt.is_fallback`` to detect
|
|
200
|
-
the last rung.
|
|
205
|
+
the last rung.
|
|
206
|
+
|
|
207
|
+
The ladder is for AVAILABILITY failures only. A selector that names
|
|
208
|
+
nothing — a ``version`` or ``label`` that doesn't exist on a prompt
|
|
209
|
+
that does — is a config error in your code and raises immediately
|
|
210
|
+
(``e.code`` is ``version_not_found`` | ``label_not_found`` |
|
|
211
|
+
``no_versions``), because being quietly handed the fallback would hide
|
|
212
|
+
the typo for as long as it ships. Pass ``cache_ttl_seconds=0`` to disable caching (handy in
|
|
201
213
|
development).
|
|
202
214
|
|
|
203
215
|
:raises ValueError: if *name* is empty, or both ``label`` and ``version``
|
|
@@ -224,6 +236,22 @@ class PromptManager:
|
|
|
224
236
|
if not res or res.get("__error") or not res.get("prompt"):
|
|
225
237
|
status = res.get("status") if isinstance(res, dict) else None
|
|
226
238
|
detail = res.get("error") if isinstance(res, dict) else None
|
|
239
|
+
# The server distinguishes a selector that names nothing from a
|
|
240
|
+
# prompt that is missing. The first is a CONFIG error -- your
|
|
241
|
+
# code asks for a version or label that does not exist -- and
|
|
242
|
+
# must throw through the availability ladder below rather than
|
|
243
|
+
# be masked by stale content or the fallback.
|
|
244
|
+
if detail in _CONFIG_ERROR_CODES:
|
|
245
|
+
if detail == "version_not_found":
|
|
246
|
+
what = f"version {version!r} does not exist on prompt {name!r}"
|
|
247
|
+
elif detail == "label_not_found":
|
|
248
|
+
what = f"label {label!r} does not exist on prompt {name!r}"
|
|
249
|
+
else:
|
|
250
|
+
what = f"prompt {name!r} has no versions yet -- save one in the dashboard"
|
|
251
|
+
err = LookupError(f"trodo: {what}" + (f" (HTTP {status})" if status else ""))
|
|
252
|
+
err.code = detail # type: ignore[attr-defined]
|
|
253
|
+
err.prompt_config_error = True # type: ignore[attr-defined]
|
|
254
|
+
raise err
|
|
227
255
|
raise LookupError(
|
|
228
256
|
f"trodo: could not fetch prompt {name!r}"
|
|
229
257
|
+ (f" (HTTP {status})" if status else "")
|
|
@@ -248,7 +276,12 @@ class PromptManager:
|
|
|
248
276
|
if ttl > 0:
|
|
249
277
|
self._cache.set(key, raw, ttl)
|
|
250
278
|
return _to_prompt(raw, trace_label=trace_label)
|
|
251
|
-
except Exception:
|
|
279
|
+
except Exception as e:
|
|
280
|
+
# A config error is not an outage: a typo'd label silently serving
|
|
281
|
+
# the fallback forever would hide the mistake for as long as it
|
|
282
|
+
# ships. Config errors surface.
|
|
283
|
+
if getattr(e, "prompt_config_error", False):
|
|
284
|
+
raise
|
|
252
285
|
stale = self._cache.get_stale(key)
|
|
253
286
|
if stale is not None:
|
|
254
287
|
return _to_prompt(stale, trace_label=trace_label)
|
trodo/otel/wrap_agent.py
CHANGED
|
@@ -258,6 +258,26 @@ def _mint_anon_distinct_id() -> str:
|
|
|
258
258
|
return f"anon_{ts}_python_{uuid.uuid4()}_{rand}"
|
|
259
259
|
|
|
260
260
|
|
|
261
|
+
def _tag_error_with_run(exc: object, run_id: Optional[str]) -> None:
|
|
262
|
+
"""Stamp the active run's id onto an exception about to propagate.
|
|
263
|
+
|
|
264
|
+
The run IS recorded server-side when an agent raises -- status, error type,
|
|
265
|
+
full message. What was missing is the join: the exception a developer's
|
|
266
|
+
error tracker captures had no reference to the recorded run, so the two
|
|
267
|
+
could only be matched by timestamp. ``exc.trodo_run_id`` is that join.
|
|
268
|
+
|
|
269
|
+
Guarded on purpose: exceptions can use ``__slots__`` or be otherwise
|
|
270
|
+
unwritable, and a crash inside error handling is the one unforgivable
|
|
271
|
+
failure mode here.
|
|
272
|
+
"""
|
|
273
|
+
if exc is None or not run_id:
|
|
274
|
+
return
|
|
275
|
+
try:
|
|
276
|
+
exc.trodo_run_id = run_id # type: ignore[attr-defined]
|
|
277
|
+
except Exception: # noqa: BLE001 -- slots/frozen; the run is still recorded
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
|
|
261
281
|
class RunHandle:
|
|
262
282
|
"""Handle returned by wrap_agent for setting input/output and getting run_id."""
|
|
263
283
|
|
|
@@ -600,6 +620,7 @@ class wrap_agent:
|
|
|
600
620
|
einfo = describe_error(exc_type, exc, tb)
|
|
601
621
|
error_summary = einfo["error_message"]
|
|
602
622
|
error_type = einfo["error_type"]
|
|
623
|
+
_tag_error_with_run(exc, self.handle.run_id)
|
|
603
624
|
elif manual_run_error:
|
|
604
625
|
status = "error"
|
|
605
626
|
error_summary = self.handle.error_summary
|
|
@@ -704,6 +725,7 @@ class wrap_agent:
|
|
|
704
725
|
for k, v in self.handle.metadata.items():
|
|
705
726
|
otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
|
|
706
727
|
if exc is not None:
|
|
728
|
+
_tag_error_with_run(exc, self.handle.run_id if self.handle else None)
|
|
707
729
|
otel_span.record_exception(exc)
|
|
708
730
|
_, status_cls, status_code = get_otel_helpers()
|
|
709
731
|
if status_cls is not None and status_code is not None:
|
trodo/prompts/compile.py
CHANGED
|
@@ -8,6 +8,7 @@ or the prompt you tested is not the prompt you shipped.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
from typing import Any, Dict, List, Optional
|
|
12
13
|
|
|
13
14
|
from .template import render
|
|
@@ -26,6 +27,15 @@ class CompileError(Exception):
|
|
|
26
27
|
self.details: List[str] = details or []
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _q(value: Any) -> str:
|
|
31
|
+
"""Format an offending value the way Node's JSON.stringify does, so the two
|
|
32
|
+
engines produce byte-identical error text. repr() was the one divergence a
|
|
33
|
+
full cross-SDK parity run found."""
|
|
34
|
+
try:
|
|
35
|
+
return json.dumps(value, ensure_ascii=False)
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
return repr(value)
|
|
38
|
+
|
|
29
39
|
def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
|
|
30
40
|
"""Coerce a caller value to the declared type.
|
|
31
41
|
|
|
@@ -36,15 +46,18 @@ def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
|
|
|
36
46
|
return value
|
|
37
47
|
|
|
38
48
|
if type_ == "number":
|
|
49
|
+
# _q (json.dumps), not repr: the Node engine formats the offending
|
|
50
|
+
# value with JSON.stringify, and the two engines' error text is
|
|
51
|
+
# asserted byte-identical.
|
|
39
52
|
if isinstance(value, bool):
|
|
40
|
-
errors.append(f"variable '{name}': expected a number, got {value
|
|
53
|
+
errors.append(f"variable '{name}': expected a number, got {_q(value)}")
|
|
41
54
|
return value
|
|
42
55
|
if isinstance(value, (int, float)):
|
|
43
56
|
return value
|
|
44
57
|
try:
|
|
45
58
|
return float(value) if "." in str(value) else int(value)
|
|
46
59
|
except (TypeError, ValueError):
|
|
47
|
-
errors.append(f"variable '{name}': expected a number, got {value
|
|
60
|
+
errors.append(f"variable '{name}': expected a number, got {_q(value)}")
|
|
48
61
|
return value
|
|
49
62
|
|
|
50
63
|
if type_ == "boolean":
|
|
@@ -54,7 +67,7 @@ def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
|
|
|
54
67
|
return True
|
|
55
68
|
if value == "false":
|
|
56
69
|
return False
|
|
57
|
-
errors.append(f"variable '{name}': expected a boolean, got {value
|
|
70
|
+
errors.append(f"variable '{name}': expected a boolean, got {_q(value)}")
|
|
58
71
|
return value
|
|
59
72
|
|
|
60
73
|
if type_ == "string":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trodo/__init__.py,sha256=
|
|
1
|
+
trodo/__init__.py,sha256=yv0gxvU4OpZhqck-U2YY0RZE7qRWDhD4ig4zenbMPI0,26928
|
|
2
2
|
trodo/client.py,sha256=9UYaHZtWPMYdlG2xWVf77PQJ9DfFgN-zobTh60i8_Cc,22453
|
|
3
3
|
trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
|
|
4
4
|
trodo/user_context.py,sha256=uHCI2WYoOI3cNwdIUEuZdX4VYSu14pzm348ksn6hn_c,8195
|
|
@@ -13,7 +13,7 @@ trodo/managers/dataset_manager.py,sha256=gx0S8ujG3cbw4IskBNMdceBjDbcEh_nebwKSzEv
|
|
|
13
13
|
trodo/managers/experiment_manager.py,sha256=V-vemLtenfSI5VIcP0HPiXzEy8AyTrRrbJVCA9Z6wjo,10579
|
|
14
14
|
trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
|
|
15
15
|
trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8pK1E,2882
|
|
16
|
-
trodo/managers/prompt_manager.py,sha256=
|
|
16
|
+
trodo/managers/prompt_manager.py,sha256=qFVHgh7PW-SlgHp62ybirdYsJBg-fcQVMTTU3jPqFvo,13435
|
|
17
17
|
trodo/managers/user_manager.py,sha256=faJYX3CHrD7ulYiShV7FhThSMX9aJ3kdOgU4Qtdy5FM,2844
|
|
18
18
|
trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
|
|
19
19
|
trodo/otel/auto_instrument.py,sha256=Iae9A9lvh2PImE6gqnyEMXeRPRpjTxZcxx1zW2KDLac,21467
|
|
@@ -23,9 +23,9 @@ trodo/otel/processor.py,sha256=LKlXxP3BeQ7DP8SzYAgXZZOeJ6-6b7e43i19gCUC_0Y,7939
|
|
|
23
23
|
trodo/otel/prompt_trace.py,sha256=BIrdLOpsR1_HoaCmWb_706GwZSs-UG3p76fjvX3CX3w,3391
|
|
24
24
|
trodo/otel/register.py,sha256=bV_ePTfUvPugig2GZnylhzxi2QfoPWs96A9mGLPMrSQ,9387
|
|
25
25
|
trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
|
|
26
|
-
trodo/otel/wrap_agent.py,sha256=
|
|
26
|
+
trodo/otel/wrap_agent.py,sha256=ipMKpdgRF5ibgjDlt2yTBPqbwb8Hq30C7o3hms0QAdU,41987
|
|
27
27
|
trodo/prompts/__init__.py,sha256=yunNc8WkTSfEEkRR-NZWXAt_ZB3Ee2INRh-4RE7uHj4,806
|
|
28
|
-
trodo/prompts/compile.py,sha256=
|
|
28
|
+
trodo/prompts/compile.py,sha256=fAYq55LdGIvglNVCnBF3YXyM8W9raLku1M8OAzaQExE,7135
|
|
29
29
|
trodo/prompts/template.py,sha256=obQiivPCR6LEdz7q1cby7uL25tYHYui4aoEaiUNqWfs,9357
|
|
30
30
|
trodo/prompts/types.py,sha256=A24njy6qcc8QNzWEfGu28BbBr360S7A5Pw7_ObIWNS4,4684
|
|
31
31
|
trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -36,7 +36,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
|
|
|
36
36
|
trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
|
|
37
37
|
trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
|
|
38
38
|
trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
|
|
39
|
-
trodo_python-2.
|
|
40
|
-
trodo_python-2.
|
|
41
|
-
trodo_python-2.
|
|
42
|
-
trodo_python-2.
|
|
39
|
+
trodo_python-2.21.0.dist-info/METADATA,sha256=La6v97ZyzSSTa0CeVffS53LhsUvF29mDpm6i92YO63Q,25308
|
|
40
|
+
trodo_python-2.21.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
41
|
+
trodo_python-2.21.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
|
|
42
|
+
trodo_python-2.21.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|