tdqs 0.1.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.
Files changed (69) hide show
  1. tdqs/__init__.py +153 -0
  2. tdqs/__main__.py +3 -0
  3. tdqs/build_server_coherence_prompt.py +63 -0
  4. tdqs/build_tool_scoring_prompt.py +61 -0
  5. tdqs/camel_model.py +17 -0
  6. tdqs/cli/__init__.py +1 -0
  7. tdqs/cli/cli_usage.py +31 -0
  8. tdqs/cli/format_lint_report_text.py +41 -0
  9. tdqs/cli/format_score_report_text.py +41 -0
  10. tdqs/cli/main.py +10 -0
  11. tdqs/cli/parse_cli_arguments.py +300 -0
  12. tdqs/cli/resolve_tool_definitions.py +43 -0
  13. tdqs/cli/run_cli.py +202 -0
  14. tdqs/cli/split_command_line.py +67 -0
  15. tdqs/compute_context_signals.py +85 -0
  16. tdqs/compute_input_hash.py +10 -0
  17. tdqs/compute_invocation_cost.py +14 -0
  18. tdqs/compute_smells.py +8 -0
  19. tdqs/compute_tdqs.py +16 -0
  20. tdqs/compute_tier.py +11 -0
  21. tdqs/context_signals.py +34 -0
  22. tdqs/create_llm_client.py +254 -0
  23. tdqs/describe_validation_error.py +12 -0
  24. tdqs/evaluate_hard_gates.py +33 -0
  25. tdqs/find_shadow_candidates.py +80 -0
  26. tdqs/format_lint_report_markdown.py +76 -0
  27. tdqs/format_score_report_markdown.py +90 -0
  28. tdqs/generated_at.py +9 -0
  29. tdqs/lint_report.py +40 -0
  30. tdqs/lint_rules.py +83 -0
  31. tdqs/lint_server.py +165 -0
  32. tdqs/llm_client.py +17 -0
  33. tdqs/load_tool_definitions.py +128 -0
  34. tdqs/package_version.py +8 -0
  35. tdqs/parse_tool_definitions.py +82 -0
  36. tdqs/prompts/__init__.py +1 -0
  37. tdqs/prompts/server_coherence_system_prompt.py +79 -0
  38. tdqs/prompts/tool_scoring_system_prompt.py +91 -0
  39. tdqs/py.typed +0 -0
  40. tdqs/request_hosted_report.py +104 -0
  41. tdqs/rollup_server_score.py +61 -0
  42. tdqs/round1.py +11 -0
  43. tdqs/round_half_up.py +7 -0
  44. tdqs/sanitize_for_prompt.py +17 -0
  45. tdqs/score_report.py +20 -0
  46. tdqs/score_server.py +75 -0
  47. tdqs/score_server_coherence.py +97 -0
  48. tdqs/score_tool_definition.py +118 -0
  49. tdqs/serialize_tool_definition.py +21 -0
  50. tdqs/server_score.py +53 -0
  51. tdqs/spec/__init__.py +1 -0
  52. tdqs/spec/coherence_dimensions.py +96 -0
  53. tdqs/spec/flags.py +84 -0
  54. tdqs/spec/spec_version.py +7 -0
  55. tdqs/spec/tiers.py +25 -0
  56. tdqs/spec/tool_dimensions.py +141 -0
  57. tdqs/stringify_json.py +179 -0
  58. tdqs/tdqs_input_error.py +4 -0
  59. tdqs/tdqs_llm_error.py +17 -0
  60. tdqs/tool_definition.py +32 -0
  61. tdqs/tool_definitions_source.py +28 -0
  62. tdqs/tool_score.py +49 -0
  63. tdqs/traverse_required_subtree.py +212 -0
  64. tdqs/trim_whitespace.py +13 -0
  65. tdqs/utf16_length.py +5 -0
  66. tdqs-0.1.0.dist-info/METADATA +134 -0
  67. tdqs-0.1.0.dist-info/RECORD +69 -0
  68. tdqs-0.1.0.dist-info/WHEEL +4 -0
  69. tdqs-0.1.0.dist-info/entry_points.txt +3 -0
tdqs/__init__.py ADDED
@@ -0,0 +1,153 @@
1
+ """The reference implementation of the Tool Definition Quality Score, for Python.
2
+
3
+ The package's public surface. The modules are one export each; this is the one stable entry
4
+ point, so `from tdqs import score_server` keeps working when a module moves.
5
+ """
6
+
7
+ from tdqs.build_server_coherence_prompt import CoherencePromptTool, build_server_coherence_prompt
8
+ from tdqs.build_tool_scoring_prompt import build_tool_scoring_prompt
9
+ from tdqs.compute_context_signals import compute_context_signals
10
+ from tdqs.compute_input_hash import compute_input_hash
11
+ from tdqs.compute_invocation_cost import compute_invocation_cost
12
+ from tdqs.compute_smells import compute_smells
13
+ from tdqs.compute_tdqs import compute_tdqs
14
+ from tdqs.compute_tier import compute_tier
15
+ from tdqs.context_signals import AnnotationValues, ContextSignals
16
+ from tdqs.create_llm_client import OpenAiCompatibleLlmClient, create_llm_client
17
+ from tdqs.evaluate_hard_gates import HardGateResult, evaluate_hard_gates
18
+ from tdqs.find_shadow_candidates import CostedTool, ShadowCandidate, find_shadow_candidates
19
+ from tdqs.format_lint_report_markdown import format_lint_report_markdown
20
+ from tdqs.format_score_report_markdown import format_score_report_markdown
21
+ from tdqs.lint_report import LintedTool, LintFinding, LintReport, ServerSummary
22
+ from tdqs.lint_rules import LINT_RULES, LintRule, LintRuleKey, LintSeverity
23
+ from tdqs.lint_server import lint_server
24
+ from tdqs.llm_client import LlmClient
25
+ from tdqs.load_tool_definitions import load_tool_definitions
26
+ from tdqs.package_version import PACKAGE_VERSION
27
+ from tdqs.parse_tool_definitions import ParsedToolDefinitions, parse_tool_definitions
28
+ from tdqs.prompts.server_coherence_system_prompt import SERVER_COHERENCE_SYSTEM_PROMPT
29
+ from tdqs.prompts.tool_scoring_system_prompt import TOOL_SCORING_SYSTEM_PROMPT
30
+ from tdqs.request_hosted_report import HostedReport, HostedReportStatus, request_hosted_report
31
+ from tdqs.rollup_server_score import rollup_server_score
32
+ from tdqs.round1 import round1
33
+ from tdqs.score_report import ScoreReport
34
+ from tdqs.score_server import score_server
35
+ from tdqs.score_server_coherence import ServerCoherence, score_server_coherence
36
+ from tdqs.score_tool_definition import score_tool_definition
37
+ from tdqs.serialize_tool_definition import serialize_tool_definition
38
+ from tdqs.server_score import (
39
+ CoherenceJustifications,
40
+ CoherenceScores,
41
+ ServerScore,
42
+ ShadowingRisk,
43
+ )
44
+ from tdqs.spec.coherence_dimensions import (
45
+ COHERENCE_DIMENSIONS,
46
+ CoherenceDimension,
47
+ CoherenceDimensionKey,
48
+ )
49
+ from tdqs.spec.flags import FLAGS, Flag, HardGateFlag, ServerFlag, ToolFlag
50
+ from tdqs.spec.spec_version import SPEC_VERSION
51
+ from tdqs.spec.tiers import TIERS, Tier, TierDefinition
52
+ from tdqs.spec.tool_dimensions import (
53
+ TOOL_DIMENSIONS,
54
+ DimensionScore,
55
+ ToolDimension,
56
+ ToolDimensionKey,
57
+ )
58
+ from tdqs.stringify_json import stringify_json
59
+ from tdqs.tdqs_input_error import TdqsInputError
60
+ from tdqs.tdqs_llm_error import TdqsLlmError
61
+ from tdqs.tool_definition import ToolDefinition
62
+ from tdqs.tool_definitions_source import (
63
+ CommandSource,
64
+ FileSource,
65
+ ToolDefinitionsSource,
66
+ UrlSource,
67
+ )
68
+ from tdqs.tool_score import DimensionJustifications, DimensionScores, Justification, ToolScore
69
+ from tdqs.traverse_required_subtree import RequiredSubtreeSignals, traverse_required_subtree
70
+
71
+ __all__ = [
72
+ "COHERENCE_DIMENSIONS",
73
+ "FLAGS",
74
+ "LINT_RULES",
75
+ "PACKAGE_VERSION",
76
+ "SERVER_COHERENCE_SYSTEM_PROMPT",
77
+ "SPEC_VERSION",
78
+ "TIERS",
79
+ "TOOL_DIMENSIONS",
80
+ "TOOL_SCORING_SYSTEM_PROMPT",
81
+ "AnnotationValues",
82
+ "CoherenceDimension",
83
+ "CoherenceDimensionKey",
84
+ "CoherenceJustifications",
85
+ "CoherencePromptTool",
86
+ "CoherenceScores",
87
+ "CommandSource",
88
+ "ContextSignals",
89
+ "CostedTool",
90
+ "DimensionJustifications",
91
+ "DimensionScore",
92
+ "DimensionScores",
93
+ "FileSource",
94
+ "Flag",
95
+ "HardGateFlag",
96
+ "HardGateResult",
97
+ "HostedReport",
98
+ "HostedReportStatus",
99
+ "Justification",
100
+ "LintFinding",
101
+ "LintReport",
102
+ "LintRule",
103
+ "LintRuleKey",
104
+ "LintSeverity",
105
+ "LintedTool",
106
+ "LlmClient",
107
+ "OpenAiCompatibleLlmClient",
108
+ "ParsedToolDefinitions",
109
+ "RequiredSubtreeSignals",
110
+ "ScoreReport",
111
+ "ServerCoherence",
112
+ "ServerFlag",
113
+ "ServerScore",
114
+ "ServerSummary",
115
+ "ShadowCandidate",
116
+ "ShadowingRisk",
117
+ "TdqsInputError",
118
+ "TdqsLlmError",
119
+ "Tier",
120
+ "TierDefinition",
121
+ "ToolDefinition",
122
+ "ToolDefinitionsSource",
123
+ "ToolDimension",
124
+ "ToolDimensionKey",
125
+ "ToolFlag",
126
+ "ToolScore",
127
+ "UrlSource",
128
+ "build_server_coherence_prompt",
129
+ "build_tool_scoring_prompt",
130
+ "compute_context_signals",
131
+ "compute_input_hash",
132
+ "compute_invocation_cost",
133
+ "compute_smells",
134
+ "compute_tdqs",
135
+ "compute_tier",
136
+ "create_llm_client",
137
+ "evaluate_hard_gates",
138
+ "find_shadow_candidates",
139
+ "format_lint_report_markdown",
140
+ "format_score_report_markdown",
141
+ "lint_server",
142
+ "load_tool_definitions",
143
+ "parse_tool_definitions",
144
+ "request_hosted_report",
145
+ "rollup_server_score",
146
+ "round1",
147
+ "score_server",
148
+ "score_server_coherence",
149
+ "score_tool_definition",
150
+ "serialize_tool_definition",
151
+ "stringify_json",
152
+ "traverse_required_subtree",
153
+ ]
tdqs/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from tdqs.cli.main import main
2
+
3
+ main()
@@ -0,0 +1,63 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+
4
+ from tdqs.context_signals import ContextSignals
5
+ from tdqs.find_shadow_candidates import ShadowCandidate
6
+ from tdqs.sanitize_for_prompt import sanitize_for_prompt
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CoherencePromptTool:
11
+ context_signals: ContextSignals
12
+ description: str | None
13
+ name: str
14
+
15
+
16
+ def build_server_coherence_prompt(
17
+ *,
18
+ server_name: str,
19
+ shadow_candidates: Sequence[ShadowCandidate],
20
+ tools: Sequence[CoherencePromptTool],
21
+ ) -> str:
22
+ """The user message of the coherence call, in the specification's template: every tool with
23
+ its invocation cost and the three signals behind it, then the candidate pairs the prefilter
24
+ emitted. The model is told to judge only those pairs, which is why they are spelled out rather
25
+ than left for it to find."""
26
+ tool_lines: list[str] = []
27
+
28
+ for tool in tools:
29
+ signals = tool.context_signals
30
+ cost = (
31
+ f"cost {signals.invocation_cost}: {signals.required_field_count} required, "
32
+ f"depth {signals.schema_depth}, {signals.union_choice_count} union choices"
33
+ )
34
+ text = (
35
+ "(no description)"
36
+ if tool.description is None
37
+ else str(sanitize_for_prompt(tool.description))
38
+ )
39
+
40
+ tool_lines.append(f"- {tool.name} [{cost}]: {text}")
41
+
42
+ candidate_lines = [
43
+ f"{candidate.tool} (cost {candidate.invocation_cost}) may be shadowed by "
44
+ f"{candidate.cheaper_sibling} (cost {candidate.cheaper_sibling_invocation_cost})"
45
+ for candidate in shadow_candidates
46
+ ]
47
+
48
+ return "\n".join(
49
+ [
50
+ f"SERVER NAME: {sanitize_for_prompt(server_name)}",
51
+ f"TOOL COUNT: {len(tools)}",
52
+ "",
53
+ "<tools>",
54
+ *tool_lines,
55
+ "</tools>",
56
+ "",
57
+ "<shadow-candidates>",
58
+ "\n".join(candidate_lines) if len(candidate_lines) > 0 else "None",
59
+ "</shadow-candidates>",
60
+ "",
61
+ "Respond with JSON only.",
62
+ ]
63
+ )
@@ -0,0 +1,61 @@
1
+ from collections.abc import Sequence
2
+
3
+ from tdqs.context_signals import ContextSignals
4
+ from tdqs.sanitize_for_prompt import sanitize_for_prompt
5
+ from tdqs.stringify_json import stringify_json
6
+ from tdqs.tool_definition import ToolDefinition
7
+
8
+
9
+ def _js_boolean(value: bool) -> str:
10
+ return "true" if value else "false"
11
+
12
+
13
+ def build_tool_scoring_prompt(
14
+ *, context_signals: ContextSignals, sibling_tool_names: Sequence[str], tool: ToolDefinition
15
+ ) -> str:
16
+ """The user message of the tool-scoring call, in the specification's template. Schemas are
17
+ pretty-printed in their own key order — the deterministic serialization is for hashing, and
18
+ the model reads the schema the way its author wrote it. The invocation-cost signals are
19
+ deliberately absent: the specification withholds them from this prompt."""
20
+ input_schema = stringify_json(
21
+ sanitize_for_prompt(tool.input_schema if tool.input_schema is not None else {}), indent=2
22
+ )
23
+ annotations = (
24
+ "None provided"
25
+ if tool.annotations is None
26
+ else stringify_json(sanitize_for_prompt(tool.annotations), indent=2)
27
+ )
28
+ siblings = "\n".join(sibling_tool_names) if len(sibling_tool_names) > 0 else "None"
29
+ description = sanitize_for_prompt(tool.description if tool.description is not None else "")
30
+
31
+ return "\n".join(
32
+ [
33
+ f"TOOL NAME: {tool.name}",
34
+ f"TITLE: {tool.title if tool.title is not None else 'null'}",
35
+ "",
36
+ "DESCRIPTION:",
37
+ f'"{description}"',
38
+ "",
39
+ "<input-schema>",
40
+ input_schema,
41
+ "</input-schema>",
42
+ "",
43
+ "<annotations>",
44
+ annotations,
45
+ "</annotations>",
46
+ "",
47
+ "CONTEXT SIGNALS:",
48
+ f"- Parameter count: {context_signals.param_count}",
49
+ f"- Required parameters: {context_signals.required_param_count}",
50
+ f"- Schema description coverage: {context_signals.schema_description_coverage}%",
51
+ f"- Parameters with enums: {context_signals.params_with_enums}",
52
+ f"- Has output schema: {_js_boolean(context_signals.has_output_schema)}",
53
+ f"- Has nested objects: {_js_boolean(context_signals.has_nested_objects)}",
54
+ "",
55
+ "<sibling-tools>",
56
+ siblings,
57
+ "</sibling-tools>",
58
+ "",
59
+ "Respond with JSON only.",
60
+ ]
61
+ )
tdqs/camel_model.py ADDED
@@ -0,0 +1,17 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+ from pydantic.alias_generators import to_camel
3
+
4
+
5
+ class CamelModel(BaseModel):
6
+ """A report model whose JSON is the specification's: camelCase keys, in the field order the
7
+ class declares. Attributes stay snake_case, and both spellings are accepted on the way in, so
8
+ a report read back from `tdqs score --format json` and one built in Python validate alike.
9
+ Dimension-keyed maps are `TypedDict`s rather than models because their keys are snake_case in
10
+ the specification and must not be renamed."""
11
+
12
+ model_config = ConfigDict(
13
+ alias_generator=to_camel,
14
+ serialize_by_alias=True,
15
+ validate_by_alias=True,
16
+ validate_by_name=True,
17
+ )
tdqs/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """The `tdqs` command: argument parsing, the two subcommands and their terminal renderings."""
tdqs/cli/cli_usage.py ADDED
@@ -0,0 +1,31 @@
1
+ CLI_USAGE = """tdqs — Tool Definition Quality Score
2
+
3
+ Usage
4
+ tdqs lint [input] [--server-name <name>] [--format json|markdown|text] [--fail-on error|warning|never] [--output <path>]
5
+ tdqs score [input] [--model <id>] [--base-url <url>] [--api-key <key>] [--concurrency <n>]
6
+ [--request-overrides <json>] [--format json|markdown|text] [--fail-under A|B|C|D]
7
+ [--output <path>] [--hosted <site url>]
8
+ tdqs --version
9
+ tdqs --help
10
+
11
+ Input (exactly one)
12
+ --file <path> A JSON file: a tools/list result ({ "tools": [...] }), an array of tools, or one tool.
13
+ "-" reads standard input.
14
+ --command "<cmd>" Spawn a stdio MCP server and read its tools/list.
15
+ --url <url> Connect to a Streamable HTTP MCP server. Repeat --header "Name: value" for auth.
16
+
17
+ lint The deterministic stages only: context signals, hard gates, the shadow prefilter and the
18
+ checklist the specification ranks highest. Needs no model and no key.
19
+ score The full pipeline: every tool through the rubric, then coherence, then the server rollup.
20
+ Needs an OpenAI-compatible endpoint (--base-url, --api-key, --model, or the TDQS_BASE_URL,
21
+ TDQS_API_KEY and TDQS_MODEL environment variables), or --hosted to score through a TDQS site
22
+ with that site's API key as --api-key or TDQS_API_KEY.
23
+ --request-overrides (or TDQS_REQUEST_OVERRIDES) merges provider-specific JSON into every
24
+ request; turn extended reasoning off with it, e.g. '{"reasoning":{"enabled":false}}' on
25
+ OpenRouter — the calibration examples reproduce with reasoning off, at seconds per call.
26
+
27
+ Exit codes
28
+ 0 done
29
+ 1 the threshold was not met (--fail-on for lint, --fail-under for score)
30
+ 2 usage error, unreadable input, unreachable server, or a model failure
31
+ """
@@ -0,0 +1,41 @@
1
+ from tdqs.lint_report import LintReport
2
+ from tdqs.lint_rules import LintSeverity
3
+
4
+
5
+ def _yes_no(value: bool) -> str:
6
+ return "yes" if value else "no"
7
+
8
+
9
+ def format_lint_report_text(report: LintReport) -> str:
10
+ """The terminal rendering: one line per finding, grouped by tool, terse enough to read in a CI
11
+ log. The markdown and JSON formats carry everything; this one carries what a person scans."""
12
+ lines: list[str] = [
13
+ (
14
+ f"TDQS lint · {report.server.name} · {report.server.tool_count} tools · specification "
15
+ f"{report.spec_version}"
16
+ ),
17
+ "",
18
+ ]
19
+
20
+ for tool in report.tools:
21
+ signals = tool.context_signals
22
+
23
+ lines.append(
24
+ f"{tool.name} params {signals.param_count} · coverage "
25
+ f"{signals.schema_description_coverage}% · annotations "
26
+ f"{_yes_no(signals.has_annotations)} · output schema "
27
+ f"{_yes_no(signals.has_output_schema)} · cost {signals.invocation_cost}"
28
+ )
29
+ lines.extend(
30
+ f" {finding.severity.ljust(7)} {finding.rule}: {finding.message}"
31
+ for finding in tool.findings
32
+ )
33
+
34
+ def count(severity: LintSeverity) -> int:
35
+ return sum(1 for finding in report.findings if finding.severity == severity)
36
+
37
+ lines.extend(
38
+ ["", f"{count('error')} errors, {count('warning')} warnings, {count('info')} notes"]
39
+ )
40
+
41
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,41 @@
1
+ from tdqs.score_report import ScoreReport
2
+ from tdqs.spec.tool_dimensions import TOOL_DIMENSIONS
3
+
4
+
5
+ def format_score_report_text(report: ScoreReport) -> str:
6
+ """The terminal rendering of a score: the rollup, then one line per tool with its six
7
+ dimension scores in rubric order. Justifications live in the markdown and JSON formats."""
8
+ server_score = report.server_score
9
+ model = "" if report.model is None else f" · model {report.model}"
10
+ headings = " ".join(dimension.key.split("_")[0][:8].ljust(8) for dimension in TOOL_DIMENSIONS)
11
+ lines: list[str] = [
12
+ (
13
+ f"TDQS {server_score.overall_tier} {server_score.overall_score:.1f} · "
14
+ f"{report.server.name} · {report.server.tool_count} tools"
15
+ ),
16
+ (
17
+ f" tool definition quality {server_score.description_quality_tier} "
18
+ f"{server_score.description_quality_score:.1f} (mean {server_score.mean_tdqs:.1f}, "
19
+ f"min {server_score.min_tdqs:.1f}) · coherence {server_score.coherence_tier} "
20
+ f"{server_score.coherence_score:.1f}"
21
+ ),
22
+ f" specification {report.spec_version}{model}",
23
+ "",
24
+ f" {'tool'.ljust(40)} tdqs {headings} flags",
25
+ ]
26
+
27
+ for tool in report.tools:
28
+ scores = " ".join(str(tool.scores[dimension.key]).ljust(8) for dimension in TOOL_DIMENSIONS)
29
+ flags = ", ".join([*tool.flags, *tool.server_flags])
30
+
31
+ lines.append(f"{tool.tier} {tool.name[:40].ljust(40)} {tool.tdqs:.1f} {scores} {flags}")
32
+
33
+ for risk in server_score.shadowing_risks:
34
+ lines.append("")
35
+ lines.append(
36
+ f"shadowing risk: {risk.tool} (cost {risk.invocation_cost}) may be shadowed by "
37
+ f"{risk.cheaper_sibling} (cost {risk.cheaper_sibling_invocation_cost}) — "
38
+ f"{risk.justification}"
39
+ )
40
+
41
+ return "\n".join(lines) + "\n"
tdqs/cli/main.py ADDED
@@ -0,0 +1,10 @@
1
+ import os
2
+ import sys
3
+
4
+ from tdqs.cli.run_cli import run_cli
5
+
6
+
7
+ def main() -> None:
8
+ """The one place the process boundary is crossed: argv, environment, the two streams and the
9
+ exit code. Everything behind `run_cli` is testable in-process."""
10
+ sys.exit(run_cli(argv=sys.argv[1:], env=os.environ, stderr=sys.stderr, stdout=sys.stdout))