mcp-mesh 0.8.0b9__py3-none-any.whl → 0.9.0b1__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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +9 -0
- _mcp_mesh/engine/mesh_llm_agent.py +53 -24
- _mcp_mesh/engine/mesh_llm_agent_injector.py +210 -39
- _mcp_mesh/engine/provider_handlers/__init__.py +2 -0
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +177 -0
- _mcp_mesh/engine/provider_handlers/claude_handler.py +129 -102
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +218 -48
- _mcp_mesh/engine/provider_handlers/generic_handler.py +31 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +4 -1
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +5 -1
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +30 -1
- _mcp_mesh/tracing/trace_context_helper.py +5 -3
- {mcp_mesh-0.8.0b9.dist-info → mcp_mesh-0.9.0b1.dist-info}/METADATA +4 -2
- {mcp_mesh-0.8.0b9.dist-info → mcp_mesh-0.9.0b1.dist-info}/RECORD +19 -19
- mesh/decorators.py +174 -92
- mesh/helpers.py +47 -2
- {mcp_mesh-0.8.0b9.dist-info → mcp_mesh-0.9.0b1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.8.0b9.dist-info → mcp_mesh-0.9.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,9 +6,12 @@ that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are call
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import copy
|
|
9
|
+
import logging
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
from typing import Any, Optional
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
from pydantic import BaseModel
|
|
13
16
|
|
|
14
17
|
# ============================================================================
|
|
@@ -61,6 +64,144 @@ def make_schema_strict(
|
|
|
61
64
|
return result
|
|
62
65
|
|
|
63
66
|
|
|
67
|
+
def is_simple_schema(schema: dict[str, Any]) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if a JSON schema is simple enough for hint mode.
|
|
70
|
+
|
|
71
|
+
Simple schema criteria:
|
|
72
|
+
- Less than 5 fields
|
|
73
|
+
- All fields are basic types (str, int, float, bool, list)
|
|
74
|
+
- No nested Pydantic models ($ref or nested objects with properties)
|
|
75
|
+
|
|
76
|
+
This is used by provider handlers to decide between hint mode
|
|
77
|
+
(prompt-based JSON instructions) and strict mode (response_format).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
schema: JSON schema dict
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if schema is simple, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
properties = schema.get("properties", {})
|
|
87
|
+
|
|
88
|
+
# Check field count
|
|
89
|
+
if len(properties) >= 5:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Check for nested objects or complex types
|
|
93
|
+
for field_name, field_schema in properties.items():
|
|
94
|
+
field_type = field_schema.get("type")
|
|
95
|
+
|
|
96
|
+
# Check for nested objects (indicates nested Pydantic model)
|
|
97
|
+
if field_type == "object" and "properties" in field_schema:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check for $ref (nested model reference)
|
|
101
|
+
if "$ref" in field_schema:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Check array items for complex types
|
|
105
|
+
if field_type == "array":
|
|
106
|
+
items = field_schema.get("items", {})
|
|
107
|
+
if items.get("type") == "object" or "$ref" in items:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
return True
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sanitize_schema_for_structured_output(schema: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Sanitize a JSON schema by removing validation keywords unsupported by LLM APIs.
|
|
118
|
+
|
|
119
|
+
LLM structured output APIs (Claude, OpenAI, Gemini) typically only support
|
|
120
|
+
the structural parts of JSON Schema, not validation constraints. This function
|
|
121
|
+
removes unsupported keywords to ensure uniform behavior across all providers.
|
|
122
|
+
|
|
123
|
+
Removed keywords:
|
|
124
|
+
- minimum, maximum (number range)
|
|
125
|
+
- exclusiveMinimum, exclusiveMaximum (exclusive bounds)
|
|
126
|
+
- minLength, maxLength (string length)
|
|
127
|
+
- minItems, maxItems (array size)
|
|
128
|
+
- pattern (regex validation)
|
|
129
|
+
- multipleOf (divisibility)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
schema: JSON schema dict (will not be mutated)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
New schema with unsupported validation keywords removed
|
|
136
|
+
"""
|
|
137
|
+
result = copy.deepcopy(schema)
|
|
138
|
+
_strip_unsupported_keywords_recursive(result)
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Sanitized schema for structured output (removed validation-only keywords)"
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Keywords that are validation-only and not supported by LLM structured output APIs
|
|
146
|
+
_UNSUPPORTED_SCHEMA_KEYWORDS = {
|
|
147
|
+
"minimum",
|
|
148
|
+
"maximum",
|
|
149
|
+
"exclusiveMinimum",
|
|
150
|
+
"exclusiveMaximum",
|
|
151
|
+
"minLength",
|
|
152
|
+
"maxLength",
|
|
153
|
+
"minItems",
|
|
154
|
+
"maxItems",
|
|
155
|
+
"pattern",
|
|
156
|
+
"multipleOf",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _strip_unsupported_keywords_recursive(obj: Any) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Recursively strip unsupported validation keywords from a schema object.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
obj: Schema object to process (mutated in place)
|
|
166
|
+
"""
|
|
167
|
+
if not isinstance(obj, dict):
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Remove unsupported keywords at this level
|
|
171
|
+
for keyword in _UNSUPPORTED_SCHEMA_KEYWORDS:
|
|
172
|
+
obj.pop(keyword, None)
|
|
173
|
+
|
|
174
|
+
# Process $defs (Pydantic uses this for nested models)
|
|
175
|
+
if "$defs" in obj:
|
|
176
|
+
for def_schema in obj["$defs"].values():
|
|
177
|
+
_strip_unsupported_keywords_recursive(def_schema)
|
|
178
|
+
|
|
179
|
+
# Process properties
|
|
180
|
+
if "properties" in obj:
|
|
181
|
+
for prop_schema in obj["properties"].values():
|
|
182
|
+
_strip_unsupported_keywords_recursive(prop_schema)
|
|
183
|
+
|
|
184
|
+
# Process items (for arrays)
|
|
185
|
+
if "items" in obj:
|
|
186
|
+
items = obj["items"]
|
|
187
|
+
if isinstance(items, dict):
|
|
188
|
+
_strip_unsupported_keywords_recursive(items)
|
|
189
|
+
elif isinstance(items, list):
|
|
190
|
+
for item in items:
|
|
191
|
+
_strip_unsupported_keywords_recursive(item)
|
|
192
|
+
|
|
193
|
+
# Process prefixItems (tuple validation)
|
|
194
|
+
if "prefixItems" in obj:
|
|
195
|
+
for item in obj["prefixItems"]:
|
|
196
|
+
_strip_unsupported_keywords_recursive(item)
|
|
197
|
+
|
|
198
|
+
# Process anyOf, oneOf, allOf
|
|
199
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
200
|
+
if key in obj:
|
|
201
|
+
for item in obj[key]:
|
|
202
|
+
_strip_unsupported_keywords_recursive(item)
|
|
203
|
+
|
|
204
|
+
|
|
64
205
|
def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
|
|
65
206
|
"""
|
|
66
207
|
Recursively add strict constraints to a schema object.
|
|
@@ -223,6 +364,42 @@ class BaseProviderHandler(ABC):
|
|
|
223
364
|
"json_mode": False,
|
|
224
365
|
}
|
|
225
366
|
|
|
367
|
+
def apply_structured_output(
|
|
368
|
+
self,
|
|
369
|
+
output_schema: dict[str, Any],
|
|
370
|
+
output_type_name: Optional[str],
|
|
371
|
+
model_params: dict[str, Any],
|
|
372
|
+
) -> dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Apply vendor-specific structured output handling to model params.
|
|
375
|
+
|
|
376
|
+
This is used by LLM providers (via mesh) when they receive an output_schema
|
|
377
|
+
from a consumer. Each vendor can customize how structured output is enforced.
|
|
378
|
+
|
|
379
|
+
Default behavior: Apply response_format with strict schema.
|
|
380
|
+
Override in subclasses for vendor-specific behavior (e.g., Claude hint mode).
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
output_schema: JSON schema dict from consumer
|
|
384
|
+
output_type_name: Name of the output type (e.g., "AnalysisResult")
|
|
385
|
+
model_params: Current model parameters dict (will be modified)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Modified model_params with structured output settings applied
|
|
389
|
+
"""
|
|
390
|
+
# Sanitize schema first to remove unsupported validation keywords
|
|
391
|
+
sanitized_schema = sanitize_schema_for_structured_output(output_schema)
|
|
392
|
+
strict_schema = make_schema_strict(sanitized_schema, add_all_required=True)
|
|
393
|
+
model_params["response_format"] = {
|
|
394
|
+
"type": "json_schema",
|
|
395
|
+
"json_schema": {
|
|
396
|
+
"name": output_type_name or "Response",
|
|
397
|
+
"schema": strict_schema,
|
|
398
|
+
"strict": True,
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
return model_params
|
|
402
|
+
|
|
226
403
|
def __repr__(self) -> str:
|
|
227
404
|
"""String representation of handler."""
|
|
228
405
|
return f"{self.__class__.__name__}(vendor='{self.vendor}')"
|
|
@@ -4,15 +4,17 @@ Claude/Anthropic provider handler.
|
|
|
4
4
|
Optimized for Claude API (Claude 3.x, Sonnet, Opus, Haiku)
|
|
5
5
|
using Anthropic's best practices for tool calling and JSON responses.
|
|
6
6
|
|
|
7
|
-
Supports
|
|
8
|
-
-
|
|
9
|
-
- hint: Use prompt-based JSON instructions (medium speed, ~95% reliable)
|
|
7
|
+
Supports two output modes:
|
|
8
|
+
- hint: Use prompt-based JSON instructions with DECISION GUIDE (~95% reliable)
|
|
10
9
|
- text: Plain text output for str return types (fastest)
|
|
11
10
|
|
|
11
|
+
Native response_format (strict mode) is NOT used due to cross-runtime
|
|
12
|
+
incompatibilities when tools are present, and grammar compilation overhead.
|
|
13
|
+
|
|
12
14
|
Features:
|
|
13
15
|
- Automatic prompt caching for system messages (up to 90% cost reduction)
|
|
14
16
|
- Anti-XML tool calling instructions
|
|
15
|
-
-
|
|
17
|
+
- DECISION GUIDE for tool vs. direct JSON response decisions
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
20
|
import json
|
|
@@ -23,15 +25,16 @@ from pydantic import BaseModel
|
|
|
23
25
|
|
|
24
26
|
from .base_provider_handler import (
|
|
25
27
|
BASE_TOOL_INSTRUCTIONS,
|
|
26
|
-
BaseProviderHandler,
|
|
27
28
|
CLAUDE_ANTI_XML_INSTRUCTION,
|
|
28
|
-
|
|
29
|
+
BaseProviderHandler,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
logger = logging.getLogger(__name__)
|
|
32
33
|
|
|
33
34
|
# Output mode constants
|
|
34
|
-
OUTPUT_MODE_STRICT =
|
|
35
|
+
OUTPUT_MODE_STRICT = (
|
|
36
|
+
"strict" # Unused for Claude (kept for override_mode compatibility)
|
|
37
|
+
)
|
|
35
38
|
OUTPUT_MODE_HINT = "hint"
|
|
36
39
|
OUTPUT_MODE_TEXT = "text"
|
|
37
40
|
|
|
@@ -42,19 +45,20 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
42
45
|
|
|
43
46
|
Claude Characteristics:
|
|
44
47
|
- Excellent at following detailed instructions
|
|
45
|
-
- Native structured output via response_format (requires strict schema)
|
|
46
48
|
- Native tool calling (via Anthropic messages API)
|
|
47
49
|
- Performs best with anti-XML tool calling instructions
|
|
48
50
|
- Automatic prompt caching for cost optimization
|
|
49
51
|
|
|
50
|
-
Output Modes:
|
|
51
|
-
-
|
|
52
|
-
- hint: JSON schema in prompt (medium speed, usually valid JSON)
|
|
52
|
+
Output Modes (TEXT + HINT only):
|
|
53
|
+
- hint: JSON schema in prompt with DECISION GUIDE (~95% reliable)
|
|
53
54
|
- text: Plain text output for str return types (fastest)
|
|
54
55
|
|
|
56
|
+
Native response_format (strict mode) is not used. HINT mode with
|
|
57
|
+
detailed prompt instructions provides sufficient reliability (~95%)
|
|
58
|
+
without the cross-runtime incompatibilities and grammar compilation
|
|
59
|
+
overhead of native structured output.
|
|
60
|
+
|
|
55
61
|
Best Practices (from Anthropic docs):
|
|
56
|
-
- Use response_format for guaranteed JSON schema compliance
|
|
57
|
-
- Schema must have additionalProperties: false on all objects
|
|
58
62
|
- Add anti-XML instructions to prevent <invoke> style tool calls
|
|
59
63
|
- Use one tool call at a time for better reliability
|
|
60
64
|
- Use cache_control for system prompts to reduce costs
|
|
@@ -64,62 +68,18 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
64
68
|
"""Initialize Claude handler."""
|
|
65
69
|
super().__init__(vendor="anthropic")
|
|
66
70
|
|
|
67
|
-
def _is_simple_schema(self, model_class: type[BaseModel]) -> bool:
|
|
68
|
-
"""
|
|
69
|
-
Check if a Pydantic model has a simple schema.
|
|
70
|
-
|
|
71
|
-
Simple schema criteria:
|
|
72
|
-
- Less than 5 fields
|
|
73
|
-
- All fields are basic types (str, int, float, bool, list, Optional)
|
|
74
|
-
- No nested Pydantic models
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
model_class: Pydantic model class
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
True if schema is simple, False otherwise
|
|
81
|
-
"""
|
|
82
|
-
try:
|
|
83
|
-
schema = model_class.model_json_schema()
|
|
84
|
-
properties = schema.get("properties", {})
|
|
85
|
-
|
|
86
|
-
# Check field count
|
|
87
|
-
if len(properties) >= 5:
|
|
88
|
-
return False
|
|
89
|
-
|
|
90
|
-
# Check for nested objects or complex types
|
|
91
|
-
for field_name, field_schema in properties.items():
|
|
92
|
-
field_type = field_schema.get("type")
|
|
93
|
-
|
|
94
|
-
# Check for nested objects (indicates nested Pydantic model)
|
|
95
|
-
if field_type == "object" and "properties" in field_schema:
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
# Check for $ref (nested model reference)
|
|
99
|
-
if "$ref" in field_schema:
|
|
100
|
-
return False
|
|
101
|
-
|
|
102
|
-
# Check array items for complex types
|
|
103
|
-
if field_type == "array":
|
|
104
|
-
items = field_schema.get("items", {})
|
|
105
|
-
if items.get("type") == "object" or "$ref" in items:
|
|
106
|
-
return False
|
|
107
|
-
|
|
108
|
-
return True
|
|
109
|
-
except Exception:
|
|
110
|
-
return False
|
|
111
|
-
|
|
112
71
|
def determine_output_mode(
|
|
113
72
|
self, output_type: type, override_mode: Optional[str] = None
|
|
114
73
|
) -> str:
|
|
115
74
|
"""
|
|
116
75
|
Determine the output mode based on return type.
|
|
117
76
|
|
|
77
|
+
Strategy: TEXT + HINT only. No STRICT mode for Claude.
|
|
78
|
+
|
|
118
79
|
Logic:
|
|
119
80
|
- If override_mode specified, use it
|
|
120
81
|
- If return type is str, use "text" mode
|
|
121
|
-
-
|
|
122
|
-
- Otherwise, use "strict" mode
|
|
82
|
+
- All schema types use "hint" mode (prompt-based JSON instructions)
|
|
123
83
|
|
|
124
84
|
Args:
|
|
125
85
|
output_type: Return type (str or BaseModel subclass)
|
|
@@ -136,15 +96,11 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
136
96
|
if output_type is str:
|
|
137
97
|
return OUTPUT_MODE_TEXT
|
|
138
98
|
|
|
139
|
-
#
|
|
99
|
+
# All schema types use HINT mode -- no STRICT for Claude
|
|
140
100
|
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
141
|
-
|
|
142
|
-
return OUTPUT_MODE_HINT
|
|
143
|
-
else:
|
|
144
|
-
return OUTPUT_MODE_STRICT
|
|
101
|
+
return OUTPUT_MODE_HINT
|
|
145
102
|
|
|
146
|
-
|
|
147
|
-
return OUTPUT_MODE_STRICT
|
|
103
|
+
return OUTPUT_MODE_HINT
|
|
148
104
|
|
|
149
105
|
def _apply_prompt_caching(
|
|
150
106
|
self, messages: list[dict[str, Any]]
|
|
@@ -224,9 +180,8 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
224
180
|
"""
|
|
225
181
|
Prepare request parameters for Claude API with output mode support.
|
|
226
182
|
|
|
227
|
-
Output Mode Strategy:
|
|
228
|
-
-
|
|
229
|
-
- hint: No response_format, rely on prompt instructions (medium speed)
|
|
183
|
+
Output Mode Strategy (TEXT + HINT only):
|
|
184
|
+
- hint: No response_format, rely on prompt instructions (~95% reliable)
|
|
230
185
|
- text: No response_format, plain text output (fastest)
|
|
231
186
|
|
|
232
187
|
Args:
|
|
@@ -238,9 +193,8 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
238
193
|
Returns:
|
|
239
194
|
Dictionary of parameters for litellm.completion()
|
|
240
195
|
"""
|
|
241
|
-
# Extract output_mode from kwargs
|
|
242
|
-
|
|
243
|
-
determined_mode = self.determine_output_mode(output_type, output_mode)
|
|
196
|
+
# Extract output_mode from kwargs to prevent it leaking into request params
|
|
197
|
+
kwargs.pop("output_mode", None)
|
|
244
198
|
|
|
245
199
|
# Remove response_format from kwargs - we control this based on output mode
|
|
246
200
|
# The decorator's response_format="json" is just a hint for parsing, not API param
|
|
@@ -259,22 +213,6 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
259
213
|
if tools:
|
|
260
214
|
request_params["tools"] = tools
|
|
261
215
|
|
|
262
|
-
# Only add response_format in "strict" mode
|
|
263
|
-
if determined_mode == OUTPUT_MODE_STRICT:
|
|
264
|
-
# Claude requires additionalProperties: false on all object types
|
|
265
|
-
# Unlike OpenAI/Gemini, Claude doesn't require all properties in 'required'
|
|
266
|
-
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
267
|
-
schema = output_type.model_json_schema()
|
|
268
|
-
strict_schema = make_schema_strict(schema, add_all_required=False)
|
|
269
|
-
request_params["response_format"] = {
|
|
270
|
-
"type": "json_schema",
|
|
271
|
-
"json_schema": {
|
|
272
|
-
"name": output_type.__name__,
|
|
273
|
-
"schema": strict_schema,
|
|
274
|
-
"strict": False, # Allow optional fields with defaults
|
|
275
|
-
},
|
|
276
|
-
}
|
|
277
|
-
|
|
278
216
|
return request_params
|
|
279
217
|
|
|
280
218
|
def format_system_prompt(
|
|
@@ -287,9 +225,8 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
287
225
|
"""
|
|
288
226
|
Format system prompt for Claude with output mode support.
|
|
289
227
|
|
|
290
|
-
Output Mode Strategy:
|
|
291
|
-
-
|
|
292
|
-
- hint: Add detailed JSON schema instructions in prompt
|
|
228
|
+
Output Mode Strategy (TEXT + HINT only):
|
|
229
|
+
- hint: Add detailed JSON schema instructions with DECISION GUIDE in prompt
|
|
293
230
|
- text: No JSON instructions (plain text output)
|
|
294
231
|
|
|
295
232
|
Args:
|
|
@@ -319,13 +256,8 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
319
256
|
# Text mode: No JSON instructions
|
|
320
257
|
pass
|
|
321
258
|
|
|
322
|
-
elif determined_mode == OUTPUT_MODE_STRICT:
|
|
323
|
-
# Strict mode: Minimal instructions (response_format handles schema)
|
|
324
|
-
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
325
|
-
system_content += f"\n\nYour final response will be structured as JSON matching the {output_type.__name__} format."
|
|
326
|
-
|
|
327
259
|
elif determined_mode == OUTPUT_MODE_HINT:
|
|
328
|
-
# Hint mode: Add detailed JSON schema instructions
|
|
260
|
+
# Hint mode: Add detailed JSON schema instructions with DECISION GUIDE
|
|
329
261
|
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
330
262
|
schema = output_type.model_json_schema()
|
|
331
263
|
properties = schema.get("properties", {})
|
|
@@ -344,8 +276,19 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
344
276
|
)
|
|
345
277
|
|
|
346
278
|
fields_text = "\n".join(field_descriptions)
|
|
347
|
-
system_content += f"""
|
|
348
279
|
|
|
280
|
+
# Add DECISION GUIDE when tools are present
|
|
281
|
+
decision_guide = ""
|
|
282
|
+
if tool_schemas:
|
|
283
|
+
decision_guide = """
|
|
284
|
+
DECISION GUIDE:
|
|
285
|
+
- If your answer requires real-time data (weather, calculations, etc.), call the appropriate tool FIRST, then format your response as JSON.
|
|
286
|
+
- If your answer is general knowledge (like facts, explanations, definitions), directly return your response as JSON WITHOUT calling tools.
|
|
287
|
+
- After calling a tool and receiving results, STOP calling tools and return your final JSON response.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
system_content += f"""
|
|
291
|
+
{decision_guide}
|
|
349
292
|
RESPONSE FORMAT:
|
|
350
293
|
You MUST respond with valid JSON matching this schema:
|
|
351
294
|
{{
|
|
@@ -355,7 +298,10 @@ You MUST respond with valid JSON matching this schema:
|
|
|
355
298
|
Example format:
|
|
356
299
|
{json.dumps({k: f"<{v.get('type', 'value')}>" for k, v in properties.items()}, indent=2)}
|
|
357
300
|
|
|
358
|
-
|
|
301
|
+
CRITICAL: Your response must be ONLY the raw JSON object.
|
|
302
|
+
- DO NOT wrap in markdown code fences (```json or ```)
|
|
303
|
+
- DO NOT include any text before or after the JSON
|
|
304
|
+
- Start directly with {{ and end with }}"""
|
|
359
305
|
|
|
360
306
|
return system_content
|
|
361
307
|
|
|
@@ -368,9 +314,90 @@ IMPORTANT: Respond ONLY with valid JSON. No markdown code fences, no preamble te
|
|
|
368
314
|
"""
|
|
369
315
|
return {
|
|
370
316
|
"native_tool_calling": True, # Claude has native function calling
|
|
371
|
-
"structured_output":
|
|
317
|
+
"structured_output": False, # Uses HINT mode (prompt-based), not native response_format
|
|
372
318
|
"streaming": True, # Supports streaming
|
|
373
319
|
"vision": True, # Claude 3+ supports vision
|
|
374
|
-
"json_mode":
|
|
320
|
+
"json_mode": False, # No native JSON mode used
|
|
375
321
|
"prompt_caching": True, # Automatic system prompt caching for cost savings
|
|
376
322
|
}
|
|
323
|
+
|
|
324
|
+
def apply_structured_output(
|
|
325
|
+
self,
|
|
326
|
+
output_schema: dict[str, Any],
|
|
327
|
+
output_type_name: Optional[str],
|
|
328
|
+
model_params: dict[str, Any],
|
|
329
|
+
) -> dict[str, Any]:
|
|
330
|
+
"""
|
|
331
|
+
Apply Claude-specific structured output for mesh delegation using HINT mode.
|
|
332
|
+
|
|
333
|
+
Instead of using response_format (strict mode), injects detailed JSON schema
|
|
334
|
+
instructions into the system message. This is consistent with the TEXT + HINT
|
|
335
|
+
only strategy and avoids cross-runtime incompatibilities.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
output_schema: JSON schema dict from consumer
|
|
339
|
+
output_type_name: Name of the output type (e.g., "AnalysisResult")
|
|
340
|
+
model_params: Current model parameters dict (will be modified)
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Modified model_params with HINT-mode instructions in system prompt
|
|
344
|
+
"""
|
|
345
|
+
# Build HINT mode instructions from the schema
|
|
346
|
+
properties = output_schema.get("properties", {})
|
|
347
|
+
required = output_schema.get("required", [])
|
|
348
|
+
|
|
349
|
+
field_descriptions = []
|
|
350
|
+
for field_name, field_schema in properties.items():
|
|
351
|
+
field_type = field_schema.get("type", "any")
|
|
352
|
+
is_required = field_name in required
|
|
353
|
+
req_marker = " (required)" if is_required else " (optional)"
|
|
354
|
+
desc = field_schema.get("description", "")
|
|
355
|
+
desc_text = f" - {desc}" if desc else ""
|
|
356
|
+
field_descriptions.append(
|
|
357
|
+
f" - {field_name}: {field_type}{req_marker}{desc_text}"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
fields_text = "\n".join(field_descriptions)
|
|
361
|
+
type_name = output_type_name or "Response"
|
|
362
|
+
|
|
363
|
+
hint_instructions = f"""
|
|
364
|
+
|
|
365
|
+
DECISION GUIDE:
|
|
366
|
+
- If your answer requires real-time data (weather, calculations, etc.), call the appropriate tool FIRST, then format your response as JSON.
|
|
367
|
+
- If your answer is general knowledge, directly return your response as JSON WITHOUT calling tools.
|
|
368
|
+
- After calling a tool and receiving results, STOP calling tools and return your final JSON response.
|
|
369
|
+
|
|
370
|
+
RESPONSE FORMAT:
|
|
371
|
+
You MUST respond with valid JSON matching this schema:
|
|
372
|
+
{{
|
|
373
|
+
{fields_text}
|
|
374
|
+
}}
|
|
375
|
+
|
|
376
|
+
Example format:
|
|
377
|
+
{json.dumps({k: f"<{v.get('type', 'value')}>" for k, v in properties.items()}, indent=2)}
|
|
378
|
+
|
|
379
|
+
CRITICAL: Your response must be ONLY the raw JSON object.
|
|
380
|
+
- DO NOT wrap in markdown code fences (```json or ```)
|
|
381
|
+
- DO NOT include any text before or after the JSON
|
|
382
|
+
- Start directly with {{ and end with }}"""
|
|
383
|
+
|
|
384
|
+
# Inject into system message
|
|
385
|
+
messages = model_params.get("messages", [])
|
|
386
|
+
for msg in messages:
|
|
387
|
+
if msg.get("role") == "system":
|
|
388
|
+
content = msg.get("content", "")
|
|
389
|
+
if isinstance(content, str):
|
|
390
|
+
msg["content"] = content + hint_instructions
|
|
391
|
+
elif isinstance(content, list):
|
|
392
|
+
# Content block format -- append to last text block
|
|
393
|
+
for block in reversed(content):
|
|
394
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
395
|
+
block["text"] = block["text"] + hint_instructions
|
|
396
|
+
break
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
logger.info(
|
|
400
|
+
f"Claude hint mode for '{type_name}' "
|
|
401
|
+
f"(mesh delegation, schema in prompt)"
|
|
402
|
+
)
|
|
403
|
+
return model_params
|