pinta-ai-sdk 0.2.2__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.
- pinta_ai_sdk-0.2.2/.gitignore +21 -0
- pinta_ai_sdk-0.2.2/LICENSE +21 -0
- pinta_ai_sdk-0.2.2/PACKAGE.md +110 -0
- pinta_ai_sdk-0.2.2/PKG-INFO +142 -0
- pinta_ai_sdk-0.2.2/pyproject.toml +87 -0
- pinta_ai_sdk-0.2.2/src/pinta/__init__.py +50 -0
- pinta_ai_sdk-0.2.2/src/pinta/_version.py +1 -0
- pinta_ai_sdk-0.2.2/src/pinta/asgi.py +114 -0
- pinta_ai_sdk-0.2.2/src/pinta/auto.py +117 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/__init__.py +1 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/config.py +131 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/context.py +175 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/export.py +229 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/guard.py +130 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/masking.py +83 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/policy_client.py +173 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/propagation.py +124 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/redact.py +34 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/runtime.py +153 -0
- pinta_ai_sdk-0.2.2/src/pinta/core/spans.py +301 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/__init__.py +36 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/analyze.py +191 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/axes.py +83 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/ceiling.py +222 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/cel.py +415 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/config.py +92 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/credential_context.py +366 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/data/default_guard_config.json +846 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/data_export.py +565 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/explain.py +245 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/export_sql.py +214 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/mask.py +126 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/pii.py +328 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/policy.py +69 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/regex_runner.py +180 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/shell_invocation.py +261 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/sql_runner.py +285 -0
- pinta_ai_sdk-0.2.2/src/pinta/guard/types.py +109 -0
- pinta_ai_sdk-0.2.2/src/pinta/langchain/__init__.py +7 -0
- pinta_ai_sdk-0.2.2/src/pinta/langchain/middleware.py +393 -0
- pinta_ai_sdk-0.2.2/src/pinta/langchain/retriever.py +25 -0
- pinta_ai_sdk-0.2.2/src/pinta/langchain/tools.py +44 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/__init__.py +7 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/__main__.py +57 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/agent.py +115 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/business.py +95 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/corpus.py +261 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/data/corpus-v1.json +186 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/data/skax-reference-policy-v1.json +10 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/evaluate.py +332 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/evidence.py +263 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/execution.py +312 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/metrics.py +245 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/model.py +64 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/observability.py +438 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/policy.py +68 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/profiles.py +212 -0
- pinta_ai_sdk-0.2.2/src/pinta/poc/report.py +163 -0
- pinta_ai_sdk-0.2.2/src/pinta/py.typed +0 -0
- pinta_ai_sdk-0.2.2/src/pinta/selfcheck.py +176 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pinta AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Pinta AI SDK for Python agents
|
|
2
|
+
|
|
3
|
+
Pinta adds observability, telemetry masking and local tool-call policy evaluation
|
|
4
|
+
to Python agents. LangChain integration uses middleware; no sidecar is required.
|
|
5
|
+
|
|
6
|
+
**Requirements:** Python 3.12 or newer. LangChain integration requires
|
|
7
|
+
`langchain >= 1.0` with `create_agent`.
|
|
8
|
+
|
|
9
|
+
The distribution name is **`pinta-ai-sdk`**; the Python import remains **`pinta`**.
|
|
10
|
+
It is not the unrelated `pinta` distribution on PyPI. If you installed an earlier
|
|
11
|
+
private `pinta-ai` wheel, uninstall that distribution before installing this one:
|
|
12
|
+
both use the same Python namespace.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
If your application already has a compatible LangChain installation:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m pip install pinta-ai-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
To install LangChain dependencies as well:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m pip install "pinta-ai-sdk[langchain]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Check the installed version and run the bundled offline self-check:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -c "import pinta; print(pinta.__version__)"
|
|
32
|
+
python -m pinta.selfcheck
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The self-check requires LangChain. It uses a deterministic local model and
|
|
36
|
+
in-memory tools, does not contact a model provider or the Pinta backend, and does
|
|
37
|
+
not prove that your application's telemetry has reached the management console.
|
|
38
|
+
|
|
39
|
+
## Offline installation into an existing LangChain application
|
|
40
|
+
|
|
41
|
+
On an internet-connected computer using the same Python minor version as the
|
|
42
|
+
target server, download the SDK and its dependencies. Replace `VERSION` with
|
|
43
|
+
the selected release number:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip download "pinta-ai-sdk==VERSION" -d sdk --only-binary=:all: --platform any
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Transfer the entire `sdk` directory through your approved file-transfer channel.
|
|
50
|
+
In the target application's Python environment:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m pip install --no-index --find-links=sdk "pinta-ai-sdk==VERSION"
|
|
54
|
+
python -m pinta.selfcheck
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This path deliberately omits the `langchain` extra: the application already
|
|
58
|
+
provides LangChain. `--platform any` accepts platform-independent wheels only,
|
|
59
|
+
instead of accidentally downloading a workstation-specific native dependency.
|
|
60
|
+
Installing or upgrading LangChain itself requires a separate bundle compatible
|
|
61
|
+
with the server's Python version, operating system and architecture.
|
|
62
|
+
|
|
63
|
+
## Connect an application
|
|
64
|
+
|
|
65
|
+
Set `PINTA_BACKEND_URL` and `PINTA_API_KEY` in the application process using your
|
|
66
|
+
organization's approved endpoint and secret-management mechanism. Start with
|
|
67
|
+
`PINTA_GUARD_MODE=monitor`; switch to `enforce` only after reviewing policies and
|
|
68
|
+
normal application behavior.
|
|
69
|
+
|
|
70
|
+
At process startup, configure Pinta. Add middleware at the agent factory and
|
|
71
|
+
provide the authenticated application's user identity around each invocation:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from langchain.agents import create_agent
|
|
75
|
+
from pinta import PintaSettings, configure, get_runtime, pinta_context
|
|
76
|
+
from pinta.langchain import PintaMiddleware
|
|
77
|
+
|
|
78
|
+
configure(PintaSettings.from_env())
|
|
79
|
+
agent = create_agent(model, tools=tools, middleware=[PintaMiddleware()])
|
|
80
|
+
|
|
81
|
+
async def answer(user, session_id, messages):
|
|
82
|
+
async with pinta_context(
|
|
83
|
+
user_id=user.id, user_email=user.email, session_id=session_id,
|
|
84
|
+
):
|
|
85
|
+
return await agent.ainvoke({"messages": messages})
|
|
86
|
+
|
|
87
|
+
# In the application's async shutdown hook:
|
|
88
|
+
# await get_runtime().ashutdown()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Here `model`, `tools` and `user` are supplied by your application. Installation
|
|
92
|
+
alone does not instrument an agent or enable telemetry transmission. Outbound
|
|
93
|
+
HTTPS to the configured Pinta endpoint is required for export and policy refresh;
|
|
94
|
+
local evaluation and the self-check do not require that connection.
|
|
95
|
+
|
|
96
|
+
## Scope and limitations
|
|
97
|
+
|
|
98
|
+
- Masking protects Pinta telemetry, not original business inputs, tool results
|
|
99
|
+
or prompts sent to a model provider.
|
|
100
|
+
- `monitor` evaluates without blocking. `enforce` blocks `DENY` and `REVIEW`
|
|
101
|
+
before the selected tool handler executes.
|
|
102
|
+
- Policy evaluation errors fail open with diagnostics; masking errors redact
|
|
103
|
+
the affected telemetry value and are reported as failures.
|
|
104
|
+
- Detection depends on configured policies and supported input shapes.
|
|
105
|
+
This SDK is not universally equivalent to every TypeScript runtime extractor
|
|
106
|
+
and does not guarantee detection of every credential or unsafe action.
|
|
107
|
+
- Treat this alpha SDK as a version-pinned integration and validate your own
|
|
108
|
+
normal and violation cases before enforcement.
|
|
109
|
+
|
|
110
|
+
MIT licensed. Learn more at [pinta.sh](https://pinta.sh).
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pinta-ai-sdk
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Pinta AI runtime guard adaptor for Python agents (OpenTelemetry spans + tool-call guard)
|
|
5
|
+
Project-URL: Homepage, https://pinta.sh
|
|
6
|
+
Project-URL: Documentation, https://pypi.org/project/pinta-ai-sdk/
|
|
7
|
+
Author: Pinta AI
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-spm,guardrails,langchain,observability,opentelemetry,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: System :: Monitoring
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: opentelemetry-api>=1.44
|
|
21
|
+
Requires-Dist: opentelemetry-sdk>=1.44
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain-core>=1.0; extra == 'langchain'
|
|
24
|
+
Requires-Dist: langchain>=1.0; extra == 'langchain'
|
|
25
|
+
Provides-Extra: poc
|
|
26
|
+
Requires-Dist: langchain-core>=1.0; extra == 'poc'
|
|
27
|
+
Requires-Dist: langchain-mcp-adapters<0.3,>=0.2.2; extra == 'poc'
|
|
28
|
+
Requires-Dist: langchain>=1.0; extra == 'poc'
|
|
29
|
+
Requires-Dist: langsmith<1,>=0.12; extra == 'poc'
|
|
30
|
+
Requires-Dist: mcp<2,>=1.30; extra == 'poc'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Pinta AI SDK for Python agents
|
|
34
|
+
|
|
35
|
+
Pinta adds observability, telemetry masking and local tool-call policy evaluation
|
|
36
|
+
to Python agents. LangChain integration uses middleware; no sidecar is required.
|
|
37
|
+
|
|
38
|
+
**Requirements:** Python 3.12 or newer. LangChain integration requires
|
|
39
|
+
`langchain >= 1.0` with `create_agent`.
|
|
40
|
+
|
|
41
|
+
The distribution name is **`pinta-ai-sdk`**; the Python import remains **`pinta`**.
|
|
42
|
+
It is not the unrelated `pinta` distribution on PyPI. If you installed an earlier
|
|
43
|
+
private `pinta-ai` wheel, uninstall that distribution before installing this one:
|
|
44
|
+
both use the same Python namespace.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
If your application already has a compatible LangChain installation:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install pinta-ai-sdk
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To install LangChain dependencies as well:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install "pinta-ai-sdk[langchain]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Check the installed version and run the bundled offline self-check:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -c "import pinta; print(pinta.__version__)"
|
|
64
|
+
python -m pinta.selfcheck
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The self-check requires LangChain. It uses a deterministic local model and
|
|
68
|
+
in-memory tools, does not contact a model provider or the Pinta backend, and does
|
|
69
|
+
not prove that your application's telemetry has reached the management console.
|
|
70
|
+
|
|
71
|
+
## Offline installation into an existing LangChain application
|
|
72
|
+
|
|
73
|
+
On an internet-connected computer using the same Python minor version as the
|
|
74
|
+
target server, download the SDK and its dependencies. Replace `VERSION` with
|
|
75
|
+
the selected release number:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m pip download "pinta-ai-sdk==VERSION" -d sdk --only-binary=:all: --platform any
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Transfer the entire `sdk` directory through your approved file-transfer channel.
|
|
82
|
+
In the target application's Python environment:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m pip install --no-index --find-links=sdk "pinta-ai-sdk==VERSION"
|
|
86
|
+
python -m pinta.selfcheck
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This path deliberately omits the `langchain` extra: the application already
|
|
90
|
+
provides LangChain. `--platform any` accepts platform-independent wheels only,
|
|
91
|
+
instead of accidentally downloading a workstation-specific native dependency.
|
|
92
|
+
Installing or upgrading LangChain itself requires a separate bundle compatible
|
|
93
|
+
with the server's Python version, operating system and architecture.
|
|
94
|
+
|
|
95
|
+
## Connect an application
|
|
96
|
+
|
|
97
|
+
Set `PINTA_BACKEND_URL` and `PINTA_API_KEY` in the application process using your
|
|
98
|
+
organization's approved endpoint and secret-management mechanism. Start with
|
|
99
|
+
`PINTA_GUARD_MODE=monitor`; switch to `enforce` only after reviewing policies and
|
|
100
|
+
normal application behavior.
|
|
101
|
+
|
|
102
|
+
At process startup, configure Pinta. Add middleware at the agent factory and
|
|
103
|
+
provide the authenticated application's user identity around each invocation:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from langchain.agents import create_agent
|
|
107
|
+
from pinta import PintaSettings, configure, get_runtime, pinta_context
|
|
108
|
+
from pinta.langchain import PintaMiddleware
|
|
109
|
+
|
|
110
|
+
configure(PintaSettings.from_env())
|
|
111
|
+
agent = create_agent(model, tools=tools, middleware=[PintaMiddleware()])
|
|
112
|
+
|
|
113
|
+
async def answer(user, session_id, messages):
|
|
114
|
+
async with pinta_context(
|
|
115
|
+
user_id=user.id, user_email=user.email, session_id=session_id,
|
|
116
|
+
):
|
|
117
|
+
return await agent.ainvoke({"messages": messages})
|
|
118
|
+
|
|
119
|
+
# In the application's async shutdown hook:
|
|
120
|
+
# await get_runtime().ashutdown()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Here `model`, `tools` and `user` are supplied by your application. Installation
|
|
124
|
+
alone does not instrument an agent or enable telemetry transmission. Outbound
|
|
125
|
+
HTTPS to the configured Pinta endpoint is required for export and policy refresh;
|
|
126
|
+
local evaluation and the self-check do not require that connection.
|
|
127
|
+
|
|
128
|
+
## Scope and limitations
|
|
129
|
+
|
|
130
|
+
- Masking protects Pinta telemetry, not original business inputs, tool results
|
|
131
|
+
or prompts sent to a model provider.
|
|
132
|
+
- `monitor` evaluates without blocking. `enforce` blocks `DENY` and `REVIEW`
|
|
133
|
+
before the selected tool handler executes.
|
|
134
|
+
- Policy evaluation errors fail open with diagnostics; masking errors redact
|
|
135
|
+
the affected telemetry value and are reported as failures.
|
|
136
|
+
- Detection depends on configured policies and supported input shapes.
|
|
137
|
+
This SDK is not universally equivalent to every TypeScript runtime extractor
|
|
138
|
+
and does not guarantee detection of every credential or unsafe action.
|
|
139
|
+
- Treat this alpha SDK as a version-pinned integration and validate your own
|
|
140
|
+
normal and violation cases before enforcement.
|
|
141
|
+
|
|
142
|
+
MIT licensed. Learn more at [pinta.sh](https://pinta.sh).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pinta-ai-sdk"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Pinta AI runtime guard adaptor for Python agents (OpenTelemetry spans + tool-call guard)"
|
|
5
|
+
readme = "PACKAGE.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Pinta AI" }]
|
|
10
|
+
keywords = ["langchain", "opentelemetry", "observability", "ai-spm", "guardrails", "security"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Topic :: Security",
|
|
17
|
+
"Topic :: System :: Monitoring",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"opentelemetry-api>=1.44",
|
|
22
|
+
"opentelemetry-sdk>=1.44",
|
|
23
|
+
"httpx>=0.27",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
langchain = ["langchain>=1.0", "langchain-core>=1.0"]
|
|
28
|
+
poc = [
|
|
29
|
+
"langchain>=1.0",
|
|
30
|
+
"langchain-core>=1.0",
|
|
31
|
+
"langsmith>=0.12,<1",
|
|
32
|
+
"mcp>=1.30,<2",
|
|
33
|
+
"langchain-mcp-adapters>=0.2.2,<0.3",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://pinta.sh"
|
|
38
|
+
Documentation = "https://pypi.org/project/pinta-ai-sdk/"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8.3",
|
|
43
|
+
"pytest-asyncio>=0.24",
|
|
44
|
+
"ruff>=0.6",
|
|
45
|
+
"mypy>=1.11",
|
|
46
|
+
"starlette>=0.40",
|
|
47
|
+
"deepagents>=0.1",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/pinta"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
include = [
|
|
59
|
+
"src/pinta",
|
|
60
|
+
"PACKAGE.md",
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"pyproject.toml",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
pythonpath = ["src"]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 100
|
|
73
|
+
target-version = "py312"
|
|
74
|
+
src = ["src", "tests"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
78
|
+
|
|
79
|
+
[tool.mypy]
|
|
80
|
+
python_version = "3.12"
|
|
81
|
+
strict = true
|
|
82
|
+
mypy_path = "src"
|
|
83
|
+
packages = ["pinta"]
|
|
84
|
+
|
|
85
|
+
[[tool.mypy.overrides]]
|
|
86
|
+
module = ["langchain.*", "langchain_core.*", "langgraph.*"]
|
|
87
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Pinta AI adaptor for Python agents.
|
|
2
|
+
|
|
3
|
+
Quickstart::
|
|
4
|
+
|
|
5
|
+
from pinta import configure, PintaSettings, pinta_context
|
|
6
|
+
|
|
7
|
+
runtime = configure(PintaSettings.from_env())
|
|
8
|
+
async with pinta_context(user_id="u1", user_email="a@corp.com", session_id="s1"):
|
|
9
|
+
... # every span in here shares one trace
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pinta._version import __version__
|
|
13
|
+
from pinta.core.config import PintaSettings
|
|
14
|
+
from pinta.core.context import Identity, current_identity, pinta_context
|
|
15
|
+
from pinta.core.guard import GuardClient, GuardDecision, GuardInput
|
|
16
|
+
from pinta.core.propagation import Propagated, extract_propagation, outbound_headers
|
|
17
|
+
from pinta.core.runtime import PintaRuntime, configure, get_runtime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def instrument() -> list[str]:
|
|
21
|
+
"""Patch agent factories so every agent gets PintaMiddleware (see ``pinta.auto``)."""
|
|
22
|
+
from pinta.auto import instrument as _instrument
|
|
23
|
+
|
|
24
|
+
return _instrument()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def uninstrument() -> None:
|
|
28
|
+
from pinta.auto import uninstrument as _uninstrument
|
|
29
|
+
|
|
30
|
+
_uninstrument()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"GuardClient",
|
|
35
|
+
"GuardDecision",
|
|
36
|
+
"GuardInput",
|
|
37
|
+
"Identity",
|
|
38
|
+
"PintaRuntime",
|
|
39
|
+
"PintaSettings",
|
|
40
|
+
"Propagated",
|
|
41
|
+
"__version__",
|
|
42
|
+
"configure",
|
|
43
|
+
"current_identity",
|
|
44
|
+
"extract_propagation",
|
|
45
|
+
"get_runtime",
|
|
46
|
+
"instrument",
|
|
47
|
+
"outbound_headers",
|
|
48
|
+
"pinta_context",
|
|
49
|
+
"uninstrument",
|
|
50
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""ASGI middleware that opens ``pinta_context`` for every HTTP request.
|
|
2
|
+
|
|
3
|
+
Pure ASGI (no Starlette dependency): the ``resolver`` receives a ``starlette.requests.Request``
|
|
4
|
+
when Starlette is installed, else the raw ASGI ``scope``. It returns an ``Identity`` (or a
|
|
5
|
+
mapping of ``pinta_context`` keyword arguments), ``None`` to leave the request anonymous, and
|
|
6
|
+
may be sync or async. Errors in the resolver are logged and the request continues anonymous.
|
|
7
|
+
|
|
8
|
+
app.add_middleware(PintaIdentityMiddleware, resolver=lambda req: Identity(
|
|
9
|
+
user_id=req.state.user.id, user_email=req.state.user.email,
|
|
10
|
+
session_id=req.headers.get("x-thread-id"),
|
|
11
|
+
))
|
|
12
|
+
|
|
13
|
+
Register it *before* the authentication middleware (Starlette wraps later ``add_middleware``
|
|
14
|
+
calls outside earlier ones), so authentication has run when the resolver reads the user.
|
|
15
|
+
Background jobs and queue workers have no request: wrap those in ``pinta_context`` yourself.
|
|
16
|
+
|
|
17
|
+
``propagate=True``: for requests the resolver trusts with an identity, read the W3C
|
|
18
|
+
``traceparent``/``baggage`` headers so the root span joins the caller's trace. Identity itself
|
|
19
|
+
is never taken from headers — only trace linkage and the calling agent are, and only once the
|
|
20
|
+
resolver has already declared the request trusted. ``propagate`` may also be a callable that
|
|
21
|
+
decides per request (e.g. only the A2A endpoint) — it receives the same request object the
|
|
22
|
+
resolver does and is consulted only when the resolver returned an identity.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import inspect
|
|
28
|
+
import logging
|
|
29
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from pinta.core.context import Identity, pinta_context
|
|
33
|
+
from pinta.core.propagation import Propagated, extract_propagation
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger("pinta.asgi")
|
|
36
|
+
|
|
37
|
+
Resolved = Identity | Mapping[str, Any] | None
|
|
38
|
+
Resolver = Callable[[Any], Resolved | Awaitable[Resolved]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _request_for(scope: dict[str, Any], receive: Any) -> Any:
|
|
42
|
+
try:
|
|
43
|
+
from starlette.requests import Request
|
|
44
|
+
except ImportError: # pragma: no cover - starlette is optional
|
|
45
|
+
return scope
|
|
46
|
+
return Request(scope, receive)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _headers(scope: dict[str, Any]) -> dict[str, str]:
|
|
50
|
+
out: dict[str, str] = {}
|
|
51
|
+
for raw_key, raw_value in scope.get("headers") or ():
|
|
52
|
+
out[bytes(raw_key).decode("latin-1").lower()] = bytes(raw_value).decode("latin-1")
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PintaIdentityMiddleware:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
app: Any,
|
|
60
|
+
resolver: Resolver,
|
|
61
|
+
*,
|
|
62
|
+
propagate: bool | Callable[[Any], bool] = False,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.app = app
|
|
65
|
+
self.resolver = resolver
|
|
66
|
+
self.propagate = propagate
|
|
67
|
+
|
|
68
|
+
async def _resolve(self, request: Any) -> Identity | None:
|
|
69
|
+
try:
|
|
70
|
+
result = self.resolver(request)
|
|
71
|
+
if inspect.isawaitable(result):
|
|
72
|
+
result = await result
|
|
73
|
+
except Exception: # noqa: BLE001 - never break the request because of us
|
|
74
|
+
log.exception("pinta: identity resolver failed; request stays anonymous")
|
|
75
|
+
return None
|
|
76
|
+
if result is None:
|
|
77
|
+
return None
|
|
78
|
+
if isinstance(result, Identity):
|
|
79
|
+
return result
|
|
80
|
+
return Identity(**dict(result))
|
|
81
|
+
|
|
82
|
+
def _should_propagate(self, request: Any) -> bool:
|
|
83
|
+
if not callable(self.propagate):
|
|
84
|
+
return bool(self.propagate)
|
|
85
|
+
try:
|
|
86
|
+
return bool(self.propagate(request))
|
|
87
|
+
except Exception: # noqa: BLE001 - never break the request because of us
|
|
88
|
+
log.exception("pinta: propagate predicate failed; request joins no trace")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
92
|
+
if scope.get("type") != "http":
|
|
93
|
+
await self.app(scope, receive, send)
|
|
94
|
+
return
|
|
95
|
+
request = _request_for(scope, receive)
|
|
96
|
+
identity = await self._resolve(request)
|
|
97
|
+
if identity is None:
|
|
98
|
+
await self.app(scope, receive, send)
|
|
99
|
+
return
|
|
100
|
+
parent: Propagated | None = None
|
|
101
|
+
if self._should_propagate(request):
|
|
102
|
+
parent = extract_propagation(_headers(scope))
|
|
103
|
+
async with pinta_context(
|
|
104
|
+
user_id=identity.user_id,
|
|
105
|
+
user_email=identity.user_email,
|
|
106
|
+
session_id=identity.session_id,
|
|
107
|
+
conversation_id=identity.conversation_id,
|
|
108
|
+
attributes=identity.attributes,
|
|
109
|
+
parent=parent,
|
|
110
|
+
):
|
|
111
|
+
await self.app(scope, receive, send)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["PintaIdentityMiddleware", "Resolver"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Zero-touch instrumentation: patch the agent factories so every agent gets ``PintaMiddleware``.
|
|
2
|
+
|
|
3
|
+
import pinta
|
|
4
|
+
pinta.instrument() # before the modules that build agents are imported
|
|
5
|
+
|
|
6
|
+
Patches ``langchain.agents.create_agent`` (and its defining module) and, when deepagents is
|
|
7
|
+
installed, ``deepagents.create_deep_agent`` — giving every ``SubAgent`` its own middleware with
|
|
8
|
+
``agent={"name": ..., "kind": "subagent"}`` and the parent a ``subagents=[...]`` list so the
|
|
9
|
+
manifest and the delegation edges are complete. Agents that already carry a ``PintaMiddleware``
|
|
10
|
+
are left alone. ``uninstrument()`` restores the originals.
|
|
11
|
+
|
|
12
|
+
Caveat of any monkeypatch: a module that did ``from langchain.agents import create_agent``
|
|
13
|
+
*before* ``instrument()`` keeps the original function. Call ``instrument()`` first thing in the
|
|
14
|
+
process (or use the explicit ``middleware=[PintaMiddleware()]`` form, which is the default
|
|
15
|
+
recommendation).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import importlib
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Callable, Sequence
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from pinta.langchain.middleware import PintaMiddleware
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger("pinta.instrument")
|
|
29
|
+
|
|
30
|
+
_PATCHED: dict[tuple[str, str], Callable[..., Any]] = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _has_pinta(middleware: Sequence[Any] | None) -> bool:
|
|
34
|
+
return any(isinstance(m, PintaMiddleware) for m in middleware or ())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _with_pinta(middleware: Sequence[Any] | None, **kwargs: Any) -> list[Any]:
|
|
38
|
+
items = list(middleware or ())
|
|
39
|
+
if not _has_pinta(items):
|
|
40
|
+
items.append(PintaMiddleware(**kwargs))
|
|
41
|
+
return items
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _wrap_create_agent(original: Callable[..., Any]) -> Callable[..., Any]:
|
|
45
|
+
@functools.wraps(original)
|
|
46
|
+
def create_agent(*args: Any, **kwargs: Any) -> Any:
|
|
47
|
+
kwargs["middleware"] = _with_pinta(kwargs.get("middleware"))
|
|
48
|
+
return original(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
return create_agent
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _wrap_create_deep_agent(original: Callable[..., Any]) -> Callable[..., Any]:
|
|
54
|
+
@functools.wraps(original)
|
|
55
|
+
def create_deep_agent(*args: Any, **kwargs: Any) -> Any:
|
|
56
|
+
subagents = kwargs.get("subagents") or []
|
|
57
|
+
names: list[str] = []
|
|
58
|
+
patched: list[Any] = []
|
|
59
|
+
for sub in subagents:
|
|
60
|
+
if isinstance(sub, dict):
|
|
61
|
+
name = str(sub.get("name") or "")
|
|
62
|
+
names.append(name)
|
|
63
|
+
sub = {
|
|
64
|
+
**sub,
|
|
65
|
+
"middleware": _with_pinta(
|
|
66
|
+
sub.get("middleware"), agent={"name": name, "kind": "subagent"}
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
patched.append(sub)
|
|
70
|
+
if subagents:
|
|
71
|
+
kwargs["subagents"] = patched
|
|
72
|
+
kwargs["middleware"] = _with_pinta(kwargs.get("middleware"), subagents=names)
|
|
73
|
+
return original(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return create_deep_agent
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _patch(
|
|
79
|
+
module_name: str, attr: str, wrap: Callable[[Callable[..., Any]], Callable[..., Any]]
|
|
80
|
+
) -> None:
|
|
81
|
+
key = (module_name, attr)
|
|
82
|
+
if key in _PATCHED:
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
module = importlib.import_module(module_name)
|
|
86
|
+
except ImportError:
|
|
87
|
+
return
|
|
88
|
+
original = getattr(module, attr, None)
|
|
89
|
+
if original is None:
|
|
90
|
+
return
|
|
91
|
+
_PATCHED[key] = original
|
|
92
|
+
setattr(module, attr, wrap(original))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def instrument() -> list[str]:
|
|
96
|
+
"""Patch the factories. Returns the fully-qualified names that were patched (idempotent)."""
|
|
97
|
+
for module_name in ("langchain.agents", "langchain.agents.factory"):
|
|
98
|
+
_patch(module_name, "create_agent", _wrap_create_agent)
|
|
99
|
+
for module_name in ("deepagents", "deepagents.graph"):
|
|
100
|
+
_patch(module_name, "create_deep_agent", _wrap_create_deep_agent)
|
|
101
|
+
names = [f"{m}.{a}" for m, a in _PATCHED]
|
|
102
|
+
log.info("pinta: instrumented %s", ", ".join(names) or "nothing")
|
|
103
|
+
return names
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def uninstrument() -> None:
|
|
107
|
+
"""Restore every patched factory."""
|
|
108
|
+
for (module_name, attr), original in list(_PATCHED.items()):
|
|
109
|
+
setattr(importlib.import_module(module_name), attr, original)
|
|
110
|
+
del _PATCHED[(module_name, attr)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_instrumented() -> bool:
|
|
114
|
+
return bool(_PATCHED)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["instrument", "is_instrumented", "uninstrument"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Framework-free core: settings, identity context, guard client, span recorder, exporter."""
|