selfship-ai 0.1.0__tar.gz
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.
- selfship_ai-0.1.0/.gitignore +20 -0
- selfship_ai-0.1.0/PKG-INFO +54 -0
- selfship_ai-0.1.0/README.md +28 -0
- selfship_ai-0.1.0/pyproject.toml +39 -0
- selfship_ai-0.1.0/src/selfship/__init__.py +26 -0
- selfship_ai-0.1.0/src/selfship/_client.py +67 -0
- selfship_ai-0.1.0/src/selfship/_config.py +26 -0
- selfship_ai-0.1.0/src/selfship/_convenience.py +60 -0
- selfship_ai-0.1.0/src/selfship/_identity.py +27 -0
- selfship_ai-0.1.0/src/selfship/_interaction.py +37 -0
- selfship_ai-0.1.0/src/selfship/_version.py +3 -0
- selfship_ai-0.1.0/tests/conftest.py +38 -0
- selfship_ai-0.1.0/tests/test_client.py +72 -0
- selfship_ai-0.1.0/tests/test_convenience.py +103 -0
- selfship_ai-0.1.0/tests/test_live_smoke.py +26 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
bin/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
tmp/*
|
|
5
|
+
!tmp/.gitkeep
|
|
6
|
+
.env
|
|
7
|
+
.env.local
|
|
8
|
+
webapp/.env.local
|
|
9
|
+
webapp/node_modules/
|
|
10
|
+
pr-runner/node_modules/
|
|
11
|
+
pr-runner/dist/
|
|
12
|
+
sdk/typescript/node_modules/
|
|
13
|
+
sdk/typescript/dist/
|
|
14
|
+
sdk/python/dist/
|
|
15
|
+
sdk/python/*.egg-info/
|
|
16
|
+
sdk/python/.pytest_cache/
|
|
17
|
+
webapp/.next/
|
|
18
|
+
*.log
|
|
19
|
+
/tmp/selfship-local-secret
|
|
20
|
+
.DS_Store
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: selfship-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SelfShip LLM observability SDK — thin wrapper over Langfuse
|
|
5
|
+
Project-URL: Homepage, https://selfship.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.selfship.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/selfship-ai/selfship
|
|
8
|
+
Author-email: SelfShip <hello@selfship.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: langfuse,llm,observability,selfship,tracing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: langfuse<4,>=3
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# selfship-ai
|
|
28
|
+
|
|
29
|
+
Python SDK for [SelfShip](https://selfship.ai) — a thin wrapper over the Langfuse Python SDK.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install selfship-ai
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import selfship
|
|
41
|
+
|
|
42
|
+
selfship.init() # reads SELFSHIP_ORG_ID / SELFSHIP_ORG_SECRET
|
|
43
|
+
|
|
44
|
+
interaction = selfship.begin(
|
|
45
|
+
user_id="user-123",
|
|
46
|
+
agent_name="support-bot",
|
|
47
|
+
input="Hello",
|
|
48
|
+
)
|
|
49
|
+
interaction.end(output="Hi there!", success=True)
|
|
50
|
+
|
|
51
|
+
selfship.shutdown()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See [docs.selfship.ai](https://docs.selfship.ai/python/getting-started) for the full API.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# selfship-ai
|
|
2
|
+
|
|
3
|
+
Python SDK for [SelfShip](https://selfship.ai) — a thin wrapper over the Langfuse Python SDK.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install selfship-ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import selfship
|
|
15
|
+
|
|
16
|
+
selfship.init() # reads SELFSHIP_ORG_ID / SELFSHIP_ORG_SECRET
|
|
17
|
+
|
|
18
|
+
interaction = selfship.begin(
|
|
19
|
+
user_id="user-123",
|
|
20
|
+
agent_name="support-bot",
|
|
21
|
+
input="Hello",
|
|
22
|
+
)
|
|
23
|
+
interaction.end(output="Hi there!", success=True)
|
|
24
|
+
|
|
25
|
+
selfship.shutdown()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See [docs.selfship.ai](https://docs.selfship.ai/python/getting-started) for the full API.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "selfship-ai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SelfShip LLM observability SDK — thin wrapper over Langfuse"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "SelfShip", email = "hello@selfship.ai" }]
|
|
13
|
+
keywords = ["selfship", "langfuse", "llm", "observability", "tracing"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["langfuse>=3,<4"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://selfship.ai"
|
|
28
|
+
Documentation = "https://docs.selfship.ai"
|
|
29
|
+
Repository = "https://github.com/selfship-ai/selfship"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["build", "pytest", "pytest-mock"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/selfship"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""SelfShip Python SDK — thin wrapper over Langfuse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
__version__ = _pkg_version("selfship-ai")
|
|
9
|
+
except Exception: # pragma: no cover — editable/dev installs
|
|
10
|
+
from selfship._version import __version__
|
|
11
|
+
|
|
12
|
+
from selfship._client import get_client, init, observe, shutdown
|
|
13
|
+
from selfship._convenience import begin, identify, track
|
|
14
|
+
from selfship._interaction import Interaction
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"begin",
|
|
19
|
+
"get_client",
|
|
20
|
+
"identify",
|
|
21
|
+
"init",
|
|
22
|
+
"Interaction",
|
|
23
|
+
"observe",
|
|
24
|
+
"shutdown",
|
|
25
|
+
"track",
|
|
26
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Langfuse client lifecycle for SelfShip."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from langfuse import Langfuse
|
|
9
|
+
from langfuse import get_client as _langfuse_get_client
|
|
10
|
+
from langfuse import observe # noqa: F401 — re-exported
|
|
11
|
+
|
|
12
|
+
from selfship._config import SDK_VERSION_HEADER, resolve_credentials, resolve_endpoint
|
|
13
|
+
from selfship._version import __version__
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from langfuse import Langfuse as LangfuseClient
|
|
17
|
+
|
|
18
|
+
_initialized = False
|
|
19
|
+
_atexit_registered = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def init(
|
|
23
|
+
org_id: str | None = None,
|
|
24
|
+
org_secret: str | None = None,
|
|
25
|
+
) -> LangfuseClient:
|
|
26
|
+
"""Initialize the SelfShip SDK (wraps Langfuse with SelfShip credentials)."""
|
|
27
|
+
global _initialized, _atexit_registered
|
|
28
|
+
|
|
29
|
+
resolved_id, resolved_secret = resolve_credentials(org_id, org_secret)
|
|
30
|
+
endpoint = resolve_endpoint()
|
|
31
|
+
|
|
32
|
+
Langfuse(
|
|
33
|
+
public_key=resolved_id,
|
|
34
|
+
secret_key=resolved_secret,
|
|
35
|
+
host=endpoint,
|
|
36
|
+
media_upload_thread_count=0,
|
|
37
|
+
additional_headers={SDK_VERSION_HEADER: __version__},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_initialized = True
|
|
41
|
+
|
|
42
|
+
if not _atexit_registered:
|
|
43
|
+
atexit.register(shutdown)
|
|
44
|
+
_atexit_registered = True
|
|
45
|
+
|
|
46
|
+
return _langfuse_get_client()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_client() -> LangfuseClient:
|
|
50
|
+
"""Return the underlying Langfuse client."""
|
|
51
|
+
if not _initialized:
|
|
52
|
+
raise RuntimeError("SelfShip SDK not initialized. Call selfship.init() first.")
|
|
53
|
+
return _langfuse_get_client()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def shutdown() -> None:
|
|
57
|
+
"""Flush pending events and shut down the Langfuse client."""
|
|
58
|
+
if not _initialized:
|
|
59
|
+
return
|
|
60
|
+
_langfuse_get_client().shutdown()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reset_client_state() -> None:
|
|
64
|
+
"""Reset module init flags (used in tests)."""
|
|
65
|
+
global _initialized, _atexit_registered
|
|
66
|
+
_initialized = False
|
|
67
|
+
_atexit_registered = False
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Configuration helpers for SelfShip SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
DEFAULT_HOST = "https://otel.selfship.ai"
|
|
8
|
+
SDK_VERSION_HEADER = "X-SelfShip-SDK-Version"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_endpoint() -> str:
|
|
12
|
+
return os.environ.get("SELFSHIP_ENDPOINT", DEFAULT_HOST).rstrip("/")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_credentials(
|
|
16
|
+
org_id: str | None,
|
|
17
|
+
org_secret: str | None,
|
|
18
|
+
) -> tuple[str, str]:
|
|
19
|
+
resolved_id = org_id or os.environ.get("SELFSHIP_ORG_ID")
|
|
20
|
+
resolved_secret = org_secret or os.environ.get("SELFSHIP_ORG_SECRET")
|
|
21
|
+
if not resolved_id or not resolved_secret:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"SelfShip credentials required. Pass org_id/org_secret to init() "
|
|
24
|
+
"or set SELFSHIP_ORG_ID and SELFSHIP_ORG_SECRET."
|
|
25
|
+
)
|
|
26
|
+
return resolved_id, resolved_secret
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Convenience tracing API (begin, track, identify)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from selfship._client import get_client
|
|
8
|
+
from selfship._identity import set_user_traits, user_metadata
|
|
9
|
+
from selfship._interaction import Interaction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def begin(
|
|
13
|
+
*,
|
|
14
|
+
user_id: str | None = None,
|
|
15
|
+
agent_name: str = "agent",
|
|
16
|
+
input: Any = None,
|
|
17
|
+
conversation_id: str | None = None,
|
|
18
|
+
) -> Interaction:
|
|
19
|
+
"""Open a timed interaction mapped to a Langfuse root span."""
|
|
20
|
+
client = get_client()
|
|
21
|
+
metadata = user_metadata(user_id)
|
|
22
|
+
|
|
23
|
+
span = client.start_span(name=agent_name, input=input, metadata=metadata or None)
|
|
24
|
+
client.update_current_trace(
|
|
25
|
+
user_id=user_id,
|
|
26
|
+
session_id=conversation_id,
|
|
27
|
+
input=input,
|
|
28
|
+
metadata=metadata or None,
|
|
29
|
+
)
|
|
30
|
+
return Interaction(span)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def track(
|
|
34
|
+
*,
|
|
35
|
+
user_id: str | None = None,
|
|
36
|
+
input: Any = None,
|
|
37
|
+
output: Any = None,
|
|
38
|
+
agent_name: str = "agent",
|
|
39
|
+
conversation_id: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Fire-and-forget a single traced event."""
|
|
42
|
+
client = get_client()
|
|
43
|
+
metadata = user_metadata(user_id)
|
|
44
|
+
|
|
45
|
+
span = client.start_span(name=agent_name, input=input, metadata=metadata or None)
|
|
46
|
+
client.update_current_trace(
|
|
47
|
+
user_id=user_id,
|
|
48
|
+
session_id=conversation_id,
|
|
49
|
+
input=input,
|
|
50
|
+
output=output,
|
|
51
|
+
metadata=metadata or None,
|
|
52
|
+
)
|
|
53
|
+
if output is not None:
|
|
54
|
+
span.update(output=output)
|
|
55
|
+
span.end()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def identify(user_id: str, traits: dict[str, Any]) -> None:
|
|
59
|
+
"""Cache user metadata merged into subsequent traces for this user."""
|
|
60
|
+
set_user_traits(user_id, traits)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Client-side user trait cache for identify()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_user_traits: dict[str, dict[str, Any]] = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_user_traits(user_id: str, traits: dict[str, Any]) -> None:
|
|
11
|
+
existing = _user_traits.get(user_id, {})
|
|
12
|
+
existing.update(traits)
|
|
13
|
+
_user_traits[user_id] = existing
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def user_metadata(user_id: str | None) -> dict[str, Any]:
|
|
17
|
+
if not user_id:
|
|
18
|
+
return {}
|
|
19
|
+
traits = _user_traits.get(user_id)
|
|
20
|
+
if not traits:
|
|
21
|
+
return {}
|
|
22
|
+
return {"user": traits}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def clear_user_traits() -> None:
|
|
26
|
+
"""Reset cached traits (used in tests)."""
|
|
27
|
+
_user_traits.clear()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Timed interaction handle returned by begin()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Interaction:
|
|
9
|
+
"""A timed agent interaction mapped to a Langfuse root span."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, span: Any) -> None:
|
|
12
|
+
self._span = span
|
|
13
|
+
self._metadata: dict[str, Any] = {}
|
|
14
|
+
|
|
15
|
+
def set_property(self, key: str, value: Any) -> None:
|
|
16
|
+
self._metadata[key] = value
|
|
17
|
+
self._span.update(metadata=dict(self._metadata))
|
|
18
|
+
|
|
19
|
+
def set_properties(self, properties: dict[str, Any]) -> None:
|
|
20
|
+
self._metadata.update(properties)
|
|
21
|
+
self._span.update(metadata=dict(self._metadata))
|
|
22
|
+
|
|
23
|
+
def end(
|
|
24
|
+
self,
|
|
25
|
+
output: Any = None,
|
|
26
|
+
*,
|
|
27
|
+
success: bool = True,
|
|
28
|
+
) -> None:
|
|
29
|
+
update: dict[str, Any] = {}
|
|
30
|
+
if output is not None:
|
|
31
|
+
update["output"] = output
|
|
32
|
+
if not success:
|
|
33
|
+
update["level"] = "ERROR"
|
|
34
|
+
update["status_message"] = "Interaction failed"
|
|
35
|
+
if update:
|
|
36
|
+
self._span.update(**update)
|
|
37
|
+
self._span.end()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Shared pytest fixtures for selfship tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import selfship
|
|
10
|
+
from selfship._client import reset_client_state
|
|
11
|
+
from selfship._identity import clear_user_traits
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(autouse=True)
|
|
15
|
+
def reset_state():
|
|
16
|
+
reset_client_state()
|
|
17
|
+
clear_user_traits()
|
|
18
|
+
yield
|
|
19
|
+
reset_client_state()
|
|
20
|
+
clear_user_traits()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_langfuse_client():
|
|
25
|
+
client = MagicMock()
|
|
26
|
+
span = MagicMock()
|
|
27
|
+
client.start_span.return_value = span
|
|
28
|
+
return client, span
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def initialized_sdk(mock_langfuse_client):
|
|
33
|
+
client, span = mock_langfuse_client
|
|
34
|
+
with patch("selfship._client.Langfuse", return_value=client), patch(
|
|
35
|
+
"selfship._client._langfuse_get_client", return_value=client
|
|
36
|
+
):
|
|
37
|
+
selfship.init(org_id="org_test", org_secret="bps_test")
|
|
38
|
+
yield client, span
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Unit tests for selfship.init() and client lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import selfship
|
|
11
|
+
from selfship._config import SDK_VERSION_HEADER
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_init_with_explicit_credentials():
|
|
15
|
+
client = MagicMock()
|
|
16
|
+
with patch("selfship._client.Langfuse", return_value=client) as mock_ctor, patch(
|
|
17
|
+
"selfship._client._langfuse_get_client", return_value=client
|
|
18
|
+
):
|
|
19
|
+
result = selfship.init(org_id="org_abc", org_secret="bps_secret")
|
|
20
|
+
|
|
21
|
+
mock_ctor.assert_called_once_with(
|
|
22
|
+
public_key="org_abc",
|
|
23
|
+
secret_key="bps_secret",
|
|
24
|
+
host="https://otel.selfship.ai",
|
|
25
|
+
media_upload_thread_count=0,
|
|
26
|
+
additional_headers={SDK_VERSION_HEADER: selfship.__version__},
|
|
27
|
+
)
|
|
28
|
+
assert result is client
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_init_reads_env_vars(monkeypatch):
|
|
32
|
+
monkeypatch.setenv("SELFSHIP_ORG_ID", "org_env")
|
|
33
|
+
monkeypatch.setenv("SELFSHIP_ORG_SECRET", "bps_env")
|
|
34
|
+
client = MagicMock()
|
|
35
|
+
with patch("selfship._client.Langfuse", return_value=client), patch(
|
|
36
|
+
"selfship._client._langfuse_get_client", return_value=client
|
|
37
|
+
):
|
|
38
|
+
selfship.init()
|
|
39
|
+
assert selfship.get_client() is client
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_init_respects_selfship_endpoint(monkeypatch):
|
|
43
|
+
monkeypatch.setenv("SELFSHIP_ENDPOINT", "https://staging.otel.selfship.ai/")
|
|
44
|
+
client = MagicMock()
|
|
45
|
+
with patch("selfship._client.Langfuse", return_value=client) as mock_ctor, patch(
|
|
46
|
+
"selfship._client._langfuse_get_client", return_value=client
|
|
47
|
+
):
|
|
48
|
+
selfship.init(org_id="org_abc", org_secret="bps_secret")
|
|
49
|
+
|
|
50
|
+
assert mock_ctor.call_args.kwargs["host"] == "https://staging.otel.selfship.ai"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_init_missing_credentials_raises():
|
|
54
|
+
for key in ("SELFSHIP_ORG_ID", "SELFSHIP_ORG_SECRET"):
|
|
55
|
+
os.environ.pop(key, None)
|
|
56
|
+
with pytest.raises(ValueError, match="credentials required"):
|
|
57
|
+
selfship.init()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_get_client_before_init_raises():
|
|
61
|
+
with pytest.raises(RuntimeError, match="not initialized"):
|
|
62
|
+
selfship.get_client()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_shutdown_calls_langfuse_shutdown(initialized_sdk):
|
|
66
|
+
client, _ = initialized_sdk
|
|
67
|
+
selfship.shutdown()
|
|
68
|
+
client.shutdown.assert_called_once()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_shutdown_before_init_is_noop():
|
|
72
|
+
selfship.shutdown()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Unit tests for begin/end, track, and identify."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import selfship
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_begin_maps_fields(initialized_sdk):
|
|
9
|
+
client, span = initialized_sdk
|
|
10
|
+
|
|
11
|
+
interaction = selfship.begin(
|
|
12
|
+
user_id="user-42",
|
|
13
|
+
agent_name="research-agent",
|
|
14
|
+
input="Summarize Q3",
|
|
15
|
+
conversation_id="conv-9f3a",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
client.start_span.assert_called_once_with(
|
|
19
|
+
name="research-agent",
|
|
20
|
+
input="Summarize Q3",
|
|
21
|
+
metadata=None,
|
|
22
|
+
)
|
|
23
|
+
client.update_current_trace.assert_called_once_with(
|
|
24
|
+
user_id="user-42",
|
|
25
|
+
session_id="conv-9f3a",
|
|
26
|
+
input="Summarize Q3",
|
|
27
|
+
metadata=None,
|
|
28
|
+
)
|
|
29
|
+
assert interaction is not None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_interaction_set_property_and_end(initialized_sdk):
|
|
33
|
+
_, span = initialized_sdk
|
|
34
|
+
|
|
35
|
+
interaction = selfship.begin(user_id="user-1", agent_name="bot", input="hi")
|
|
36
|
+
interaction.set_property("model", "gpt-4o")
|
|
37
|
+
interaction.set_properties({"temperature": 0.2})
|
|
38
|
+
interaction.end(output="done", success=True)
|
|
39
|
+
|
|
40
|
+
span.update.assert_any_call(metadata={"model": "gpt-4o"})
|
|
41
|
+
span.update.assert_any_call(metadata={"model": "gpt-4o", "temperature": 0.2})
|
|
42
|
+
span.update.assert_any_call(output="done")
|
|
43
|
+
span.end.assert_called_once()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_interaction_end_failure_sets_error_level(initialized_sdk):
|
|
47
|
+
_, span = initialized_sdk
|
|
48
|
+
|
|
49
|
+
interaction = selfship.begin(agent_name="bot", input="hi")
|
|
50
|
+
interaction.end(output="failed", success=False)
|
|
51
|
+
|
|
52
|
+
span.update.assert_any_call(
|
|
53
|
+
output="failed",
|
|
54
|
+
level="ERROR",
|
|
55
|
+
status_message="Interaction failed",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_track_one_shot_trace(initialized_sdk):
|
|
60
|
+
client, span = initialized_sdk
|
|
61
|
+
|
|
62
|
+
selfship.track(
|
|
63
|
+
user_id="user-42",
|
|
64
|
+
input="question",
|
|
65
|
+
output="answer",
|
|
66
|
+
agent_name="banking-bot",
|
|
67
|
+
conversation_id="conv-9f3a",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
client.start_span.assert_called_once_with(
|
|
71
|
+
name="banking-bot",
|
|
72
|
+
input="question",
|
|
73
|
+
metadata=None,
|
|
74
|
+
)
|
|
75
|
+
client.update_current_trace.assert_called_once_with(
|
|
76
|
+
user_id="user-42",
|
|
77
|
+
session_id="conv-9f3a",
|
|
78
|
+
input="question",
|
|
79
|
+
output="answer",
|
|
80
|
+
metadata=None,
|
|
81
|
+
)
|
|
82
|
+
span.update.assert_called_once_with(output="answer")
|
|
83
|
+
span.end.assert_called_once()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_identify_merges_into_begin_metadata(initialized_sdk):
|
|
87
|
+
client, span = initialized_sdk
|
|
88
|
+
|
|
89
|
+
selfship.identify("user-42", {"email": "alex@example.com", "plan": "pro"})
|
|
90
|
+
selfship.begin(user_id="user-42", agent_name="bot", input="hi")
|
|
91
|
+
|
|
92
|
+
expected_metadata = {"user": {"email": "alex@example.com", "plan": "pro"}}
|
|
93
|
+
client.start_span.assert_called_once_with(
|
|
94
|
+
name="bot",
|
|
95
|
+
input="hi",
|
|
96
|
+
metadata=expected_metadata,
|
|
97
|
+
)
|
|
98
|
+
client.update_current_trace.assert_called_once_with(
|
|
99
|
+
user_id="user-42",
|
|
100
|
+
session_id=None,
|
|
101
|
+
input="hi",
|
|
102
|
+
metadata=expected_metadata,
|
|
103
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Opt-in live smoke test against otel.selfship.ai."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import selfship
|
|
10
|
+
|
|
11
|
+
HAS_LIVE_CREDS = bool(
|
|
12
|
+
os.environ.get("SELFSHIP_ORG_ID") and os.environ.get("SELFSHIP_ORG_SECRET")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.skipif(not HAS_LIVE_CREDS, reason="SELFSHIP_ORG_ID/SELFSHIP_ORG_SECRET not set")
|
|
17
|
+
def test_live_trace_smoke():
|
|
18
|
+
"""Send a real trace to the SelfShip gateway."""
|
|
19
|
+
selfship.init()
|
|
20
|
+
interaction = selfship.begin(
|
|
21
|
+
user_id="sdk-smoke-test",
|
|
22
|
+
agent_name="sdk-python-smoke",
|
|
23
|
+
input="ping",
|
|
24
|
+
)
|
|
25
|
+
interaction.end(output="pong", success=True)
|
|
26
|
+
selfship.shutdown()
|