Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,52 @@ Roadmap:

### Initializing the SDK

The SDK can be initialized with your API key and custom configurations.
The SDK can be initialized with the following parameters, from environemnt variable or when calling `ScopeAI.init(...)`:

| Attribute | Environment Variable | Description | Can be customized per tracer |
|-----------------------|--------------------------|--------------------------------|------------------------------|
| **`api_key`** | **`SCOPE3AI_API_KEY`** | Your Scope3AI API key. Default: `None` | **No** |
| `api_url` | `SCOPE3AI_API_URL` | The API endpoint URL. Default: `https://aiapi.scope3.com` | No |
| `enable_debug_logging`| `SCOPE3AI_DEBUG_LOGGING` | Enable debug logging. Default: `False` | No |
| `sync_mode` | `SCOPE3AI_SYNC_MODE` | Enable synchronous mode. Default: `False` | No |
| `environment` | `SCOPE3AI_ENVIRONMENT` | The user-defined environment name, such as "production" or "staging". Default: `None` | No |
| `application_id` | `SCOPE3AI_APPLICATION_ID`| The user-defined application identifier. Default: `default` | ✅ Yes |
| `client_id` | `SCOPE3AI_CLIENT_ID` | The user-defined client identifier. Default: `None` | ✅ Yes |
| `project_id` | `SCOPE3AI_PROJECT_ID` | The user-defined project identifier. Default: `None` | ✅ Yes |
| `session_id` | - | The user-defined session identifier, used to track user session. Default `None`. Available only at tracer() level. | ✅ Yes |


**Here is an example of how to initialize the SDK**:

```python
from scope3ai import Scope3AI

scope3 = Scope3AI.init(
api_key="YOUR_API_KEY", # Replace "YOUR_API_KEY" with your actual key
api_url="https://api.scope3.ai/v1", # Optional: Specify the API URL
enable_debug_logging=False, # Enable debug logging (default: False)
sync_mode=False, # Enable synchronous mode when sending telemetry to the API (default: False)
api_key="YOUR_API_KEY",
environment="staging",
application_id="my-app",
project_id="my-webinar-2024"
)
```

### Environment variables
**You could also use environment variables to initialize the SDK**:

You can also use environment variable to setup the SDK:
1. Create a `.env` file with the following content:

- `SCOPE3AI_API_KEY`: Your Scope3AI API key
- `SCOPE3AI_API_URL`: The API endpoint URL. Default: `https://api.scope3.ai/v1`
- `SCOPE3AI_SYNC_MODE`: If `True`, every interaction will be send synchronously to the API, otherwise it will use a background worker. Default: `False`
```env
SCOPE3AI_API_KEY=YOUR_API_KEY
SCOPE3AI_ENVIRONMENT=staging
SCOPE3AI_APPLICATION_ID=my-app
SCOPE3AI_PROJECT_ID=my-webinar-2024
```

2. Use dotenv to load the environment variables:

```python
from dotenv import load_dotenv
from scope3ai import Scope3AI

load_dotenv()
scope3 = Scope3AI.init()
```

Expand All @@ -94,6 +116,23 @@ with scope3.trace() as tracer:
print(f"Total MLH2O: {impact.total_mlh2o}")
```

### 2. Configure per-tracer metadata

Some global metadata can be overridden per-tracer. This is useful when you want to mark a specific tracer with a different `client_id` or `project_id`.

```python
with scope3.trace(client_id="my-client", project_id="my-project") as tracer:
...
```

You can track session with the `session_id` parameter of the tracer. This is only for categorizing the traces in the dashboard.
but works at tracer level, not in global level like `client_id` or `project_id` or others.

```python
with scope3.trace(session_id="my-session") as tracer:
...
```

### 2. Single interaction

For a single interaction, the response is augmented with a `scope3ai` attribute that contains the
Expand Down
1 change: 1 addition & 0 deletions scope3ai/api/defaults.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DEFAULT_API_URL = "https://aiapi.scope3.com"
DEFAULT_APPLICATION_ID = "default"
12 changes: 12 additions & 0 deletions scope3ai/api/tracer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List, Optional
from uuid import uuid4

from .typesgen import ImpactResponse, ModeledRow

Expand All @@ -8,16 +9,27 @@ def __init__(
self,
name: str = None,
keep_traces: bool = False,
client_id: Optional[str] = None,
project_id: Optional[str] = None,
application_id: Optional[str] = None,
session_id: Optional[str] = None,
trace_id: Optional[str] = None,
) -> None:
from scope3ai.lib import Scope3AI

self.trace_id = trace_id or uuid4().hex
self.scope3ai = Scope3AI.get_instance()
self.name = name
self.keep_traces = keep_traces
self.children: List[Tracer] = []
self.rows: List[ModeledRow] = []
self.traces = [] # type: List[Scope3AIContext]

self.client_id = client_id
self.project_id = project_id
self.application_id = application_id
self.session_id = session_id

def impact(self, timeout: Optional[int] = None) -> ImpactResponse:
"""
Return an aggregated impact response for the current tracer and its children.
Expand Down
110 changes: 107 additions & 3 deletions scope3ai/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import logging
from contextlib import contextmanager
from contextvars import ContextVar
from datetime import datetime, timezone
from functools import partial
from os import getenv
from typing import List, Optional
from uuid import uuid4

from .api.client import AsyncClient, Client
from .api.defaults import DEFAULT_API_URL
from .api.defaults import DEFAULT_API_URL, DEFAULT_APPLICATION_ID
from .api.tracer import Tracer
from .api.types import ImpactResponse, ImpactRow, Scope3AIContext
from .constants import PROVIDERS
Expand Down Expand Up @@ -120,6 +121,10 @@ def __init__(self):
self.sync_mode: bool = False
self._sync_client: Optional[Client] = None
self._async_client: Optional[AsyncClient] = None
self.environment: Optional[str] = None
self.client_id: Optional[str] = None
self.project_id: Optional[str] = None
self.application_id: Optional[str] = None

@classmethod
def init(
Expand All @@ -129,6 +134,11 @@ def init(
sync_mode: bool = False,
enable_debug_logging: bool = False,
providers: Optional[List[str]] = None,
# metadata for scope3
environment: Optional[str] = None,
client_id: Optional[str] = None,
project_id: Optional[str] = None,
application_id: Optional[str] = None,
) -> "Scope3AI":
if cls._instance is not None:
raise Scope3AIError("Scope3AI is already initialized")
Expand All @@ -149,6 +159,16 @@ def init(
"or by setting the SCOPE3AI_API_URL environment variable"
)

# metadata
self.environment = environment or getenv("SCOPE3AI_ENVIRONMENT")
self.client_id = client_id or getenv("SCOPE3AI_CLIENT_ID")
self.project_id = project_id or getenv("SCOPE3AI_PROJECT_ID")
self.application_id = (
application_id
or getenv("SCOPE3AI_APPLICATION_ID")
or DEFAULT_APPLICATION_ID
)

if enable_debug_logging:
self._init_logging()

Expand Down Expand Up @@ -211,6 +231,7 @@ def submit_impact(
return response

tracer = self.current_tracer
self._fill_impact_row(impact_row, tracer, self.root_tracer)
ctx = Scope3AIContext(request=impact_row)
ctx._tracer = tracer
if tracer:
Expand All @@ -237,11 +258,14 @@ async def asubmit_impact(
# and the background worker is not async (does not have to be).
# so we just redirect the call to the sync version.
return self.submit_impact(impact_row)

tracer = self.current_tracer
self._fill_impact_row(impact_row, tracer, self.root_tracer)
ctx = Scope3AIContext(request=impact_row)
ctx._tracer = tracer
if tracer:
tracer._link_trace(ctx)
self._fill_impact_row_for_tracer(impact_row, tracer, self.root_tracer)

response = await self._async_client.impact(
rows=[impact_row],
Expand Down Expand Up @@ -283,8 +307,32 @@ def current_tracer(self):
return tracers[-1] if tracers else None

@contextmanager
def trace(self, keep_traces=False):
tracer = Tracer(keep_traces=keep_traces)
def trace(
self,
keep_traces=False,
client_id: Optional[str] = None,
project_id: Optional[str] = None,
application_id: Optional[str] = None,
session_id: Optional[str] = None,
):
root_tracer = self.root_tracer
if not client_id:
client_id = root_tracer.client_id if root_tracer else self.client_id
if not project_id:
project_id = root_tracer.project_id if root_tracer else self.project_id
if not application_id:
application_id = (
root_tracer.application_id if root_tracer else self.application_id
)
if not session_id:
session_id = root_tracer.session_id if root_tracer else None
tracer = Tracer(
keep_traces=keep_traces,
client_id=client_id,
project_id=project_id,
application_id=application_id,
session_id=session_id,
)
try:
self._push_tracer(tracer)
yield tracer
Expand Down Expand Up @@ -345,3 +393,59 @@ def _shutdown():
logging.debug("Waiting background informations to be processed")
scope3ai._worker._queue.join()
logging.debug("Shutting down Scope3AI")

def _fill_impact_row(
self,
row: ImpactRow,
tracer: Optional[Tracer] = None,
root_tracer: Optional[Tracer] = None,
):
# fill fields with information we know about
# One trick is to not set anything on the ImpactRow if it's already set or if the value is None
# because the model are dumped and exclude the fields unset.
# If we set a field to None, it will be added for nothing.
def set_only_if(row, field, *values):
if getattr(row, field) is not None:
return
for value in values:
if value is not None:
setattr(row, field, value)
return

row.request_id = generate_id()
if root_tracer:
set_only_if(row, "trace_id", root_tracer.trace_id)
if row.utc_datetime is None:
row.utc_datetime = datetime.now(tz=timezone.utc)

# copy global-only metadata
set_only_if(
row,
"environment",
self.environment,
)

# copy tracer or global metadata
set_only_if(
row,
"client_id",
tracer.client_id if tracer else None,
self.client_id,
)
set_only_if(
row,
"project_id",
tracer.project_id if tracer else None,
self.project_id,
)
set_only_if(
row,
"application_id",
tracer.application_id if tracer else None,
self.application_id,
)
set_only_if(
row,
"session_id",
tracer.session_id if tracer else None,
)
4 changes: 1 addition & 3 deletions scope3ai/worker.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import logging
import queue
import threading
from time import sleep
from time import monotonic, sleep
from typing import Callable, Optional

from time import monotonic

logger = logging.getLogger("scope3ai.worker")


Expand Down
Loading
Loading