• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scope3data / scope3ai-py / 12643388241

07 Jan 2025 01:24AM UTC coverage: 94.179% (+13.6%) from 80.557%
12643388241

Pull #50

github

4e648d
tito
fix(py39): fix for python 3.0
Pull Request #50: feat(openai): add support for speech to text

59 of 65 new or added lines in 3 files covered. (90.77%)

26 existing lines in 5 files now uncovered.

1618 of 1718 relevant lines covered (94.18%)

3.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.66
/scope3ai/lib.py
1
import importlib.metadata
4✔
2
import importlib.util
4✔
3
import logging
4✔
4
from contextlib import contextmanager
4✔
5
from contextvars import ContextVar
4✔
6
from enum import Enum
4✔
7
from functools import partial
4✔
8
from os import getenv
4✔
9
from typing import Optional, List
4✔
10
from uuid import uuid4
4✔
11
import atexit
4✔
12

13
from .api.client import Client, AsyncClient
4✔
14
from .api.tracer import Tracer
4✔
15
from .api.types import ImpactRow, ImpactResponse, Scope3AIContext
4✔
16
from .api.defaults import DEFAULT_API_URL
4✔
17

18
from .worker import BackgroundWorker
4✔
19

20
logger = logging.getLogger("scope3ai.lib")
4✔
21

22

23
def init_anthropic_instrumentor() -> None:
4✔
24
    if importlib.util.find_spec("anthropic") is not None:
4✔
25
        from scope3ai.tracers.anthropic.instrument import AnthropicInstrumentor
4✔
26

27
        instrumentor = AnthropicInstrumentor()
4✔
28
        instrumentor.instrument()
4✔
29

30

31
def init_cohere_instrumentor() -> None:
4✔
32
    if importlib.util.find_spec("cohere") is not None:
4✔
33
        from scope3ai.tracers.cohere.instrument import CohereInstrumentor
4✔
34

35
        instrumentor = CohereInstrumentor()
4✔
36
        instrumentor.instrument()
4✔
37

38

39
def init_openai_instrumentor() -> None:
4✔
40
    if importlib.util.find_spec("openai") is not None:
4✔
41
        from scope3ai.tracers.openai.instrument import OpenAIInstrumentor
4✔
42

43
        instrumentor = OpenAIInstrumentor()
4✔
44
        instrumentor.instrument()
4✔
45

46

47
def init_huggingface_hub_instrumentor() -> None:
4✔
48
    if importlib.util.find_spec("huggingface_hub") is not None:
4✔
49
        from scope3ai.tracers.huggingface.instrument import HuggingfaceInstrumentor
4✔
50

51
        instrumentor = HuggingfaceInstrumentor()
4✔
52
        instrumentor.instrument()
4✔
53

54

55
def init_litellm_instrumentor() -> None:
4✔
56
    if importlib.util.find_spec("litellm") is not None:
4✔
57
        from scope3ai.tracers.litellm.instrument import LiteLLMInstrumentor
4✔
58

59
        instrumentor = LiteLLMInstrumentor()
4✔
60
        instrumentor.instrument()
4✔
61

62

63
def init_mistral_v1_instrumentor() -> None:
4✔
64
    if importlib.util.find_spec("mistralai") is not None:
4✔
65
        from scope3ai.tracers.mistrarlai_v1.instrument import MistralAIInstrumentor
4✔
66

67
        instrumentor = MistralAIInstrumentor()
4✔
68
        instrumentor.instrument()
4✔
69

70

71
def init_response_instrumentor() -> None:
4✔
72
    from scope3ai.response_interceptor.instrument import ResponseInterceptor
4✔
73

74
    instrumentor = ResponseInterceptor()
4✔
75
    instrumentor.instrument()
4✔
76

77

78
class PROVIDERS(Enum):
4✔
79
    ANTROPIC = "anthropic"
4✔
80
    COHERE = "cohere"
4✔
81
    OPENAI = "openai"
4✔
82
    HUGGINGFACE_HUB = "huggingface_hub"
4✔
83
    LITELLM = "litellm"
4✔
84
    MISTRALAI = "mistralai"
4✔
85
    RESPONSE = "response"
4✔
86

87

88
_INSTRUMENTS = {
4✔
89
    PROVIDERS.ANTROPIC.value: init_anthropic_instrumentor,
90
    PROVIDERS.COHERE.value: init_cohere_instrumentor,
91
    PROVIDERS.OPENAI.value: init_openai_instrumentor,
92
    PROVIDERS.HUGGINGFACE_HUB.value: init_huggingface_hub_instrumentor,
93
    PROVIDERS.LITELLM.value: init_litellm_instrumentor,
94
    PROVIDERS.MISTRALAI.value: init_mistral_v1_instrumentor,
95
    PROVIDERS.RESPONSE.value: init_response_instrumentor,
96
}
97

98
_RE_INIT_PROVIDERS = [PROVIDERS.RESPONSE.value]
4✔
99

100

101
def generate_id() -> str:
4✔
UNCOV
102
    return uuid4().hex
×
103

104

105
class Scope3AIError(Exception):
4✔
106
    pass
4✔
107

108

109
class Scope3AI:
4✔
110
    """
111
    Scope3AI tracer class
112

113
    This class is a singleton that provides a context manager for tracing
114
    inference metadata and submitting impact requests to the Scope3 AI API.
115
    """
116

117
    _instance: Optional["Scope3AI"] = None
4✔
118
    _tracer: ContextVar[List[Tracer]] = ContextVar("tracer", default=[])
4✔
119
    _worker: Optional[BackgroundWorker] = None
4✔
120
    _providers: List[str] = []
4✔
121

122
    def __new__(cls, *args, **kwargs):
4✔
123
        if cls._instance is None:
4✔
124
            cls._instance = super(Scope3AI, cls).__new__(cls)
4✔
125
        return cls._instance
4✔
126

127
    @classmethod
4✔
128
    def init(
4✔
129
        cls,
130
        api_key: str = None,
131
        api_url: str = None,
132
        sync_mode: bool = False,
133
        enable_debug_logging: bool = False,
134
        providers: Optional[List[str]] = None,
135
    ) -> None:
136
        if cls._instance is not None:
4✔
UNCOV
137
            raise Scope3AIError("Scope3AI is already initialized")
×
138
        cls._instance = self = Scope3AI()
4✔
139
        self.api_key = api_key or getenv("SCOPE3AI_API_KEY")
4✔
140
        self.api_url = api_url or getenv("SCOPE3AI_API_URL") or DEFAULT_API_URL
4✔
141
        self.sync_mode = sync_mode or bool(getenv("SCOPE3AI_SYNC_MODE", False))
4✔
142
        if not self.api_key:
4✔
UNCOV
143
            raise Scope3AIError(
×
144
                "The scope3 api_key option must be set either by "
145
                "passing the API key to the Scope3AI.init(api_key='xxx') "
146
                "or by setting the SCOPE3AI_API_KEY environment variable"
147
            )
148
        if not self.api_url:
4✔
UNCOV
149
            raise Scope3AIError(
×
150
                "The api_url option must be set either by "
151
                "passing the API URL to the Scope3AI.init(api_url='xxx') "
152
                "or by setting the SCOPE3AI_API_URL environment variable"
153
            )
154

155
        if enable_debug_logging:
4✔
156
            self._init_logging()
4✔
157

158
        if providers is None:
4✔
159
            providers = list(_INSTRUMENTS.keys())
4✔
160

161
        http_client_options = {"api_key": self.api_key, "api_url": self.api_url}
4✔
162
        self._sync_client = Client(**http_client_options)
4✔
163
        self._async_client = AsyncClient(**http_client_options)
4✔
164
        self._init_providers(providers)
4✔
165
        self._init_atexit()
4✔
166
        return cls._instance
4✔
167

168
    @classmethod
4✔
169
    def get_instance(cls) -> "Scope3AI":
4✔
170
        """
171
        Return the instance of the Scope3AI singleton.
172

173
        This method provides access to the default global state of the
174
        Scope3AI library. The returned instance can be used to trace
175
        inference metadata and submit impact requests to the Scope3 AI
176
        API from anywhere in the application.
177

178
        Returns:
179
            Scope3AI: The singleton instance of the Scope3AI class.
180
        """
181
        return cls._instance
4✔
182

183
    def submit_impact(
4✔
184
        self,
185
        impact_row: ImpactRow,
186
    ) -> Scope3AIContext:
187
        """
188
        Submit an impact request to the Scope3 AI API.
189

190
        This function sends an impact request represented by the `impact_row`
191
        to the Scope3 AI API and optionally returns the response.
192

193
        Args:
194
            impact_row (ImpactRow): The impact request data
195
                that needs to be submitted to the Scope3 AI API.
196

197
        Returns:
198
            Scope3AIContext: A context object containing the request data and
199
            the response from the API.
200
        """
201

202
        def submit_impact(
4✔
203
            impact_row: ImpactRow,
204
            ctx: Scope3AIContext,
205
        ) -> Optional[ImpactResponse]:
206
            response = self._sync_client.impact(
4✔
207
                rows=[impact_row],
208
                with_response=True,
209
            )
210
            ctx.set_impact(response.rows[0])
4✔
211
            if ctx._tracer:
4✔
212
                ctx._tracer.add_impact(response.rows[0])
4✔
213
                ctx._tracer._unlink_trace(ctx)
4✔
214
            return response
4✔
215

216
        tracer = self.current_tracer
4✔
217
        ctx = Scope3AIContext(request=impact_row)
4✔
218
        ctx._tracer = tracer
4✔
219
        if tracer:
4✔
220
            tracer._link_trace(ctx)
4✔
221

222
        if self.sync_mode:
4✔
223
            submit_impact(impact_row, ctx=ctx)
4✔
224
            return ctx
4✔
225

226
        self._ensure_worker()
4✔
227
        self._worker.submit(partial(submit_impact, impact_row=impact_row, ctx=ctx))
4✔
228
        return ctx
4✔
229

230
    async def asubmit_impact(
4✔
231
        self,
232
        impact_row: ImpactRow,
233
    ) -> Scope3AIContext:
234
        """
235
        Async version of Scope3AI::submit_impact.
236
        """
237

238
        if not self.sync_mode:
4✔
239
            # in non sync-mode, it uses the background worker,
240
            # and the background worker is not async (does not have to be).
241
            # so we just redirect the call to the sync version.
242
            return self.submit_impact(impact_row)
4✔
243

244
        tracer = self.current_tracer
4✔
245
        ctx = Scope3AIContext(request=impact_row)
4✔
246
        ctx._tracer = tracer
4✔
247
        if tracer:
4✔
248
            tracer._link_trace(ctx)
×
249

250
        response = await self._async_client.impact(
4✔
251
            rows=[impact_row],
252
            with_response=True,
253
        )
254
        ctx.set_impact(response.rows[0])
4✔
255
        if tracer:
4✔
UNCOV
256
            tracer.add_impact(response.rows[0])
×
UNCOV
257
            tracer._unlink_trace(ctx)
×
258

259
        return ctx
4✔
260

261
    @property
4✔
262
    def root_tracer(self):
4✔
263
        """
264
        Return the root tracer.
265

266
        The root tracer is the first tracer in the current execution context
267
        (tracer stack). If no tracers are currently active, it returns None.
268

269
        Returns:
270
            Tracer: The root tracer if available, otherwise None.
271
        """
UNCOV
272
        tracers = self._tracer.get()
×
UNCOV
273
        return tracers[0] if tracers else None
×
274

275
    @property
4✔
276
    def current_tracer(self):
4✔
277
        """
278
        Return the current tracer.
279

280
        The current tracer is the last tracer in the current execution context
281
        (tracer stack). If no tracers are currently active, it returns None.
282

283
        Returns:
284
            Tracer: The current tracer if available, otherwise None.
285
        """
286
        tracers = self._tracer.get()
4✔
287
        return tracers[-1] if tracers else None
4✔
288

289
    @contextmanager
4✔
290
    def trace(self):
4✔
291
        tracer = Tracer()
4✔
292
        try:
4✔
293
            self._push_tracer(tracer)
4✔
294
            yield tracer
4✔
295
        finally:
296
            self._pop_tracer(tracer)
4✔
297

298
    def close(self):
4✔
299
        if self._worker:
4✔
300
            self._worker.kill()
4✔
301
        self.__class__._instance = None
4✔
302

303
    #
304
    # Internals
305
    #
306

307
    def _push_tracer(self, tracer: Tracer) -> None:
4✔
308
        tracer._link_parent(self.current_tracer)
4✔
309
        self._tracer.get().append(tracer)
4✔
310

311
    def _pop_tracer(self, tracer: Tracer) -> None:
4✔
312
        self._tracer.get().remove(tracer)
4✔
313
        tracer._unlink_parent(self.current_tracer)
4✔
314

315
    def _init_providers(self, providers: List[str]) -> None:
4✔
316
        """Initialize the specified providers."""
317
        for provider in providers:
4✔
318
            if provider not in _INSTRUMENTS:
4✔
UNCOV
319
                raise Scope3AIError(
×
320
                    f"Could not find tracer for the `{provider}` provider."
321
                )
322
            if provider in self._providers and provider not in _RE_INIT_PROVIDERS:
4✔
323
                # already initialized
324
                continue
4✔
325
            init_func = _INSTRUMENTS[provider]
4✔
326
            init_func()
4✔
327
            self._providers.append(provider)
4✔
328

329
    def _ensure_worker(self) -> None:
4✔
330
        if not self._worker:
4✔
331
            self._worker = BackgroundWorker(-1)
4✔
332

333
    def _init_logging(self) -> None:
4✔
334
        logging.basicConfig(
4✔
335
            level=logging.INFO,
336
            format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
337
            handlers=[logging.StreamHandler()],
338
        )
339
        logging.getLogger("scope3ai").setLevel(logging.DEBUG)
4✔
340

341
    def _init_atexit(self):
4✔
342
        @atexit.register
4✔
343
        def _shutdown():
4✔
344
            # do not reinstanciate the singleton here if somehow it was deleted
UNCOV
345
            scope3ai = Scope3AI._instance
×
UNCOV
346
            if not scope3ai:
×
UNCOV
347
                return
×
UNCOV
348
            if scope3ai._worker and scope3ai._worker._queue:
×
UNCOV
349
                logging.debug("Waiting background informations to be processed")
×
UNCOV
350
                scope3ai._worker._queue.join()
×
UNCOV
351
                logging.debug("Shutting down Scope3AI")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc