gcf-python 2.5.3__py3-none-any.whl → 2.7.0__py3-none-any.whl
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.
- gcf/__init__.py +1 -1
- gcf/decode_generic.py +21 -1
- gcf/fastmcp.py +131 -0
- gcf/scalar.py +28 -6
- {gcf_python-2.5.3.dist-info → gcf_python-2.7.0.dist-info}/METADATA +21 -3
- {gcf_python-2.5.3.dist-info → gcf_python-2.7.0.dist-info}/RECORD +9 -8
- {gcf_python-2.5.3.dist-info → gcf_python-2.7.0.dist-info}/WHEEL +1 -1
- {gcf_python-2.5.3.dist-info → gcf_python-2.7.0.dist-info}/entry_points.txt +0 -0
- {gcf_python-2.5.3.dist-info → gcf_python-2.7.0.dist-info}/licenses/LICENSE +0 -0
gcf/__init__.py
CHANGED
gcf/decode_generic.py
CHANGED
|
@@ -171,7 +171,7 @@ def _parse_object_body(
|
|
|
171
171
|
|
|
172
172
|
# Inline array (e.g. items[3]: a,b,c). Only reached if no = found.
|
|
173
173
|
if not content.startswith("@") and not content.startswith("##"):
|
|
174
|
-
bracket_idx = content
|
|
174
|
+
bracket_idx = _find_array_bracket(content)
|
|
175
175
|
if bracket_idx > 0:
|
|
176
176
|
rest = content[bracket_idx:]
|
|
177
177
|
close_idx = rest.find("]")
|
|
@@ -196,6 +196,26 @@ def _parse_object_body(
|
|
|
196
196
|
return i - start
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
def _find_array_bracket(s: str) -> int:
|
|
200
|
+
"""Index of the '[' that opens a named-array marker (``key[N]: ...``).
|
|
201
|
+
|
|
202
|
+
A quoted key is scanned first so a '[' inside the key name is not mistaken
|
|
203
|
+
for the array bracket (bare keys cannot contain '['). Returns -1 when the
|
|
204
|
+
key is a quoted string not immediately followed by '['.
|
|
205
|
+
"""
|
|
206
|
+
if s and s[0] == '"':
|
|
207
|
+
i = 1
|
|
208
|
+
while i < len(s):
|
|
209
|
+
if s[i] == "\\":
|
|
210
|
+
i += 2
|
|
211
|
+
continue
|
|
212
|
+
if s[i] == '"':
|
|
213
|
+
return i + 1 if i + 1 < len(s) and s[i + 1] == "[" else -1
|
|
214
|
+
i += 1
|
|
215
|
+
return -1
|
|
216
|
+
return s.find("[")
|
|
217
|
+
|
|
218
|
+
|
|
199
219
|
def _find_kv_split(s: str) -> int:
|
|
200
220
|
if not s:
|
|
201
221
|
return -1
|
gcf/fastmcp.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Optional FastMCP middleware for opt-in GCF response encoding.
|
|
2
|
+
|
|
3
|
+
This is a small integration helper for `FastMCP <https://gofastmcp.com>`_ servers.
|
|
4
|
+
It requires the ``fastmcp`` extra; the core ``gcf`` package stays
|
|
5
|
+
zero-dependency::
|
|
6
|
+
|
|
7
|
+
pip install "gcf-python[fastmcp]"
|
|
8
|
+
|
|
9
|
+
Register it once on a FastMCP server::
|
|
10
|
+
|
|
11
|
+
from gcf.fastmcp import GcfResponseMiddleware
|
|
12
|
+
|
|
13
|
+
mcp.add_middleware(GcfResponseMiddleware())
|
|
14
|
+
|
|
15
|
+
When ``RESPONSE_FORMAT=gcf`` is set in the environment, each tool result whose
|
|
16
|
+
model-facing content is a single JSON text block is re-encoded as a GCF generic
|
|
17
|
+
wire, so the response uses fewer tokens when it crosses the LLM boundary. It is:
|
|
18
|
+
|
|
19
|
+
- **Opt-in** — nothing changes unless ``RESPONSE_FORMAT=gcf`` is set (or
|
|
20
|
+
``enabled=True`` is passed).
|
|
21
|
+
- **Lossless and fail-safe** — on any encoding error the original result is
|
|
22
|
+
returned, so a tool call is never dropped over formatting.
|
|
23
|
+
- **Non-destructive** — only a lone JSON text block is re-encoded; a result
|
|
24
|
+
carrying an image or any second block is left untouched, and the tool's
|
|
25
|
+
``structuredContent`` (if any) is preserved so output-schema validation and
|
|
26
|
+
non-model clients keep receiving JSON.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
from typing import Any, Optional
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
|
38
|
+
from fastmcp.tools.tool import ToolResult
|
|
39
|
+
from mcp.types import TextContent
|
|
40
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"gcf.fastmcp requires the optional 'fastmcp' dependency: "
|
|
43
|
+
'pip install "gcf-python[fastmcp]"'
|
|
44
|
+
) from exc
|
|
45
|
+
|
|
46
|
+
from .decode_generic import decode_generic
|
|
47
|
+
from .generic import encode_generic
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
DEFAULT_ENV_VAR = "RESPONSE_FORMAT"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def gcf_response_enabled(env_var: str = DEFAULT_ENV_VAR) -> bool:
|
|
55
|
+
"""Return True when ``env_var`` is set to ``gcf`` in the environment."""
|
|
56
|
+
return os.environ.get(env_var, "").strip().lower() == "gcf"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GcfResponseMiddleware(Middleware):
|
|
60
|
+
"""FastMCP middleware that re-encodes JSON tool results as GCF, opt-in.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
env_var: Environment variable that gates encoding. Defaults to
|
|
64
|
+
``RESPONSE_FORMAT``; encoding is on when its value is ``gcf``.
|
|
65
|
+
enabled: Force encoding on or off, bypassing the environment gate.
|
|
66
|
+
``None`` (the default) reads the environment on each call.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
env_var: str = DEFAULT_ENV_VAR,
|
|
73
|
+
enabled: Optional[bool] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._env_var = env_var
|
|
76
|
+
self._enabled = enabled
|
|
77
|
+
|
|
78
|
+
def _is_enabled(self) -> bool:
|
|
79
|
+
if self._enabled is not None:
|
|
80
|
+
return self._enabled
|
|
81
|
+
return gcf_response_enabled(self._env_var)
|
|
82
|
+
|
|
83
|
+
async def on_call_tool(
|
|
84
|
+
self,
|
|
85
|
+
context: "MiddlewareContext",
|
|
86
|
+
call_next: "CallNext",
|
|
87
|
+
) -> "ToolResult":
|
|
88
|
+
result = await call_next(context)
|
|
89
|
+
|
|
90
|
+
if not self._is_enabled():
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
payload = _json_payload(result)
|
|
94
|
+
if payload is None:
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
wire = encode_generic(payload)
|
|
99
|
+
# Verify the wire decodes back to the same value before shipping it, so a
|
|
100
|
+
# result is never replaced with an unparseable or lossy encoding.
|
|
101
|
+
if decode_generic(wire) != payload:
|
|
102
|
+
return result
|
|
103
|
+
except Exception as exc: # noqa: BLE001 - liveness over correctness of format
|
|
104
|
+
logger.debug("GCF encoding skipped: %s", exc)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
return ToolResult(
|
|
108
|
+
content=[TextContent(type="text", text=wire)],
|
|
109
|
+
structured_content=result.structured_content,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _json_payload(result: "ToolResult") -> Optional[Any]:
|
|
114
|
+
"""The JSON value to encode, or None if the result is not a single JSON body.
|
|
115
|
+
|
|
116
|
+
Only a result whose content is exactly one text block is re-encoded, so an
|
|
117
|
+
image or other block sent alongside it is never dropped. ``structuredContent``
|
|
118
|
+
(the tool's typed value) is preferred as the payload; otherwise the lone text
|
|
119
|
+
block is parsed as JSON.
|
|
120
|
+
"""
|
|
121
|
+
content = result.content or []
|
|
122
|
+
if len(content) != 1 or not isinstance(content[0], TextContent):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
if result.structured_content is not None:
|
|
126
|
+
return result.structured_content
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
return json.loads(content[0].text)
|
|
130
|
+
except (json.JSONDecodeError, ValueError):
|
|
131
|
+
return None
|
gcf/scalar.py
CHANGED
|
@@ -93,6 +93,15 @@ def format_scalar(v: Any, delimiter: str = "") -> str:
|
|
|
93
93
|
if isinstance(v, bool):
|
|
94
94
|
return "true" if v else "false"
|
|
95
95
|
if isinstance(v, int) and not isinstance(v, bool):
|
|
96
|
+
# The encoder enforces the int64 domain (SPEC 2.3.2): a Python int is
|
|
97
|
+
# arbitrary-precision, so a host integer outside int64 is rejected here
|
|
98
|
+
# rather than emitted as a bare token the decoder would reject.
|
|
99
|
+
if v < -(2**63) or v > 2**63 - 1:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"out_of_range: integer {v} is outside the canonical int64 "
|
|
102
|
+
"domain [-9223372036854775808, 9223372036854775807]; "
|
|
103
|
+
"model larger values as strings (SPEC 2.3.2)"
|
|
104
|
+
)
|
|
96
105
|
return str(v)
|
|
97
106
|
if isinstance(v, float):
|
|
98
107
|
return format_number(v)
|
|
@@ -111,7 +120,12 @@ def format_number(f: float) -> str:
|
|
|
111
120
|
# Negative zero canonicalizes to 0 (SPEC 2.3.1): -0.0 equals 0.0 by value.
|
|
112
121
|
return "0"
|
|
113
122
|
a = abs(f)
|
|
114
|
-
|
|
123
|
+
# Plain decimal only below 2^53. Every double at or above 2^53 is integer-valued,
|
|
124
|
+
# so a plain rendering would emit a bare-integer token: indistinguishable from an
|
|
125
|
+
# int64 on the wire and beyond the binary64 safe-integer range (2^53-1), so a
|
|
126
|
+
# JavaScript decoder rejects it under its default policy. Exponent shape keeps bare
|
|
127
|
+
# tokens int64 and decimal/exponent tokens doubles (SPEC 2.3.1). Ints format above.
|
|
128
|
+
if 1e-6 <= a < 2**53:
|
|
115
129
|
# Use repr for shortest round-trippable form.
|
|
116
130
|
s = repr(f)
|
|
117
131
|
# If repr chose scientific notation, convert to plain decimal.
|
|
@@ -165,12 +179,20 @@ def parse_scalar(s: str, tabular_context: bool = False) -> Any:
|
|
|
165
179
|
if s == "false":
|
|
166
180
|
return False
|
|
167
181
|
if _JSON_NUMBER_RE.match(s):
|
|
182
|
+
# Token shape follows domain (SPEC 2.3.2): a bare-integer literal (no
|
|
183
|
+
# fraction, no exponent) is an int64-domain integer parsed exactly, not
|
|
184
|
+
# routed through float(); a decimal or exponent literal is a double.
|
|
185
|
+
if "." not in s and "e" not in s and "E" not in s:
|
|
186
|
+
n = int(s)
|
|
187
|
+
if n < -(2**63) or n > 2**63 - 1:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"out_of_range: integer {s} is outside the canonical int64 "
|
|
190
|
+
"domain [-9223372036854775808, 9223372036854775807]; "
|
|
191
|
+
"model larger values as strings (SPEC 2.3.2)"
|
|
192
|
+
)
|
|
193
|
+
return n
|
|
168
194
|
try:
|
|
169
|
-
|
|
170
|
-
if "." not in s and "e" not in s and "E" not in s:
|
|
171
|
-
if abs(f) <= 2**53:
|
|
172
|
-
return int(f)
|
|
173
|
-
return f
|
|
195
|
+
return float(s)
|
|
174
196
|
except ValueError:
|
|
175
197
|
pass
|
|
176
198
|
return s
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: gcf-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
|
|
6
6
|
Project-URL: Documentation, https://gcformat.com/
|
|
@@ -21,6 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: fastmcp
|
|
25
|
+
Requires-Dist: fastmcp>=2.11.0; extra == 'fastmcp'
|
|
24
26
|
Description-Content-Type: text/markdown
|
|
25
27
|
|
|
26
28
|
<p align="center">
|
|
@@ -244,6 +246,22 @@ for snapshot in stream: # each turn's current GenericSet
|
|
|
244
246
|
|
|
245
247
|
`fixed_n(15)` re-anchors every N turns; `size_guard()` (recommended) re-anchors once the cumulative delta reaches a full payload's size. It introduces no new wire syntax and the decoder stays cadence-agnostic, so a re-anchor is just the protocol's "full" outcome on a schedule.
|
|
246
248
|
|
|
249
|
+
## FastMCP middleware
|
|
250
|
+
|
|
251
|
+
A drop-in middleware for [FastMCP](https://gofastmcp.com) servers that re-encodes JSON tool results as GCF, opt-in. It requires the `fastmcp` extra (the core package stays zero-dependency):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pip install "gcf-python[fastmcp]"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from gcf.fastmcp import GcfResponseMiddleware
|
|
259
|
+
|
|
260
|
+
mcp.add_middleware(GcfResponseMiddleware())
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When `RESPONSE_FORMAT=gcf` is set in the environment, each tool result whose model-facing content is a single JSON text block is returned as a GCF generic wire instead of JSON. It is opt-in (nothing changes unless the variable is set), lossless and fail-safe (on any encoding error the original result is returned), and non-destructive: only a lone JSON text block is re-encoded, and the tool's `structuredContent` is preserved so output-schema validation and non-model clients keep receiving JSON.
|
|
264
|
+
|
|
247
265
|
## API
|
|
248
266
|
|
|
249
267
|
| Function | Description |
|
|
@@ -304,7 +322,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
|
|
|
304
322
|
|
|
305
323
|
**Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
306
324
|
|
|
307
|
-
**Specification:** [SPEC v3.5.
|
|
325
|
+
**Specification:** [SPEC v3.5.3 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 279 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.7.0, TypeScript v2.6.0, Python v2.6.0, Rust v3.0.0, Swift v2.7.0, Kotlin v2.6.0, .NET v0.2.0. Cross-language conformance verified across all seven SDKs.
|
|
308
326
|
|
|
309
327
|
## Adopted by
|
|
310
328
|
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
gcf/__init__.py,sha256=
|
|
1
|
+
gcf/__init__.py,sha256=qRPchyWVeUYtuubktzAg96l-O0CKI3gMGH1CSm8aqvw,2616
|
|
2
2
|
gcf/__main__.py,sha256=EpvBz1yc8H0D5OJ1zy2tYke-kRzvudKa4DEbfeW14ao,71
|
|
3
3
|
gcf/cli.py,sha256=UEe1CAZn-rKGNIo_ap8-oez3ucl6DSRbsdv6RDnzygY,5256
|
|
4
4
|
gcf/constants.py,sha256=cmZ8YJSOB0im_eyfN8v4UvrLpBC6Fuf4cfcKZGbutxY,638
|
|
5
5
|
gcf/decode.py,sha256=TP58_7UBhfeI0o9zpNgtQvAnLQGhNt0RaUjku7Ke75A,7162
|
|
6
|
-
gcf/decode_generic.py,sha256=
|
|
6
|
+
gcf/decode_generic.py,sha256=X59J2tV_ybZdWupVIVRtfLiqdxko2wfIn78Kkm5lNls,31170
|
|
7
7
|
gcf/delta.py,sha256=oviQ9WsDRYXPXE8bw6SFutmzMRIvu-U_Zzw-39Nd5Ic,8053
|
|
8
8
|
gcf/encode.py,sha256=OYGDyF3oP2I8Y2PrPL_5yDVURqB0mghtqlOGyDEd-Fs,4081
|
|
9
|
+
gcf/fastmcp.py,sha256=Q0qxk87VcCjwH2H8retxyTmieaIGZS7Q8aDsDPQa8sw,4554
|
|
9
10
|
gcf/generic.py,sha256=1c53utF2GcD8lS_wks2opQw08hMz4KGL-7C8g7JB5bA,21577
|
|
10
11
|
gcf/generic_delta.py,sha256=5KSUT_QUeH7OILLWfu9JveaXRSrqRIi3c6E8WjlgC4Q,18785
|
|
11
12
|
gcf/keyed_map.py,sha256=qCwhehXB1FQ2iYCPhcvXOqGwrVqdXhGYZGxv56CSUgc,3644
|
|
12
13
|
gcf/packroot.py,sha256=0rZY7TEVcLzA8XhoIOc6_g9lbRCn-MUr54YTRVhzsO4,2019
|
|
13
|
-
gcf/scalar.py,sha256=
|
|
14
|
+
gcf/scalar.py,sha256=fQvfYLuJVbrbB46fd0V1BPDdVaxdthaOsrZJ2B8l2Gk,11499
|
|
14
15
|
gcf/session.py,sha256=rPqR4xsHKpi_G37t1ODwkTUdHhVAGa3Uig8F60h34Ms,5335
|
|
15
16
|
gcf/stream.py,sha256=3eyzmLMZPyEGC1MS9pujxio2x_t7rCL-eNuJ4DjYkaA,5938
|
|
16
17
|
gcf/stream_generic.py,sha256=h2mJ-jI-c1H2L0uoQVhrvDc7uJEcOwS1MB51k4SCPNM,5775
|
|
17
18
|
gcf/types.py,sha256=AWm-LQoSqLHAYtEjcAxWQZqJ4JXqNreLUKO2mJFgNMA,1465
|
|
18
|
-
gcf_python-2.
|
|
19
|
-
gcf_python-2.
|
|
20
|
-
gcf_python-2.
|
|
21
|
-
gcf_python-2.
|
|
22
|
-
gcf_python-2.
|
|
19
|
+
gcf_python-2.7.0.dist-info/METADATA,sha256=AI5njTxmfrya82omvWIRaU8yzDYuAujw8irf2ENQu6s,17107
|
|
20
|
+
gcf_python-2.7.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
21
|
+
gcf_python-2.7.0.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
|
|
22
|
+
gcf_python-2.7.0.dist-info/licenses/LICENSE,sha256=2Fit9wnaIe--RMSAgyQqxC5hfZTyZqn4fIdBtp9qPDw,1072
|
|
23
|
+
gcf_python-2.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|