pi-codemcp 1.0.0 → 1.1.0
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.
- package/README.md +144 -13
- package/extensions/index.ts +4 -1
- package/package.json +7 -7
- package/sidecar/catalog_cache.py +2 -1
- package/sidecar/cli.py +402 -0
- package/sidecar/executor.py +349 -36
- package/sidecar/gateway.py +499 -60
- package/sidecar/models.py +41 -4
- package/sidecar/pyproject.toml +33 -40
- package/sidecar/runtime_paths.py +71 -0
- package/sidecar/settings.py +7 -3
- package/sidecar/stats.py +424 -0
- package/sidecar/tool_catalog.py +311 -46
- package/sidecar/uv.lock +99 -244
- package/src/chains.ts +1 -2
- package/src/execution-rendering.ts +7 -5
- package/src/mcp-client.ts +5 -1
- package/src/modal.ts +258 -14
- package/src/output.ts +27 -24
- package/src/prompts.ts +24 -0
- package/src/settings.ts +18 -17
- package/src/tools.ts +270 -36
package/sidecar/tool_catalog.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
import hashlib
|
|
4
5
|
import json
|
|
5
6
|
import keyword
|
|
7
|
+
import math
|
|
6
8
|
import re
|
|
9
|
+
import textwrap
|
|
10
|
+
from collections import Counter
|
|
7
11
|
from typing import TYPE_CHECKING, Literal
|
|
8
12
|
|
|
9
13
|
from fastmcp.utilities.json_schema_type import json_schema_to_type
|
|
10
14
|
from mcp import types as mcp_types
|
|
11
15
|
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|
12
16
|
from pydantic_core import to_jsonable_python
|
|
13
|
-
from rapidfuzz import fuzz
|
|
17
|
+
from rapidfuzz import fuzz
|
|
14
18
|
|
|
15
19
|
from .json_types import (
|
|
16
20
|
JSON_OBJECT_ADAPTER,
|
|
@@ -19,20 +23,24 @@ from .json_types import (
|
|
|
19
23
|
JsonSchema,
|
|
20
24
|
JsonValue,
|
|
21
25
|
)
|
|
22
|
-
from .models import ToolSchemaView
|
|
26
|
+
from .models import SearchDetail, ToolSchemaView
|
|
23
27
|
|
|
24
28
|
if TYPE_CHECKING:
|
|
25
29
|
from collections.abc import Iterable
|
|
26
30
|
|
|
27
31
|
from .chains import SavedChainManifest
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
SEARCH_SCORE_CUTOFF = 20.0
|
|
34
|
+
MIN_PLURAL_TOKEN_LENGTH = 4
|
|
31
35
|
STUB_IMPORTS = "from typing import Literal, Never, NotRequired, TypeAlias, TypedDict"
|
|
32
36
|
JSON_TYPE_STUBS = (
|
|
33
37
|
"JsonScalar: TypeAlias = bool | int | float | str | None",
|
|
34
38
|
'JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]',
|
|
35
39
|
)
|
|
40
|
+
INSPECT_JSON_STUB = (
|
|
41
|
+
"def inspect_json(value: JsonValue, *, samples: int = 2, max_depth: int = 3) -> JsonValue: ..."
|
|
42
|
+
)
|
|
43
|
+
STUB_PRELUDE = "\n\n".join([STUB_IMPORTS, *JSON_TYPE_STUBS, INSPECT_JSON_STUB])
|
|
36
44
|
|
|
37
45
|
|
|
38
46
|
class ToolSpec(BaseModel):
|
|
@@ -54,6 +62,7 @@ class ToolSpec(BaseModel):
|
|
|
54
62
|
signature: str
|
|
55
63
|
stub: str
|
|
56
64
|
search_blob: str
|
|
65
|
+
search_fields: dict[str, str]
|
|
57
66
|
input_adapter: TypeAdapter[object]
|
|
58
67
|
kind: Literal["mcp_tool", "saved_chain"] = "mcp_tool"
|
|
59
68
|
chain_id: str | None = None
|
|
@@ -69,6 +78,7 @@ class ToolCatalog(BaseModel):
|
|
|
69
78
|
fingerprint: str
|
|
70
79
|
tools: dict[str, ToolSpec]
|
|
71
80
|
type_stubs: str
|
|
81
|
+
stub_prelude: str
|
|
72
82
|
servers: tuple[str, ...]
|
|
73
83
|
server_aliases: dict[str, str]
|
|
74
84
|
facade_calls: dict[tuple[str, str], str]
|
|
@@ -240,7 +250,7 @@ class ToolCatalog(BaseModel):
|
|
|
240
250
|
input_type_name=input_type,
|
|
241
251
|
output_type_name=output_type,
|
|
242
252
|
signature=signature,
|
|
243
|
-
stub="\n\n".join(
|
|
253
|
+
stub="\n\n".join(tool_definitions),
|
|
244
254
|
search_blob=_build_search_blob(
|
|
245
255
|
public_name,
|
|
246
256
|
call,
|
|
@@ -249,6 +259,14 @@ class ToolCatalog(BaseModel):
|
|
|
249
259
|
tool.description,
|
|
250
260
|
input_schema,
|
|
251
261
|
),
|
|
262
|
+
search_fields=_build_search_fields(
|
|
263
|
+
public_name,
|
|
264
|
+
call,
|
|
265
|
+
server,
|
|
266
|
+
tool.name,
|
|
267
|
+
tool.description,
|
|
268
|
+
input_schema,
|
|
269
|
+
),
|
|
252
270
|
input_adapter=_schema_adapter(input_schema),
|
|
253
271
|
kind=kind,
|
|
254
272
|
chain_id=chain_id,
|
|
@@ -284,8 +302,7 @@ class ToolCatalog(BaseModel):
|
|
|
284
302
|
))
|
|
285
303
|
|
|
286
304
|
type_stubs = "\n\n".join([
|
|
287
|
-
|
|
288
|
-
*JSON_TYPE_STUBS,
|
|
305
|
+
STUB_PRELUDE,
|
|
289
306
|
*_dedupe(definitions),
|
|
290
307
|
*facade_stubs,
|
|
291
308
|
])
|
|
@@ -293,6 +310,7 @@ class ToolCatalog(BaseModel):
|
|
|
293
310
|
fingerprint=fingerprint,
|
|
294
311
|
tools=specs,
|
|
295
312
|
type_stubs=type_stubs,
|
|
313
|
+
stub_prelude=STUB_PRELUDE,
|
|
296
314
|
servers=server_names,
|
|
297
315
|
server_aliases=server_aliases,
|
|
298
316
|
facade_calls=facade_calls,
|
|
@@ -304,32 +322,129 @@ class ToolCatalog(BaseModel):
|
|
|
304
322
|
limit: int = 5,
|
|
305
323
|
*,
|
|
306
324
|
server: str | None = None,
|
|
325
|
+
detail: SearchDetail = "signatures",
|
|
326
|
+
offset: int = 0,
|
|
307
327
|
) -> list[ToolSchemaView]:
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
ranked = self._ranked(query, server=server)
|
|
329
|
+
return [
|
|
330
|
+
_tool_view(spec, detail=detail, score=score, matched_fields=matched_fields)
|
|
331
|
+
for spec, score, matched_fields in ranked[offset : offset + limit]
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
def _ranked(
|
|
335
|
+
self,
|
|
336
|
+
query: str,
|
|
337
|
+
*,
|
|
338
|
+
server: str | None = None,
|
|
339
|
+
) -> list[tuple[ToolSpec, float, list[str]]]:
|
|
340
|
+
candidates = [
|
|
341
|
+
spec
|
|
310
342
|
for spec in sorted(self.tools.values(), key=lambda item: item.name)
|
|
311
343
|
if server is None or spec.server == server
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
344
|
+
]
|
|
345
|
+
if not candidates:
|
|
346
|
+
return []
|
|
347
|
+
query_tokens = _search_tokens(query)
|
|
348
|
+
normalized_query = " ".join(query_tokens)
|
|
349
|
+
document_tokens = [_search_tokens(spec.search_blob) for spec in candidates]
|
|
350
|
+
document_frequency = Counter(token for tokens in document_tokens for token in set(tokens))
|
|
351
|
+
average_length = sum(map(len, document_tokens)) / max(1, len(document_tokens))
|
|
352
|
+
scored: list[tuple[ToolSpec, float, list[str]]] = []
|
|
353
|
+
for spec, tokens in zip(candidates, document_tokens, strict=True):
|
|
354
|
+
exact = _normalized_identifier(query) in {
|
|
355
|
+
_normalized_identifier(spec.name),
|
|
356
|
+
_normalized_identifier(spec.call),
|
|
357
|
+
_normalized_identifier(spec.short_name),
|
|
358
|
+
}
|
|
359
|
+
bm25 = _bm25_score(
|
|
360
|
+
query_tokens,
|
|
361
|
+
tokens,
|
|
362
|
+
document_frequency,
|
|
363
|
+
len(candidates),
|
|
364
|
+
average_length,
|
|
330
365
|
)
|
|
331
|
-
|
|
366
|
+
coverage = (
|
|
367
|
+
len(set(query_tokens) & set(tokens)) / len(set(query_tokens))
|
|
368
|
+
if query_tokens
|
|
369
|
+
else 0.0
|
|
370
|
+
)
|
|
371
|
+
fuzzy_score = fuzz.token_set_ratio(normalized_query, spec.search_blob) / 100
|
|
372
|
+
server_match = bool(set(query_tokens) & set(_search_tokens(spec.server)))
|
|
373
|
+
score = (
|
|
374
|
+
100.0
|
|
375
|
+
if exact
|
|
376
|
+
else min(
|
|
377
|
+
99.0,
|
|
378
|
+
bm25 * 12 + coverage * 45 + fuzzy_score * 20 + (20 if server_match else 0),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
if score < SEARCH_SCORE_CUTOFF:
|
|
382
|
+
continue
|
|
383
|
+
matched_fields = [
|
|
384
|
+
field
|
|
385
|
+
for field, value in spec.search_fields.items()
|
|
386
|
+
if set(query_tokens) & set(_search_tokens(value))
|
|
387
|
+
]
|
|
388
|
+
scored.append((spec, score, matched_fields))
|
|
389
|
+
return sorted(scored, key=lambda item: (-item[1], item[0].name))
|
|
390
|
+
|
|
391
|
+
def inventory(
|
|
392
|
+
self,
|
|
393
|
+
*,
|
|
394
|
+
server: str | None = None,
|
|
395
|
+
detail: SearchDetail = "names",
|
|
396
|
+
offset: int = 0,
|
|
397
|
+
limit: int = 20,
|
|
398
|
+
) -> list[ToolSchemaView]:
|
|
399
|
+
candidates = [
|
|
400
|
+
spec
|
|
401
|
+
for spec in sorted(self.tools.values(), key=lambda item: item.call)
|
|
402
|
+
if server is None or spec.server == server
|
|
332
403
|
]
|
|
404
|
+
return [_tool_view(spec, detail=detail) for spec in candidates[offset : offset + limit]]
|
|
405
|
+
|
|
406
|
+
def inspect(self, calls: list[str]) -> list[ToolSchemaView]:
|
|
407
|
+
by_identifier = {
|
|
408
|
+
identifier: spec
|
|
409
|
+
for spec in self.tools.values()
|
|
410
|
+
for identifier in (spec.name, spec.call)
|
|
411
|
+
}
|
|
412
|
+
unknown = [call for call in calls if call not in by_identifier]
|
|
413
|
+
if unknown:
|
|
414
|
+
suggestions = {
|
|
415
|
+
call: [
|
|
416
|
+
match[0]
|
|
417
|
+
for match in sorted(
|
|
418
|
+
((spec.call, fuzz.ratio(call, spec.call)) for spec in self.tools.values()),
|
|
419
|
+
key=lambda item: (-item[1], item[0]),
|
|
420
|
+
)[:3]
|
|
421
|
+
]
|
|
422
|
+
for call in unknown
|
|
423
|
+
}
|
|
424
|
+
raise ValueError(f"Unknown MCP calls: {unknown}; suggestions: {suggestions}")
|
|
425
|
+
return [_tool_view(by_identifier[call], detail="full") for call in dict.fromkeys(calls)]
|
|
426
|
+
|
|
427
|
+
def type_stubs_for(self, public_names: set[str], *, include: str | None = None) -> str:
|
|
428
|
+
names = set(public_names)
|
|
429
|
+
if include is not None:
|
|
430
|
+
names.add(include)
|
|
431
|
+
specs = [self.tools[name] for name in sorted(names) if name in self.tools]
|
|
432
|
+
facade_methods: dict[str, list[str]] = {}
|
|
433
|
+
definitions: list[str] = []
|
|
434
|
+
for spec in specs:
|
|
435
|
+
definitions.extend(spec.stub.split("\n\n") if spec.stub else [])
|
|
436
|
+
facade_methods.setdefault(spec.namespace, []).append(
|
|
437
|
+
f" async def {spec.method}(self, arguments: {spec.input_type_name}) "
|
|
438
|
+
f"-> {spec.output_type_name}: ..."
|
|
439
|
+
)
|
|
440
|
+
facades: list[str] = []
|
|
441
|
+
for namespace in sorted(facade_methods):
|
|
442
|
+
class_name = f"_{_pascal_case(namespace)}Sdk"
|
|
443
|
+
facades.extend((
|
|
444
|
+
"\n".join([f"class {class_name}:", *facade_methods[namespace]]),
|
|
445
|
+
f"{namespace}: {class_name}",
|
|
446
|
+
))
|
|
447
|
+
return "\n\n".join([STUB_PRELUDE, *_dedupe(definitions), *facades])
|
|
333
448
|
|
|
334
449
|
def counts_by_server(self) -> dict[str, int]:
|
|
335
450
|
counts = dict.fromkeys(self.servers, 0)
|
|
@@ -389,13 +504,15 @@ class ToolCatalog(BaseModel):
|
|
|
389
504
|
if wrap_from_meta and not spec.output_wrap_result:
|
|
390
505
|
adapter = spec.wrapped_output_adapter
|
|
391
506
|
if adapter is None:
|
|
392
|
-
|
|
393
|
-
|
|
507
|
+
normalized = JSON_VALUE_ADAPTER.validate_python(to_jsonable_python(structured))
|
|
508
|
+
else:
|
|
509
|
+
validated = adapter.validate_python(structured)
|
|
510
|
+
normalized = JSON_VALUE_ADAPTER.validate_python(
|
|
511
|
+
adapter.dump_python(validated, mode="json")
|
|
394
512
|
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)
|
|
513
|
+
if isinstance(normalized, str) and (spec.output_wrap_result or wrap_from_meta):
|
|
514
|
+
return _normalize_text_result(normalized)
|
|
515
|
+
return normalized
|
|
399
516
|
|
|
400
517
|
if result.structuredContent is not None:
|
|
401
518
|
return _normalize_json_value(
|
|
@@ -497,11 +614,13 @@ class StubBuilder:
|
|
|
497
614
|
output_schema = _as_schema(_object_property(self.output_schema, "result"))
|
|
498
615
|
if (
|
|
499
616
|
self.normalize_json_string_output
|
|
617
|
+
and self.output_schema
|
|
618
|
+
and self.output_schema.get("x-fastmcp-wrap-result")
|
|
500
619
|
and isinstance(output_schema, dict)
|
|
501
620
|
and output_schema.get("type") == "string"
|
|
502
621
|
):
|
|
503
|
-
#
|
|
504
|
-
#
|
|
622
|
+
# FastMCP-wrapped tools often expose JSON payloads as a `result` string.
|
|
623
|
+
# The runtime intentionally unwraps and parses that top-level string.
|
|
505
624
|
output_schema = True
|
|
506
625
|
output_type = self._ensure_named_type(
|
|
507
626
|
f"{_pascal_case(self.tool_name)}Result",
|
|
@@ -644,7 +763,90 @@ class StubBuilder:
|
|
|
644
763
|
self._active_refs.remove(ref)
|
|
645
764
|
|
|
646
765
|
|
|
647
|
-
def
|
|
766
|
+
def schema_path_summary(schema: JsonObject, *, limit: int = 20) -> list[str]:
|
|
767
|
+
paths: list[str] = []
|
|
768
|
+
|
|
769
|
+
def visit(value: JsonSchema, path: str, *, required: bool) -> None:
|
|
770
|
+
if len(paths) >= limit:
|
|
771
|
+
return
|
|
772
|
+
label = _schema_type_label(value)
|
|
773
|
+
paths.append(f"{path}: {label}{' (required)' if required else ' (optional)'}")
|
|
774
|
+
if not isinstance(value, dict):
|
|
775
|
+
return
|
|
776
|
+
raw_properties = value.get("properties")
|
|
777
|
+
properties = raw_properties if isinstance(raw_properties, dict) else {}
|
|
778
|
+
raw_required = value.get("required")
|
|
779
|
+
required_names = (
|
|
780
|
+
{item for item in raw_required if isinstance(item, str)}
|
|
781
|
+
if isinstance(raw_required, list)
|
|
782
|
+
else set()
|
|
783
|
+
)
|
|
784
|
+
for name, child in properties.items():
|
|
785
|
+
if isinstance(child, (dict, bool)):
|
|
786
|
+
visit(child, f"{path}.{name}", required=name in required_names)
|
|
787
|
+
items = value.get("items")
|
|
788
|
+
if isinstance(items, (dict, bool)):
|
|
789
|
+
visit(items, f"{path}[]", required=True)
|
|
790
|
+
|
|
791
|
+
visit(schema, "$", required=True)
|
|
792
|
+
if len(paths) >= limit:
|
|
793
|
+
paths.append(f"<limited to {limit} paths>")
|
|
794
|
+
return paths
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def referenced_calls(code: str, facade_calls: dict[tuple[str, str], str]) -> set[str]:
|
|
798
|
+
normalized = textwrap.dedent(code).strip("\n")
|
|
799
|
+
wrapped = f"async def __codemcp_main():\n{textwrap.indent(normalized, ' ')}\n"
|
|
800
|
+
try:
|
|
801
|
+
tree = ast.parse(wrapped, mode="exec")
|
|
802
|
+
except SyntaxError:
|
|
803
|
+
return set()
|
|
804
|
+
referenced: set[str] = set()
|
|
805
|
+
for node in ast.walk(tree):
|
|
806
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
807
|
+
continue
|
|
808
|
+
if not isinstance(node.func.value, ast.Name):
|
|
809
|
+
continue
|
|
810
|
+
public_name = facade_calls.get((node.func.value.id, node.func.attr))
|
|
811
|
+
if public_name is not None:
|
|
812
|
+
referenced.add(public_name)
|
|
813
|
+
return referenced
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _tool_view(
|
|
817
|
+
spec: ToolSpec,
|
|
818
|
+
*,
|
|
819
|
+
detail: SearchDetail,
|
|
820
|
+
score: float | None = None,
|
|
821
|
+
matched_fields: list[str] | None = None,
|
|
822
|
+
) -> ToolSchemaView:
|
|
823
|
+
return ToolSchemaView(
|
|
824
|
+
name=spec.name,
|
|
825
|
+
call=spec.call,
|
|
826
|
+
source=spec.kind,
|
|
827
|
+
server=spec.server,
|
|
828
|
+
description=_short_description(spec.description),
|
|
829
|
+
signature=spec.signature if detail in {"signatures", "full"} else None,
|
|
830
|
+
stub=spec.stub if detail == "full" else None,
|
|
831
|
+
score=score,
|
|
832
|
+
matched_fields=matched_fields or [],
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _schema_type_label(schema: JsonSchema) -> str:
|
|
837
|
+
if isinstance(schema, bool):
|
|
838
|
+
return "any JSON value" if schema else "never"
|
|
839
|
+
raw_type = schema.get("type")
|
|
840
|
+
if isinstance(raw_type, str):
|
|
841
|
+
return raw_type
|
|
842
|
+
if isinstance(raw_type, list):
|
|
843
|
+
return " | ".join(str(item) for item in raw_type)
|
|
844
|
+
if "oneOf" in schema or "anyOf" in schema:
|
|
845
|
+
return "union"
|
|
846
|
+
return "JSON value"
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _short_description(description: str | None, limit: int = 72) -> str | None:
|
|
648
850
|
if description is None:
|
|
649
851
|
return None
|
|
650
852
|
first_paragraph = description.split("\n\n", 1)[0]
|
|
@@ -685,6 +887,22 @@ def _field_comment(schema: JsonSchema) -> str | None:
|
|
|
685
887
|
return "; ".join(notes) or None
|
|
686
888
|
|
|
687
889
|
|
|
890
|
+
def _build_search_fields(
|
|
891
|
+
public_name: str,
|
|
892
|
+
call: str,
|
|
893
|
+
server: str,
|
|
894
|
+
short_name: str,
|
|
895
|
+
description: str | None,
|
|
896
|
+
input_schema: JsonObject,
|
|
897
|
+
) -> dict[str, str]:
|
|
898
|
+
return {
|
|
899
|
+
"name": f"{public_name} {call} {short_name}",
|
|
900
|
+
"server": server,
|
|
901
|
+
"description": description or "",
|
|
902
|
+
"parameters": " ".join(_collect_property_names(input_schema)),
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
|
|
688
906
|
def _build_search_blob(
|
|
689
907
|
public_name: str,
|
|
690
908
|
call: str,
|
|
@@ -693,14 +911,59 @@ def _build_search_blob(
|
|
|
693
911
|
description: str | None,
|
|
694
912
|
input_schema: JsonObject,
|
|
695
913
|
) -> str:
|
|
696
|
-
return " ".join(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
914
|
+
return " ".join(
|
|
915
|
+
_build_search_fields(
|
|
916
|
+
public_name,
|
|
917
|
+
call,
|
|
918
|
+
server,
|
|
919
|
+
short_name,
|
|
920
|
+
description,
|
|
921
|
+
input_schema,
|
|
922
|
+
).values()
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _search_tokens(value: str) -> list[str]:
|
|
927
|
+
expanded = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", value)
|
|
928
|
+
tokens = [
|
|
929
|
+
token
|
|
930
|
+
for token in re.findall(r"[a-z0-9]+", expanded.lower().replace("_", " "))
|
|
931
|
+
if len(token) > 1
|
|
932
|
+
]
|
|
933
|
+
return [
|
|
934
|
+
token[:-1] if token.endswith("s") and len(token) >= MIN_PLURAL_TOKEN_LENGTH else token
|
|
935
|
+
for token in tokens
|
|
936
|
+
]
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _normalized_identifier(value: str) -> str:
|
|
940
|
+
return "".join(_search_tokens(value))
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _bm25_score(
|
|
944
|
+
query_tokens: list[str],
|
|
945
|
+
document_tokens: list[str],
|
|
946
|
+
document_frequency: Counter[str],
|
|
947
|
+
document_count: int,
|
|
948
|
+
average_length: float,
|
|
949
|
+
) -> float:
|
|
950
|
+
if not query_tokens or not document_tokens:
|
|
951
|
+
return 0.0
|
|
952
|
+
frequencies = Counter(document_tokens)
|
|
953
|
+
score = 0.0
|
|
954
|
+
k1 = 1.2
|
|
955
|
+
b = 0.75
|
|
956
|
+
for token in set(query_tokens):
|
|
957
|
+
frequency = frequencies[token]
|
|
958
|
+
if frequency == 0:
|
|
959
|
+
continue
|
|
960
|
+
frequency_in_documents = document_frequency[token]
|
|
961
|
+
inverse_document_frequency = math.log(
|
|
962
|
+
1 + (document_count - frequency_in_documents + 0.5) / (frequency_in_documents + 0.5)
|
|
963
|
+
)
|
|
964
|
+
denominator = frequency + k1 * (1 - b + b * len(document_tokens) / max(1.0, average_length))
|
|
965
|
+
score += inverse_document_frequency * frequency * (k1 + 1) / denominator
|
|
966
|
+
return score
|
|
704
967
|
|
|
705
968
|
|
|
706
969
|
def _collect_property_names(schema: JsonSchema) -> list[str]:
|
|
@@ -816,7 +1079,9 @@ def _merge_all_of(
|
|
|
816
1079
|
if isinstance(required, str) and required not in merged_required:
|
|
817
1080
|
merged_required.append(required)
|
|
818
1081
|
if "additionalProperties" in resolved:
|
|
819
|
-
additional_properties =
|
|
1082
|
+
additional_properties = JSON_VALUE_ADAPTER.validate_python(
|
|
1083
|
+
resolved["additionalProperties"]
|
|
1084
|
+
)
|
|
820
1085
|
has_additional_properties = True
|
|
821
1086
|
merged: JsonObject = {
|
|
822
1087
|
"type": "object",
|