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.
@@ -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,3 @@
1
+ """Package version — keep in sync with pyproject.toml."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()