mcp-mesh 0.7.20__py3-none-any.whl → 0.8.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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +13 -15
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +29 -10
- _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/signature_analyzer.py +58 -68
- _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
- mcp_mesh-0.8.0.dist-info/RECORD +85 -0
- mesh/__init__.py +12 -1
- mesh/decorators.py +248 -33
- mesh/helpers.py +52 -0
- mesh/types.py +40 -13
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -247
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.20.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,11 +17,16 @@ Features:
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
|
-
from typing import Any, Optional
|
|
20
|
+
from typing import Any, Optional
|
|
21
21
|
|
|
22
22
|
from pydantic import BaseModel
|
|
23
23
|
|
|
24
|
-
from .base_provider_handler import
|
|
24
|
+
from .base_provider_handler import (
|
|
25
|
+
BASE_TOOL_INSTRUCTIONS,
|
|
26
|
+
BaseProviderHandler,
|
|
27
|
+
CLAUDE_ANTI_XML_INSTRUCTION,
|
|
28
|
+
make_schema_strict,
|
|
29
|
+
)
|
|
25
30
|
|
|
26
31
|
logger = logging.getLogger(__name__)
|
|
27
32
|
|
|
@@ -141,51 +146,6 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
141
146
|
# Default to strict for unknown types
|
|
142
147
|
return OUTPUT_MODE_STRICT
|
|
143
148
|
|
|
144
|
-
def _make_schema_strict(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
-
"""
|
|
146
|
-
Make a JSON schema strict for Claude's structured output.
|
|
147
|
-
|
|
148
|
-
Claude requires additionalProperties: false on all object types.
|
|
149
|
-
This recursively processes the schema to add this constraint.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
schema: JSON schema dict
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
Schema with additionalProperties: false on all objects
|
|
156
|
-
"""
|
|
157
|
-
if not isinstance(schema, dict):
|
|
158
|
-
return schema
|
|
159
|
-
|
|
160
|
-
result = schema.copy()
|
|
161
|
-
|
|
162
|
-
# If this is an object type, add additionalProperties: false
|
|
163
|
-
if result.get("type") == "object":
|
|
164
|
-
result["additionalProperties"] = False
|
|
165
|
-
|
|
166
|
-
# Recursively process nested schemas
|
|
167
|
-
if "properties" in result:
|
|
168
|
-
result["properties"] = {
|
|
169
|
-
k: self._make_schema_strict(v) for k, v in result["properties"].items()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# Process $defs (Pydantic uses this for nested models)
|
|
173
|
-
if "$defs" in result:
|
|
174
|
-
result["$defs"] = {
|
|
175
|
-
k: self._make_schema_strict(v) for k, v in result["$defs"].items()
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
# Process items for arrays
|
|
179
|
-
if "items" in result:
|
|
180
|
-
result["items"] = self._make_schema_strict(result["items"])
|
|
181
|
-
|
|
182
|
-
# Process anyOf, oneOf, allOf
|
|
183
|
-
for key in ["anyOf", "oneOf", "allOf"]:
|
|
184
|
-
if key in result:
|
|
185
|
-
result[key] = [self._make_schema_strict(s) for s in result[key]]
|
|
186
|
-
|
|
187
|
-
return result
|
|
188
|
-
|
|
189
149
|
def _apply_prompt_caching(
|
|
190
150
|
self, messages: list[dict[str, Any]]
|
|
191
151
|
) -> list[dict[str, Any]]:
|
|
@@ -302,9 +262,10 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
302
262
|
# Only add response_format in "strict" mode
|
|
303
263
|
if determined_mode == OUTPUT_MODE_STRICT:
|
|
304
264
|
# Claude requires additionalProperties: false on all object types
|
|
265
|
+
# Unlike OpenAI/Gemini, Claude doesn't require all properties in 'required'
|
|
305
266
|
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
306
267
|
schema = output_type.model_json_schema()
|
|
307
|
-
strict_schema =
|
|
268
|
+
strict_schema = make_schema_strict(schema, add_all_required=False)
|
|
308
269
|
request_params["response_format"] = {
|
|
309
270
|
"type": "json_schema",
|
|
310
271
|
"json_schema": {
|
|
@@ -346,15 +307,12 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
346
307
|
# Add tool calling instructions if tools available
|
|
347
308
|
# These prevent Claude from using XML-style <invoke> syntax
|
|
348
309
|
if tool_schemas:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
- After receiving tool results, you can make additional calls if needed
|
|
356
|
-
- Once you have all needed information, provide your final response
|
|
357
|
-
"""
|
|
310
|
+
# Use base instructions but insert anti-XML rule for Claude
|
|
311
|
+
instructions = BASE_TOOL_INSTRUCTIONS.replace(
|
|
312
|
+
"- Make ONE tool call at a time",
|
|
313
|
+
f"- Make ONE tool call at a time\n{CLAUDE_ANTI_XML_INSTRUCTION}",
|
|
314
|
+
)
|
|
315
|
+
system_content += instructions
|
|
358
316
|
|
|
359
317
|
# Add output format instructions based on mode
|
|
360
318
|
if determined_mode == OUTPUT_MODE_TEXT:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini/Google provider handler.
|
|
3
|
+
|
|
4
|
+
Optimized for Gemini models (Gemini 2.0 Flash, Gemini 1.5 Pro, etc.)
|
|
5
|
+
using Google's best practices for tool calling and structured output.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Native structured output via response_format (similar to OpenAI)
|
|
9
|
+
- Native function calling support
|
|
10
|
+
- Support for Gemini 2.x and 3.x models
|
|
11
|
+
- Large context windows (up to 2M tokens)
|
|
12
|
+
|
|
13
|
+
Reference:
|
|
14
|
+
- https://docs.litellm.ai/docs/providers/gemini
|
|
15
|
+
- https://ai.google.dev/gemini-api/docs
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
|
|
23
|
+
from .base_provider_handler import (
|
|
24
|
+
BASE_TOOL_INSTRUCTIONS,
|
|
25
|
+
BaseProviderHandler,
|
|
26
|
+
make_schema_strict,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GeminiHandler(BaseProviderHandler):
|
|
33
|
+
"""
|
|
34
|
+
Provider handler for Google Gemini models.
|
|
35
|
+
|
|
36
|
+
Gemini Characteristics:
|
|
37
|
+
- Native structured output via response_format parameter (LiteLLM translates)
|
|
38
|
+
- Native function calling support
|
|
39
|
+
- Large context windows (1M-2M tokens)
|
|
40
|
+
- Multimodal support (text, images, video, audio)
|
|
41
|
+
- Works well with concise, focused prompts
|
|
42
|
+
|
|
43
|
+
Key Similarities with OpenAI:
|
|
44
|
+
- Uses response_format for structured output (via LiteLLM translation)
|
|
45
|
+
- Native function calling format
|
|
46
|
+
- Similar schema enforcement requirements
|
|
47
|
+
|
|
48
|
+
Supported Models (via LiteLLM):
|
|
49
|
+
- gemini/gemini-2.0-flash (fast, efficient)
|
|
50
|
+
- gemini/gemini-2.0-flash-lite (fastest, most efficient)
|
|
51
|
+
- gemini/gemini-1.5-pro (high capability)
|
|
52
|
+
- gemini/gemini-1.5-flash (balanced)
|
|
53
|
+
- gemini/gemini-3-flash-preview (reasoning support)
|
|
54
|
+
- gemini/gemini-3-pro-preview (advanced reasoning)
|
|
55
|
+
|
|
56
|
+
Reference:
|
|
57
|
+
https://docs.litellm.ai/docs/providers/gemini
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
"""Initialize Gemini handler."""
|
|
62
|
+
super().__init__(vendor="gemini")
|
|
63
|
+
|
|
64
|
+
def prepare_request(
|
|
65
|
+
self,
|
|
66
|
+
messages: list[dict[str, Any]],
|
|
67
|
+
tools: Optional[list[dict[str, Any]]],
|
|
68
|
+
output_type: type,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Prepare request parameters for Gemini API via LiteLLM.
|
|
73
|
+
|
|
74
|
+
Gemini Strategy:
|
|
75
|
+
- Use response_format parameter for structured JSON output
|
|
76
|
+
- LiteLLM handles translation to Gemini's native format
|
|
77
|
+
- Skip structured output for str return types (text mode)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
messages: List of message dicts
|
|
81
|
+
tools: Optional list of tool schemas
|
|
82
|
+
output_type: Return type (str or Pydantic model)
|
|
83
|
+
**kwargs: Additional model parameters
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary of parameters for litellm.completion()
|
|
87
|
+
"""
|
|
88
|
+
# Build base request
|
|
89
|
+
request_params = {
|
|
90
|
+
"messages": messages,
|
|
91
|
+
**kwargs, # Pass through temperature, max_tokens, etc.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Add tools if provided
|
|
95
|
+
# LiteLLM will convert OpenAI tool format to Gemini's function_declarations
|
|
96
|
+
if tools:
|
|
97
|
+
request_params["tools"] = tools
|
|
98
|
+
|
|
99
|
+
# Skip structured output for str return type (text mode)
|
|
100
|
+
if output_type is str:
|
|
101
|
+
return request_params
|
|
102
|
+
|
|
103
|
+
# Only add response_format for Pydantic models
|
|
104
|
+
if not (isinstance(output_type, type) and issubclass(output_type, BaseModel)):
|
|
105
|
+
return request_params
|
|
106
|
+
|
|
107
|
+
# Add response_format for structured output
|
|
108
|
+
# LiteLLM translates this to Gemini's native format
|
|
109
|
+
schema = output_type.model_json_schema()
|
|
110
|
+
|
|
111
|
+
# Transform schema for strict mode compliance
|
|
112
|
+
# Gemini requires additionalProperties: false and all properties in required
|
|
113
|
+
schema = make_schema_strict(schema, add_all_required=True)
|
|
114
|
+
|
|
115
|
+
# Gemini structured output format (via LiteLLM)
|
|
116
|
+
request_params["response_format"] = {
|
|
117
|
+
"type": "json_schema",
|
|
118
|
+
"json_schema": {
|
|
119
|
+
"name": output_type.__name__,
|
|
120
|
+
"schema": schema,
|
|
121
|
+
"strict": True, # Enforce schema compliance
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return request_params
|
|
126
|
+
|
|
127
|
+
def format_system_prompt(
|
|
128
|
+
self,
|
|
129
|
+
base_prompt: str,
|
|
130
|
+
tool_schemas: Optional[list[dict[str, Any]]],
|
|
131
|
+
output_type: type,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Format system prompt for Gemini (concise approach).
|
|
135
|
+
|
|
136
|
+
Gemini Strategy:
|
|
137
|
+
1. Use base prompt as-is
|
|
138
|
+
2. Add tool calling instructions if tools present
|
|
139
|
+
3. Minimal JSON instructions (response_format handles structure)
|
|
140
|
+
4. Keep prompt concise - Gemini works well with clear, direct prompts
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
base_prompt: Base system prompt
|
|
144
|
+
tool_schemas: Optional tool schemas
|
|
145
|
+
output_type: Expected response type (str or Pydantic model)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Formatted system prompt optimized for Gemini
|
|
149
|
+
"""
|
|
150
|
+
system_content = base_prompt
|
|
151
|
+
|
|
152
|
+
# Add tool calling instructions if tools available
|
|
153
|
+
if tool_schemas:
|
|
154
|
+
system_content += BASE_TOOL_INSTRUCTIONS
|
|
155
|
+
|
|
156
|
+
# Skip JSON note for str return type (text mode)
|
|
157
|
+
if output_type is str:
|
|
158
|
+
return system_content
|
|
159
|
+
|
|
160
|
+
# Add brief JSON note (response_format handles enforcement)
|
|
161
|
+
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
162
|
+
system_content += f"\n\nYour final response will be structured as JSON matching the {output_type.__name__} format."
|
|
163
|
+
|
|
164
|
+
return system_content
|
|
165
|
+
|
|
166
|
+
def get_vendor_capabilities(self) -> dict[str, bool]:
|
|
167
|
+
"""
|
|
168
|
+
Return Gemini-specific capabilities.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Capability flags for Gemini
|
|
172
|
+
"""
|
|
173
|
+
return {
|
|
174
|
+
"native_tool_calling": True, # Gemini has native function calling
|
|
175
|
+
"structured_output": True, # Supports structured output via response_format
|
|
176
|
+
"streaming": True, # Supports streaming
|
|
177
|
+
"vision": True, # Gemini supports multimodal (images, video, audio)
|
|
178
|
+
"json_mode": True, # Native JSON mode via response_format
|
|
179
|
+
"large_context": True, # Up to 2M tokens context window
|
|
180
|
+
}
|
|
181
|
+
|
|
@@ -5,12 +5,15 @@ Optimized for OpenAI models (GPT-4, GPT-4 Turbo, GPT-3.5-turbo)
|
|
|
5
5
|
using OpenAI's native structured output capabilities.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import json
|
|
9
8
|
from typing import Any, Optional
|
|
10
9
|
|
|
11
10
|
from pydantic import BaseModel
|
|
12
11
|
|
|
13
|
-
from .base_provider_handler import
|
|
12
|
+
from .base_provider_handler import (
|
|
13
|
+
BASE_TOOL_INSTRUCTIONS,
|
|
14
|
+
BaseProviderHandler,
|
|
15
|
+
make_schema_strict,
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class OpenAIHandler(BaseProviderHandler):
|
|
@@ -94,8 +97,8 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
94
97
|
schema = output_type.model_json_schema()
|
|
95
98
|
|
|
96
99
|
# Transform schema for OpenAI strict mode
|
|
97
|
-
# OpenAI requires additionalProperties: false
|
|
98
|
-
schema =
|
|
100
|
+
# OpenAI requires additionalProperties: false and all properties in required
|
|
101
|
+
schema = make_schema_strict(schema, add_all_required=True)
|
|
99
102
|
|
|
100
103
|
# OpenAI structured output format
|
|
101
104
|
# See: https://platform.openai.com/docs/guides/structured-outputs
|
|
@@ -143,14 +146,7 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
143
146
|
|
|
144
147
|
# Add tool calling instructions if tools available
|
|
145
148
|
if tool_schemas:
|
|
146
|
-
system_content +=
|
|
147
|
-
|
|
148
|
-
IMPORTANT TOOL CALLING RULES:
|
|
149
|
-
- You have access to tools that you can call to gather information
|
|
150
|
-
- Make ONE tool call at a time
|
|
151
|
-
- After receiving tool results, you can make additional calls if needed
|
|
152
|
-
- Once you have all needed information, provide your final response
|
|
153
|
-
"""
|
|
149
|
+
system_content += BASE_TOOL_INSTRUCTIONS
|
|
154
150
|
|
|
155
151
|
# Skip JSON note for str return type (text mode)
|
|
156
152
|
if output_type is str:
|
|
@@ -181,54 +177,3 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
181
177
|
"json_mode": True, # Has dedicated JSON mode via response_format
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
def _add_additional_properties_false(
|
|
185
|
-
self, schema: dict[str, Any]
|
|
186
|
-
) -> dict[str, Any]:
|
|
187
|
-
"""
|
|
188
|
-
Recursively add additionalProperties: false to all object schemas.
|
|
189
|
-
|
|
190
|
-
OpenAI strict mode requires this for all object schemas.
|
|
191
|
-
See: https://platform.openai.com/docs/guides/structured-outputs
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
schema: JSON schema from Pydantic model
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Modified schema with additionalProperties: false on all objects
|
|
198
|
-
"""
|
|
199
|
-
import copy
|
|
200
|
-
|
|
201
|
-
schema = copy.deepcopy(schema)
|
|
202
|
-
self._add_additional_properties_recursive(schema)
|
|
203
|
-
return schema
|
|
204
|
-
|
|
205
|
-
def _add_additional_properties_recursive(self, obj: Any) -> None:
|
|
206
|
-
"""Recursively process schema for OpenAI strict mode compliance."""
|
|
207
|
-
if isinstance(obj, dict):
|
|
208
|
-
# If this is an object type, add additionalProperties: false
|
|
209
|
-
# and ensure required includes all properties
|
|
210
|
-
if obj.get("type") == "object":
|
|
211
|
-
obj["additionalProperties"] = False
|
|
212
|
-
# OpenAI strict mode: required must include ALL property keys
|
|
213
|
-
if "properties" in obj:
|
|
214
|
-
obj["required"] = list(obj["properties"].keys())
|
|
215
|
-
|
|
216
|
-
# Process $defs (Pydantic uses this for nested models)
|
|
217
|
-
if "$defs" in obj:
|
|
218
|
-
for def_schema in obj["$defs"].values():
|
|
219
|
-
self._add_additional_properties_recursive(def_schema)
|
|
220
|
-
|
|
221
|
-
# Process properties
|
|
222
|
-
if "properties" in obj:
|
|
223
|
-
for prop_schema in obj["properties"].values():
|
|
224
|
-
self._add_additional_properties_recursive(prop_schema)
|
|
225
|
-
|
|
226
|
-
# Process items (for arrays)
|
|
227
|
-
if "items" in obj:
|
|
228
|
-
self._add_additional_properties_recursive(obj["items"])
|
|
229
|
-
|
|
230
|
-
# Process anyOf, oneOf, allOf
|
|
231
|
-
for key in ("anyOf", "oneOf", "allOf"):
|
|
232
|
-
if key in obj:
|
|
233
|
-
for item in obj[key]:
|
|
234
|
-
self._add_additional_properties_recursive(item)
|
|
@@ -5,10 +5,11 @@ Manages selection and instantiation of provider handlers based on vendor name.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
from .base_provider_handler import BaseProviderHandler
|
|
11
11
|
from .claude_handler import ClaudeHandler
|
|
12
|
+
from .gemini_handler import GeminiHandler
|
|
12
13
|
from .generic_handler import GenericHandler
|
|
13
14
|
from .openai_handler import OpenAIHandler
|
|
14
15
|
|
|
@@ -39,16 +40,17 @@ class ProviderHandlerRegistry:
|
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
# Built-in vendor mappings
|
|
42
|
-
_handlers:
|
|
43
|
+
_handlers: dict[str, type[BaseProviderHandler]] = {
|
|
43
44
|
"anthropic": ClaudeHandler,
|
|
44
45
|
"openai": OpenAIHandler,
|
|
46
|
+
"gemini": GeminiHandler,
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
# Cache of instantiated handlers (singleton per vendor)
|
|
48
|
-
_instances:
|
|
50
|
+
_instances: dict[str, BaseProviderHandler] = {}
|
|
49
51
|
|
|
50
52
|
@classmethod
|
|
51
|
-
def register(cls, vendor: str, handler_class:
|
|
53
|
+
def register(cls, vendor: str, handler_class: type[BaseProviderHandler]) -> None:
|
|
52
54
|
"""
|
|
53
55
|
Register a custom provider handler.
|
|
54
56
|
|
|
@@ -73,7 +75,9 @@ class ProviderHandlerRegistry:
|
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
cls._handlers[vendor] = handler_class
|
|
76
|
-
logger.info(
|
|
78
|
+
logger.info(
|
|
79
|
+
f"📝 Registered provider handler: {vendor} -> {handler_class.__name__}"
|
|
80
|
+
)
|
|
77
81
|
|
|
78
82
|
# Clear cached instance if it exists (force re-instantiation)
|
|
79
83
|
if vendor in cls._instances:
|
|
@@ -119,9 +123,7 @@ class ProviderHandlerRegistry:
|
|
|
119
123
|
# Get handler class (or fallback to Generic)
|
|
120
124
|
if vendor in cls._handlers:
|
|
121
125
|
handler_class = cls._handlers[vendor]
|
|
122
|
-
logger.info(
|
|
123
|
-
f"✅ Selected {handler_class.__name__} for vendor: {vendor}"
|
|
124
|
-
)
|
|
126
|
+
logger.info(f"✅ Selected {handler_class.__name__} for vendor: {vendor}")
|
|
125
127
|
else:
|
|
126
128
|
handler_class = GenericHandler
|
|
127
129
|
if vendor != "unknown":
|
|
@@ -132,14 +134,18 @@ class ProviderHandlerRegistry:
|
|
|
132
134
|
logger.debug("Using GenericHandler for unknown vendor")
|
|
133
135
|
|
|
134
136
|
# Instantiate and cache
|
|
135
|
-
handler =
|
|
137
|
+
handler = (
|
|
138
|
+
handler_class()
|
|
139
|
+
if handler_class != GenericHandler
|
|
140
|
+
else GenericHandler(vendor)
|
|
141
|
+
)
|
|
136
142
|
cls._instances[vendor] = handler
|
|
137
143
|
|
|
138
144
|
logger.debug(f"🆕 Instantiated handler: {handler}")
|
|
139
145
|
return handler
|
|
140
146
|
|
|
141
147
|
@classmethod
|
|
142
|
-
def list_vendors(cls) ->
|
|
148
|
+
def list_vendors(cls) -> dict[str, str]:
|
|
143
149
|
"""
|
|
144
150
|
List all registered vendors and their handlers.
|
|
145
151
|
|
|
@@ -94,8 +94,9 @@ class ResponseParser:
|
|
|
94
94
|
|
|
95
95
|
Tries multiple strategies to find JSON in mixed responses:
|
|
96
96
|
1. Find ```json ... ``` code fence blocks
|
|
97
|
-
2. Find any JSON object {...}
|
|
98
|
-
3.
|
|
97
|
+
2. Find any JSON object {...} using progressive json.loads
|
|
98
|
+
3. Find any JSON array [...] using progressive json.loads
|
|
99
|
+
4. Return original content if no extraction needed
|
|
99
100
|
|
|
100
101
|
Args:
|
|
101
102
|
content: Raw content that may contain narrative, XML, and JSON
|
|
@@ -109,25 +110,70 @@ class ResponseParser:
|
|
|
109
110
|
extracted = json_match.group(1).strip()
|
|
110
111
|
return extracted
|
|
111
112
|
|
|
112
|
-
# Strategy 2: Try to find
|
|
113
|
-
#
|
|
113
|
+
# Strategy 2: Try to find JSON object using progressive json.loads
|
|
114
|
+
# This correctly handles braces inside string values
|
|
114
115
|
brace_start = content.find("{")
|
|
115
116
|
if brace_start != -1:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
result = ResponseParser._try_progressive_parse(
|
|
118
|
+
content, brace_start, "{", "}"
|
|
119
|
+
)
|
|
120
|
+
if result:
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
# Strategy 3: Try to find JSON array using progressive json.loads
|
|
124
|
+
bracket_start = content.find("[")
|
|
125
|
+
if bracket_start != -1:
|
|
126
|
+
result = ResponseParser._try_progressive_parse(
|
|
127
|
+
content, bracket_start, "[", "]"
|
|
128
|
+
)
|
|
129
|
+
if result:
|
|
130
|
+
return result
|
|
127
131
|
|
|
128
132
|
# No JSON found, return original
|
|
129
133
|
return content
|
|
130
134
|
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _try_progressive_parse(
|
|
137
|
+
content: str, start: int, open_char: str, close_char: str
|
|
138
|
+
) -> str | None:
|
|
139
|
+
"""
|
|
140
|
+
Try to extract valid JSON by progressively extending the end position.
|
|
141
|
+
This correctly handles braces/brackets inside string values.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
content: The full content string
|
|
145
|
+
start: Starting index of the JSON
|
|
146
|
+
open_char: Opening character ('{' or '[')
|
|
147
|
+
close_char: Closing character ('}' or ']')
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Extracted JSON string or None if not found
|
|
151
|
+
"""
|
|
152
|
+
# Find potential end positions based on depth counting
|
|
153
|
+
depth = 0
|
|
154
|
+
potential_ends: list[int] = []
|
|
155
|
+
|
|
156
|
+
for i in range(start, len(content)):
|
|
157
|
+
char = content[i]
|
|
158
|
+
if char == open_char:
|
|
159
|
+
depth += 1
|
|
160
|
+
elif char == close_char:
|
|
161
|
+
depth -= 1
|
|
162
|
+
if depth == 0:
|
|
163
|
+
potential_ends.append(i)
|
|
164
|
+
|
|
165
|
+
# Try each potential end position (shortest first for efficiency)
|
|
166
|
+
for end in potential_ends:
|
|
167
|
+
candidate = content[start : end + 1]
|
|
168
|
+
try:
|
|
169
|
+
json.loads(candidate)
|
|
170
|
+
return candidate
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
# Not valid JSON, try next potential end
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
131
177
|
@staticmethod
|
|
132
178
|
def _strip_markdown_fences(content: str) -> str:
|
|
133
179
|
"""
|