divi 0.0.1.dev23__py3-none-any.whl → 0.0.1.dev47__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.
divi/__init__.py CHANGED
@@ -2,8 +2,10 @@ from typing import Optional
2
2
 
3
3
  from . import proto
4
4
  from .decorators import obs_openai, observable
5
+ from .evaluation import Evaluator, Score
5
6
  from .services import Auth, Core, DataPark
6
7
  from .session import Session
8
+ from .signals import Kind
7
9
 
8
10
  name: str = "divi"
9
11
 
@@ -11,6 +13,7 @@ _session: Optional[Session] = None
11
13
  _core: Optional[Core] = None
12
14
  _auth: Optional[Auth] = None
13
15
  _datapark: Optional[DataPark] = None
16
+ _evaluator: Optional[Evaluator] = None
14
17
 
15
- __version__ = "0.0.1.dev23"
16
- __all__ = ["proto", "obs_openai", "observable"]
18
+ __version__ = "0.0.1.dev47"
19
+ __all__ = ["proto", "obs_openai", "observable", "Score", "Kind"]
@@ -0,0 +1,34 @@
1
+ from typing import Any
2
+
3
+ from google.protobuf.message import Error
4
+ from openai.types.chat import ChatCompletion
5
+ from typing_extensions import Dict
6
+
7
+ import divi
8
+ from divi.evaluation.evaluator import EvaluationScore
9
+ from divi.signals.span import Span
10
+
11
+
12
+ def collect(span: Span, input: Dict[str, Any], result: Any):
13
+ if not divi._datapark or span.trace_id is None:
14
+ raise Error("divi._datapark or span.trace_id is None")
15
+ # TODO: collect inputs and outputs for SPAN_KIND_FUNCTION
16
+
17
+ # collect inputs and outputs for SPAN_KIND_LLM
18
+ if isinstance(result, ChatCompletion):
19
+ divi._datapark.create_chat_completion(
20
+ span_id=span.span_id,
21
+ trace_id=span.trace_id,
22
+ inputs=input,
23
+ completion=result,
24
+ )
25
+
26
+ # collect inputs and outputs for SPAN_KIND_EVALUATION
27
+ if isinstance(result, list) and all(
28
+ isinstance(x, EvaluationScore) for x in result
29
+ ):
30
+ divi._datapark.create_scores(
31
+ span_id=span.span_id,
32
+ trace_id=span.trace_id,
33
+ scores=result,
34
+ )
@@ -2,7 +2,11 @@ import functools
2
2
  from collections.abc import Callable
3
3
  from typing import TYPE_CHECKING, TypeVar, Union
4
4
 
5
+ from typing_extensions import Optional
6
+
5
7
  from divi.decorators.observable import observable
8
+ from divi.evaluation.scores import Score
9
+ from divi.signals.span import Kind
6
10
  from divi.utils import is_async
7
11
 
8
12
  if TYPE_CHECKING:
@@ -11,22 +15,34 @@ if TYPE_CHECKING:
11
15
  C = TypeVar("C", bound=Union["OpenAI", "AsyncOpenAI"])
12
16
 
13
17
 
14
- def _get_observable_create(create: Callable) -> Callable:
18
+ def _get_observable_create(
19
+ create: Callable,
20
+ name: Optional[str] = None,
21
+ scores: Optional[list[Score]] = None,
22
+ ) -> Callable:
15
23
  @functools.wraps(create)
16
24
  def observable_create(*args, stream: bool = False, **kwargs):
17
- decorator = observable(kind="llm")
25
+ decorator = observable(kind=Kind.llm, name=name, scores=scores)
18
26
  return decorator(create)(*args, stream=stream, **kwargs)
19
27
 
20
28
  # TODO Async Observable Create
21
29
  return observable_create if not is_async(create) else create
22
30
 
23
31
 
24
- def obs_openai(client: C) -> C:
32
+ def obs_openai(
33
+ client: C,
34
+ name: Optional[str] = "Agent",
35
+ scores: Optional[list[Score]] = None,
36
+ ) -> C:
25
37
  """Make OpenAI client observable."""
26
38
  client.chat.completions.create = _get_observable_create(
27
- client.chat.completions.create
39
+ client.chat.completions.create,
40
+ name=name,
41
+ scores=scores,
28
42
  )
29
43
  client.completions.create = _get_observable_create(
30
- client.completions.create
44
+ client.completions.create,
45
+ name=name,
46
+ scores=scores,
31
47
  )
32
48
  return client
@@ -1,11 +1,8 @@
1
- import contextvars
2
1
  import functools
3
- import inspect
4
2
  from typing import (
5
3
  Any,
6
4
  Callable,
7
5
  Generic,
8
- List,
9
6
  Mapping,
10
7
  Optional,
11
8
  ParamSpec,
@@ -16,24 +13,15 @@ from typing import (
16
13
  runtime_checkable,
17
14
  )
18
15
 
19
- from openai.types.chat import ChatCompletion
20
-
21
- import divi
22
- from divi.proto.trace.v1.trace_pb2 import ScopeSpans
16
+ from divi.decorators.observe import observe
17
+ from divi.evaluation.evaluate import evaluate_scores
18
+ from divi.evaluation.scores import Score
23
19
  from divi.session import SessionExtra
24
- from divi.session.setup import setup
25
- from divi.signals.trace import Span
26
- from divi.utils import extract_flattened_inputs
20
+ from divi.signals.span import Kind, Span
27
21
 
28
22
  R = TypeVar("R", covariant=True)
29
23
  P = ParamSpec("P")
30
24
 
31
- # ContextVar to store the extra information
32
- # from the Session and parent Span
33
- _SESSION_EXTRA = contextvars.ContextVar[Optional[SessionExtra]](
34
- "_SESSION_EXTRA", default=None
35
- )
36
-
37
25
 
38
26
  @runtime_checkable
39
27
  class WithSessionExtra(Protocol, Generic[P, R]):
@@ -51,9 +39,10 @@ def observable(func: Callable[P, R]) -> WithSessionExtra[P, R]: ...
51
39
 
52
40
  @overload
53
41
  def observable(
54
- kind: str = "function",
42
+ kind: Kind = Kind.function,
55
43
  *,
56
44
  name: Optional[str] = None,
45
+ scores: Optional[list[Score]] = None,
57
46
  metadata: Optional[Mapping[str, Any]] = None,
58
47
  ) -> Callable[[Callable[P, R]], WithSessionExtra[P, R]]: ...
59
48
 
@@ -63,50 +52,33 @@ def observable(
63
52
  ) -> Union[Callable, Callable[[Callable], Callable]]:
64
53
  """Observable decorator factory."""
65
54
 
66
- kind = kwargs.pop("kind", "function")
55
+ kind = kwargs.pop("kind", Kind.function)
67
56
  name = kwargs.pop("name", None)
68
57
  metadata = kwargs.pop("metadata", None)
58
+ scores: list[Score] = kwargs.pop("scores", None)
69
59
 
70
60
  def decorator(func):
71
61
  @functools.wraps(func)
72
62
  def wrapper(
73
63
  *args, session_extra: Optional[SessionExtra] = None, **kwargs
74
64
  ):
65
+ # 1. init the span
75
66
  span = Span(
76
67
  kind=kind, name=name or func.__name__, metadata=metadata
77
68
  )
78
- session_extra = setup(span, _SESSION_EXTRA.get() or session_extra)
79
- # set current context
80
- token = _SESSION_EXTRA.set(session_extra)
81
- # execute the function
82
- span.start()
83
- result = func(*args, **kwargs)
84
- span.end()
85
- # recover parent context
86
- _SESSION_EXTRA.reset(token)
87
-
88
- # get the trace to collect data
89
- trace = session_extra.get("trace")
90
- if not trace:
91
- raise ValueError("Trace not found in session_extra")
92
- # TODO: collect inputs and outputs for SPAN_KIND_FUNCTION
93
- inputs = extract_flattened_inputs(func, *args, **kwargs)
94
- # create the span if it is the root span
95
- if divi._datapark and span.trace_id:
96
- divi._datapark.create_spans(
97
- span.trace_id, ScopeSpans(spans=[span.signal])
98
- )
99
- # end the trace if it is the root span
100
- if divi._datapark and not span.parent_span_id:
101
- trace.end()
102
- # create the chat completion if it is a chat completion
103
- if divi._datapark and isinstance(result, ChatCompletion):
104
- divi._datapark.create_chat_completion(
105
- span_id=span.span_id,
106
- trace_id=trace.trace_id,
107
- inputs=inputs,
108
- completion=result,
109
- )
69
+
70
+ # 2. observe the function
71
+ result = observe(
72
+ *args,
73
+ func=func,
74
+ span=span,
75
+ session_extra=session_extra,
76
+ **kwargs,
77
+ )
78
+
79
+ # 3. evaluate the scores if they are provided
80
+ messages = kwargs.get("messages", [])
81
+ evaluate_scores(messages, outputs=result, scores=scores)
110
82
 
111
83
  return result
112
84
 
@@ -0,0 +1,47 @@
1
+ import contextvars
2
+ from typing import (
3
+ Callable,
4
+ Optional,
5
+ )
6
+
7
+ from divi.decorators.collect import collect
8
+ from divi.session import SessionExtra
9
+ from divi.session.setup import setup
10
+ from divi.signals.span import Span
11
+ from divi.utils import extract_flattened_inputs
12
+
13
+ # ContextVar to store the extra information
14
+ # from the Session and parent Span
15
+ _SESSION_EXTRA = contextvars.ContextVar[Optional[SessionExtra]](
16
+ "_SESSION_EXTRA", default=None
17
+ )
18
+
19
+
20
+ def observe(
21
+ *args,
22
+ func: Callable,
23
+ span: Span,
24
+ session_extra: Optional[SessionExtra] = None,
25
+ **kwargs,
26
+ ):
27
+ session_extra = setup(span, _SESSION_EXTRA.get() or session_extra)
28
+ # set current context
29
+ token = _SESSION_EXTRA.set(session_extra)
30
+ # execute the function
31
+ span.start()
32
+ result = func(*args, **kwargs)
33
+ span.end()
34
+ # recover parent context
35
+ _SESSION_EXTRA.reset(token)
36
+
37
+ # get the trace to collect data
38
+ trace = session_extra.get("trace")
39
+ # end the trace if it is the root span
40
+ if trace and not span.parent_span_id:
41
+ trace.end()
42
+
43
+ # collect inputs and outputs
44
+ inputs = extract_flattened_inputs(func, *args, **kwargs)
45
+ collect(span, inputs, result)
46
+
47
+ return result
@@ -0,0 +1,4 @@
1
+ from .evaluator import Evaluator
2
+ from .scores import Score
3
+
4
+ __all__ = ["Evaluator", "Score"]
@@ -0,0 +1,61 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ from openai.types.chat import (
5
+ ChatCompletion,
6
+ ChatCompletionMessageParam,
7
+ )
8
+ from typing_extensions import List
9
+
10
+ import divi
11
+ from divi.decorators.observe import observe
12
+ from divi.evaluation import Evaluator
13
+ from divi.evaluation.evaluator import EvaluatorConfig
14
+ from divi.evaluation.scores import Score
15
+ from divi.signals.span import Kind, Span
16
+
17
+ OPENAI_API_KEY = "OPENAI_API_KEY"
18
+ OPENAI_BASE_URL = "OPENAI_BASE_URL"
19
+
20
+
21
+ def init_evaluator(config: Optional[EvaluatorConfig] = None):
22
+ _config = config or EvaluatorConfig()
23
+ api_key = _config.api_key if _config.api_key else os.getenv(OPENAI_API_KEY)
24
+ base_url = (
25
+ _config.base_url if _config.base_url else os.getenv(OPENAI_BASE_URL)
26
+ )
27
+ if api_key is None:
28
+ raise ValueError("API key is required for evaluator")
29
+ _config.api_key = api_key
30
+ _config.base_url = base_url
31
+ evaluator = Evaluator(_config)
32
+ return evaluator
33
+
34
+
35
+ def evaluate_scores(
36
+ messages: Optional[List[ChatCompletionMessageParam]],
37
+ outputs: Optional[ChatCompletion],
38
+ scores: Optional[List[Score]],
39
+ config: Optional[EvaluatorConfig] = None,
40
+ ):
41
+ if messages is None or scores is None or scores.__len__() == 0:
42
+ return
43
+ if not divi._evaluator:
44
+ divi._evaluator = init_evaluator(config)
45
+
46
+ if isinstance(outputs, ChatCompletion):
47
+ output_message = outputs.choices[0].message.content
48
+ if not output_message:
49
+ return
50
+
51
+ evaluation_span = Span(kind=Kind.evaluation, name="Evaluation")
52
+ observe(
53
+ func=divi._evaluator.evaluate,
54
+ span=evaluation_span,
55
+ target=output_message,
56
+ conversation="\n".join(
57
+ f"{m.get('role', 'unknown')}: {m.get('content')}"
58
+ for m in messages
59
+ ),
60
+ scores=scores,
61
+ )
@@ -0,0 +1,174 @@
1
+ import asyncio
2
+ import concurrent.futures
3
+ import random
4
+ from typing import List, Literal, Optional
5
+
6
+ import openai
7
+ from pydantic import BaseModel
8
+
9
+ from divi.evaluation.prompts import PRESET_PROMPT, PROMPT_TEMPLATE
10
+ from divi.evaluation.scores import Score
11
+
12
+
13
+ class EvaluatorConfig:
14
+ def __init__(
15
+ self,
16
+ model: str = "gpt-4.1-nano",
17
+ temperature: float = 0.5,
18
+ max_concurrency: int = 10,
19
+ api_key: Optional[str] = None,
20
+ base_url: Optional[str] = None,
21
+ ):
22
+ self.model = model
23
+ self.api_key = api_key
24
+ self.base_url = base_url
25
+ self.temperature = temperature
26
+ self.max_concurrency = max_concurrency
27
+
28
+
29
+ class EvaluationResult(BaseModel):
30
+ name: Score
31
+ judgment: bool
32
+ reasoning: str
33
+
34
+
35
+ class EvaluationScore(BaseModel):
36
+ name: Score
37
+ score: float
38
+ representative_reasoning: str
39
+ all_evaluations: List[EvaluationResult]
40
+
41
+
42
+ class Evaluator:
43
+ def __init__(self, config: Optional[EvaluatorConfig] = None):
44
+ self.config = config or EvaluatorConfig()
45
+ self.async_client = openai.AsyncOpenAI(
46
+ api_key=self.config.api_key, base_url=self.config.base_url
47
+ )
48
+ self.sync_client = openai.OpenAI(
49
+ api_key=self.config.api_key, base_url=self.config.base_url
50
+ )
51
+
52
+ @staticmethod
53
+ def generate_prompt(target: str, conversation: str, score: Score) -> str:
54
+ return PROMPT_TEMPLATE.format(
55
+ requirements=PRESET_PROMPT[score.value],
56
+ target=target,
57
+ conversation=conversation,
58
+ )
59
+
60
+ def _sync_evaluate_once(
61
+ self, target: str, conversation: str, score: Score
62
+ ) -> Optional[EvaluationResult]:
63
+ prompt = self.generate_prompt(target, conversation, score)
64
+ response = self.sync_client.beta.chat.completions.parse(
65
+ model=self.config.model,
66
+ messages=[{"role": "user", "content": prompt}],
67
+ temperature=self.config.temperature,
68
+ response_format=EvaluationResult,
69
+ )
70
+ result = response.choices[0].message.parsed
71
+ if result is not None:
72
+ result.name = score
73
+ return result
74
+
75
+ async def _async_evaluate_once(
76
+ self, target: str, conversation: str, score: Score
77
+ ) -> Optional[EvaluationResult]:
78
+ prompt = self.generate_prompt(target, conversation, score)
79
+ response = await self.async_client.beta.chat.completions.parse(
80
+ model=self.config.model,
81
+ messages=[{"role": "user", "content": prompt}],
82
+ temperature=self.config.temperature,
83
+ response_format=EvaluationResult,
84
+ )
85
+ result = response.choices[0].message.parsed
86
+ if result is not None:
87
+ result.name = score
88
+ return result
89
+
90
+ def _aggregate_result(
91
+ self, name: Score, evaluations: List[EvaluationResult]
92
+ ) -> EvaluationScore:
93
+ n = len(evaluations)
94
+ true_count = sum(1 for e in evaluations if e.judgment is True)
95
+ score = true_count / n
96
+ majority_judgment = True if true_count >= (n / 2) else False
97
+ majority_reasons = [
98
+ e.reasoning for e in evaluations if e.judgment == majority_judgment
99
+ ]
100
+ representative_reasoning = (
101
+ random.choice(majority_reasons) if majority_reasons else ""
102
+ )
103
+ return EvaluationScore(
104
+ name=name,
105
+ score=score,
106
+ representative_reasoning=representative_reasoning,
107
+ all_evaluations=evaluations,
108
+ )
109
+
110
+ def _aggregate_results(
111
+ self, evaluations: List[EvaluationResult]
112
+ ) -> List[EvaluationScore]:
113
+ scores = {}
114
+ for evaluation in evaluations:
115
+ if evaluation.name not in scores:
116
+ scores[evaluation.name] = []
117
+ scores[evaluation.name].append(evaluation)
118
+
119
+ aggregated_results = [
120
+ self._aggregate_result(name, evals)
121
+ for name, evals in scores.items()
122
+ ]
123
+ return aggregated_results
124
+
125
+ def evaluate_sync(
126
+ self, target: str, conversation: str, scores: list[Score], n_rounds: int
127
+ ) -> List[EvaluationScore]:
128
+ with concurrent.futures.ThreadPoolExecutor(
129
+ max_workers=self.config.max_concurrency
130
+ ) as executor:
131
+ futures = [
132
+ executor.submit(
133
+ self._sync_evaluate_once, target, conversation, score
134
+ )
135
+ for _ in range(n_rounds)
136
+ for score in scores
137
+ ]
138
+ evaluations = [
139
+ f.result() for f in concurrent.futures.as_completed(futures)
140
+ ]
141
+ return self._aggregate_results(
142
+ [e for e in evaluations if e is not None]
143
+ )
144
+
145
+ async def evaluate_async(
146
+ self, target: str, conversation: str, scores: list[Score], n_rounds: int
147
+ ) -> List[EvaluationScore]:
148
+ semaphore = asyncio.Semaphore(self.config.max_concurrency)
149
+
150
+ async def sem_task(score):
151
+ async with semaphore:
152
+ return await self._async_evaluate_once(
153
+ target, conversation, score
154
+ )
155
+
156
+ tasks = [sem_task(score) for _ in range(n_rounds) for score in scores]
157
+ evaluations = await asyncio.gather(*tasks)
158
+ return self._aggregate_results(
159
+ [e for e in evaluations if e is not None]
160
+ )
161
+
162
+ def evaluate(
163
+ self,
164
+ target: str,
165
+ conversation: str,
166
+ scores: list[Score],
167
+ n_rounds: int = 5,
168
+ mode: Literal["sync", "async"] = "sync",
169
+ ) -> List[EvaluationScore]:
170
+ if mode == "async":
171
+ return asyncio.run(
172
+ self.evaluate_async(target, conversation, scores, n_rounds)
173
+ )
174
+ return self.evaluate_sync(target, conversation, scores, n_rounds)
@@ -0,0 +1,19 @@
1
+ PROMPT_TEMPLATE = (
2
+ "The *requirements* of the evaluation task is: {requirements}\n\n"
3
+ "Below is the *context* of the conversation (for reference only):\n"
4
+ "{conversation}\n\n"
5
+ "Now, in view of both the requirements and the context, evaluate the assistant’s response:\n"
6
+ "{target}\n\n"
7
+ "Please perform step-by-step reasoning to reach your judgment.\n\n"
8
+ "Strictly output your answer in the following JSON format:\n"
9
+ "{{\n"
10
+ ' "judgment": bool, # true if the response meets all requirements\n'
11
+ ' "reasoning": "string" # concise explanation, hitting only the key points\n'
12
+ "}}\n"
13
+ "Do not output anything else."
14
+ )
15
+
16
+ PRESET_PROMPT = {
17
+ "task_completion": "Assess whether the assistant response fulfills the user's task requirements.",
18
+ "instruction_adherence": "Assess whether the assistant response strictly follows every instruction given by the user, without omissions, deviations, or hallucinations.",
19
+ }
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Score(str, Enum):
5
+ """Enum for score types."""
6
+
7
+ task_completion = "task_completion"
8
+ instruction_adherence = "instruction_adherence"
@@ -31,6 +31,9 @@ message Span {
31
31
 
32
32
  // LLM represents a llm api call.
33
33
  SPAN_KIND_LLM = 1;
34
+
35
+ // EVALUATION represents an evaluation.
36
+ SPAN_KIND_EVALUATION = 2;
34
37
  }
35
38
 
36
39
  // The kind of the span.
@@ -25,7 +25,7 @@ _sym_db = _symbol_database.Default()
25
25
  from divi.proto.common.v1 import common_pb2 as divi_dot_proto_dot_common_dot_v1_dot_common__pb2
26
26
 
27
27
 
28
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x64ivi/proto/trace/v1/trace.proto\x12\x13\x64ivi.proto.trace.v1\x1a!divi/proto/common/v1/common.proto\"6\n\nScopeSpans\x12(\n\x05spans\x18\x02 \x03(\x0b\x32\x19.divi.proto.trace.v1.Span\"\xa4\x02\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x16\n\x0eparent_span_id\x18\x03 \x01(\x0c\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x30\n\x04kind\x18\x05 \x01(\x0e\x32\".divi.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x06 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x07 \x01(\x06\x12\x30\n\x08metadata\x18\x08 \x03(\x0b\x32\x1e.divi.proto.common.v1.KeyValue\"5\n\x08SpanKind\x12\x16\n\x12SPAN_KIND_FUNCTION\x10\x00\x12\x11\n\rSPAN_KIND_LLM\x10\x01\x42\rZ\x0bservices/pbb\x06proto3')
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x64ivi/proto/trace/v1/trace.proto\x12\x13\x64ivi.proto.trace.v1\x1a!divi/proto/common/v1/common.proto\"6\n\nScopeSpans\x12(\n\x05spans\x18\x02 \x03(\x0b\x32\x19.divi.proto.trace.v1.Span\"\xbe\x02\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x16\n\x0eparent_span_id\x18\x03 \x01(\x0c\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x30\n\x04kind\x18\x05 \x01(\x0e\x32\".divi.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x06 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x07 \x01(\x06\x12\x30\n\x08metadata\x18\x08 \x03(\x0b\x32\x1e.divi.proto.common.v1.KeyValue\"O\n\x08SpanKind\x12\x16\n\x12SPAN_KIND_FUNCTION\x10\x00\x12\x11\n\rSPAN_KIND_LLM\x10\x01\x12\x18\n\x14SPAN_KIND_EVALUATION\x10\x02\x42\rZ\x0bservices/pbb\x06proto3')
29
29
 
30
30
  _globals = globals()
31
31
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -36,7 +36,7 @@ if not _descriptor._USE_C_DESCRIPTORS:
36
36
  _globals['_SCOPESPANS']._serialized_start=91
37
37
  _globals['_SCOPESPANS']._serialized_end=145
38
38
  _globals['_SPAN']._serialized_start=148
39
- _globals['_SPAN']._serialized_end=440
39
+ _globals['_SPAN']._serialized_end=466
40
40
  _globals['_SPAN_SPANKIND']._serialized_start=387
41
- _globals['_SPAN_SPANKIND']._serialized_end=440
41
+ _globals['_SPAN_SPANKIND']._serialized_end=466
42
42
  # @@protoc_insertion_point(module_scope)
@@ -19,8 +19,10 @@ class Span(_message.Message):
19
19
  __slots__ = ()
20
20
  SPAN_KIND_FUNCTION: _ClassVar[Span.SpanKind]
21
21
  SPAN_KIND_LLM: _ClassVar[Span.SpanKind]
22
+ SPAN_KIND_EVALUATION: _ClassVar[Span.SpanKind]
22
23
  SPAN_KIND_FUNCTION: Span.SpanKind
23
24
  SPAN_KIND_LLM: Span.SpanKind
25
+ SPAN_KIND_EVALUATION: Span.SpanKind
24
26
  TRACE_ID_FIELD_NUMBER: _ClassVar[int]
25
27
  SPAN_ID_FIELD_NUMBER: _ClassVar[int]
26
28
  PARENT_SPAN_ID_FIELD_NUMBER: _ClassVar[int]
@@ -4,13 +4,14 @@ from google.protobuf.json_format import MessageToDict
4
4
  from openai import NotGiven
5
5
  from openai.types.chat import ChatCompletion
6
6
  from pydantic import UUID4
7
- from typing_extensions import Mapping
7
+ from typing_extensions import List, Mapping
8
8
 
9
9
  import divi
10
+ from divi.evaluation.evaluator import EvaluationScore
10
11
  from divi.proto.trace.v1.trace_pb2 import ScopeSpans
11
12
  from divi.services.service import Service
12
13
  from divi.session.session import SessionSignal
13
- from divi.signals.trace.trace import TraceSignal
14
+ from divi.signals.trace import TraceSignal
14
15
 
15
16
 
16
17
  class DataPark(Service):
@@ -58,6 +59,8 @@ class DataPark(Service):
58
59
  completion: ChatCompletion,
59
60
  ) -> None:
60
61
  hex_span_id = span_id.hex()
62
+ str_trace_id = str(trace_id)
63
+
61
64
  self.post_concurrent(
62
65
  {
63
66
  "/api/v1/chat/completions/input": {
@@ -66,8 +69,23 @@ class DataPark(Service):
66
69
  },
67
70
  "/api/v1/chat/completions": {
68
71
  "span_id": hex_span_id,
69
- "trace_id": str(trace_id),
72
+ "trace_id": str_trace_id,
70
73
  "data": completion.model_dump(),
71
74
  },
72
75
  }
73
76
  )
77
+
78
+ def create_scores(
79
+ self,
80
+ span_id: bytes,
81
+ trace_id: UUID4,
82
+ scores: List[EvaluationScore],
83
+ ) -> None:
84
+ self.post(
85
+ "/api/v1/chat/completions/scores",
86
+ payload={
87
+ "span_id": span_id.hex(),
88
+ "trace_id": str(trace_id),
89
+ "data": [score.model_dump() for score in scores],
90
+ },
91
+ )
divi/services/init.py CHANGED
@@ -1,12 +1,15 @@
1
1
  import divi
2
2
  from divi.services.auth import init as init_auth
3
- from divi.services.core import init as init_core
3
+
4
+ # from divi.services.core import init as init_core
4
5
  from divi.services.datapark import init as init_datapark
5
6
 
6
7
 
7
8
  def init():
8
9
  if not divi._auth:
9
- divi._auth = init_auth()
10
+ divi._auth = init_auth(
11
+ api_key="divi-aa31aef9-bb4c-4a98-aaad-7e12bdacec83"
12
+ )
10
13
  if not divi._datapark:
11
14
  divi._datapark = init_datapark()
12
15
  # TODO - Uncomment this when the core service is ready
divi/session/session.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import Optional, TypedDict
2
2
  from uuid import uuid4
3
3
 
4
- from divi.signals.trace.trace import Trace
4
+ from divi.signals.trace import Trace
5
5
 
6
6
 
7
7
  class SessionExtra(TypedDict, total=False):
divi/session/setup.py CHANGED
@@ -1,12 +1,13 @@
1
1
  from typing_extensions import Optional
2
+
2
3
  import divi
3
4
  from divi.services import init as init_services
4
5
  from divi.session import Session, SessionExtra
5
- from divi.signals.trace import Span
6
- from divi.signals.trace.trace import Trace
6
+ from divi.signals.span import Span
7
+ from divi.signals.trace import Trace
7
8
 
8
9
 
9
- def init_session(name: Optional[str]=None) -> Session:
10
+ def init_session(name: Optional[str] = None) -> Session:
10
11
  """init initializes the services and the Run"""
11
12
  init_services()
12
13
  session = Session(name=name)
@@ -29,7 +30,9 @@ def setup(
29
30
 
30
31
  # init the session if not already initialized
31
32
  if not divi._session:
32
- divi._session = init_session(name=session_extra.get('session_name') or span.name)
33
+ divi._session = init_session(
34
+ name=session_extra.get("session_name") or span.name
35
+ )
33
36
 
34
37
  # setup trace
35
38
  trace = session_extra.get("trace") or Trace(divi._session.id, span.name)
divi/signals/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from .trace import Span
1
+ from .span import Kind
2
2
 
3
- __all__ = ["Span"]
3
+ __all__ = ["Kind"]
divi/signals/span.py ADDED
@@ -0,0 +1,83 @@
1
+ import os
2
+ import time
3
+ from enum import Enum
4
+ from typing import Any, Mapping, Optional
5
+
6
+ from pydantic import UUID4
7
+
8
+ import divi
9
+ from divi.proto.common.v1.common_pb2 import KeyValue
10
+ from divi.proto.trace.v1.trace_pb2 import ScopeSpans
11
+ from divi.proto.trace.v1.trace_pb2 import Span as SpanProto
12
+
13
+
14
+ class Kind(int, Enum):
15
+ """Enum for the kind of span."""
16
+
17
+ function = SpanProto.SpanKind.SPAN_KIND_FUNCTION
18
+ llm = SpanProto.SpanKind.SPAN_KIND_LLM
19
+ evaluation = SpanProto.SpanKind.SPAN_KIND_EVALUATION
20
+
21
+
22
+ class Span:
23
+ def __init__(
24
+ self,
25
+ kind: Kind = Kind.function,
26
+ name: Optional[str] = None,
27
+ metadata: Optional[Mapping[str, Any]] = None,
28
+ ):
29
+ # span_id is a FixedString(8)
30
+ self.span_id: bytes = self._generate_span_id()
31
+ self.name = name
32
+ self.kind = kind
33
+ self.metadata = metadata
34
+ self.start_time_unix_nano: int | None = None
35
+ self.end_time_unix_nano: int | None = None
36
+
37
+ self.trace_id: UUID4 | None = None
38
+ self.parent_span_id: bytes | None = None
39
+
40
+ @property
41
+ def signal(self) -> SpanProto:
42
+ signal: SpanProto = SpanProto(
43
+ name=self.name,
44
+ span_id=self.span_id,
45
+ kind=SpanProto.SpanKind.Name(self.kind),
46
+ start_time_unix_nano=self.start_time_unix_nano,
47
+ end_time_unix_nano=self.end_time_unix_nano,
48
+ trace_id=self.trace_id.bytes if self.trace_id else None,
49
+ parent_span_id=self.parent_span_id,
50
+ )
51
+ signal.metadata.extend(
52
+ KeyValue(key=k, value=v)
53
+ for k, v in (self.metadata or dict()).items()
54
+ )
55
+ return signal
56
+
57
+ @classmethod
58
+ def _generate_span_id(cls) -> bytes:
59
+ return os.urandom(8)
60
+
61
+ def start(self):
62
+ """Start the span by recording the current time in nanoseconds."""
63
+ self.start_time_unix_nano = time.time_ns()
64
+ self.upsert_span()
65
+
66
+ def end(self):
67
+ """End the span by recording the end time in nanoseconds."""
68
+ if self.start_time_unix_nano is None:
69
+ raise ValueError("Span must be started before ending.")
70
+ self.end_time_unix_nano = time.time_ns()
71
+ self.upsert_span()
72
+
73
+ def _add_node(self, trace_id: UUID4, parent_id: Optional[bytes] = None):
74
+ """Add node for obs tree."""
75
+ self.trace_id = trace_id
76
+ self.parent_span_id = parent_id
77
+
78
+ def upsert_span(self):
79
+ """Upsert span with datapark."""
80
+ if divi._datapark and self.trace_id:
81
+ divi._datapark.create_spans(
82
+ self.trace_id, ScopeSpans(spans=[self.signal])
83
+ )
divi/signals/trace.py ADDED
@@ -0,0 +1,79 @@
1
+ from datetime import UTC, datetime
2
+ from typing import Optional
3
+ from uuid import uuid4
4
+
5
+ from pydantic import UUID4
6
+ from typing_extensions import TypedDict
7
+
8
+ import divi
9
+
10
+
11
+ class NullTime(TypedDict, total=False):
12
+ """Null time"""
13
+
14
+ Time: str
15
+ """Time in iso format"""
16
+ Valid: bool
17
+ """Valid"""
18
+
19
+
20
+ class TraceSignal(TypedDict, total=False):
21
+ """Trace request"""
22
+
23
+ id: str
24
+ """Trace ID UUID4"""
25
+ start_time: str
26
+ """Start time in iso format"""
27
+ end_time: NullTime
28
+ """End time in iso format"""
29
+ name: Optional[str]
30
+
31
+
32
+ class Trace:
33
+ def __init__(self, session_id: UUID4, name: Optional[str] = None):
34
+ self.trace_id: UUID4 = uuid4()
35
+ self.start_time: str | None = None
36
+ self.end_time: str | None = None
37
+ self.name: Optional[str] = name
38
+ self.session_id: UUID4 = session_id
39
+
40
+ self.start()
41
+
42
+ @property
43
+ def signal(self) -> TraceSignal:
44
+ if self.start_time is None:
45
+ raise ValueError("Trace must be started.")
46
+ signal = TraceSignal(
47
+ id=str(self.trace_id),
48
+ start_time=self.start_time,
49
+ name=self.name,
50
+ )
51
+ if self.end_time is not None:
52
+ signal["end_time"] = NullTime(
53
+ Time=self.end_time,
54
+ Valid=True,
55
+ )
56
+ return signal
57
+
58
+ @staticmethod
59
+ def unix_nano_to_iso(unix_nano: int) -> str:
60
+ return datetime.utcfromtimestamp(unix_nano / 1e9).isoformat()
61
+
62
+ def start(self):
63
+ """Start the trace by recording the current time in nanoseconds."""
64
+ self.start_time = datetime.now(UTC).isoformat()
65
+ self.upsert_trace()
66
+
67
+ def end(self):
68
+ """End the trace by recording the end time in nanoseconds."""
69
+ if self.start_time is None:
70
+ raise ValueError("Span must be started before ending.")
71
+ self.end_time = datetime.now(UTC).isoformat()
72
+ self.upsert_trace()
73
+
74
+ def upsert_trace(self):
75
+ """Upsert trace with datapark."""
76
+ if divi._datapark:
77
+ divi._datapark.upsert_traces(
78
+ session_id=self.session_id, traces=[self.signal]
79
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: divi
3
- Version: 0.0.1.dev23
3
+ Version: 0.0.1.dev47
4
4
  Summary: The Agent Platform for Observability & Evaluation
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,9 +1,15 @@
1
- divi/__init__.py,sha256=lcpSVEjIcXy-e9CuJB8V1izwaoEK8YpH8FtkzH0IqJc,396
1
+ divi/__init__.py,sha256=e3T9Znmiwfihr_X4Ly0Z0yoW23xGNQowPz6lCfXcXPI,519
2
2
  divi/utils.py,sha256=fXkjoyo_Lh8AZliKICOP460m0czUcNQjcEcceJbaOVA,1439
3
- divi/config/config.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
3
  divi/decorators/__init__.py,sha256=HkyWdC1ctTsVFucCWCkj57JB4NmwONus1d2S2dUbvs4,110
5
- divi/decorators/obs_openai.py,sha256=GI9c6gFArA6pTfa3EPednMtRqV2zIlofuTDdUYtS_x4,963
6
- divi/decorators/observable.py,sha256=xKapTyHL50mUyIDhDSYIXR1yZIdHTuw6WtI4H0j1xDE,3464
4
+ divi/decorators/collect.py,sha256=5iUxAnbHYx4ISkFg64IK_4miGdrWgbOXLJxKz8lGIv8,1074
5
+ divi/decorators/obs_openai.py,sha256=ouw3GYDFg6S27tcUzY0dIqz8JX_JM8IOXttzo7HK7nk,1359
6
+ divi/decorators/observable.py,sha256=isUS3P_07wbZBj2UcRAoYNDceQTIn6zdein3-PWVsi8,2289
7
+ divi/decorators/observe.py,sha256=I2RVsp2WQep6iTLSxkAlMP8wiRsSYiiYrxR2hJzPxcI,1211
8
+ divi/evaluation/__init__.py,sha256=3qMHWu_zBh6FJa6-1dZZEWiAblQZurn5doa0OjGvDGs,93
9
+ divi/evaluation/evaluate.py,sha256=lVMCw5vHGa5sJvUyhVDZ9m3Sgl4baCjWhw2OKazhvgM,1861
10
+ divi/evaluation/evaluator.py,sha256=ulTyfSg2JXxzCCL7hRsn-EBb9UKcpQFA6rVT42mouVQ,5819
11
+ divi/evaluation/prompts.py,sha256=qiv7TljwV8NTy0iLS2GEWIDFFNXhHKUlgVb-WoZhm4Q,970
12
+ divi/evaluation/scores.py,sha256=ZgSxfve-ZivX3WU4TGcgPOSpUQVMbG5a15IQNPeq_bQ,173
7
13
  divi/proto/common/v1/common.proto,sha256=Rx8wr0_tOtQ1NseTMnsav4ApD1MDALzQDBA2IvLRTU0,1775
8
14
  divi/proto/common/v1/common_pb2.py,sha256=br61OHQVAi6SI3baFcb5xJv2Xd-AZ04A19xeSjLNMXo,2442
9
15
  divi/proto/common/v1/common_pb2.pyi,sha256=LmTpFFLxHg2a_qPIdNXXwGEMkbiDcTJdarR9eC-6Fq8,2133
@@ -14,12 +20,12 @@ divi/proto/core/health/v1/health_service_pb2_grpc.py,sha256=YmlO94d-G71YBW1XZDSb
14
20
  divi/proto/metric/v1/metric.proto,sha256=YHRMLUW-MtakHuibR3PJ0s2w5KgV12kc4737iHw0DTk,585
15
21
  divi/proto/metric/v1/metric_pb2.py,sha256=uvBhyy8QpaES3Jl82yVfsGazW5654XpRnsdGlpVgIRE,1974
16
22
  divi/proto/metric/v1/metric_pb2.pyi,sha256=S7ipsojkD7QZAYefDE4b3PO99Yzc6mOdtSLxH3-b67A,1304
17
- divi/proto/trace/v1/trace.proto,sha256=mh1nzEgufzRTJx3p8NNute-ozEwEYwClWJTdWUGGVA8,1284
18
- divi/proto/trace/v1/trace_pb2.py,sha256=CuTkSSvhxCa1bk3Ku7tgLqRSovp_Gi52CZ0zLcLP2Ew,2327
19
- divi/proto/trace/v1/trace_pb2.pyi,sha256=rPo2Oa3NWrINE_dyOVU9HUYHo5LY82Bm5TMenj5dnK8,2136
23
+ divi/proto/trace/v1/trace.proto,sha256=tPRIgBZB5KOKj7AoD3NoDZvLwoiJkbLiLqW53Ah-2-0,1367
24
+ divi/proto/trace/v1/trace_pb2.py,sha256=zMuQO5mN2xl11USHkhi0lLwBAPlYXRU_UG1r0Uu3mJg,2369
25
+ divi/proto/trace/v1/trace_pb2.pyi,sha256=k4dHYKAusH4I-XSW9KP3maogSWdRL7hVy8HCHhqFWzM,2231
20
26
  divi/services/__init__.py,sha256=TcVJ_gKxyPIcwhT9GgttqHeyk0icW44uE285KmUiyh4,185
21
27
  divi/services/finish.py,sha256=XKPKGJ5cWd5H95G_VpIOlOZOLrcf9StoTs7ayRic2jY,173
22
- divi/services/init.py,sha256=JVzRQ1m1DTHXFVGUMYnsv-vRvzCO8XFdR6MjIwOL_NY,433
28
+ divi/services/init.py,sha256=dwXXXbf1-V0iAHZOETiv527TZQ07-waMIR5cSiU3QjI,509
23
29
  divi/services/service.py,sha256=539MhcYfMvsVGjDdu0UtYSZnL2cloaPeYeOSMl2eUy8,1532
24
30
  divi/services/auth/__init__.py,sha256=PIQ9rQ0jcRqcy03a3BOY7wbzwluIRG_4kI_H4J4mRFk,74
25
31
  divi/services/auth/auth.py,sha256=eRcE6Kq8jbBr6YL93HCGDIoga90SoZf3ogOAKeza9WY,445
@@ -30,16 +36,16 @@ divi/services/core/core.py,sha256=PRwPtLgrgmCrejUfKf7HJNrAhGS0paFNZ7JwDToEUAk,12
30
36
  divi/services/core/finish.py,sha256=dIGQpVXcJY4-tKe7A1_VV3yoSHNCDPfOlUltvzvk6VI,231
31
37
  divi/services/core/init.py,sha256=e7-fgpOPglBXyEoPkgOAnpJk2ApdFbo7LPupxOb8N-w,1966
32
38
  divi/services/datapark/__init__.py,sha256=GbV1mwHE07yutgOlCIYHykSEL5KJ-ApgLutGMzu2eUE,86
33
- divi/services/datapark/datapark.py,sha256=d2pbrzVJtR3mNW1eQpbm-Wca-SvcfJqT7IuaQy7yHT0,2285
39
+ divi/services/datapark/datapark.py,sha256=f-qE2kmkLAniIj9mOP3nCbI3A3qkfIUnoVekwQ5w0QE,2781
34
40
  divi/services/datapark/init.py,sha256=C32f9t3eLsxcYNqEyheh6nW455G2oR0YhhdqBcbN3ec,92
35
41
  divi/session/__init__.py,sha256=6lYemv21VQCIHx-xIdi7BxXcPNxVdvE60--8ArReUew,82
36
- divi/session/session.py,sha256=LlB2W2qGo0Vf-0L0CTQoXfzg_gCGpf0MTFsXQW7E6i4,817
37
- divi/session/setup.py,sha256=NeCxCb-uYhkKnOEiw8dBQHz0DEL8j1oxzQY3cBAmHbo,1380
42
+ divi/session/session.py,sha256=QxtEezI447PbtKG2U6cxL1ACae55e8nFfTufAY8pEYI,811
43
+ divi/session/setup.py,sha256=SHNzCuvOzlrlBJj34_jbzhfa6SXX3oaXrcG8bN0-Xvo,1398
38
44
  divi/session/teardown.py,sha256=YiBz_3yCiljMFEofZ60VmRL5sb8WA5GT7EYF8nFznZ4,133
39
- divi/signals/__init__.py,sha256=K1PaTAMwyBDsK6jJUg4QWy0xVJ_5MA6dlWiUyJeiSQA,44
40
- divi/signals/trace/__init__.py,sha256=K1PaTAMwyBDsK6jJUg4QWy0xVJ_5MA6dlWiUyJeiSQA,44
41
- divi/signals/trace/trace.py,sha256=OsfrZPHp241_NN8W79U4O69HsHQajez_d3rz6yJRN9s,4508
42
- divi-0.0.1.dev23.dist-info/METADATA,sha256=KdspT1iEra11ixCEtl94Mz_fCziMVOOER08rDE4yCWU,497
43
- divi-0.0.1.dev23.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
- divi-0.0.1.dev23.dist-info/licenses/LICENSE,sha256=5OJuZ4wMMEV0DgF0tofhAlS_KLkaUsZwwwDS2U_GwQ0,1063
45
- divi-0.0.1.dev23.dist-info/RECORD,,
45
+ divi/signals/__init__.py,sha256=wfSkkCwkRsFP4aLj8aGHk_k6Y50P5yN44WWlO3XyW18,43
46
+ divi/signals/span.py,sha256=FQWql6ivAeXGk1HPZCsCjL5mXW6S6Nn9SmOiKH4aXik,2629
47
+ divi/signals/trace.py,sha256=IoYeTfd6x_Xmxcp4HbFSEne0d48hol4ng2Mb_AO8hZw,2144
48
+ divi-0.0.1.dev47.dist-info/METADATA,sha256=3QEVpc6O2YUEyMtFJ8kJn3rd8y_xMHGc67ObLg20vfs,497
49
+ divi-0.0.1.dev47.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
+ divi-0.0.1.dev47.dist-info/licenses/LICENSE,sha256=5OJuZ4wMMEV0DgF0tofhAlS_KLkaUsZwwwDS2U_GwQ0,1063
51
+ divi-0.0.1.dev47.dist-info/RECORD,,
divi/config/config.py DELETED
File without changes
@@ -1,3 +0,0 @@
1
- from .trace import Span
2
-
3
- __all__ = ["Span"]
@@ -1,151 +0,0 @@
1
- import os
2
- import time
3
- from datetime import UTC, datetime
4
- from typing import Any, Mapping, Optional
5
- from uuid import uuid4
6
-
7
- from pydantic import UUID4
8
- from typing_extensions import TypedDict
9
-
10
- import divi
11
- from divi.proto.common.v1.common_pb2 import KeyValue
12
- from divi.proto.trace.v1.trace_pb2 import Span as SpanProto
13
-
14
-
15
- class NullTime(TypedDict, total=False):
16
- """Null time"""
17
-
18
- Time: str
19
- """Time in iso format"""
20
- Valid: bool
21
- """Valid"""
22
-
23
-
24
- class TraceSignal(TypedDict, total=False):
25
- """Trace request"""
26
-
27
- id: str
28
- """Trace ID UUID4"""
29
- start_time: str
30
- """Start time in iso format"""
31
- end_time: NullTime
32
- """End time in iso format"""
33
- name: Optional[str]
34
-
35
-
36
- class Trace:
37
- def __init__(self, session_id: UUID4, name: Optional[str] = None):
38
- self.trace_id: UUID4 = uuid4()
39
- self.start_time: str | None = None
40
- self.end_time: str | None = None
41
- self.name: Optional[str] = name
42
- self.session_id: UUID4 = session_id
43
-
44
- self.start()
45
-
46
- @property
47
- def signal(self) -> TraceSignal:
48
- if self.start_time is None:
49
- raise ValueError("Trace must be started.")
50
- signal = TraceSignal(
51
- id=str(self.trace_id),
52
- start_time=self.start_time,
53
- name=self.name,
54
- )
55
- if self.end_time is not None:
56
- signal["end_time"] = NullTime(
57
- Time=self.end_time,
58
- Valid=True,
59
- )
60
- return signal
61
-
62
- @staticmethod
63
- def unix_nano_to_iso(unix_nano: int) -> str:
64
- return datetime.utcfromtimestamp(unix_nano / 1e9).isoformat()
65
-
66
- def start(self):
67
- """Start the trace by recording the current time in nanoseconds."""
68
- self.start_time = datetime.now(UTC).isoformat()
69
- self.upsert_trace()
70
-
71
- def end(self):
72
- """End the trace by recording the end time in nanoseconds."""
73
- if self.start_time is None:
74
- raise ValueError("Span must be started before ending.")
75
- self.end_time = datetime.now(UTC).isoformat()
76
- self.upsert_trace()
77
-
78
- def upsert_trace(self):
79
- """Upsert trace with datapark."""
80
- if divi._datapark:
81
- divi._datapark.upsert_traces(
82
- session_id=self.session_id, traces=[self.signal]
83
- )
84
-
85
-
86
- class Span:
87
- KIND_MAP = {
88
- "function": SpanProto.SpanKind.SPAN_KIND_FUNCTION,
89
- "llm": SpanProto.SpanKind.SPAN_KIND_LLM,
90
- }
91
-
92
- def __init__(
93
- self,
94
- kind: str = "function",
95
- name: Optional[str] = None,
96
- metadata: Optional[Mapping[str, Any]] = None,
97
- ):
98
- # span_id is a FixedString(8)
99
- self.span_id: bytes = self._generate_span_id()
100
- self.name = name
101
- self.kind = kind
102
- self.metadata = metadata
103
- self.start_time_unix_nano: int | None = None
104
- self.end_time_unix_nano: int | None = None
105
-
106
- self.trace_id: UUID4 | None = None
107
- self.parent_span_id: bytes | None = None
108
-
109
- @property
110
- def signal(self) -> SpanProto:
111
- signal: SpanProto = SpanProto(
112
- name=self.name,
113
- span_id=self.span_id,
114
- kind=self._get_kind(self.kind),
115
- start_time_unix_nano=self.start_time_unix_nano,
116
- end_time_unix_nano=self.end_time_unix_nano,
117
- trace_id=self.trace_id.bytes if self.trace_id else None,
118
- parent_span_id=self.parent_span_id,
119
- )
120
- signal.metadata.extend(
121
- KeyValue(key=k, value=v)
122
- for k, v in (self.metadata or dict()).items()
123
- )
124
- return signal
125
-
126
- @classmethod
127
- def _get_kind(cls, kind: str) -> SpanProto.SpanKind:
128
- if (k := cls.KIND_MAP.get(kind)) is None:
129
- raise ValueError(
130
- f"Unknown kind: {kind}. Now allowed: {cls.KIND_MAP.keys()}"
131
- )
132
- return k
133
-
134
- @classmethod
135
- def _generate_span_id(cls) -> bytes:
136
- return os.urandom(8)
137
-
138
- def start(self):
139
- """Start the span by recording the current time in nanoseconds."""
140
- self.start_time_unix_nano = time.time_ns()
141
-
142
- def end(self):
143
- """End the span by recording the end time in nanoseconds."""
144
- if self.start_time_unix_nano is None:
145
- raise ValueError("Span must be started before ending.")
146
- self.end_time_unix_nano = time.time_ns()
147
-
148
- def _add_node(self, trace_id: UUID4, parent_id: Optional[bytes] = None):
149
- """Add node for obs tree."""
150
- self.trace_id = trace_id
151
- self.parent_span_id = parent_id