diff --git a/01_funccall.ipynb b/01_funccall.ipynb index b717242..c29a603 100644 --- a/01_funccall.ipynb +++ b/01_funccall.ipynb @@ -353,7 +353,9 @@ " args = get_args(t)\n", " if not args: return {'type': 'array', 'items': {}}\n", " if args[-1] is Ellipsis: return {'type': 'array', 'items': _handle_type(args[0], defs)}\n", - " return {'type': 'array', 'prefixItems': [_handle_type(a, defs) for a in args]}\n", + " prefix = [_handle_type(a, defs) for a in args]\n", + " items = prefix[0] if all(p == prefix[0] for p in prefix) else {'anyOf': prefix}\n", + " return {'type': 'array', 'prefixItems': prefix, 'items': items, 'minItems': len(args), 'maxItems': len(args)}\n", " if ot in (list, set):\n", " args = get_args(t)\n", " schema = {'type': 'array', 'items': _handle_type(args[0], defs) if args else {}}\n", @@ -388,6 +390,14 @@ "_handle_type(int, None), _handle_type(Path, None)" ] }, + { + "cell_type": "markdown", + "id": "e669cbc1", + "metadata": {}, + "source": [ + "Fixed-length tuples (e.g. `tuple[int, str]`) are mapped to a JSON Schema array with `prefixItems` for per-position types, which is accepted by Anthropic. OpenAI/Gemini require `items` so this is added, along with `minItems`/`maxItems` to enforce the fixed length. If all positions share the same type, `items` is that type directly; otherwise it's an `anyOf` of the unique types." + ] + }, { "cell_type": "code", "execution_count": null, @@ -398,7 +408,11 @@ "data": { "text/plain": [ "({'type': 'array', 'items': {}},\n", - " {'type': 'array', 'prefixItems': [{'type': 'string'}]},\n", + " {'type': 'array',\n", + " 'prefixItems': [{'type': 'string'}],\n", + " 'items': {'type': 'string'},\n", + " 'minItems': 1,\n", + " 'maxItems': 1},\n", " {'type': 'array', 'items': {'type': 'string'}, 'uniqueItems': True})" ] }, @@ -571,7 +585,7 @@ "source": [ "# Test primitive types in containers\n", "test_eq(_handle_type(list[int], defs), {'type': 'array', 'items': {'type': 'integer'}})\n", - "test_eq(_handle_type(tuple[str], defs), {'type': 'array', 'prefixItems': [{'type': 'string'}]})\n", + "test_eq(_handle_type(tuple[str], defs), {'type': 'array', 'prefixItems': [{'type': 'string'}], 'items': {'type': 'string'}, 'minItems': 1, 'maxItems': 1})\n", "test_eq(_handle_type(set[str], defs), dict(type='array', items={'type': 'string'}, uniqueItems=True))\n", "test_eq(_handle_type(dict[str,bool], defs), {'type': 'object', 'additionalProperties': {'type': 'boolean'}})" ] @@ -736,7 +750,10 @@ " 'properties': {'o': {'description': 'the o', 'type': 'object'},\n", " 'q': {'description': '',\n", " 'type': 'array',\n", - " 'prefixItems': [{'type': 'integer'}, {'type': 'string'}]},\n", + " 'prefixItems': [{'type': 'integer'}, {'type': 'string'}],\n", + " 'items': {'anyOf': [{'type': 'integer'}, {'type': 'string'}]},\n", + " 'minItems': 2,\n", + " 'maxItems': 2},\n", " 'p': {'description': '',\n", " 'default': 'a',\n", " 'anyOf': [{'type': 'string'},\n", @@ -756,7 +773,7 @@ "test_eq(s['name'], 'f')\n", "inpp = s['input_schema']['properties']\n", "test_eq(inpp['o'], {'type': 'object', 'description': 'the o'})\n", - "test_eq(inpp['q'], dict(type='array', description='', prefixItems=[{'type': 'integer'}, {'type': 'string'}]))\n", + "test_eq(inpp['q'], dict(type='array', description='', prefixItems=[{'type': 'integer'}, {'type': 'string'}], items=dict(anyOf=[{'type': 'integer'}, {'type': 'string'}]), minItems=2, maxItems=2))\n", "test_eq(inpp['p'], dict(description='', default='a', anyOf=[{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]))\n", "s" ] @@ -1172,7 +1189,10 @@ " 'properties': {'opt_tup': {'description': '',\n", " 'default': None,\n", " 'anyOf': [{'type': 'array',\n", - " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}]},\n", + " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}],\n", + " 'items': {'type': 'integer'},\n", + " 'minItems': 2,\n", + " 'maxItems': 2},\n", " {'type': 'string'},\n", " {'type': 'integer'}]}}}}" ] @@ -1215,7 +1235,10 @@ " 'properties': {'opt_tup': {'description': '',\n", " 'default': None,\n", " 'anyOf': [{'type': 'array',\n", - " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}]},\n", + " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}],\n", + " 'items': {'type': 'integer'},\n", + " 'minItems': 2,\n", + " 'maxItems': 2},\n", " {'type': 'string'},\n", " {'type': 'integer'}]}}}}" ] @@ -1257,7 +1280,10 @@ " 'properties': {'opt_tup': {'description': '',\n", " 'default': None,\n", " 'anyOf': [{'type': 'array',\n", - " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}]},\n", + " 'prefixItems': [{'type': 'integer'}, {'type': 'integer'}],\n", + " 'items': {'type': 'integer'},\n", + " 'minItems': 2,\n", + " 'maxItems': 2},\n", " {'type': 'null'}]}}}}" ] }, @@ -1620,7 +1646,26 @@ "execution_count": null, "id": "b2c66a73", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': '_cust_type',\n", + " 'description': 'Mandatory docstring',\n", + " 'input_schema': {'type': 'object',\n", + " 'properties': {'a': {'description': '',\n", + " 'anyOf': [{'type': 'string'},\n", + " {'type': 'array', 'items': {'type': 'string'}}]}},\n", + " 'required': ['a']}}" + ] + }, + "execution_count": null, + "metadata": { + "__type": "dict" + }, + "output_type": "execute_result" + } + ], "source": [ "Cmd = str | list[str]\n", "\n", @@ -1856,14 +1901,14 @@ "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_38179/3422083997.py\", line 13, in python\n", + " File \"/var/folders/wm/y9k35r7n7q56mvx2wnndd0880000gp/T/ipykernel_61561/3422083997.py\", line 13, in python\n", " try: return _run(code, glb, loc)\n", " ^^^^^^^^^^^^^^^^^^^^\n", - " File \"/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_38179/2387754473.py\", line 17, in _run\n", + " File \"/var/folders/wm/y9k35r7n7q56mvx2wnndd0880000gp/T/ipykernel_61561/2387754473.py\", line 17, in _run\n", " try: exec(compiled_code, glb, loc)\n", " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", " File \"\", line 1, in \n", - " File \"/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/ipykernel_38179/3422083997.py\", line 8, in handler\n", + " File \"/var/folders/wm/y9k35r7n7q56mvx2wnndd0880000gp/T/ipykernel_61561/3422083997.py\", line 8, in handler\n", " def handler(*args): raise TimeoutError()\n", " ^^^^^^^^^^^^^^^^^^^^\n", "TimeoutError\n", @@ -2771,7 +2816,10 @@ ] } ], - "metadata": {}, + "metadata": { + "solveit_dialog_mode": "learning", + "solveit_ver": 2 + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/toolslm/funccall.py b/toolslm/funccall.py index 986477d..3e61bca 100644 --- a/toolslm/funccall.py +++ b/toolslm/funccall.py @@ -58,7 +58,9 @@ def _handle_type(t, defs): args = get_args(t) if not args: return {'type': 'array', 'items': {}} if args[-1] is Ellipsis: return {'type': 'array', 'items': _handle_type(args[0], defs)} - return {'type': 'array', 'prefixItems': [_handle_type(a, defs) for a in args]} + prefix = [_handle_type(a, defs) for a in args] + items = prefix[0] if all(p == prefix[0] for p in prefix) else {'anyOf': prefix} + return {'type': 'array', 'prefixItems': prefix, 'items': items, 'minItems': len(args), 'maxItems': len(args)} if ot in (list, set): args = get_args(t) schema = {'type': 'array', 'items': _handle_type(args[0], defs) if args else {}}