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 +5 -2
- divi/decorators/collect.py +34 -0
- divi/decorators/obs_openai.py +21 -5
- divi/decorators/observable.py +22 -50
- divi/decorators/observe.py +47 -0
- divi/evaluation/__init__.py +4 -0
- divi/evaluation/evaluate.py +61 -0
- divi/evaluation/evaluator.py +174 -0
- divi/evaluation/prompts.py +19 -0
- divi/evaluation/scores.py +8 -0
- divi/proto/trace/v1/trace.proto +3 -0
- divi/proto/trace/v1/trace_pb2.py +3 -3
- divi/proto/trace/v1/trace_pb2.pyi +2 -0
- divi/services/datapark/datapark.py +21 -3
- divi/services/init.py +5 -2
- divi/session/session.py +1 -1
- divi/session/setup.py +7 -4
- divi/signals/__init__.py +2 -2
- divi/signals/span.py +83 -0
- divi/signals/trace.py +79 -0
- {divi-0.0.1.dev23.dist-info → divi-0.0.1.dev47.dist-info}/METADATA +1 -1
- {divi-0.0.1.dev23.dist-info → divi-0.0.1.dev47.dist-info}/RECORD +24 -18
- divi/config/config.py +0 -0
- divi/signals/trace/__init__.py +0 -3
- divi/signals/trace/trace.py +0 -151
- {divi-0.0.1.dev23.dist-info → divi-0.0.1.dev47.dist-info}/WHEEL +0 -0
- {divi-0.0.1.dev23.dist-info → divi-0.0.1.dev47.dist-info}/licenses/LICENSE +0 -0
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.
|
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
|
+
)
|
divi/decorators/obs_openai.py
CHANGED
@@ -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(
|
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=
|
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(
|
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
|
divi/decorators/observable.py
CHANGED
@@ -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
|
20
|
-
|
21
|
-
import
|
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.
|
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:
|
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",
|
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
|
-
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
|
90
|
-
|
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,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
|
+
}
|
divi/proto/trace/v1/trace.proto
CHANGED
divi/proto/trace/v1/trace_pb2.py
CHANGED
@@ -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\"\
|
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=
|
39
|
+
_globals['_SPAN']._serialized_end=466
|
40
40
|
_globals['_SPAN_SPANKIND']._serialized_start=387
|
41
|
-
_globals['_SPAN_SPANKIND']._serialized_end=
|
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
|
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":
|
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
|
-
|
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
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.
|
6
|
-
from divi.signals.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(
|
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 .
|
1
|
+
from .span import Kind
|
2
2
|
|
3
|
-
__all__ = ["
|
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,9 +1,15 @@
|
|
1
|
-
divi/__init__.py,sha256=
|
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/
|
6
|
-
divi/decorators/
|
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=
|
18
|
-
divi/proto/trace/v1/trace_pb2.py,sha256=
|
19
|
-
divi/proto/trace/v1/trace_pb2.pyi,sha256=
|
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=
|
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=
|
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=
|
37
|
-
divi/session/setup.py,sha256=
|
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=
|
40
|
-
divi/signals/
|
41
|
-
divi/signals/trace
|
42
|
-
divi-0.0.1.
|
43
|
-
divi-0.0.1.
|
44
|
-
divi-0.0.1.
|
45
|
-
divi-0.0.1.
|
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
|
divi/signals/trace/__init__.py
DELETED
divi/signals/trace/trace.py
DELETED
@@ -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
|
File without changes
|
File without changes
|