trodo-python 2.18.1__tar.gz → 2.19.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.
- {trodo_python-2.18.1 → trodo_python-2.19.0}/PKG-INFO +1 -1
- {trodo_python-2.18.1 → trodo_python-2.19.0}/pyproject.toml +1 -1
- trodo_python-2.19.0/tests/test_get_prompt_version_types.py +54 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_prompts.py +32 -3
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/__init__.py +5 -3
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/prompts/compile.py +21 -4
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/prompts/types.py +10 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo_python.egg-info/SOURCES.txt +1 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/README.md +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/setup.cfg +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_auto_instrument_fixes.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_datasets.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_end_run.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_experiments.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_multi_agent_shape.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_prompt_trace.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_register_otel.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_start_run.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_user_traits.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/api/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/api/async_client.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/api/http_client.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/client.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/dataset_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/experiment_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/prompt_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/managers/user_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/auto_instrument.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/context.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/processor.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/prompt_trace.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/register.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/transport.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/otel/wrap_agent.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/prompts/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/prompts/template.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/session/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/session/server_session.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/types.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/user_context.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/util/__init__.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo/util/lru.py +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.18.1 → trodo_python-2.19.0}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""The public get_prompt must accept a version_hash, not just an int.
|
|
2
|
+
|
|
3
|
+
The manager has always taken ``Union[int, str]`` and documents hash support —
|
|
4
|
+
the hash is the stable identity the dashboard shows and the SDK returns. But
|
|
5
|
+
the top-level ``trodo.get_prompt`` wrapper narrowed the hint to ``int``, so a
|
|
6
|
+
type checker flagged the documented usage, and the dashboard's own "Use this
|
|
7
|
+
prompt" snippet emits ``version="<hash>"`` for Python.
|
|
8
|
+
|
|
9
|
+
Checked by signature rather than by a network call: the wrapper only forwards,
|
|
10
|
+
so the contract IS the signature.
|
|
11
|
+
"""
|
|
12
|
+
import inspect
|
|
13
|
+
import typing
|
|
14
|
+
|
|
15
|
+
import trodo
|
|
16
|
+
from trodo.managers.prompt_manager import PromptManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _version_annotation(fn):
|
|
20
|
+
return typing.get_type_hints(fn)["version"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_public_get_prompt_accepts_int_or_str():
|
|
24
|
+
ann = _version_annotation(trodo.get_prompt)
|
|
25
|
+
args = typing.get_args(ann)
|
|
26
|
+
assert int in args, f"version should accept int, got {ann}"
|
|
27
|
+
assert str in args, f"version should accept a version_hash (str), got {ann}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_matches_the_manager_it_forwards_to():
|
|
31
|
+
assert _version_annotation(trodo.get_prompt) == _version_annotation(PromptManager.get)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_forwards_version_through_untouched():
|
|
35
|
+
"""A hash must reach the manager as-is — not coerced, not dropped."""
|
|
36
|
+
seen = {}
|
|
37
|
+
|
|
38
|
+
class _Stub:
|
|
39
|
+
def get(self, name, **kwargs):
|
|
40
|
+
seen.update(kwargs, name=name)
|
|
41
|
+
return "sentinel"
|
|
42
|
+
|
|
43
|
+
class _Client:
|
|
44
|
+
prompts = _Stub()
|
|
45
|
+
|
|
46
|
+
original = trodo._get_client
|
|
47
|
+
trodo._get_client = lambda: _Client()
|
|
48
|
+
try:
|
|
49
|
+
assert trodo.get_prompt("p", version="a3f9c2") == "sentinel"
|
|
50
|
+
finally:
|
|
51
|
+
trodo._get_client = original
|
|
52
|
+
|
|
53
|
+
assert seen["version"] == "a3f9c2"
|
|
54
|
+
assert seen["label"] is None
|
|
@@ -106,12 +106,41 @@ def test_compile_carries_model_and_tools():
|
|
|
106
106
|
assert out.response_format is None
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def
|
|
110
|
-
# `who`
|
|
111
|
-
|
|
109
|
+
def test_compile_raises_for_required_variable_with_no_value():
|
|
110
|
+
# `who` is required with no default. Catching it here is the point: the
|
|
111
|
+
# alternative is paying for a call to a prompt that says "Hello ".
|
|
112
|
+
with pytest.raises(CompileError, match="'who': required"):
|
|
113
|
+
compile_prompt(PROMPT, {})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_compile_treats_none_and_empty_string_as_no_value():
|
|
117
|
+
# Both render as nothing, the exact outcome `required` exists to stop.
|
|
118
|
+
with pytest.raises(CompileError, match="'who': required"):
|
|
119
|
+
compile_prompt(PROMPT, {"who": None})
|
|
120
|
+
with pytest.raises(CompileError, match="'who': required"):
|
|
121
|
+
compile_prompt(PROMPT, {"who": ""})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_compile_renders_missing_optional_variable_empty():
|
|
125
|
+
# `required` defaults to False, so a declaration that doesn't ask for it
|
|
126
|
+
# behaves exactly as before -- no stored prompt changes meaning.
|
|
127
|
+
optional = {**PROMPT, "variables": [
|
|
128
|
+
{"name": "persona", "type": "string", "default": "a greeter"},
|
|
129
|
+
{"name": "who", "type": "string"},
|
|
130
|
+
]}
|
|
131
|
+
out = compile_prompt(optional, {})
|
|
112
132
|
assert out.messages[1]["content"][0] == {"type": "text", "text": "Hello "}
|
|
113
133
|
|
|
114
134
|
|
|
135
|
+
def test_compile_lets_a_default_satisfy_required():
|
|
136
|
+
with_default = {**PROMPT, "variables": [
|
|
137
|
+
{"name": "persona", "type": "string", "default": "a greeter"},
|
|
138
|
+
{"name": "who", "type": "string", "required": True, "default": "world"},
|
|
139
|
+
]}
|
|
140
|
+
out = compile_prompt(with_default, {})
|
|
141
|
+
assert out.messages[1]["content"][0] == {"type": "text", "text": "Hello world"}
|
|
142
|
+
|
|
143
|
+
|
|
115
144
|
def test_compile_rejects_undeclared_variable():
|
|
116
145
|
# Passing a variable the prompt doesn't declare is the one variable mistake
|
|
117
146
|
# compile still catches (it's almost always a typo/rename).
|
|
@@ -41,7 +41,7 @@ Downstream microservice (join the caller's run instead of making a new one):
|
|
|
41
41
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
|
-
__version__ = "2.
|
|
44
|
+
__version__ = "2.19.0"
|
|
45
45
|
|
|
46
46
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
47
47
|
|
|
@@ -322,7 +322,7 @@ def reset(distinct_id: str) -> ResetResult:
|
|
|
322
322
|
def get_prompt(
|
|
323
323
|
name: str,
|
|
324
324
|
label: Optional[str] = None,
|
|
325
|
-
version: Optional[int] = None,
|
|
325
|
+
version: Optional[Union[int, str]] = None,
|
|
326
326
|
cache_ttl_seconds: Optional[float] = None,
|
|
327
327
|
fallback: Optional[Dict[str, Any]] = None,
|
|
328
328
|
max_retries: int = 2,
|
|
@@ -330,7 +330,9 @@ def get_prompt(
|
|
|
330
330
|
"""Fetch a managed prompt by name.
|
|
331
331
|
|
|
332
332
|
Follows the ``production`` label by default; pass ``label`` for a different
|
|
333
|
-
one or ``version`` to pin exactly.
|
|
333
|
+
one or ``version`` to pin exactly. ``version`` may be the integer
|
|
334
|
+
``version_no`` or a ``version_hash`` (full, or an unambiguous short prefix
|
|
335
|
+
like ``a3f9c2``) — the hash is the stable identity, so prefer it.
|
|
334
336
|
|
|
335
337
|
Cached for 60s with stale-while-revalidate, so a Trodo outage degrades
|
|
336
338
|
instead of taking your app down. Pass ``fallback`` to cover cold start::
|
|
@@ -74,8 +74,12 @@ def build_scope(
|
|
|
74
74
|
) -> Dict[str, Any]:
|
|
75
75
|
"""Build the render scope from declarations + caller values.
|
|
76
76
|
|
|
77
|
-
Resolution order: caller value
|
|
78
|
-
raises
|
|
77
|
+
Resolution order: caller value -> declared default -> required-and-absent
|
|
78
|
+
raises -> optional-and-absent renders empty.
|
|
79
|
+
|
|
80
|
+
`required` is enforced HERE, in your process, before the model call -- Trodo
|
|
81
|
+
only stores the declaration. A variable with a default can never fail this
|
|
82
|
+
check, because the default is itself a value.
|
|
79
83
|
"""
|
|
80
84
|
errors: List[str] = []
|
|
81
85
|
scope: Dict[str, Any] = {}
|
|
@@ -100,11 +104,24 @@ def build_scope(
|
|
|
100
104
|
has = name in values
|
|
101
105
|
value = values.get(name) if has else None
|
|
102
106
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
107
|
+
# `required` asks a narrower question than default-substitution does:
|
|
108
|
+
# did the caller supply something that will actually render? None and ""
|
|
109
|
+
# both render as nothing, the very outcome the flag exists to prevent,
|
|
110
|
+
# so neither counts. (0 and False do -- they render.)
|
|
111
|
+
supplied = has and value is not None and value != ""
|
|
112
|
+
|
|
113
|
+
# Missing value -> the declared default, or empty.
|
|
105
114
|
if not has or value is None:
|
|
106
115
|
value = default if has_default else ([] if type_ == "messages" else "")
|
|
107
116
|
|
|
117
|
+
# A default always satisfies the requirement, so the two never both
|
|
118
|
+
# apply; the editor won't let you author both, but a hand-written
|
|
119
|
+
# declaration can.
|
|
120
|
+
required = v.get("required") if isinstance(v, dict) else getattr(v, "required", None)
|
|
121
|
+
if not supplied and required is True and not has_default:
|
|
122
|
+
errors.append(f"variable '{name}': required, but no value was provided")
|
|
123
|
+
continue
|
|
124
|
+
|
|
108
125
|
scope[name] = _coerce(value, type_, name, errors)
|
|
109
126
|
|
|
110
127
|
# Passing something the prompt doesn't declare is nearly always a rename
|
|
@@ -29,6 +29,16 @@ class PromptVariable:
|
|
|
29
29
|
regex, with no defaults and no required-ness. The declaration is what lets
|
|
30
30
|
``compile()`` fail before the model call rather than shipping a literal
|
|
31
31
|
``{{typo}}`` and finding out from the bill.
|
|
32
|
+
|
|
33
|
+
``required`` is enforced by ``compile()``, which runs in YOUR process —
|
|
34
|
+
Trodo only stores the declaration. Nothing server-side rejects a call for a
|
|
35
|
+
missing variable, and nothing rejects a version for declaring one.
|
|
36
|
+
|
|
37
|
+
Marking a variable required means the caller must pass a value that will
|
|
38
|
+
actually render: an omitted key, ``None`` and ``""`` all fail, since all
|
|
39
|
+
three render as nothing (``0`` and ``False`` pass). A variable with
|
|
40
|
+
a ``default`` can never fail the check — the default is a value — so the two
|
|
41
|
+
are mutually exclusive in practice.
|
|
32
42
|
"""
|
|
33
43
|
|
|
34
44
|
name: str
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|