rippletide-python 0.1.0__tar.gz → 0.3.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,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: rippletide-python
3
+ Version: 0.3.0
4
+ Summary: Monitor-first Python SDK for Rippletide-connected agents
5
+ License: UNLICENSED
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: opentelemetry-sdk==1.40.0
9
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.40.0
10
+
11
+ # Rippletide Python
12
+
13
+ `rippletide-python` connects a code-owned Python agent to Rippletide without
14
+ taking ownership of its model traffic. It has no provider dependencies: wrap a
15
+ hand-written provider or internal HTTP call at the existing model seam.
16
+
17
+ ## Install and connect
18
+
19
+ The current release is `rippletide-python==0.3.0`. It requires
20
+ the platform's OTLP ingestion/readers and does not provide V1 trace compatibility.
21
+ Until it is published, validate it by installing a locally built wheel; do not
22
+ assume the unversioned registry install below contains candidate changes.
23
+
24
+ Requires Python 3.10 or later. From the existing agent repository, use its Python
25
+ environment, or create one if it has none:
26
+
27
+ ```bash
28
+ python3 -m venv .venv
29
+ source .venv/bin/activate
30
+ python -m pip install rippletide-python
31
+ ```
32
+
33
+ On Windows, activate with `.venv\Scripts\Activate.ps1` in PowerShell instead.
34
+ Keep the dependency in the project's requirements or package-manager manifest.
35
+
36
+ In Rippletide, choose **Connect an agent → Agent (Python)**. Save the generated
37
+ credentials in the repository's git-ignored `.env`, then apply the generated
38
+ prompt with your coding agent in that repository. The CLI also supports
39
+ `rippletide connect --kind agent --language python --print-prompt`.
40
+
41
+ Run a real turn that uses a tool and delivers a response. Confirm **Runtime
42
+ verified (5/5)** in Fleet and the run's tool calls and **Delivered** outcome in
43
+ Activity. Copying a prompt or storing credentials alone does not prove a
44
+ connection.
45
+
46
+ ## Instrument existing call sites
47
+
48
+ The following fragment belongs inside the existing request handler; reuse the
49
+ agent's own provider, messages, tools, context, and response sink.
50
+
51
+ ```python
52
+ from rippletide import Rippletide, load_rippletide_env
53
+
54
+ # Keep these values aligned with the agent's existing provider call.
55
+ PROVIDER_NAME = "existing-provider"
56
+ PROVIDER_API_KEY_ENV = "EXISTING_PROVIDER_API_KEY"
57
+ MODEL_OPERATION = "existing-provider.request"
58
+
59
+ load_rippletide_env("/path/to/connected-repo/.env")
60
+ rippletide = Rippletide(name="Support agent")
61
+
62
+ try:
63
+ rippletide.register_prompts({"system": {"fallback": SYSTEM_PROMPT}}) # prompt text is withheld by default
64
+ rippletide.tools("core", TOOLS)
65
+ rippletide.connection(PROVIDER_NAME, env={"api_key": PROVIDER_API_KEY_ENV})
66
+ with rippletide.run(name="inbound_request", input=request, conversation_id=conversation_id):
67
+ with rippletide.agent(name="orchestrator"):
68
+ call_model = rippletide.traced(raw_model_call, name=MODEL_OPERATION, kind="model")
69
+ candidate = call_model(messages)
70
+ return rippletide.deliver_response(
71
+ request=request, response=candidate, context=trusted_context,
72
+ deliver=send_to_caller,
73
+ )
74
+ finally:
75
+ rippletide.flush()
76
+ ```
77
+
78
+ The CLI writes an agent-scoped `RIPPLETIDE=rippletide://…` connection string
79
+ into the connected repository's `.env`. `load_rippletide_env()` loads it without
80
+ overriding the process environment. The SDK also accepts the legacy
81
+ `RIPPLETIDE_API_KEY`, `RIPPLETIDE_AGENT_ID` (or `RIPPLETIDE_APP`), and optional
82
+ `RIPPLETIDE_BASE_URL` generated by the Connection page. A valid `RIPPLETIDE`
83
+ connection takes precedence over those legacy values; explicit constructor
84
+ arguments take precedence over either format. Never use a Platform key in the
85
+ agent or commit the `.env`.
86
+
87
+ ## Async and streaming agents
88
+
89
+ The same client serves synchronous and asyncio code. `guard_tool_call()`,
90
+ `guard_tool_result()` and `deliver_response()` accept `async def` callbacks and
91
+ then return a coroutine to await; the policy round trip and receipts run in a
92
+ worker thread, so the event loop is never blocked. `run()`, `agent()` and
93
+ `span()` are plain `with` blocks in both cases. `traced()` wraps sync
94
+ functions, coroutine functions, and calls that return a provider stream: the
95
+ stream is passed through untouched and its span ends once the stream is
96
+ consumed, closed, or abandoned, in which case the span is marked
97
+ `stream_not_consumed`. Policy decides on the complete response: pass the text
98
+ the agent has assembled from the stream to `deliver_response()` before the
99
+ existing sink delivers it. When the tokens must reach the caller as they arrive,
100
+ pass the stream itself as `response`: chunks flow through to `deliver` untouched
101
+ and the decision is recorded on the assembled text once the stream ends, with no
102
+ suppression possible, so a BLOCK is marked as decided after delivery.
103
+ Use `await rippletide.aflush()` and
104
+ `await rippletide.ashutdown()` from asyncio code.
105
+
106
+ ```python
107
+ async def handle(request):
108
+ with rippletide.run(name="inbound_request", input=request):
109
+ stream = rippletide.traced(open_model_stream, name=MODEL_OPERATION, kind="model")(messages)
110
+ candidate = "".join([chunk.text async for chunk in stream])
111
+ await rippletide.guard_tool_call(toolset="core", tool="lookup", params=args, execute=lookup_async)
112
+ return await rippletide.deliver_response(request=request, response=candidate, deliver=send_async)
113
+ ```
114
+
115
+ ## Policy failures and capture
116
+
117
+ The client starts disabled when no Connection key is present. For
118
+ `guard_tool_call()` and `deliver_response()`:
119
+
120
+ - Network failures, 5xx responses, rate limits, and malformed or interrupted HTTP
121
+ responses fail open: the protected callback can still run.
122
+ - An enforce-mode `BLOCK` raises `RippletidePolicyBlockedError` before the
123
+ protected callback runs. An observe-only `WOULD_BLOCK` permits execution.
124
+ - HTTP `400`, `401`, `403`, or `404` from the policy decision endpoint raises
125
+ `RippletidePolicyDecisionError` before the callback runs. These request,
126
+ authentication, authorization, or routing failures are not swallowed as
127
+ telemetry outages.
128
+
129
+ Result filtering has its own fallback rules, described below. Application input
130
+ validation and exceptions from the agent's own callbacks can also propagate.
131
+
132
+ Runtime instrumentation uses native OpenTelemetry spans and start logs, exported as
133
+ OTLP/HTTP JSON to `/v1/traces` and `/v1/logs`. Providers are local to the client;
134
+ the SDK does not replace application providers. The only profile is `full`, but
135
+ content is omitted unless `Rippletide(capture_content=True)` is explicit. Captured
136
+ content is bounded and credential-redacted, including nested Python mappings
137
+ such as `UserDict` and `MappingProxyType`, not generally PII-filtered. The old
138
+ capture modes are unsupported. `runtime_traces=False` disables evidence export
139
+ without disabling policy checks. Always call `flush()` before a short-lived
140
+ worker exits, and `shutdown()` when the client is no longer needed.
141
+
142
+ Tool and response policy spans include the invocation ID; once a decision arrives, their end
143
+ events and the protected action spans include its decision ID, including in
144
+ the default content-withheld posture. These IDs correlate the action with its policy receipt.
145
+
146
+ Prompt inventory is recorded with its content withheld by default. Pass
147
+ `allow_content_export=True` to `register_prompts(...)` only when the prompt
148
+ owner has explicitly approved storing the prompt text in Rippletide.
149
+
150
+ ## Filter a completed tool result (explicit opt-in)
151
+
152
+ For an eligible collection returned by a tool, declare its result contract and
153
+ pass `allow_result_export=True`. This evaluates the complete result transiently
154
+ at Rippletide; do this only with the owner's explicit approval. It is false by
155
+ default. Transient policy failures and malformed or mismatched plans forward
156
+ every item unchanged. A missing result endpoint (`404` with no error code or
157
+ `NOT_FOUND`) also uses this fallback. Other deterministic request or permission
158
+ rejections raise `RippletidePolicyDecisionError` before `apply` runs.
159
+
160
+ ```python
161
+ rippletide.tools("catalog", TOOLS, results={
162
+ "list_products": {
163
+ "resultSchema": {"type": "object", "properties": {"products": {"type": "array"}}},
164
+ "resultCollection": {"itemsPath": ["products"], "itemIdPath": ["id"]},
165
+ },
166
+ })
167
+
168
+ result = list_products(params)
169
+ return rippletide.guard_tool_result(
170
+ toolset="catalog", tool="list_products", params=params,
171
+ result=result, result_items=result["products"], context=trusted_context,
172
+ allow_result_export=True,
173
+ apply=lambda guarded: {**guarded["result"], "products": guarded["deliveredItems"]},
174
+ )
175
+ ```
176
+
177
+ `ALLOW` and observe-only `WOULD_BLOCK` items are delivered; only enforce-mode
178
+ `BLOCK` items are excluded. The callback receives the original result snapshot,
179
+ the delivered and excluded items, and the decision. It is responsible for the
180
+ native response shape, and the SDK records the applied outcome as a receipt.
181
+ Result fingerprints use the server's ECMAScript JSON encoding, including its
182
+ number formatting and UTF-16 object-key ordering.
183
+
184
+ Python MCP servers remain unsupported. A response that is streamed to the
185
+ caller before it is complete cannot be protected by `deliver_response()`; the
186
+ SDK does not represent such a path as complete response-delivery protection.
@@ -0,0 +1,176 @@
1
+ # Rippletide Python
2
+
3
+ `rippletide-python` connects a code-owned Python agent to Rippletide without
4
+ taking ownership of its model traffic. It has no provider dependencies: wrap a
5
+ hand-written provider or internal HTTP call at the existing model seam.
6
+
7
+ ## Install and connect
8
+
9
+ The current release is `rippletide-python==0.3.0`. It requires
10
+ the platform's OTLP ingestion/readers and does not provide V1 trace compatibility.
11
+ Until it is published, validate it by installing a locally built wheel; do not
12
+ assume the unversioned registry install below contains candidate changes.
13
+
14
+ Requires Python 3.10 or later. From the existing agent repository, use its Python
15
+ environment, or create one if it has none:
16
+
17
+ ```bash
18
+ python3 -m venv .venv
19
+ source .venv/bin/activate
20
+ python -m pip install rippletide-python
21
+ ```
22
+
23
+ On Windows, activate with `.venv\Scripts\Activate.ps1` in PowerShell instead.
24
+ Keep the dependency in the project's requirements or package-manager manifest.
25
+
26
+ In Rippletide, choose **Connect an agent → Agent (Python)**. Save the generated
27
+ credentials in the repository's git-ignored `.env`, then apply the generated
28
+ prompt with your coding agent in that repository. The CLI also supports
29
+ `rippletide connect --kind agent --language python --print-prompt`.
30
+
31
+ Run a real turn that uses a tool and delivers a response. Confirm **Runtime
32
+ verified (5/5)** in Fleet and the run's tool calls and **Delivered** outcome in
33
+ Activity. Copying a prompt or storing credentials alone does not prove a
34
+ connection.
35
+
36
+ ## Instrument existing call sites
37
+
38
+ The following fragment belongs inside the existing request handler; reuse the
39
+ agent's own provider, messages, tools, context, and response sink.
40
+
41
+ ```python
42
+ from rippletide import Rippletide, load_rippletide_env
43
+
44
+ # Keep these values aligned with the agent's existing provider call.
45
+ PROVIDER_NAME = "existing-provider"
46
+ PROVIDER_API_KEY_ENV = "EXISTING_PROVIDER_API_KEY"
47
+ MODEL_OPERATION = "existing-provider.request"
48
+
49
+ load_rippletide_env("/path/to/connected-repo/.env")
50
+ rippletide = Rippletide(name="Support agent")
51
+
52
+ try:
53
+ rippletide.register_prompts({"system": {"fallback": SYSTEM_PROMPT}}) # prompt text is withheld by default
54
+ rippletide.tools("core", TOOLS)
55
+ rippletide.connection(PROVIDER_NAME, env={"api_key": PROVIDER_API_KEY_ENV})
56
+ with rippletide.run(name="inbound_request", input=request, conversation_id=conversation_id):
57
+ with rippletide.agent(name="orchestrator"):
58
+ call_model = rippletide.traced(raw_model_call, name=MODEL_OPERATION, kind="model")
59
+ candidate = call_model(messages)
60
+ return rippletide.deliver_response(
61
+ request=request, response=candidate, context=trusted_context,
62
+ deliver=send_to_caller,
63
+ )
64
+ finally:
65
+ rippletide.flush()
66
+ ```
67
+
68
+ The CLI writes an agent-scoped `RIPPLETIDE=rippletide://…` connection string
69
+ into the connected repository's `.env`. `load_rippletide_env()` loads it without
70
+ overriding the process environment. The SDK also accepts the legacy
71
+ `RIPPLETIDE_API_KEY`, `RIPPLETIDE_AGENT_ID` (or `RIPPLETIDE_APP`), and optional
72
+ `RIPPLETIDE_BASE_URL` generated by the Connection page. A valid `RIPPLETIDE`
73
+ connection takes precedence over those legacy values; explicit constructor
74
+ arguments take precedence over either format. Never use a Platform key in the
75
+ agent or commit the `.env`.
76
+
77
+ ## Async and streaming agents
78
+
79
+ The same client serves synchronous and asyncio code. `guard_tool_call()`,
80
+ `guard_tool_result()` and `deliver_response()` accept `async def` callbacks and
81
+ then return a coroutine to await; the policy round trip and receipts run in a
82
+ worker thread, so the event loop is never blocked. `run()`, `agent()` and
83
+ `span()` are plain `with` blocks in both cases. `traced()` wraps sync
84
+ functions, coroutine functions, and calls that return a provider stream: the
85
+ stream is passed through untouched and its span ends once the stream is
86
+ consumed, closed, or abandoned, in which case the span is marked
87
+ `stream_not_consumed`. Policy decides on the complete response: pass the text
88
+ the agent has assembled from the stream to `deliver_response()` before the
89
+ existing sink delivers it. When the tokens must reach the caller as they arrive,
90
+ pass the stream itself as `response`: chunks flow through to `deliver` untouched
91
+ and the decision is recorded on the assembled text once the stream ends, with no
92
+ suppression possible, so a BLOCK is marked as decided after delivery.
93
+ Use `await rippletide.aflush()` and
94
+ `await rippletide.ashutdown()` from asyncio code.
95
+
96
+ ```python
97
+ async def handle(request):
98
+ with rippletide.run(name="inbound_request", input=request):
99
+ stream = rippletide.traced(open_model_stream, name=MODEL_OPERATION, kind="model")(messages)
100
+ candidate = "".join([chunk.text async for chunk in stream])
101
+ await rippletide.guard_tool_call(toolset="core", tool="lookup", params=args, execute=lookup_async)
102
+ return await rippletide.deliver_response(request=request, response=candidate, deliver=send_async)
103
+ ```
104
+
105
+ ## Policy failures and capture
106
+
107
+ The client starts disabled when no Connection key is present. For
108
+ `guard_tool_call()` and `deliver_response()`:
109
+
110
+ - Network failures, 5xx responses, rate limits, and malformed or interrupted HTTP
111
+ responses fail open: the protected callback can still run.
112
+ - An enforce-mode `BLOCK` raises `RippletidePolicyBlockedError` before the
113
+ protected callback runs. An observe-only `WOULD_BLOCK` permits execution.
114
+ - HTTP `400`, `401`, `403`, or `404` from the policy decision endpoint raises
115
+ `RippletidePolicyDecisionError` before the callback runs. These request,
116
+ authentication, authorization, or routing failures are not swallowed as
117
+ telemetry outages.
118
+
119
+ Result filtering has its own fallback rules, described below. Application input
120
+ validation and exceptions from the agent's own callbacks can also propagate.
121
+
122
+ Runtime instrumentation uses native OpenTelemetry spans and start logs, exported as
123
+ OTLP/HTTP JSON to `/v1/traces` and `/v1/logs`. Providers are local to the client;
124
+ the SDK does not replace application providers. The only profile is `full`, but
125
+ content is omitted unless `Rippletide(capture_content=True)` is explicit. Captured
126
+ content is bounded and credential-redacted, including nested Python mappings
127
+ such as `UserDict` and `MappingProxyType`, not generally PII-filtered. The old
128
+ capture modes are unsupported. `runtime_traces=False` disables evidence export
129
+ without disabling policy checks. Always call `flush()` before a short-lived
130
+ worker exits, and `shutdown()` when the client is no longer needed.
131
+
132
+ Tool and response policy spans include the invocation ID; once a decision arrives, their end
133
+ events and the protected action spans include its decision ID, including in
134
+ the default content-withheld posture. These IDs correlate the action with its policy receipt.
135
+
136
+ Prompt inventory is recorded with its content withheld by default. Pass
137
+ `allow_content_export=True` to `register_prompts(...)` only when the prompt
138
+ owner has explicitly approved storing the prompt text in Rippletide.
139
+
140
+ ## Filter a completed tool result (explicit opt-in)
141
+
142
+ For an eligible collection returned by a tool, declare its result contract and
143
+ pass `allow_result_export=True`. This evaluates the complete result transiently
144
+ at Rippletide; do this only with the owner's explicit approval. It is false by
145
+ default. Transient policy failures and malformed or mismatched plans forward
146
+ every item unchanged. A missing result endpoint (`404` with no error code or
147
+ `NOT_FOUND`) also uses this fallback. Other deterministic request or permission
148
+ rejections raise `RippletidePolicyDecisionError` before `apply` runs.
149
+
150
+ ```python
151
+ rippletide.tools("catalog", TOOLS, results={
152
+ "list_products": {
153
+ "resultSchema": {"type": "object", "properties": {"products": {"type": "array"}}},
154
+ "resultCollection": {"itemsPath": ["products"], "itemIdPath": ["id"]},
155
+ },
156
+ })
157
+
158
+ result = list_products(params)
159
+ return rippletide.guard_tool_result(
160
+ toolset="catalog", tool="list_products", params=params,
161
+ result=result, result_items=result["products"], context=trusted_context,
162
+ allow_result_export=True,
163
+ apply=lambda guarded: {**guarded["result"], "products": guarded["deliveredItems"]},
164
+ )
165
+ ```
166
+
167
+ `ALLOW` and observe-only `WOULD_BLOCK` items are delivered; only enforce-mode
168
+ `BLOCK` items are excluded. The callback receives the original result snapshot,
169
+ the delivered and excluded items, and the decision. It is responsible for the
170
+ native response shape, and the SDK records the applied outcome as a receipt.
171
+ Result fingerprints use the server's ECMAScript JSON encoding, including its
172
+ number formatting and UTF-16 object-key ordering.
173
+
174
+ Python MCP servers remain unsupported. A response that is streamed to the
175
+ caller before it is complete cannot be protected by `deliver_response()`; the
176
+ SDK does not represent such a path as complete response-delivery protection.
@@ -4,10 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rippletide-python"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "Monitor-first Python SDK for Rippletide-connected agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
+ dependencies = ["opentelemetry-sdk==1.40.0", "opentelemetry-exporter-otlp-proto-common==1.40.0"]
11
12
  license = { text = "UNLICENSED" }
12
13
 
13
14
  [tool.setuptools]