trodo-python 2.21.0__py3-none-any.whl → 2.23.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/otel/auto_instrument.py +58 -53
- trodo/otel/prompt_trace.py +65 -3
- trodo/otel/wrap_agent.py +53 -7
- trodo/prompts/compile.py +38 -2
- trodo/prompts/template.py +16 -1
- trodo/prompts/types.py +21 -12
- {trodo_python-2.21.0.dist-info → trodo_python-2.23.0.dist-info}/METADATA +1 -1
- {trodo_python-2.21.0.dist-info → trodo_python-2.23.0.dist-info}/RECORD +10 -10
- {trodo_python-2.21.0.dist-info → trodo_python-2.23.0.dist-info}/WHEEL +0 -0
- {trodo_python-2.21.0.dist-info → trodo_python-2.23.0.dist-info}/top_level.txt +0 -0
trodo/otel/auto_instrument.py
CHANGED
|
@@ -401,6 +401,46 @@ class _OtelAdapter(_SpanProcessorBase): # type: ignore[valid-type,misc]
|
|
|
401
401
|
return True
|
|
402
402
|
|
|
403
403
|
|
|
404
|
+
def _instrument(module_name: str, *preferred: str) -> None:
|
|
405
|
+
"""Import ``module_name`` and call ``instrument()`` on its Instrumentor.
|
|
406
|
+
|
|
407
|
+
Upstream renames the exported class often enough that pinning one exact
|
|
408
|
+
spelling is a silent-failure generator: the import raises, the caller's
|
|
409
|
+
``except Exception`` treats it as "package not installed", and the user who
|
|
410
|
+
installed exactly what we told them to gets no spans and no error. That is
|
|
411
|
+
how ``LangChainInstrumentor`` (upstream ships ``LangchainInstrumentor``) and
|
|
412
|
+
``GoogleGenerativeAIInstrumentor`` both went dark.
|
|
413
|
+
|
|
414
|
+
So: try the known spellings in order, then fall back to whatever single
|
|
415
|
+
``*Instrumentor`` the module exports. Mirrors ``instrCtor`` in the Node SDK.
|
|
416
|
+
Raises ImportError if nothing usable is found, which the caller reports.
|
|
417
|
+
"""
|
|
418
|
+
import importlib
|
|
419
|
+
|
|
420
|
+
mod = importlib.import_module(module_name)
|
|
421
|
+
cls = None
|
|
422
|
+
for name in preferred:
|
|
423
|
+
cls = getattr(mod, name, None)
|
|
424
|
+
if cls is not None:
|
|
425
|
+
break
|
|
426
|
+
if cls is None:
|
|
427
|
+
found = [
|
|
428
|
+
n for n in dir(mod)
|
|
429
|
+
if n.endswith("Instrumentor") and n != "BaseInstrumentor"
|
|
430
|
+
and isinstance(getattr(mod, n, None), type)
|
|
431
|
+
]
|
|
432
|
+
if len(found) == 1:
|
|
433
|
+
cls = getattr(mod, found[0])
|
|
434
|
+
elif found:
|
|
435
|
+
raise ImportError(
|
|
436
|
+
f"{module_name} exports several Instrumentor classes {found}; "
|
|
437
|
+
f"none matched the expected names {list(preferred)}"
|
|
438
|
+
)
|
|
439
|
+
if cls is None:
|
|
440
|
+
raise ImportError(f"no Instrumentor class exported by {module_name}")
|
|
441
|
+
cls().instrument()
|
|
442
|
+
|
|
443
|
+
|
|
404
444
|
_INSTRUMENTORS: List[tuple[str, Callable[[], Any]]] = []
|
|
405
445
|
|
|
406
446
|
|
|
@@ -416,84 +456,49 @@ def _register_instrumentors() -> None:
|
|
|
416
456
|
return
|
|
417
457
|
|
|
418
458
|
def _anthropic() -> Any:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
AnthropicInstrumentor().instrument()
|
|
459
|
+
_instrument("opentelemetry.instrumentation.anthropic", "AnthropicInstrumentor")
|
|
422
460
|
|
|
423
461
|
def _openai() -> Any:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
OpenAIInstrumentor().instrument()
|
|
462
|
+
_instrument("opentelemetry.instrumentation.openai", "OpenAIInstrumentor")
|
|
427
463
|
|
|
428
464
|
def _openai_v2() -> Any:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
OpenAIInstrumentor().instrument()
|
|
465
|
+
_instrument("opentelemetry.instrumentation.openai_v2", "OpenAIInstrumentor")
|
|
432
466
|
|
|
433
467
|
def _langchain() -> Any:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
LangChainInstrumentor().instrument()
|
|
468
|
+
_instrument("opentelemetry.instrumentation.langchain", "LangchainInstrumentor", "LangChainInstrumentor")
|
|
437
469
|
|
|
438
470
|
def _llama_index() -> Any:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
LlamaIndexInstrumentor().instrument()
|
|
471
|
+
_instrument("opentelemetry.instrumentation.llama_index", "LlamaIndexInstrumentor")
|
|
442
472
|
|
|
443
473
|
def _google_generativeai() -> Any:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# SDK, i.e. the `google-genai` package, not legacy google-generativeai.)
|
|
451
|
-
instr = (
|
|
452
|
-
getattr(_m, "GoogleGenerativeAiInstrumentor", None)
|
|
453
|
-
or getattr(_m, "GoogleGenerativeAIInstrumentor", None)
|
|
454
|
-
or getattr(_m, "GenAIInstrumentor", None)
|
|
474
|
+
# Patches the new google-genai SDK, not legacy google-generativeai.
|
|
475
|
+
_instrument(
|
|
476
|
+
"opentelemetry.instrumentation.google_generativeai",
|
|
477
|
+
"GoogleGenerativeAiInstrumentor",
|
|
478
|
+
"GoogleGenerativeAIInstrumentor",
|
|
479
|
+
"GenAIInstrumentor",
|
|
455
480
|
)
|
|
456
|
-
if instr is None:
|
|
457
|
-
raise ImportError(
|
|
458
|
-
"no GoogleGenerativeAi/GenAI Instrumentor in "
|
|
459
|
-
"opentelemetry.instrumentation.google_generativeai"
|
|
460
|
-
)
|
|
461
|
-
instr().instrument()
|
|
462
481
|
|
|
463
482
|
def _vertexai() -> Any:
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
VertexAIInstrumentor().instrument()
|
|
483
|
+
_instrument("opentelemetry.instrumentation.vertexai", "VertexAIInstrumentor")
|
|
467
484
|
|
|
468
485
|
def _bedrock() -> Any:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
BedrockInstrumentor().instrument()
|
|
486
|
+
_instrument("opentelemetry.instrumentation.bedrock", "BedrockInstrumentor")
|
|
472
487
|
|
|
473
488
|
def _cohere() -> Any:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
CohereInstrumentor().instrument()
|
|
489
|
+
_instrument("opentelemetry.instrumentation.cohere", "CohereInstrumentor")
|
|
477
490
|
|
|
478
491
|
def _mistralai() -> Any:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
MistralAiInstrumentor().instrument()
|
|
492
|
+
_instrument("opentelemetry.instrumentation.mistralai", "MistralAiInstrumentor")
|
|
482
493
|
|
|
483
494
|
def _haystack() -> Any:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
HaystackInstrumentor().instrument()
|
|
495
|
+
_instrument("opentelemetry.instrumentation.haystack", "HaystackInstrumentor")
|
|
487
496
|
|
|
488
497
|
def _httpx() -> Any:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
HTTPXClientInstrumentor().instrument()
|
|
498
|
+
_instrument("opentelemetry.instrumentation.httpx", "HTTPXClientInstrumentor")
|
|
492
499
|
|
|
493
500
|
def _requests() -> Any:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
RequestsInstrumentor().instrument()
|
|
501
|
+
_instrument("opentelemetry.instrumentation.requests", "RequestsInstrumentor")
|
|
497
502
|
|
|
498
503
|
_INSTRUMENTORS = [
|
|
499
504
|
("anthropic", _anthropic),
|
trodo/otel/prompt_trace.py
CHANGED
|
@@ -14,6 +14,7 @@ Mirrors ``sdks/trodo-node-sdk/src/otel/promptTrace.ts``.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from collections import OrderedDict
|
|
17
18
|
from typing import Any, Dict, List, Optional
|
|
18
19
|
|
|
19
20
|
from .context import get_active_context
|
|
@@ -27,6 +28,55 @@ def new_prompt_state() -> Dict[str, Any]:
|
|
|
27
28
|
return {"current": None, "all": []}
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
# Compiled prompt (or its messages list) -> the version it came from.
|
|
32
|
+
#
|
|
33
|
+
# Mirrors the Node SDK. The run-scope mechanism below only fires when compile()
|
|
34
|
+
# runs inside a run, which leaves the ordinary "fetch and compile up front, open
|
|
35
|
+
# the run around the model call" shape with no link at all. Tagging the compiled
|
|
36
|
+
# value means a span handed those messages as ``input`` recovers the version
|
|
37
|
+
# wherever compile() happened.
|
|
38
|
+
#
|
|
39
|
+
# Keyed by id() with a weak-ish discipline: lists are unhashable and cannot go
|
|
40
|
+
# in a WeakValueDictionary, so entries are bounded and evicted FIFO rather than
|
|
41
|
+
# held forever. The link is best-effort — losing an old entry costs one span's
|
|
42
|
+
# label, never correctness.
|
|
43
|
+
_MAX_CARRIERS = 4096
|
|
44
|
+
_carrier_refs: "OrderedDict[int, PromptRef]" = OrderedDict()
|
|
45
|
+
_carrier_keepalive: "OrderedDict[int, Any]" = OrderedDict()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tag_carrier_with_prompt(carrier: Any, ref: PromptRef) -> None:
|
|
49
|
+
"""Attach a prompt identity to a value the caller will pass to a span."""
|
|
50
|
+
if carrier is None or isinstance(carrier, (str, bytes, int, float, bool)):
|
|
51
|
+
return
|
|
52
|
+
key = id(carrier)
|
|
53
|
+
_carrier_refs[key] = ref
|
|
54
|
+
# Hold a reference so the id cannot be recycled by another object while the
|
|
55
|
+
# mapping still names it — that would mislabel an unrelated span.
|
|
56
|
+
_carrier_keepalive[key] = carrier
|
|
57
|
+
_carrier_refs.move_to_end(key)
|
|
58
|
+
_carrier_keepalive.move_to_end(key)
|
|
59
|
+
while len(_carrier_refs) > _MAX_CARRIERS:
|
|
60
|
+
old, _ = _carrier_refs.popitem(last=False)
|
|
61
|
+
_carrier_keepalive.pop(old, None)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def prompt_ref_from_carrier(carrier: Any) -> Optional[PromptRef]:
|
|
65
|
+
"""Recover the prompt identity from a span's input, if it carries one."""
|
|
66
|
+
if carrier is None or isinstance(carrier, (str, bytes, int, float, bool)):
|
|
67
|
+
return None
|
|
68
|
+
return _carrier_refs.get(id(carrier))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def remember_prompt(state: Any, ref: PromptRef) -> None:
|
|
72
|
+
"""Add a ref to the run's deduped set if it isn't already there."""
|
|
73
|
+
if not isinstance(state, dict) or not ref or not ref.get("name"):
|
|
74
|
+
return
|
|
75
|
+
key = _ref_key(ref)
|
|
76
|
+
if not any(_ref_key(r) == key for r in state.get("all", [])):
|
|
77
|
+
state.setdefault("all", []).append(ref)
|
|
78
|
+
|
|
79
|
+
|
|
30
80
|
def _ref_key(ref: PromptRef) -> str:
|
|
31
81
|
return ref.get("version_hash") or ref.get("name") or ""
|
|
32
82
|
|
|
@@ -66,12 +116,24 @@ def prompt_attributes(ref: Optional[PromptRef]) -> Dict[str, str]:
|
|
|
66
116
|
|
|
67
117
|
|
|
68
118
|
def merge_prompt_attrs(
|
|
69
|
-
active: Any, attrs: Optional[Dict[str, Any]]
|
|
119
|
+
active: Any, attrs: Optional[Dict[str, Any]], from_input: Optional[PromptRef] = None
|
|
70
120
|
) -> Optional[Dict[str, Any]]:
|
|
71
|
-
"""Merge the
|
|
121
|
+
"""Merge the prompt this span used into its attributes.
|
|
122
|
+
|
|
123
|
+
The span's own INPUT wins over the run's most-recently-compiled prompt: it
|
|
124
|
+
is more specific (in a run using two prompts, a span holding prompt A's
|
|
125
|
+
messages must report A even if B was compiled later), and it is the only
|
|
126
|
+
signal available when compile() ran outside the run.
|
|
127
|
+
|
|
128
|
+
A prompt found this way is also folded into the run's deduped set so the
|
|
129
|
+
run-level ``trodo.prompts`` list stays complete either way.
|
|
130
|
+
"""
|
|
72
131
|
merged: Dict[str, Any] = dict(attrs or {})
|
|
73
132
|
state = getattr(active, "prompt_state", None) if active is not None else None
|
|
74
|
-
if
|
|
133
|
+
if from_input is not None:
|
|
134
|
+
remember_prompt(state, from_input)
|
|
135
|
+
merged.update(prompt_attributes(from_input))
|
|
136
|
+
elif isinstance(state, dict):
|
|
75
137
|
merged.update(prompt_attributes(state.get("current")))
|
|
76
138
|
return merged or None
|
|
77
139
|
|
trodo/otel/wrap_agent.py
CHANGED
|
@@ -32,7 +32,12 @@ from datetime import datetime, timezone
|
|
|
32
32
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
33
33
|
|
|
34
34
|
from .context import ActiveSpanContext, get_active_context, run_with_context
|
|
35
|
-
from .prompt_trace import
|
|
35
|
+
from .prompt_trace import (
|
|
36
|
+
merge_prompt_attrs,
|
|
37
|
+
new_prompt_state,
|
|
38
|
+
prompt_ref_from_carrier,
|
|
39
|
+
remember_prompt,
|
|
40
|
+
)
|
|
36
41
|
from .processor import TrodoSpanProcessor, TrodoRun, TrodoSpan
|
|
37
42
|
from .transport import get_transport_mode, get_otel_tracer, get_otel_helpers
|
|
38
43
|
|
|
@@ -281,13 +286,21 @@ def _tag_error_with_run(exc: object, run_id: Optional[str]) -> None:
|
|
|
281
286
|
class RunHandle:
|
|
282
287
|
"""Handle returned by wrap_agent for setting input/output and getting run_id."""
|
|
283
288
|
|
|
284
|
-
def __init__(
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
run_id: str,
|
|
292
|
+
agent_name: str,
|
|
293
|
+
distinct_id: str,
|
|
294
|
+
prompt_state: Optional[Dict[str, Any]] = None,
|
|
295
|
+
) -> None:
|
|
285
296
|
self.run_id = run_id
|
|
286
297
|
self.agent_name = agent_name
|
|
287
298
|
# Always populated — wrap_agent mints anon if caller didn't pass one
|
|
288
299
|
# so downstream ``trodo.feedback(distinct_id=...)`` always has a target.
|
|
289
300
|
self.distinct_id = distinct_id
|
|
290
301
|
self.input: Optional[Union[str, Dict[str, Any]]] = None
|
|
302
|
+
#: The run's prompt accumulator, so set_input can register a prompt.
|
|
303
|
+
self._prompt_state = prompt_state
|
|
291
304
|
self.output: Optional[Union[str, Dict[str, Any]]] = None
|
|
292
305
|
self.metadata: Dict[str, Any] = {}
|
|
293
306
|
# Manually-recorded run-level error (via set_error_summary). When set
|
|
@@ -298,7 +311,16 @@ class RunHandle:
|
|
|
298
311
|
def set_input(self, value: Any) -> None:
|
|
299
312
|
"""Set the run input. Prefer a chat-message list
|
|
300
313
|
``[{"role": "user", "content": ...}, ...]`` — the backend embeds the
|
|
301
|
-
user messages as the run's semantic input.
|
|
314
|
+
user messages as the run's semantic input.
|
|
315
|
+
|
|
316
|
+
Handing this the messages from a managed prompt also records that
|
|
317
|
+
prompt on the run: ``run.set_input(compiled.messages)`` is a common
|
|
318
|
+
shape, and the run demonstrably used that prompt, so the run-level
|
|
319
|
+
``trodo.prompts`` list should say so even when compile() ran outside
|
|
320
|
+
the run."""
|
|
321
|
+
ref = prompt_ref_from_carrier(value)
|
|
322
|
+
if ref is not None:
|
|
323
|
+
remember_prompt(self._prompt_state, ref)
|
|
302
324
|
self.input = _prepare_value(value)
|
|
303
325
|
|
|
304
326
|
def set_output(self, value: Any) -> None:
|
|
@@ -330,6 +352,10 @@ class SpanHandle:
|
|
|
330
352
|
self.span_id = span_id
|
|
331
353
|
self.name = name
|
|
332
354
|
self.input: Optional[Union[str, Dict[str, Any]]] = None
|
|
355
|
+
#: Prompt this span used, recovered from the value passed as input.
|
|
356
|
+
#: Captured from the ORIGINAL object: _prepare_value JSON-serialises
|
|
357
|
+
#: anything that isn't a dict/list, and identity cannot survive that.
|
|
358
|
+
self.prompt_ref: Optional[Dict[str, Any]] = None
|
|
333
359
|
self.output: Optional[Union[str, Dict[str, Any]]] = None
|
|
334
360
|
self.attributes: Dict[str, Any] = {}
|
|
335
361
|
self.model: Optional[str] = None
|
|
@@ -393,6 +419,7 @@ class SpanHandle:
|
|
|
393
419
|
``context`` for RAG docs, any order, multiple per role) — each role is
|
|
394
420
|
embedded separately and powers the AI-score detectors. Anything else
|
|
395
421
|
is stored as one opaque input."""
|
|
422
|
+
self.prompt_ref = prompt_ref_from_carrier(value) or self.prompt_ref
|
|
396
423
|
self.input = _prepare_value(value)
|
|
397
424
|
|
|
398
425
|
def set_output(self, value: Any) -> None:
|
|
@@ -594,7 +621,6 @@ class wrap_agent:
|
|
|
594
621
|
self._started_iso = _now_iso()
|
|
595
622
|
self._started_ms = time.time() * 1000.0
|
|
596
623
|
|
|
597
|
-
self.handle = RunHandle(run_id, self._agent_name, self._distinct_id)
|
|
598
624
|
ctx = ActiveSpanContext(
|
|
599
625
|
run_id=run_id,
|
|
600
626
|
span_id=root_span_id,
|
|
@@ -603,6 +629,9 @@ class wrap_agent:
|
|
|
603
629
|
processor=self._processor,
|
|
604
630
|
prompt_state=new_prompt_state(),
|
|
605
631
|
)
|
|
632
|
+
self.handle = RunHandle(
|
|
633
|
+
run_id, self._agent_name, self._distinct_id, ctx.prompt_state
|
|
634
|
+
)
|
|
606
635
|
self._ctx = ctx
|
|
607
636
|
self._ctx_mgr = run_with_context(ctx)
|
|
608
637
|
self._ctx_mgr.__enter__()
|
|
@@ -708,7 +737,12 @@ class wrap_agent:
|
|
|
708
737
|
otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
|
|
709
738
|
|
|
710
739
|
self._otel_span = otel_span
|
|
711
|
-
|
|
740
|
+
# OTLP mode builds no Trodo run context (documented limitation), so
|
|
741
|
+
# the handle gets a standalone accumulator — enough for set_input() to
|
|
742
|
+
# register a prompt even here.
|
|
743
|
+
self.handle = RunHandle(
|
|
744
|
+
run_id, self._agent_name, self._distinct_id, new_prompt_state()
|
|
745
|
+
)
|
|
712
746
|
return self.handle
|
|
713
747
|
|
|
714
748
|
def _exit_otel(self, exc_type, exc, tb) -> None:
|
|
@@ -770,6 +804,7 @@ class join_run:
|
|
|
770
804
|
self._parent_span_id = parent_span_id
|
|
771
805
|
self._name = name
|
|
772
806
|
self._kind = kind
|
|
807
|
+
self._prompt_ref = prompt_ref_from_carrier(input)
|
|
773
808
|
self._input = _prepare_value(input) if input is not None else None
|
|
774
809
|
self._attributes = attributes
|
|
775
810
|
self._ctx_mgr: Optional[run_with_context] = None
|
|
@@ -786,6 +821,8 @@ class join_run:
|
|
|
786
821
|
self.handle = SpanHandle(self._span_id, self._name)
|
|
787
822
|
if self._input is not None:
|
|
788
823
|
self.handle.input = self._input
|
|
824
|
+
if self._prompt_ref is not None:
|
|
825
|
+
self.handle.prompt_ref = self._prompt_ref
|
|
789
826
|
if self._attributes:
|
|
790
827
|
self.handle.attributes.update(self._attributes)
|
|
791
828
|
|
|
@@ -838,7 +875,9 @@ class join_run:
|
|
|
838
875
|
cost_details=self.handle.cost_details,
|
|
839
876
|
temperature=self.handle.temperature,
|
|
840
877
|
tool_name=self.handle.tool_name,
|
|
841
|
-
attributes=merge_prompt_attrs(
|
|
878
|
+
attributes=merge_prompt_attrs(
|
|
879
|
+
self._ctx, self.handle.attributes, self.handle.prompt_ref
|
|
880
|
+
),
|
|
842
881
|
)
|
|
843
882
|
try:
|
|
844
883
|
self._processor.append_spans(self._run_id, [trodo_span])
|
|
@@ -865,6 +904,7 @@ class span:
|
|
|
865
904
|
) -> None:
|
|
866
905
|
self._name = name
|
|
867
906
|
self._kind = kind
|
|
907
|
+
self._prompt_ref = prompt_ref_from_carrier(input)
|
|
868
908
|
self._input = _prepare_value(input) if input is not None else None
|
|
869
909
|
self._attributes = attributes
|
|
870
910
|
self._ctx_mgr: Optional[run_with_context] = None
|
|
@@ -889,6 +929,8 @@ class span:
|
|
|
889
929
|
self.handle = SpanHandle(self._span_id, self._name)
|
|
890
930
|
if self._input is not None:
|
|
891
931
|
self.handle.input = self._input
|
|
932
|
+
if self._prompt_ref is not None:
|
|
933
|
+
self.handle.prompt_ref = self._prompt_ref
|
|
892
934
|
if self._attributes:
|
|
893
935
|
self.handle.attributes.update(self._attributes)
|
|
894
936
|
if self._active is None:
|
|
@@ -945,7 +987,9 @@ class span:
|
|
|
945
987
|
cost_details=self.handle.cost_details,
|
|
946
988
|
temperature=self.handle.temperature,
|
|
947
989
|
tool_name=self.handle.tool_name,
|
|
948
|
-
attributes=merge_prompt_attrs(
|
|
990
|
+
attributes=merge_prompt_attrs(
|
|
991
|
+
self._active, self.handle.attributes, self.handle.prompt_ref
|
|
992
|
+
),
|
|
949
993
|
)
|
|
950
994
|
processor: TrodoSpanProcessor = self._active.processor # type: ignore[assignment]
|
|
951
995
|
processor.enqueue_span(trodo_span)
|
|
@@ -971,6 +1015,8 @@ class span:
|
|
|
971
1015
|
self.handle = SpanHandle(self._span_id, self._name)
|
|
972
1016
|
if self._input is not None:
|
|
973
1017
|
self.handle.input = self._input
|
|
1018
|
+
if self._prompt_ref is not None:
|
|
1019
|
+
self.handle.prompt_ref = self._prompt_ref
|
|
974
1020
|
if self._attributes:
|
|
975
1021
|
self.handle.attributes.update(self._attributes)
|
|
976
1022
|
return self.handle
|
trodo/prompts/compile.py
CHANGED
|
@@ -36,6 +36,36 @@ def _q(value: Any) -> str:
|
|
|
36
36
|
except (TypeError, ValueError):
|
|
37
37
|
return repr(value)
|
|
38
38
|
|
|
39
|
+
def _js_string(value: Any) -> str:
|
|
40
|
+
"""JavaScript ``String()`` semantics, byte-for-byte.
|
|
41
|
+
|
|
42
|
+
A non-string passed for a string-typed variable is a caller type mismatch,
|
|
43
|
+
but all three engines must agree on what it renders as -- the backend
|
|
44
|
+
reference (which powers the playground) and the Node SDK both go through
|
|
45
|
+
JS ``String()``, so ``True`` must render ``true``, ``3.0`` must render
|
|
46
|
+
``3``, a list joins with commas, and a dict renders the infamous
|
|
47
|
+
``[object Object]``. The JSON rendering callers actually want lives on the
|
|
48
|
+
``json`` variable type, where all three engines already emit compact JSON.
|
|
49
|
+
"""
|
|
50
|
+
if value is None:
|
|
51
|
+
return ""
|
|
52
|
+
if isinstance(value, bool):
|
|
53
|
+
return "true" if value else "false"
|
|
54
|
+
if isinstance(value, float) and value.is_integer() and abs(value) < 1e21:
|
|
55
|
+
return str(int(value))
|
|
56
|
+
if isinstance(value, (int, float)):
|
|
57
|
+
return str(value)
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
return value
|
|
60
|
+
if isinstance(value, (list, tuple)):
|
|
61
|
+
# Array.prototype.toString: elements joined by ',', null/undefined
|
|
62
|
+
# rendering empty, recursively.
|
|
63
|
+
return ",".join(_js_string(x) for x in value)
|
|
64
|
+
if isinstance(value, dict):
|
|
65
|
+
return "[object Object]"
|
|
66
|
+
return str(value)
|
|
67
|
+
|
|
68
|
+
|
|
39
69
|
def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
|
|
40
70
|
"""Coerce a caller value to the declared type.
|
|
41
71
|
|
|
@@ -71,7 +101,7 @@ def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
|
|
|
71
101
|
return value
|
|
72
102
|
|
|
73
103
|
if type_ == "string":
|
|
74
|
-
return value if isinstance(value, str) else
|
|
104
|
+
return value if isinstance(value, str) else _js_string(value)
|
|
75
105
|
|
|
76
106
|
if type_ == "messages":
|
|
77
107
|
if not isinstance(value, list):
|
|
@@ -119,7 +149,13 @@ def build_scope(
|
|
|
119
149
|
# empty. That IS the point of declaring a default -- there is no third
|
|
120
150
|
# case where the caller has to have supplied something, and adding one
|
|
121
151
|
# would turn an empty render into a crash for no gain.
|
|
122
|
-
|
|
152
|
+
#
|
|
153
|
+
# The default applies to an ABSENT key only. An explicit ``None`` is a
|
|
154
|
+
# value -- "render this empty" -- exactly as ``null`` is in the backend
|
|
155
|
+
# reference engine and the Node SDK. This engine used to substitute the
|
|
156
|
+
# default for an explicit ``None``, so the same call rendered different
|
|
157
|
+
# text in Python than everywhere else, including the playground.
|
|
158
|
+
if not has:
|
|
123
159
|
value = default if has_default else ([] if type_ == "messages" else "")
|
|
124
160
|
|
|
125
161
|
scope[name] = _coerce(value, type_, name, errors)
|
trodo/prompts/template.py
CHANGED
|
@@ -159,6 +159,21 @@ def _lookup(path: str, scopes: Sequence[Any]) -> Tuple[bool, Any]:
|
|
|
159
159
|
return False, None
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _jsonable(value):
|
|
163
|
+
"""Normalise for JSON.stringify parity: JS has one number type, so a float
|
|
164
|
+
that is a whole number must serialise as ``1`` and not ``1.0`` -- at any
|
|
165
|
+
depth. Everything else passes through untouched."""
|
|
166
|
+
if isinstance(value, bool):
|
|
167
|
+
return value
|
|
168
|
+
if isinstance(value, float) and value.is_integer() and abs(value) < 1e21:
|
|
169
|
+
return int(value)
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
return {k: _jsonable(v) for k, v in value.items()}
|
|
172
|
+
if isinstance(value, (list, tuple)):
|
|
173
|
+
return [_jsonable(v) for v in value]
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
162
177
|
def _stringify(value: Any) -> str:
|
|
163
178
|
if value is None:
|
|
164
179
|
return ""
|
|
@@ -173,7 +188,7 @@ def _stringify(value: Any) -> str:
|
|
|
173
188
|
return str(int(value))
|
|
174
189
|
return str(value)
|
|
175
190
|
try:
|
|
176
|
-
return json.dumps(value, separators=(",", ":"))
|
|
191
|
+
return json.dumps(_jsonable(value), separators=(",", ":"), ensure_ascii=False)
|
|
177
192
|
except (TypeError, ValueError):
|
|
178
193
|
return str(value)
|
|
179
194
|
|
trodo/prompts/types.py
CHANGED
|
@@ -89,21 +89,30 @@ class ManagedPrompt:
|
|
|
89
89
|
values = dict(variables or {})
|
|
90
90
|
values.update(kwargs)
|
|
91
91
|
compiled = compile_prompt(self, values)
|
|
92
|
-
# Prompt traceability
|
|
93
|
-
#
|
|
94
|
-
# outside a run context. Never records a fallback (no real hash).
|
|
92
|
+
# Prompt traceability. Never records a fallback — it has no real hash,
|
|
93
|
+
# so a link to it would name a version that does not exist.
|
|
95
94
|
if not self.is_fallback:
|
|
96
95
|
try:
|
|
97
|
-
from ..otel.prompt_trace import
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
"name": self.name,
|
|
102
|
-
"version_hash": self.version_hash,
|
|
103
|
-
"content_hash": self.content_hash,
|
|
104
|
-
"label": self.trace_label,
|
|
105
|
-
}
|
|
96
|
+
from ..otel.prompt_trace import (
|
|
97
|
+
record_compiled_prompt,
|
|
98
|
+
tag_carrier_with_prompt,
|
|
106
99
|
)
|
|
100
|
+
|
|
101
|
+
ref = {
|
|
102
|
+
"name": self.name,
|
|
103
|
+
"version_hash": self.version_hash,
|
|
104
|
+
"content_hash": self.content_hash,
|
|
105
|
+
"label": self.trace_label,
|
|
106
|
+
}
|
|
107
|
+
# (1) the surrounding run scope, if any — the only route that
|
|
108
|
+
# reaches auto-instrumented spans, whose input we never see.
|
|
109
|
+
record_compiled_prompt(ref)
|
|
110
|
+
# (2) the compiled value itself, so a span handed these messages
|
|
111
|
+
# as ``input`` recovers the version even when compile() ran
|
|
112
|
+
# outside the run. Both the wrapper and the list are tagged,
|
|
113
|
+
# since callers pass one or the other.
|
|
114
|
+
tag_carrier_with_prompt(compiled, ref)
|
|
115
|
+
tag_carrier_with_prompt(getattr(compiled, "messages", None), ref)
|
|
107
116
|
except Exception: # noqa: BLE001
|
|
108
117
|
pass # traceability must never break compile
|
|
109
118
|
return compiled
|
|
@@ -16,18 +16,18 @@ trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8p
|
|
|
16
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
|
-
trodo/otel/auto_instrument.py,sha256=
|
|
19
|
+
trodo/otel/auto_instrument.py,sha256=kcfajnmaVuX_giLYBr2P7GiWk-lzZt-iniY8rJEALYw,21819
|
|
20
20
|
trodo/otel/context.py,sha256=Jd0aTc0Q-1dM5kXXinhZD8YtnpdKgvneNiODyboVGKY,1418
|
|
21
21
|
trodo/otel/helpers.py,sha256=XOMWcgZHaq5SQbkFxDaXPE4CDFjn01xjJmJ1vIxvwpw,20730
|
|
22
22
|
trodo/otel/processor.py,sha256=LKlXxP3BeQ7DP8SzYAgXZZOeJ6-6b7e43i19gCUC_0Y,7939
|
|
23
|
-
trodo/otel/prompt_trace.py,sha256=
|
|
23
|
+
trodo/otel/prompt_trace.py,sha256=X-8AIfwMmegOZl9yMifUkpYu1rV4wE6-kuYYtts0wgA,6184
|
|
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=Izty1jxF4ANVqS72L0CR9jmMuW372AApaEO_0LC5Thw,43860
|
|
27
27
|
trodo/prompts/__init__.py,sha256=yunNc8WkTSfEEkRR-NZWXAt_ZB3Ee2INRh-4RE7uHj4,806
|
|
28
|
-
trodo/prompts/compile.py,sha256=
|
|
29
|
-
trodo/prompts/template.py,sha256=
|
|
30
|
-
trodo/prompts/types.py,sha256=
|
|
28
|
+
trodo/prompts/compile.py,sha256=XoKDKz6Yaofhrk-87AVW-SnwbiXLvfp161AL2bAFzNY,8802
|
|
29
|
+
trodo/prompts/template.py,sha256=eGO2ZVNOw6JK7Ffj1UCYDMJFYyX5qQW2POlmFJSQpAo,9975
|
|
30
|
+
trodo/prompts/types.py,sha256=OYpaLi1H9pJjeq0gsJo8eb-bWoF8rw2u_7pdukudtqo,5258
|
|
31
31
|
trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
|
|
33
33
|
trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
|
|
@@ -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.23.0.dist-info/METADATA,sha256=HklaG2r4SZNydUnzdRUluF9KLh1d-IszAlS1Sr4UEFI,25308
|
|
40
|
+
trodo_python-2.23.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
41
|
+
trodo_python-2.23.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
|
|
42
|
+
trodo_python-2.23.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|