usegradient 1.3.1__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.
- gradient_sdk/__init__.py +122 -0
- gradient_sdk/agent.py +368 -0
- gradient_sdk/client.py +1059 -0
- gradient_sdk/context.py +91 -0
- gradient_sdk/credentials.py +92 -0
- gradient_sdk/decorators.py +202 -0
- gradient_sdk/errors.py +30 -0
- gradient_sdk/graph.py +190 -0
- gradient_sdk/integrations/__init__.py +7 -0
- gradient_sdk/integrations/agent.py +263 -0
- gradient_sdk/integrations/anthropic.py +51 -0
- gradient_sdk/integrations/openai.py +105 -0
- gradient_sdk/model_catalog.py +75 -0
- gradient_sdk/models.py +370 -0
- gradient_sdk/py.typed +1 -0
- gradient_sdk/runtime.py +1474 -0
- gradient_sdk/session.py +132 -0
- gradient_sdk/tools.py +156 -0
- gradient_sdk/tracing.py +361 -0
- usegradient-1.3.1.dist-info/METADATA +235 -0
- usegradient-1.3.1.dist-info/RECORD +22 -0
- usegradient-1.3.1.dist-info/WHEEL +4 -0
gradient_sdk/session.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from .client import GradientClient
|
|
8
|
+
from .models import CreateTraceRunResponse, MachineCreateRequest
|
|
9
|
+
from .tracing import SpanExporter, Tracer, set_tracer
|
|
10
|
+
|
|
11
|
+
_current_session: "GradientSession | None" = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GradientSession:
|
|
16
|
+
project: str
|
|
17
|
+
client: GradientClient
|
|
18
|
+
run: CreateTraceRunResponse
|
|
19
|
+
tracer: Tracer
|
|
20
|
+
machine: dict[str, Any] | None = None
|
|
21
|
+
full_proxy_url: str | None = None
|
|
22
|
+
session_id: str | None = None
|
|
23
|
+
user_id: str | None = None
|
|
24
|
+
_shutdown: bool = False
|
|
25
|
+
|
|
26
|
+
def flush(self) -> None:
|
|
27
|
+
self.tracer.flush()
|
|
28
|
+
|
|
29
|
+
def shutdown(self) -> None:
|
|
30
|
+
if self._shutdown:
|
|
31
|
+
return
|
|
32
|
+
self._shutdown = True
|
|
33
|
+
self.flush()
|
|
34
|
+
self.tracer.shutdown()
|
|
35
|
+
try:
|
|
36
|
+
self.client.finish_trace_run(self.run.id)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
global _current_session
|
|
40
|
+
if _current_session is self:
|
|
41
|
+
_current_session = None
|
|
42
|
+
set_tracer(None)
|
|
43
|
+
|
|
44
|
+
def __enter__(self) -> "GradientSession":
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
48
|
+
self.shutdown()
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def current_session() -> GradientSession | None:
|
|
53
|
+
return _current_session
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def init(
|
|
57
|
+
project: str = "default",
|
|
58
|
+
*,
|
|
59
|
+
client: GradientClient | None = None,
|
|
60
|
+
environment: MachineCreateRequest | Mapping[str, Any] | None = None,
|
|
61
|
+
trace_mode: str = "full",
|
|
62
|
+
session_id: str | None = None,
|
|
63
|
+
user_id: str | None = None,
|
|
64
|
+
metadata: Mapping[str, Any] | None = None,
|
|
65
|
+
capture_policy: Mapping[str, Any] | None = None,
|
|
66
|
+
wait_for_environment: bool = False,
|
|
67
|
+
wait_timeout_seconds: int = 30,
|
|
68
|
+
) -> GradientSession:
|
|
69
|
+
"""Initialize a Gradient project session with automatic tracing."""
|
|
70
|
+
|
|
71
|
+
global _current_session
|
|
72
|
+
if _current_session is not None:
|
|
73
|
+
_current_session.shutdown()
|
|
74
|
+
|
|
75
|
+
resolved_client = client or GradientClient.from_credentials()
|
|
76
|
+
run_metadata = {"project": project, **dict(metadata or {})}
|
|
77
|
+
machine: dict[str, Any] | None = None
|
|
78
|
+
full_proxy_url: str | None = None
|
|
79
|
+
|
|
80
|
+
if environment is not None:
|
|
81
|
+
traced = resolved_client.create_traced_machine(
|
|
82
|
+
environment,
|
|
83
|
+
trace_mode=trace_mode,
|
|
84
|
+
metadata=run_metadata,
|
|
85
|
+
wait=wait_for_environment,
|
|
86
|
+
wait_timeout_seconds=wait_timeout_seconds,
|
|
87
|
+
)
|
|
88
|
+
run = CreateTraceRunResponse.from_dict(traced["trace_run"])
|
|
89
|
+
run.ingest_token = traced["ingest_token"]
|
|
90
|
+
run.ingest_url = traced["ingest_url"]
|
|
91
|
+
machine = traced["machine"]
|
|
92
|
+
full_proxy_url = traced["full_proxy_url"]
|
|
93
|
+
else:
|
|
94
|
+
run = resolved_client.create_trace_run(
|
|
95
|
+
trace_mode=trace_mode,
|
|
96
|
+
metadata=run_metadata,
|
|
97
|
+
capture_policy=capture_policy,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ingest_endpoint = resolved_client.api_url.rstrip("/") + "/v1/traces"
|
|
101
|
+
exporter = SpanExporter(
|
|
102
|
+
endpoint=ingest_endpoint,
|
|
103
|
+
ingest_token=run.ingest_token,
|
|
104
|
+
resource={
|
|
105
|
+
"service.name": project,
|
|
106
|
+
"openinference.project.name": project,
|
|
107
|
+
"gradient.run.id": run.id,
|
|
108
|
+
},
|
|
109
|
+
transport=resolved_client._transport,
|
|
110
|
+
timeout=resolved_client.timeout,
|
|
111
|
+
)
|
|
112
|
+
tracer = Tracer(
|
|
113
|
+
project=project,
|
|
114
|
+
session_id=session_id,
|
|
115
|
+
user_id=user_id,
|
|
116
|
+
exporter=exporter,
|
|
117
|
+
)
|
|
118
|
+
set_tracer(tracer)
|
|
119
|
+
|
|
120
|
+
session = GradientSession(
|
|
121
|
+
project=project,
|
|
122
|
+
client=resolved_client,
|
|
123
|
+
run=run,
|
|
124
|
+
tracer=tracer,
|
|
125
|
+
machine=machine,
|
|
126
|
+
full_proxy_url=full_proxy_url,
|
|
127
|
+
session_id=session_id,
|
|
128
|
+
user_id=user_id,
|
|
129
|
+
)
|
|
130
|
+
_current_session = session
|
|
131
|
+
atexit.register(session.shutdown)
|
|
132
|
+
return session
|
gradient_sdk/tools.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, get_args, get_origin, get_type_hints
|
|
7
|
+
|
|
8
|
+
from .context import current_context
|
|
9
|
+
from .tracing import get_tracer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Tool:
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
fn: Callable[..., Any]
|
|
17
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
def invoke(self, **kwargs: Any) -> Any:
|
|
20
|
+
tracer = get_tracer()
|
|
21
|
+
with tracer.start_span(f"tool.{self.name}", kind="TOOL") as span:
|
|
22
|
+
span.set_attribute("tool.name", self.name)
|
|
23
|
+
span.set_attribute("input.value", json.dumps(kwargs, default=str))
|
|
24
|
+
context = current_context()
|
|
25
|
+
if context is not None:
|
|
26
|
+
span.set_attribute("gradient.run.id", context.run_id)
|
|
27
|
+
span.set_attribute("gradient.project", context.project)
|
|
28
|
+
if context.session_id:
|
|
29
|
+
span.set_attribute("session.id", context.session_id)
|
|
30
|
+
if context.user_id:
|
|
31
|
+
span.set_attribute("user.id", context.user_id)
|
|
32
|
+
try:
|
|
33
|
+
result = self.fn(**kwargs)
|
|
34
|
+
span.set_output(result)
|
|
35
|
+
return result
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
span.record_exception(exc)
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
def to_openai(self) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"type": "function",
|
|
43
|
+
"function": {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"description": self.description,
|
|
46
|
+
"parameters": self.parameters,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def to_anthropic(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"name": self.name,
|
|
53
|
+
"description": self.description,
|
|
54
|
+
"input_schema": self.parameters,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _json_type(annotation: Any) -> str:
|
|
59
|
+
origin = get_origin(annotation)
|
|
60
|
+
if origin is list:
|
|
61
|
+
return "array"
|
|
62
|
+
if origin is dict:
|
|
63
|
+
return "object"
|
|
64
|
+
if origin is tuple:
|
|
65
|
+
return "array"
|
|
66
|
+
if annotation in (str, inspect.Parameter.empty):
|
|
67
|
+
return "string"
|
|
68
|
+
if annotation is int:
|
|
69
|
+
return "integer"
|
|
70
|
+
if annotation is float:
|
|
71
|
+
return "number"
|
|
72
|
+
if annotation is bool:
|
|
73
|
+
return "boolean"
|
|
74
|
+
if origin is type(None) or annotation is type(None):
|
|
75
|
+
return "null"
|
|
76
|
+
args = get_args(annotation)
|
|
77
|
+
if args:
|
|
78
|
+
non_none = [arg for arg in args if arg is not type(None)]
|
|
79
|
+
if non_none:
|
|
80
|
+
return _json_type(non_none[0])
|
|
81
|
+
return "string"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def infer_parameters(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
85
|
+
signature = inspect.signature(fn)
|
|
86
|
+
try:
|
|
87
|
+
hints = get_type_hints(fn)
|
|
88
|
+
except Exception:
|
|
89
|
+
hints = {}
|
|
90
|
+
properties: dict[str, Any] = {}
|
|
91
|
+
required: list[str] = []
|
|
92
|
+
for name, param in signature.parameters.items():
|
|
93
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
94
|
+
continue
|
|
95
|
+
annotation = hints.get(name, param.annotation)
|
|
96
|
+
properties[name] = {"type": _json_type(annotation)}
|
|
97
|
+
if param.default is inspect.Parameter.empty:
|
|
98
|
+
required.append(name)
|
|
99
|
+
schema: dict[str, Any] = {"type": "object", "properties": properties}
|
|
100
|
+
if required:
|
|
101
|
+
schema["required"] = required
|
|
102
|
+
return schema
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def tool_from_function(
|
|
106
|
+
fn: Callable[..., Any],
|
|
107
|
+
*,
|
|
108
|
+
name: str | None = None,
|
|
109
|
+
description: str | None = None,
|
|
110
|
+
parameters: dict[str, Any] | None = None,
|
|
111
|
+
) -> Tool:
|
|
112
|
+
resolved_name = name or getattr(fn, "__name__", "tool")
|
|
113
|
+
resolved_description = description or inspect.getdoc(fn) or f"Tool {resolved_name}"
|
|
114
|
+
return Tool(
|
|
115
|
+
name=resolved_name,
|
|
116
|
+
description=resolved_description,
|
|
117
|
+
fn=fn,
|
|
118
|
+
parameters=parameters or infer_parameters(fn),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def execute_tool_calls(tools: list[Tool], calls: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
123
|
+
by_name = {tool.name: tool for tool in tools}
|
|
124
|
+
results: list[dict[str, Any]] = []
|
|
125
|
+
for call in calls:
|
|
126
|
+
name = call.get("name") or call.get("function", {}).get("name")
|
|
127
|
+
if not name:
|
|
128
|
+
continue
|
|
129
|
+
tool = by_name.get(name)
|
|
130
|
+
if tool is None:
|
|
131
|
+
results.append({"tool_call_id": call.get("id"), "name": name, "error": f"unknown tool: {name}"})
|
|
132
|
+
continue
|
|
133
|
+
raw_args = call.get("arguments") or call.get("function", {}).get("arguments") or call.get("input") or {}
|
|
134
|
+
if isinstance(raw_args, str):
|
|
135
|
+
try:
|
|
136
|
+
raw_args = json.loads(raw_args)
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
raw_args = {}
|
|
139
|
+
try:
|
|
140
|
+
output = tool.invoke(**raw_args)
|
|
141
|
+
results.append(
|
|
142
|
+
{
|
|
143
|
+
"tool_call_id": call.get("id"),
|
|
144
|
+
"name": name,
|
|
145
|
+
"output": output,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
results.append(
|
|
150
|
+
{
|
|
151
|
+
"tool_call_id": call.get("id"),
|
|
152
|
+
"name": name,
|
|
153
|
+
"error": str(exc),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return results
|
gradient_sdk/tracing.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import secrets
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
from urllib.request import Request
|
|
13
|
+
|
|
14
|
+
Transport = Callable[[Request, float], Any]
|
|
15
|
+
|
|
16
|
+
_current_span: ContextVar["Span | None"] = ContextVar("gradient_current_span", default=None)
|
|
17
|
+
_global_tracer: "Tracer | None" = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _now_ns() -> int:
|
|
21
|
+
return time.time_ns()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _otlp_value(value: Any) -> dict[str, Any]:
|
|
25
|
+
if isinstance(value, bool):
|
|
26
|
+
return {"boolValue": value}
|
|
27
|
+
if isinstance(value, int):
|
|
28
|
+
return {"intValue": str(value)}
|
|
29
|
+
if isinstance(value, float):
|
|
30
|
+
return {"doubleValue": value}
|
|
31
|
+
if isinstance(value, (list, tuple)):
|
|
32
|
+
return {"arrayValue": {"values": [_otlp_value(item) for item in value]}}
|
|
33
|
+
return {"stringValue": str(value)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _otlp_attributes(attributes: dict[str, Any]) -> list[dict[str, Any]]:
|
|
37
|
+
return [{"key": key, "value": _otlp_value(value)} for key, value in attributes.items()]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _serialize_span(span: "Span", resource: dict[str, Any], scope: dict[str, str]) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"traceId": span.trace_id,
|
|
43
|
+
"spanId": span.span_id,
|
|
44
|
+
"parentSpanId": span.parent_span_id or "",
|
|
45
|
+
"name": span.name,
|
|
46
|
+
"kind": "SPAN_KIND_INTERNAL",
|
|
47
|
+
"startTimeUnixNano": str(span.start_time_ns),
|
|
48
|
+
"endTimeUnixNano": str(span.end_time_ns or _now_ns()),
|
|
49
|
+
"attributes": _otlp_attributes(span.attributes),
|
|
50
|
+
"events": [
|
|
51
|
+
{
|
|
52
|
+
"timeUnixNano": str(event.get("time_unix_nano", _now_ns())),
|
|
53
|
+
"name": event.get("name", "exception"),
|
|
54
|
+
"attributes": _otlp_attributes(event.get("attributes", {})),
|
|
55
|
+
}
|
|
56
|
+
for event in span.events
|
|
57
|
+
],
|
|
58
|
+
"status": {
|
|
59
|
+
"code": "STATUS_CODE_ERROR" if span.status_code == "ERROR" else "STATUS_CODE_OK",
|
|
60
|
+
"message": span.status_message or "",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Span:
|
|
67
|
+
name: str
|
|
68
|
+
trace_id: str
|
|
69
|
+
span_id: str
|
|
70
|
+
parent_span_id: str | None = None
|
|
71
|
+
kind: str = "CHAIN"
|
|
72
|
+
start_time_ns: int = field(default_factory=_now_ns)
|
|
73
|
+
end_time_ns: int | None = None
|
|
74
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
events: list[dict[str, Any]] = field(default_factory=list)
|
|
76
|
+
status_code: str = "OK"
|
|
77
|
+
status_message: str = ""
|
|
78
|
+
_ended: bool = False
|
|
79
|
+
_token: Any = None
|
|
80
|
+
_tracer: "Tracer | None" = None
|
|
81
|
+
|
|
82
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
83
|
+
self.attributes[key] = value
|
|
84
|
+
|
|
85
|
+
def set_attributes(self, values: dict[str, Any]) -> None:
|
|
86
|
+
self.attributes.update(values)
|
|
87
|
+
|
|
88
|
+
def set_output(self, value: Any, *, mime_type: str = "application/json") -> None:
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
self.set_attribute("output.value", value)
|
|
91
|
+
self.set_attribute("output.mime_type", "text/plain")
|
|
92
|
+
return
|
|
93
|
+
self.set_attribute("output.value", json.dumps(value, default=str))
|
|
94
|
+
self.set_attribute("output.mime_type", mime_type)
|
|
95
|
+
|
|
96
|
+
def record_exception(self, exc: BaseException) -> None:
|
|
97
|
+
self.status_code = "ERROR"
|
|
98
|
+
self.status_message = str(exc)
|
|
99
|
+
self.events.append(
|
|
100
|
+
{
|
|
101
|
+
"name": "exception",
|
|
102
|
+
"time_unix_nano": _now_ns(),
|
|
103
|
+
"attributes": {
|
|
104
|
+
"exception.type": type(exc).__name__,
|
|
105
|
+
"exception.message": str(exc),
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def end(self) -> None:
|
|
111
|
+
if self._ended:
|
|
112
|
+
return
|
|
113
|
+
self.end_time_ns = _now_ns()
|
|
114
|
+
self._ended = True
|
|
115
|
+
if self._token is not None:
|
|
116
|
+
_current_span.reset(self._token)
|
|
117
|
+
self._token = None
|
|
118
|
+
if self._tracer is not None:
|
|
119
|
+
self._tracer._on_span_end(self)
|
|
120
|
+
|
|
121
|
+
def __enter__(self) -> "Span":
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
125
|
+
if exc is not None and exc_type is not None:
|
|
126
|
+
self.record_exception(exc) # type: ignore[arg-type]
|
|
127
|
+
self.end()
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class NoOpSpan:
|
|
132
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def set_attributes(self, values: dict[str, Any]) -> None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def set_output(self, value: Any, *, mime_type: str = "application/json") -> None:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def record_exception(self, exc: BaseException) -> None:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def end(self) -> None:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def __enter__(self) -> "NoOpSpan":
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SpanExporter:
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
endpoint: str,
|
|
159
|
+
ingest_token: str,
|
|
160
|
+
resource: dict[str, Any],
|
|
161
|
+
scope_name: str = "gradient-sdk",
|
|
162
|
+
scope_version: str = "0.3.0",
|
|
163
|
+
transport: Transport | None = None,
|
|
164
|
+
timeout: float = 10.0,
|
|
165
|
+
batch_size: int = 32,
|
|
166
|
+
flush_interval_seconds: float = 1.0,
|
|
167
|
+
) -> None:
|
|
168
|
+
self.endpoint = endpoint.rstrip("/")
|
|
169
|
+
self.ingest_token = ingest_token
|
|
170
|
+
self.resource = resource
|
|
171
|
+
self.scope = {"name": scope_name, "version": scope_version}
|
|
172
|
+
self.transport = transport
|
|
173
|
+
self.timeout = timeout
|
|
174
|
+
self.batch_size = batch_size
|
|
175
|
+
self.flush_interval_seconds = flush_interval_seconds
|
|
176
|
+
self._queue: list[Span] = []
|
|
177
|
+
self._lock = threading.Lock()
|
|
178
|
+
self._stop = threading.Event()
|
|
179
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
180
|
+
self._thread.start()
|
|
181
|
+
atexit.register(self.shutdown)
|
|
182
|
+
|
|
183
|
+
def enqueue(self, span: Span) -> None:
|
|
184
|
+
with self._lock:
|
|
185
|
+
self._queue.append(span)
|
|
186
|
+
if len(self._queue) >= self.batch_size:
|
|
187
|
+
self._flush_locked()
|
|
188
|
+
|
|
189
|
+
def flush(self) -> None:
|
|
190
|
+
with self._lock:
|
|
191
|
+
self._flush_locked()
|
|
192
|
+
|
|
193
|
+
def shutdown(self) -> None:
|
|
194
|
+
self._stop.set()
|
|
195
|
+
self.flush()
|
|
196
|
+
if self._thread.is_alive():
|
|
197
|
+
self._thread.join(timeout=2.0)
|
|
198
|
+
|
|
199
|
+
def _worker(self) -> None:
|
|
200
|
+
while not self._stop.wait(self.flush_interval_seconds):
|
|
201
|
+
self.flush()
|
|
202
|
+
|
|
203
|
+
def _flush_locked(self) -> None:
|
|
204
|
+
if not self._queue:
|
|
205
|
+
return
|
|
206
|
+
spans = self._queue[:]
|
|
207
|
+
self._queue.clear()
|
|
208
|
+
payload = {
|
|
209
|
+
"resourceSpans": [
|
|
210
|
+
{
|
|
211
|
+
"resource": {"attributes": _otlp_attributes(self.resource)},
|
|
212
|
+
"scopeSpans": [
|
|
213
|
+
{
|
|
214
|
+
"scope": self.scope,
|
|
215
|
+
"spans": [_serialize_span(span, self.resource, self.scope) for span in spans],
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
req = Request(
|
|
222
|
+
self.endpoint,
|
|
223
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
224
|
+
method="POST",
|
|
225
|
+
headers={
|
|
226
|
+
"Authorization": f"Bearer {self.ingest_token}",
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Accept": "application/json",
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
try:
|
|
232
|
+
if self.transport is not None:
|
|
233
|
+
with self.transport(req, self.timeout) as res:
|
|
234
|
+
_ = res.read()
|
|
235
|
+
else:
|
|
236
|
+
from urllib.request import urlopen
|
|
237
|
+
|
|
238
|
+
with urlopen(req, timeout=self.timeout) as res:
|
|
239
|
+
_ = res.read()
|
|
240
|
+
except Exception:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Tracer:
|
|
245
|
+
def __init__(
|
|
246
|
+
self,
|
|
247
|
+
*,
|
|
248
|
+
project: str,
|
|
249
|
+
session_id: str | None = None,
|
|
250
|
+
user_id: str | None = None,
|
|
251
|
+
exporter: SpanExporter | None = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
self.project = project
|
|
254
|
+
self.session_id = session_id
|
|
255
|
+
self.user_id = user_id
|
|
256
|
+
self.exporter = exporter
|
|
257
|
+
self.resource = {
|
|
258
|
+
"service.name": project,
|
|
259
|
+
"openinference.project.name": project,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def start_span(
|
|
263
|
+
self,
|
|
264
|
+
name: str,
|
|
265
|
+
*,
|
|
266
|
+
kind: str = "CHAIN",
|
|
267
|
+
attributes: dict[str, Any] | None = None,
|
|
268
|
+
) -> Span | NoOpSpan:
|
|
269
|
+
if self.exporter is None:
|
|
270
|
+
return NoOpSpan()
|
|
271
|
+
|
|
272
|
+
parent = _current_span.get()
|
|
273
|
+
trace_id = parent.trace_id if parent is not None else secrets.token_hex(16)
|
|
274
|
+
parent_span_id = parent.span_id if parent is not None else None
|
|
275
|
+
span = Span(
|
|
276
|
+
name=name,
|
|
277
|
+
trace_id=trace_id,
|
|
278
|
+
span_id=secrets.token_hex(8),
|
|
279
|
+
parent_span_id=parent_span_id,
|
|
280
|
+
kind=kind,
|
|
281
|
+
_tracer=self,
|
|
282
|
+
)
|
|
283
|
+
span.set_attribute("openinference.span.kind", kind)
|
|
284
|
+
if self.session_id:
|
|
285
|
+
span.set_attribute("session.id", self.session_id)
|
|
286
|
+
if self.user_id:
|
|
287
|
+
span.set_attribute("user.id", self.user_id)
|
|
288
|
+
if attributes:
|
|
289
|
+
span.set_attributes(attributes)
|
|
290
|
+
span._token = _current_span.set(span)
|
|
291
|
+
return span
|
|
292
|
+
|
|
293
|
+
def _on_span_end(self, span: Span) -> None:
|
|
294
|
+
if self.exporter is not None:
|
|
295
|
+
self.exporter.enqueue(span)
|
|
296
|
+
|
|
297
|
+
def flush(self) -> None:
|
|
298
|
+
if self.exporter is not None:
|
|
299
|
+
self.exporter.flush()
|
|
300
|
+
|
|
301
|
+
def shutdown(self) -> None:
|
|
302
|
+
if self.exporter is not None:
|
|
303
|
+
self.exporter.shutdown()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class NoOpTracer:
|
|
307
|
+
def start_span(
|
|
308
|
+
self,
|
|
309
|
+
name: str,
|
|
310
|
+
*,
|
|
311
|
+
kind: str = "CHAIN",
|
|
312
|
+
attributes: dict[str, Any] | None = None,
|
|
313
|
+
) -> NoOpSpan:
|
|
314
|
+
return NoOpSpan()
|
|
315
|
+
|
|
316
|
+
def flush(self) -> None:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def shutdown(self) -> None:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
_NO_OP_TRACER = NoOpTracer()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_tracer() -> Tracer | NoOpTracer:
|
|
327
|
+
global _global_tracer
|
|
328
|
+
if _global_tracer is None:
|
|
329
|
+
token = os.environ.get("GRADIENT_TRACE_TOKEN", "")
|
|
330
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "")
|
|
331
|
+
if token and endpoint:
|
|
332
|
+
project = (
|
|
333
|
+
os.environ.get("GRADIENT_PROJECT")
|
|
334
|
+
or os.environ.get("OTEL_SERVICE_NAME")
|
|
335
|
+
or "gradient"
|
|
336
|
+
)
|
|
337
|
+
run_id = os.environ.get("GRADIENT_TRACE_RUN_ID", "")
|
|
338
|
+
resource = {
|
|
339
|
+
"service.name": project,
|
|
340
|
+
"openinference.project.name": project,
|
|
341
|
+
}
|
|
342
|
+
if run_id:
|
|
343
|
+
resource["gradient.run.id"] = run_id
|
|
344
|
+
_global_tracer = Tracer(
|
|
345
|
+
project=project,
|
|
346
|
+
exporter=SpanExporter(
|
|
347
|
+
endpoint=endpoint,
|
|
348
|
+
ingest_token=token,
|
|
349
|
+
resource=resource,
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
return _global_tracer or _NO_OP_TRACER
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def set_tracer(tracer: Tracer | None) -> None:
|
|
356
|
+
global _global_tracer
|
|
357
|
+
_global_tracer = tracer
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def current_span() -> Span | NoOpSpan | None:
|
|
361
|
+
return _current_span.get()
|