contexory-sdk 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.
- contexory_sdk-0.1.0/.gitignore +6 -0
- contexory_sdk-0.1.0/LICENSE +21 -0
- contexory_sdk-0.1.0/PKG-INFO +160 -0
- contexory_sdk-0.1.0/README.md +127 -0
- contexory_sdk-0.1.0/pyproject.toml +57 -0
- contexory_sdk-0.1.0/src/contexory/__init__.py +67 -0
- contexory_sdk-0.1.0/src/contexory/adapters/__init__.py +0 -0
- contexory_sdk-0.1.0/src/contexory/adapters/claude_agent_sdk.py +58 -0
- contexory_sdk-0.1.0/src/contexory/adapters/langchain.py +36 -0
- contexory_sdk-0.1.0/src/contexory/adapters/pydantic_ai.py +27 -0
- contexory_sdk-0.1.0/src/contexory/apply_tools.py +149 -0
- contexory_sdk-0.1.0/src/contexory/cache.py +103 -0
- contexory_sdk-0.1.0/src/contexory/client.py +229 -0
- contexory_sdk-0.1.0/src/contexory/errors.py +23 -0
- contexory_sdk-0.1.0/src/contexory/instrument/__init__.py +118 -0
- contexory_sdk-0.1.0/src/contexory/mcp_upstream.py +258 -0
- contexory_sdk-0.1.0/src/contexory/payload.py +67 -0
- contexory_sdk-0.1.0/src/contexory/py.typed +0 -0
- contexory_sdk-0.1.0/src/contexory/release.py +30 -0
- contexory_sdk-0.1.0/src/contexory/signing.py +60 -0
- contexory_sdk-0.1.0/src/contexory/types.py +74 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Contexory
|
|
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,160 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: contexory-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Resolve a Contexory agent's released context at runtime: cached, signature-verified, and safe when Contexory is unreachable.
|
|
5
|
+
Project-URL: Homepage, https://github.com/wedgey/contexory/tree/main/packages/sdk-python#readme
|
|
6
|
+
Project-URL: Repository, https://github.com/wedgey/contexory
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent-skills,ai-agents,claude-agent-sdk,contexory,langchain,prompt-management,pydantic-ai
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: cryptography>=42
|
|
15
|
+
Provides-Extra: claude-agent-sdk
|
|
16
|
+
Requires-Dist: claude-agent-sdk>=0.1; extra == 'claude-agent-sdk'
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: claude-agent-sdk>=0.1; extra == 'dev'
|
|
19
|
+
Requires-Dist: langchain>=1.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: opentelemetry-api>=1.25; extra == 'dev'
|
|
21
|
+
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'dev'
|
|
22
|
+
Requires-Dist: pydantic-ai-slim>=1.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Provides-Extra: langchain
|
|
26
|
+
Requires-Dist: langchain>=1.0; extra == 'langchain'
|
|
27
|
+
Provides-Extra: otel
|
|
28
|
+
Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
|
|
29
|
+
Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
|
|
30
|
+
Provides-Extra: pydantic-ai
|
|
31
|
+
Requires-Dist: pydantic-ai-slim>=1.0; extra == 'pydantic-ai'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# contexory-sdk (Python)
|
|
35
|
+
|
|
36
|
+
Resolve a Contexory agent's released context at runtime: the system prompt, skills and tool
|
|
37
|
+
descriptions under a label such as `production`. Cached, signature-verified, and safe when Contexory is
|
|
38
|
+
unreachable. It is the Python twin of [`@contexory/sdk`](../sdk): same wire format, same cache layout,
|
|
39
|
+
same failure rules, and a shared parity suite that fails if they drift.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install contexory-sdk # resolve + apply_tools
|
|
43
|
+
pip install 'contexory-sdk[otel]' # instrument()
|
|
44
|
+
pip install 'contexory-sdk[langchain]' # + adapters, per framework
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Python 3.10 or newer. The only hard dependency is `cryptography`, for Ed25519.
|
|
48
|
+
|
|
49
|
+
## resolve
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from contexory import Contexory
|
|
53
|
+
|
|
54
|
+
contexory = Contexory() # CONTEXORY_API_TOKEN holds a con_rt_ runtime key
|
|
55
|
+
ctx = contexory.resolve("support-agent") # label defaults to "production"
|
|
56
|
+
|
|
57
|
+
system = ctx.system
|
|
58
|
+
ctx.release.number # 43
|
|
59
|
+
ctx.source # "network" | "cache" | "stale-cache"
|
|
60
|
+
ctx.stale # True when Contexory failed and this is the last verified release
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`await contexory.aresolve(...)` runs the same call off the event loop.
|
|
64
|
+
|
|
65
|
+
- Every response is verified with Ed25519 against the keys at `/api/v1/signing-keys`. A bad or missing
|
|
66
|
+
signature raises `ContexorySignatureError` and never falls back to the cache.
|
|
67
|
+
- A refusal (401, 403, 404) raises `ContexoryResolveError` with `.status`. Any other failure serves the
|
|
68
|
+
last verified release and sets `ctx.stale` and `ctx.error`; with nothing cached it raises.
|
|
69
|
+
- Releases are cached on disk (`CONTEXORY_CACHE_DIR`, default `~/.cache/contexory/sdk`) for `ttl_ms`
|
|
70
|
+
(60s), then revalidated with `If-None-Match`. The layout is byte-compatible with the TypeScript SDK's.
|
|
71
|
+
- Missing key: `resolve` raises immediately. The TypeScript SDK would serve a cache written under the same
|
|
72
|
+
empty key; there is no such cache to serve.
|
|
73
|
+
|
|
74
|
+
## apply_tools
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
tools = ctx.apply_tools(my_tools) # dict keyed by name, or list of tools
|
|
78
|
+
tools = ctx.apply_tools(my_tools, renames={"refund": "issue_refund"})
|
|
79
|
+
ctx.original_tool_name("issue_refund") # "refund"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Tools may be dicts, dataclasses, Pydantic models or plain objects with `name`/`description`. Hosted tools
|
|
83
|
+
(no `description`) pass through untouched.
|
|
84
|
+
|
|
85
|
+
## Upstream MCP descriptions
|
|
86
|
+
|
|
87
|
+
A toolset override can be pinned to the MCP server it overrides (`upstream: {server, hash}` in `tools.json`).
|
|
88
|
+
`apply_tools` then applies it only while the server's description still hashes to the one Contexory
|
|
89
|
+
accepted; otherwise the server's own text stands and the tool is listed in `held` (`ctx.held` after
|
|
90
|
+
`ctx.apply_tools`).
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from contexory import watch_mcp_tools
|
|
94
|
+
|
|
95
|
+
watch = watch_mcp_tools(
|
|
96
|
+
server="zendesk", # the name the toolset uses in upstream.server
|
|
97
|
+
list_tools=lambda: {"tools": mcp.list_tools(), "ttlMs": None}, # your MCP client's tools/list
|
|
98
|
+
report={"ingest_key": "con_ing_...", "agent": "support-agent", "release": ctx.release.number},
|
|
99
|
+
)
|
|
100
|
+
watch.start() # polls on the server's ttlMs, clamped to 5s..1h (default 5min)
|
|
101
|
+
tools = ctx.apply_tools(my_tools, upstream_hashes=watch.latest())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`hash_mcp_tools`, `mcp_tools_hash`, `description_hash`, `schema_hash` and `canonical_json` are the same
|
|
105
|
+
hashes the server and the TypeScript SDK compute, held to `packages/shared/src/mcp-upstream/vectors.json`.
|
|
106
|
+
`report_upstream_hashes` posts with a write-only `con_ing_` ingest key, never a runtime key.
|
|
107
|
+
|
|
108
|
+
## instrument
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
112
|
+
from contexory import instrument
|
|
113
|
+
|
|
114
|
+
handle = instrument(ctx) # or instrument(43), instrument("r43")
|
|
115
|
+
provider = TracerProvider()
|
|
116
|
+
provider.add_span_processor(handle.processor)
|
|
117
|
+
|
|
118
|
+
with handle.run_with_release(44): # per-request scope; concurrent scopes never cross
|
|
119
|
+
...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Stamps `gen_ai.agent.version` and the Langfuse, LangSmith and Braintrust metadata attributes on every
|
|
123
|
+
span. `vendors=[]` writes only `gen_ai.agent.version`; `baggage=True` carries the scope in W3C baggage.
|
|
124
|
+
|
|
125
|
+
## Adapters
|
|
126
|
+
|
|
127
|
+
| Framework | Use |
|
|
128
|
+
|---|---|
|
|
129
|
+
| LangChain / LangGraph | `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])` from `contexory.adapters.langchain`; implements `wrap_model_call`. `apply_system=True` also swaps in the released system prompt |
|
|
130
|
+
| Pydantic AI | `Agent(model, prepare_tools=prepare_tools(ctx))` from `contexory.adapters.pydantic_ai`; descriptions only |
|
|
131
|
+
| Claude Agent SDK | `create_proxy_server(ctx, upstream_tools)` from `contexory.adapters.claude_agent_sdk` returns an in-process MCP server that re-exposes external tools with the released descriptions and forwards calls. Pass it in `mcp_servers` and disallow the originals; the model sees `mcp__contexory__<tool>` |
|
|
132
|
+
|
|
133
|
+
## Develop
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd packages/sdk-python
|
|
137
|
+
python -m venv .venv && . .venv/bin/activate
|
|
138
|
+
pip install -e '.[dev]'
|
|
139
|
+
ruff check . && ruff format --check . && pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`parity/fixtures.json` is generated by the TypeScript SDK (`UPDATE_PARITY=1 pnpm exec vitest run --project
|
|
143
|
+
sdk-python`) and replayed by `tests/test_parity.py`. Change `parity/cases.ts`, regenerate, and both suites
|
|
144
|
+
must pass. `pnpm test` runs the TypeScript half.
|
|
145
|
+
|
|
146
|
+
## Releasing
|
|
147
|
+
|
|
148
|
+
Releases go to PyPI as `contexory-sdk` through trusted publishing (OIDC): no token exists. The
|
|
149
|
+
`publish` job in `.github/workflows/sdk-python.yml` runs only for a pushed `sdk-python-v*` tag, after
|
|
150
|
+
the test matrix and build pass.
|
|
151
|
+
|
|
152
|
+
1. Bump `version` in `pyproject.toml` and merge it to `main`.
|
|
153
|
+
2. Tag that commit and push the tag. The tag must equal the version, or the job fails:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
One-time setup (pending trusted publisher on PyPI, GitHub environment `pypi`):
|
|
160
|
+
[`docs/RELEASING.md`](../../docs/RELEASING.md#python-sdk-pypi).
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# contexory-sdk (Python)
|
|
2
|
+
|
|
3
|
+
Resolve a Contexory agent's released context at runtime: the system prompt, skills and tool
|
|
4
|
+
descriptions under a label such as `production`. Cached, signature-verified, and safe when Contexory is
|
|
5
|
+
unreachable. It is the Python twin of [`@contexory/sdk`](../sdk): same wire format, same cache layout,
|
|
6
|
+
same failure rules, and a shared parity suite that fails if they drift.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install contexory-sdk # resolve + apply_tools
|
|
10
|
+
pip install 'contexory-sdk[otel]' # instrument()
|
|
11
|
+
pip install 'contexory-sdk[langchain]' # + adapters, per framework
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python 3.10 or newer. The only hard dependency is `cryptography`, for Ed25519.
|
|
15
|
+
|
|
16
|
+
## resolve
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from contexory import Contexory
|
|
20
|
+
|
|
21
|
+
contexory = Contexory() # CONTEXORY_API_TOKEN holds a con_rt_ runtime key
|
|
22
|
+
ctx = contexory.resolve("support-agent") # label defaults to "production"
|
|
23
|
+
|
|
24
|
+
system = ctx.system
|
|
25
|
+
ctx.release.number # 43
|
|
26
|
+
ctx.source # "network" | "cache" | "stale-cache"
|
|
27
|
+
ctx.stale # True when Contexory failed and this is the last verified release
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`await contexory.aresolve(...)` runs the same call off the event loop.
|
|
31
|
+
|
|
32
|
+
- Every response is verified with Ed25519 against the keys at `/api/v1/signing-keys`. A bad or missing
|
|
33
|
+
signature raises `ContexorySignatureError` and never falls back to the cache.
|
|
34
|
+
- A refusal (401, 403, 404) raises `ContexoryResolveError` with `.status`. Any other failure serves the
|
|
35
|
+
last verified release and sets `ctx.stale` and `ctx.error`; with nothing cached it raises.
|
|
36
|
+
- Releases are cached on disk (`CONTEXORY_CACHE_DIR`, default `~/.cache/contexory/sdk`) for `ttl_ms`
|
|
37
|
+
(60s), then revalidated with `If-None-Match`. The layout is byte-compatible with the TypeScript SDK's.
|
|
38
|
+
- Missing key: `resolve` raises immediately. The TypeScript SDK would serve a cache written under the same
|
|
39
|
+
empty key; there is no such cache to serve.
|
|
40
|
+
|
|
41
|
+
## apply_tools
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
tools = ctx.apply_tools(my_tools) # dict keyed by name, or list of tools
|
|
45
|
+
tools = ctx.apply_tools(my_tools, renames={"refund": "issue_refund"})
|
|
46
|
+
ctx.original_tool_name("issue_refund") # "refund"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Tools may be dicts, dataclasses, Pydantic models or plain objects with `name`/`description`. Hosted tools
|
|
50
|
+
(no `description`) pass through untouched.
|
|
51
|
+
|
|
52
|
+
## Upstream MCP descriptions
|
|
53
|
+
|
|
54
|
+
A toolset override can be pinned to the MCP server it overrides (`upstream: {server, hash}` in `tools.json`).
|
|
55
|
+
`apply_tools` then applies it only while the server's description still hashes to the one Contexory
|
|
56
|
+
accepted; otherwise the server's own text stands and the tool is listed in `held` (`ctx.held` after
|
|
57
|
+
`ctx.apply_tools`).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from contexory import watch_mcp_tools
|
|
61
|
+
|
|
62
|
+
watch = watch_mcp_tools(
|
|
63
|
+
server="zendesk", # the name the toolset uses in upstream.server
|
|
64
|
+
list_tools=lambda: {"tools": mcp.list_tools(), "ttlMs": None}, # your MCP client's tools/list
|
|
65
|
+
report={"ingest_key": "con_ing_...", "agent": "support-agent", "release": ctx.release.number},
|
|
66
|
+
)
|
|
67
|
+
watch.start() # polls on the server's ttlMs, clamped to 5s..1h (default 5min)
|
|
68
|
+
tools = ctx.apply_tools(my_tools, upstream_hashes=watch.latest())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`hash_mcp_tools`, `mcp_tools_hash`, `description_hash`, `schema_hash` and `canonical_json` are the same
|
|
72
|
+
hashes the server and the TypeScript SDK compute, held to `packages/shared/src/mcp-upstream/vectors.json`.
|
|
73
|
+
`report_upstream_hashes` posts with a write-only `con_ing_` ingest key, never a runtime key.
|
|
74
|
+
|
|
75
|
+
## instrument
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
79
|
+
from contexory import instrument
|
|
80
|
+
|
|
81
|
+
handle = instrument(ctx) # or instrument(43), instrument("r43")
|
|
82
|
+
provider = TracerProvider()
|
|
83
|
+
provider.add_span_processor(handle.processor)
|
|
84
|
+
|
|
85
|
+
with handle.run_with_release(44): # per-request scope; concurrent scopes never cross
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Stamps `gen_ai.agent.version` and the Langfuse, LangSmith and Braintrust metadata attributes on every
|
|
90
|
+
span. `vendors=[]` writes only `gen_ai.agent.version`; `baggage=True` carries the scope in W3C baggage.
|
|
91
|
+
|
|
92
|
+
## Adapters
|
|
93
|
+
|
|
94
|
+
| Framework | Use |
|
|
95
|
+
|---|---|
|
|
96
|
+
| LangChain / LangGraph | `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])` from `contexory.adapters.langchain`; implements `wrap_model_call`. `apply_system=True` also swaps in the released system prompt |
|
|
97
|
+
| Pydantic AI | `Agent(model, prepare_tools=prepare_tools(ctx))` from `contexory.adapters.pydantic_ai`; descriptions only |
|
|
98
|
+
| Claude Agent SDK | `create_proxy_server(ctx, upstream_tools)` from `contexory.adapters.claude_agent_sdk` returns an in-process MCP server that re-exposes external tools with the released descriptions and forwards calls. Pass it in `mcp_servers` and disallow the originals; the model sees `mcp__contexory__<tool>` |
|
|
99
|
+
|
|
100
|
+
## Develop
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd packages/sdk-python
|
|
104
|
+
python -m venv .venv && . .venv/bin/activate
|
|
105
|
+
pip install -e '.[dev]'
|
|
106
|
+
ruff check . && ruff format --check . && pytest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`parity/fixtures.json` is generated by the TypeScript SDK (`UPDATE_PARITY=1 pnpm exec vitest run --project
|
|
110
|
+
sdk-python`) and replayed by `tests/test_parity.py`. Change `parity/cases.ts`, regenerate, and both suites
|
|
111
|
+
must pass. `pnpm test` runs the TypeScript half.
|
|
112
|
+
|
|
113
|
+
## Releasing
|
|
114
|
+
|
|
115
|
+
Releases go to PyPI as `contexory-sdk` through trusted publishing (OIDC): no token exists. The
|
|
116
|
+
`publish` job in `.github/workflows/sdk-python.yml` runs only for a pushed `sdk-python-v*` tag, after
|
|
117
|
+
the test matrix and build pass.
|
|
118
|
+
|
|
119
|
+
1. Bump `version` in `pyproject.toml` and merge it to `main`.
|
|
120
|
+
2. Tag that commit and push the tag. The tag must equal the version, or the job fails:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
One-time setup (pending trusted publisher on PyPI, GitHub environment `pypi`):
|
|
127
|
+
[`docs/RELEASING.md`](../../docs/RELEASING.md#python-sdk-pypi).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "contexory-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Resolve a Contexory agent's released context at runtime: cached, signature-verified, and safe when Contexory is unreachable."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["contexory", "ai-agents", "agent-skills", "prompt-management", "langchain", "pydantic-ai", "claude-agent-sdk"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
dependencies = ["cryptography>=42"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/wedgey/contexory/tree/main/packages/sdk-python#readme"
|
|
22
|
+
Repository = "https://github.com/wedgey/contexory"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
otel = ["opentelemetry-api>=1.25", "opentelemetry-sdk>=1.25"]
|
|
26
|
+
langchain = ["langchain>=1.0"]
|
|
27
|
+
pydantic-ai = ["pydantic-ai-slim>=1.0"]
|
|
28
|
+
claude-agent-sdk = ["claude-agent-sdk>=0.1"]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8",
|
|
31
|
+
"ruff>=0.6",
|
|
32
|
+
"opentelemetry-api>=1.25",
|
|
33
|
+
"opentelemetry-sdk>=1.25",
|
|
34
|
+
"langchain>=1.0",
|
|
35
|
+
"pydantic-ai-slim>=1.0",
|
|
36
|
+
"claude-agent-sdk>=0.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/contexory"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["src/contexory", "README.md", "LICENSE"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
addopts = "-q"
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 120
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint.isort]
|
|
57
|
+
known-first-party = ["contexory", "conftest"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from .apply_tools import AppliedTools, apply_tools
|
|
2
|
+
from .client import (
|
|
3
|
+
DEFAULT_BASE_URL,
|
|
4
|
+
DEFAULT_LABEL,
|
|
5
|
+
Contexory,
|
|
6
|
+
FetchResponse,
|
|
7
|
+
ResolvedContext,
|
|
8
|
+
default_fetch,
|
|
9
|
+
)
|
|
10
|
+
from .errors import ContexoryResolveError, ContexorySignatureError
|
|
11
|
+
from .instrument import (
|
|
12
|
+
AGENT_VERSION_ATTRIBUTE,
|
|
13
|
+
RELEASE_BAGGAGE_KEY,
|
|
14
|
+
VENDOR_ATTRIBUTES,
|
|
15
|
+
VENDORS,
|
|
16
|
+
Instrumentation,
|
|
17
|
+
instrument,
|
|
18
|
+
open_inference_metadata,
|
|
19
|
+
open_llmetry_association_properties,
|
|
20
|
+
openai_agents_trace_metadata,
|
|
21
|
+
)
|
|
22
|
+
from .mcp_upstream import (
|
|
23
|
+
canonical_json,
|
|
24
|
+
clamp_ttl,
|
|
25
|
+
description_hash,
|
|
26
|
+
hash_mcp_tools,
|
|
27
|
+
mcp_tools_hash,
|
|
28
|
+
report_upstream_hashes,
|
|
29
|
+
schema_hash,
|
|
30
|
+
watch_mcp_tools,
|
|
31
|
+
)
|
|
32
|
+
from .release import normalize_release
|
|
33
|
+
from .types import ResolvedTool, ResolvePayload, Upstream
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AGENT_VERSION_ATTRIBUTE",
|
|
37
|
+
"DEFAULT_BASE_URL",
|
|
38
|
+
"DEFAULT_LABEL",
|
|
39
|
+
"RELEASE_BAGGAGE_KEY",
|
|
40
|
+
"VENDORS",
|
|
41
|
+
"VENDOR_ATTRIBUTES",
|
|
42
|
+
"AppliedTools",
|
|
43
|
+
"Contexory",
|
|
44
|
+
"ContexoryResolveError",
|
|
45
|
+
"ContexorySignatureError",
|
|
46
|
+
"FetchResponse",
|
|
47
|
+
"Instrumentation",
|
|
48
|
+
"ResolvePayload",
|
|
49
|
+
"ResolvedContext",
|
|
50
|
+
"ResolvedTool",
|
|
51
|
+
"Upstream",
|
|
52
|
+
"canonical_json",
|
|
53
|
+
"clamp_ttl",
|
|
54
|
+
"description_hash",
|
|
55
|
+
"hash_mcp_tools",
|
|
56
|
+
"mcp_tools_hash",
|
|
57
|
+
"report_upstream_hashes",
|
|
58
|
+
"schema_hash",
|
|
59
|
+
"watch_mcp_tools",
|
|
60
|
+
"apply_tools",
|
|
61
|
+
"default_fetch",
|
|
62
|
+
"instrument",
|
|
63
|
+
"normalize_release",
|
|
64
|
+
"open_inference_metadata",
|
|
65
|
+
"open_llmetry_association_properties",
|
|
66
|
+
"openai_agents_trace_metadata",
|
|
67
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Claude Agent SDK: an in-process MCP proxy that re-exposes external tools with released descriptions.
|
|
2
|
+
|
|
3
|
+
Claude Code's built-in tools and external MCP servers describe themselves; their
|
|
4
|
+
descriptions cannot be rewritten in flight. The proxy registers each upstream tool again
|
|
5
|
+
under an in-process MCP server with the released description and forwards the call, so
|
|
6
|
+
the model reads Contexory's text. Name the proxy server so the model sees
|
|
7
|
+
`mcp__<server>__<tool>` and disallow the upstream originals with `disallowed_tools`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class UpstreamTool:
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
input_schema: Any
|
|
22
|
+
#: Forwards one call: `await call(args)` returns the MCP result dict (`{"content": [...]}`).
|
|
23
|
+
call: Callable[[Mapping[str, Any]], Awaitable[dict[str, Any]]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def proxied_tools(context: Any, upstream: Iterable[UpstreamTool], *, include_unmatched: bool = True) -> list[Any]:
|
|
27
|
+
"""`UpstreamTool`s with the released description applied.
|
|
28
|
+
|
|
29
|
+
A tool with no released description is kept as-is unless `include_unmatched=False`.
|
|
30
|
+
"""
|
|
31
|
+
tools = list(upstream)
|
|
32
|
+
applied = context.apply_tools({t.name: {"name": t.name, "description": t.description} for t in tools})
|
|
33
|
+
out: list[UpstreamTool] = []
|
|
34
|
+
released = {t.name for t in context.tools}
|
|
35
|
+
for tool in tools:
|
|
36
|
+
if tool.name not in released and not include_unmatched:
|
|
37
|
+
continue
|
|
38
|
+
description = applied[tool.name]["description"]
|
|
39
|
+
out.append(UpstreamTool(tool.name, description, tool.input_schema, tool.call))
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_proxy_server(
|
|
44
|
+
context: Any, upstream: Iterable[UpstreamTool], *, name: str = "contexory", include_unmatched: bool = True
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""An SDK MCP server config for `ClaudeAgentOptions(mcp_servers={name: server})`."""
|
|
47
|
+
try:
|
|
48
|
+
from claude_agent_sdk import create_sdk_mcp_server, tool
|
|
49
|
+
except ImportError as error:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"create_proxy_server() needs the Claude Agent SDK. pip install 'contexory-sdk[claude-agent-sdk]'"
|
|
52
|
+
) from error
|
|
53
|
+
|
|
54
|
+
wrapped = [
|
|
55
|
+
tool(t.name, t.description, t.input_schema)(t.call)
|
|
56
|
+
for t in proxied_tools(context, upstream, include_unmatched=include_unmatched)
|
|
57
|
+
]
|
|
58
|
+
return create_sdk_mcp_server(name=name, tools=wrapped)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""LangChain / LangGraph: apply released tool descriptions and the system prompt per model call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from langchain.agents.middleware import AgentMiddleware as _Base
|
|
9
|
+
except ImportError: # pragma: no cover - the adapter still works duck-typed against any object with `override`
|
|
10
|
+
_Base = object # type: ignore[assignment,misc]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContexoryMiddleware(_Base): # type: ignore[misc,valid-type]
|
|
14
|
+
"""Agent middleware: `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])`.
|
|
15
|
+
|
|
16
|
+
Rewrites each model request's tools to the released descriptions. `apply_system=True`
|
|
17
|
+
also replaces the system message with the release's system prompt.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, context: Any, *, apply_system: bool = False):
|
|
21
|
+
self.context = context
|
|
22
|
+
self.apply_system = apply_system
|
|
23
|
+
|
|
24
|
+
def _rewrite(self, request: Any) -> Any:
|
|
25
|
+
overrides: dict[str, Any] = {}
|
|
26
|
+
if request.tools:
|
|
27
|
+
overrides["tools"] = self.context.apply_tools(list(request.tools))
|
|
28
|
+
if self.apply_system:
|
|
29
|
+
overrides["system_prompt"] = self.context.system
|
|
30
|
+
return request.override(**overrides) if overrides else request
|
|
31
|
+
|
|
32
|
+
def wrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
33
|
+
return handler(self._rewrite(request))
|
|
34
|
+
|
|
35
|
+
async def awrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
36
|
+
return await handler(self._rewrite(request))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Pydantic AI: apply released tool descriptions through `prepare_tools`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..apply_tools import describe_tool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def prepare_tools(context: Any) -> Any:
|
|
12
|
+
"""`Agent(model, prepare_tools=prepare_tools(ctx))`.
|
|
13
|
+
|
|
14
|
+
Descriptions only: Pydantic AI routes a call back by the tool name it registered,
|
|
15
|
+
so renames are not offered here.
|
|
16
|
+
"""
|
|
17
|
+
released = {tool.name: tool for tool in context.tools}
|
|
18
|
+
|
|
19
|
+
async def prepare(_run_context: Any, tool_defs: list[Any]) -> list[Any]:
|
|
20
|
+
return [
|
|
21
|
+
dataclasses.replace(definition, description=describe_tool(released[definition.name]))
|
|
22
|
+
if definition.name in released
|
|
23
|
+
else definition
|
|
24
|
+
for definition in tool_defs
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
return prepare
|